{
  "info": {
    "name": "SEP CAMARA APIs - All-in-One (Germany Staging)",
    "description": "Every SEP CAMARA service API in this app, with every grant-type variant that exists as its own collection (JWT Bearer, CIBA, Authorization Code - whichever were built for that API) - organized as one top-level folder per API, each containing one sub-folder per grant type.\n\nFill in client_id, private_key (PKCS8 PEM) and optionally kid ONCE at the top of the collection's Variables tab - every folder and sub-folder below reuses those same three variables, so you never have to re-enter your credentials per API or per grant type.\n\nIMPORTANT LIMITATION: this only works if your Application was actually onboarded/ordered for ALL of the APIs you want to test. Per the TMF onboarding model, only ONE VERSION of a given API family can be ordered onto a single Application (e.g. Sim Swap v1 and Sim Swap v2 - or KYC Match v0.2 and v0.3 - cannot both be ordered onto the same Application). If your client_id/private_key were only provisioned for Sim Swap v1, the 'Sim Swap v2' folder here will fail with a 401 UNAUTHENTICATED/InvalidClaim error even though your credentials are otherwise valid and correctly entered - that folder simply requires a different Application's credentials. Skip/ignore the folders for API versions you did not order, or swap in the matching credentials before running them.\n\nEverything else (scope, phone numbers, and other per-API/per-grant parameters) is namespaced per sub-folder with sensible defaults already filled in, so you can run any sub-folder immediately after setting your credentials.\n\nEach sub-folder is fully self-contained - RS256 signing happens entirely inside its own pre-request scripts using pure JavaScript (no external tools/plugins, no client secret ever transmitted). CIBA sub-folders poll automatically; Authorization Code sub-folders require the one manual browser step described in that sub-folder's first request.\n\nbase_url defaults to Germany staging (https://stg.api.telekom.com). Change only that one variable to target production or another country (see the base_url variable description and this app's /documentation/endpoints page). See the /documentation/postman-collections page for per-country/per-grant-type availability (flags) and the single-grant-type collections this master collection was built from.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Sim Swap v1",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Sim Swap v1 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "function b64urlFromBytes(bytes) {\n    let bin = '';\n    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n    let b64 = btoa(bin);\n    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlFromString(str) {\n    const utf8 = unescape(encodeURIComponent(str));\n    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));\n}\n\nfunction Uint8ArrayFromBinaryString(bin) {\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);\n    return arr;\n}\n\nfunction pemToDerBytes(pem) {\n    const b64 = pem\n        .replace(/-----BEGIN [^-]+-----/, '')\n        .replace(/-----END [^-]+-----/, '')\n        .replace(/\\s+/g, '');\n    const bin = atob(b64);\n    return Uint8ArrayFromBinaryString(bin);\n}\n\nfunction readDerTlv(bytes, offset) {\n    const tag = bytes[offset];\n    let lenByte = bytes[offset + 1];\n    let len, lenBytesUsed;\n    if ((lenByte & 0x80) === 0) {\n        len = lenByte;\n        lenBytesUsed = 1;\n    } else {\n        const numLenBytes = lenByte & 0x7f;\n        len = 0;\n        for (let i = 0; i < numLenBytes; i++) {\n            len = (len * 256) + bytes[offset + 2 + i];\n        }\n        lenBytesUsed = 1 + numLenBytes;\n    }\n    const contentStart = offset + 1 + lenBytesUsed;\n    return { tag, len, contentStart, nextOffset: contentStart + len };\n}\n\nfunction derToBigInt(bytes, start, len) {\n    let hex = '';\n    for (let i = 0; i < len; i++) {\n        const b = bytes[start + i];\n        hex += (b < 16 ? '0' : '') + b.toString(16);\n    }\n    if (hex === '') return BigInt(0);\n    return BigInt('0x' + hex);\n}\n\nfunction extractRsaPrivateKeyParams(pem) {\n    const bytes = pemToDerBytes(pem);\n    let tlv = readDerTlv(bytes, 0);\n    let cursor = tlv.contentStart;\n    let versionPeek = readDerTlv(bytes, cursor);\n    let second = readDerTlv(bytes, versionPeek.nextOffset);\n    let rsaKeyStart;\n    if (second.tag === 0x02) {\n        rsaKeyStart = cursor;\n    } else {\n        let versionTlv = readDerTlv(bytes, cursor);\n        cursor = versionTlv.nextOffset;\n        let algTlv = readDerTlv(bytes, cursor);\n        cursor = algTlv.nextOffset;\n        let octetTlv = readDerTlv(bytes, cursor);\n        let inner = readDerTlv(bytes, octetTlv.contentStart);\n        rsaKeyStart = inner.contentStart;\n    }\n    let p = rsaKeyStart;\n    let versionField = readDerTlv(bytes, p);\n    p = versionField.nextOffset;\n    let nField = readDerTlv(bytes, p);\n    const n = derToBigInt(bytes, nField.contentStart, nField.len);\n    p = nField.nextOffset;\n    let eField = readDerTlv(bytes, p);\n    p = eField.nextOffset;\n    let dField = readDerTlv(bytes, p);\n    const d = derToBigInt(bytes, dField.contentStart, dField.len);\n    const modulusByteLength = Math.ceil(n.toString(2).length / 8);\n    return { n, d, modulusByteLength };\n}\n\nfunction modPow(base, exp, mod) {\n    let result = BigInt(1);\n    base = base % mod;\n    while (exp > BigInt(0)) {\n        if (exp % BigInt(2) === BigInt(1)) {\n            result = (result * base) % mod;\n        }\n        exp = exp / BigInt(2);\n        base = (base * base) % mod;\n    }\n    return result;\n}\n\nconst SHA256_DIGEST_INFO_PREFIX_HEX =\n    '3031300d060960864801650304020105000420';\n\nfunction hexToBytes(hex) {\n    const arr = new Uint8Array(hex.length / 2);\n    for (let i = 0; i < arr.length; i++) {\n        arr[i] = parseInt(hex.substr(i * 2, 2), 16);\n    }\n    return arr;\n}\n\nfunction bytesToHex(bytes) {\n    let hex = '';\n    for (let i = 0; i < bytes.length; i++) {\n        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);\n    }\n    return hex;\n}\n\nfunction emsaPkcs1v15Encode(digestHex, k) {\n    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;\n    const digestInfo = hexToBytes(digestInfoHex);\n    const tLen = digestInfo.length;\n    if (k < tLen + 11) {\n        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');\n    }\n    const psLen = k - tLen - 3;\n    const em = new Uint8Array(k);\n    em[0] = 0x00;\n    em[1] = 0x01;\n    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;\n    em[2 + psLen] = 0x00;\n    em.set(digestInfo, 3 + psLen);\n    return em;\n}\n\nfunction bytesToBigInt(bytes) {\n    return BigInt('0x' + (bytesToHex(bytes) || '0'));\n}\n\nfunction bigIntToBytes(bi, length) {\n    let hex = bi.toString(16);\n    if (hex.length % 2 !== 0) hex = '0' + hex;\n    let bytes = hexToBytes(hex);\n    if (bytes.length < length) {\n        const padded = new Uint8Array(length);\n        padded.set(bytes, length - bytes.length);\n        bytes = padded;\n    }\n    return bytes;\n}\n\nfunction rs256Sign(signingInput, privateKeyPem) {\n    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);\n    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);\n    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);\n    const m = bytesToBigInt(em);\n    const s = modPow(m, d, n);\n    const sigBytes = bigIntToBytes(s, modulusByteLength);\n    return b64urlFromBytes(sigBytes);\n}\n\nfunction buildSignedJwt(headerObj, payloadObj, privateKeyPem) {\n    const headerB64 = b64urlFromString(JSON.stringify(headerObj));\n    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));\n    const signingInput = headerB64 + '.' + payloadB64;\n    const sig = rs256Sign(signingInput, privateKeyPem);\n    return signingInput + '.' + sig;\n}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv1_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv1_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('simswapv1_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('simswapv1_jwt_phone_number');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('simswapv1_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv1_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{simswapv1_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv1_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv1_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / JWT Bearer] 2) Sim Swap v1 - Check",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv1_jwt_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/check",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "check"
                  ]
                },
                "description": "CAMARA Sim Swap v1 check. maxAge=240 hours matches this app's SEP_APIs.razor default. The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / JWT Bearer] 3) Sim Swap v1 - Retrieve Date",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/retrieve-date",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v1 retrieve-date. No maxAge field here - matches SEPAPIs.cs RetrieveSimSwapDateEndpoint, which only ever sends phoneNumber (never maxAge) in the body."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / JWT Bearer] 4) Sim Swap v1 - Health",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/health",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "health"
                  ]
                },
                "description": "Health check GET for Sim Swap v1 (no request body, matches SEPAPIs.cs ExecuteHealthGet)."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v1 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv1_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Sim Swap v1 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "function b64urlFromBytes(bytes) {\n    let bin = '';\n    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n    let b64 = btoa(bin);\n    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlFromString(str) {\n    const utf8 = unescape(encodeURIComponent(str));\n    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));\n}\n\nfunction Uint8ArrayFromBinaryString(bin) {\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);\n    return arr;\n}\n\nfunction pemToDerBytes(pem) {\n    const b64 = pem\n        .replace(/-----BEGIN [^-]+-----/, '')\n        .replace(/-----END [^-]+-----/, '')\n        .replace(/\\s+/g, '');\n    const bin = atob(b64);\n    return Uint8ArrayFromBinaryString(bin);\n}\n\nfunction readDerTlv(bytes, offset) {\n    const tag = bytes[offset];\n    let lenByte = bytes[offset + 1];\n    let len, lenBytesUsed;\n    if ((lenByte & 0x80) === 0) {\n        len = lenByte;\n        lenBytesUsed = 1;\n    } else {\n        const numLenBytes = lenByte & 0x7f;\n        len = 0;\n        for (let i = 0; i < numLenBytes; i++) {\n            len = (len * 256) + bytes[offset + 2 + i];\n        }\n        lenBytesUsed = 1 + numLenBytes;\n    }\n    const contentStart = offset + 1 + lenBytesUsed;\n    return { tag, len, contentStart, nextOffset: contentStart + len };\n}\n\nfunction derToBigInt(bytes, start, len) {\n    let hex = '';\n    for (let i = 0; i < len; i++) {\n        const b = bytes[start + i];\n        hex += (b < 16 ? '0' : '') + b.toString(16);\n    }\n    if (hex === '') return BigInt(0);\n    return BigInt('0x' + hex);\n}\n\nfunction extractRsaPrivateKeyParams(pem) {\n    const bytes = pemToDerBytes(pem);\n    let tlv = readDerTlv(bytes, 0);\n    let cursor = tlv.contentStart;\n    let versionPeek = readDerTlv(bytes, cursor);\n    let second = readDerTlv(bytes, versionPeek.nextOffset);\n    let rsaKeyStart;\n    if (second.tag === 0x02) {\n        rsaKeyStart = cursor;\n    } else {\n        let versionTlv = readDerTlv(bytes, cursor);\n        cursor = versionTlv.nextOffset;\n        let algTlv = readDerTlv(bytes, cursor);\n        cursor = algTlv.nextOffset;\n        let octetTlv = readDerTlv(bytes, cursor);\n        let inner = readDerTlv(bytes, octetTlv.contentStart);\n        rsaKeyStart = inner.contentStart;\n    }\n    let p = rsaKeyStart;\n    let versionField = readDerTlv(bytes, p);\n    p = versionField.nextOffset;\n    let nField = readDerTlv(bytes, p);\n    const n = derToBigInt(bytes, nField.contentStart, nField.len);\n    p = nField.nextOffset;\n    let eField = readDerTlv(bytes, p);\n    p = eField.nextOffset;\n    let dField = readDerTlv(bytes, p);\n    const d = derToBigInt(bytes, dField.contentStart, dField.len);\n    const modulusByteLength = Math.ceil(n.toString(2).length / 8);\n    return { n, d, modulusByteLength };\n}\n\nfunction modPow(base, exp, mod) {\n    let result = BigInt(1);\n    base = base % mod;\n    while (exp > BigInt(0)) {\n        if (exp % BigInt(2) === BigInt(1)) {\n            result = (result * base) % mod;\n        }\n        exp = exp / BigInt(2);\n        base = (base * base) % mod;\n    }\n    return result;\n}\n\nconst SHA256_DIGEST_INFO_PREFIX_HEX =\n    '3031300d060960864801650304020105000420';\n\nfunction hexToBytes(hex) {\n    const arr = new Uint8Array(hex.length / 2);\n    for (let i = 0; i < arr.length; i++) {\n        arr[i] = parseInt(hex.substr(i * 2, 2), 16);\n    }\n    return arr;\n}\n\nfunction bytesToHex(bytes) {\n    let hex = '';\n    for (let i = 0; i < bytes.length; i++) {\n        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);\n    }\n    return hex;\n}\n\nfunction emsaPkcs1v15Encode(digestHex, k) {\n    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;\n    const digestInfo = hexToBytes(digestInfoHex);\n    const tLen = digestInfo.length;\n    if (k < tLen + 11) {\n        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');\n    }\n    const psLen = k - tLen - 3;\n    const em = new Uint8Array(k);\n    em[0] = 0x00;\n    em[1] = 0x01;\n    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;\n    em[2 + psLen] = 0x00;\n    em.set(digestInfo, 3 + psLen);\n    return em;\n}\n\nfunction bytesToBigInt(bytes) {\n    return BigInt('0x' + (bytesToHex(bytes) || '0'));\n}\n\nfunction bigIntToBytes(bi, length) {\n    let hex = bi.toString(16);\n    if (hex.length % 2 !== 0) hex = '0' + hex;\n    let bytes = hexToBytes(hex);\n    if (bytes.length < length) {\n        const padded = new Uint8Array(length);\n        padded.set(bytes, length - bytes.length);\n        bytes = padded;\n    }\n    return bytes;\n}\n\nfunction rs256Sign(signingInput, privateKeyPem) {\n    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);\n    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);\n    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);\n    const m = bytesToBigInt(em);\n    const s = modPow(m, d, n);\n    const sigBytes = bigIntToBytes(s, modulusByteLength);\n    return b64urlFromBytes(sigBytes);\n}\n\nfunction buildSignedJwt(headerObj, payloadObj, privateKeyPem) {\n    const headerB64 = b64urlFromString(JSON.stringify(headerObj));\n    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));\n    const signingInput = headerB64 + '.' + payloadB64;\n    const sig = rs256Sign(signingInput, privateKeyPem);\n    return signingInput + '.' + sig;\n}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv1_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv1_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('simswapv1_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('simswapv1_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('simswapv1_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('simswapv1_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{simswapv1_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{simswapv1_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv1_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv1_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{simswapv1_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "function b64urlFromBytes(bytes) {\n    let bin = '';\n    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n    let b64 = btoa(bin);\n    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlFromString(str) {\n    const utf8 = unescape(encodeURIComponent(str));\n    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));\n}\n\nfunction Uint8ArrayFromBinaryString(bin) {\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);\n    return arr;\n}\n\nfunction pemToDerBytes(pem) {\n    const b64 = pem\n        .replace(/-----BEGIN [^-]+-----/, '')\n        .replace(/-----END [^-]+-----/, '')\n        .replace(/\\s+/g, '');\n    const bin = atob(b64);\n    return Uint8ArrayFromBinaryString(bin);\n}\n\nfunction readDerTlv(bytes, offset) {\n    const tag = bytes[offset];\n    let lenByte = bytes[offset + 1];\n    let len, lenBytesUsed;\n    if ((lenByte & 0x80) === 0) {\n        len = lenByte;\n        lenBytesUsed = 1;\n    } else {\n        const numLenBytes = lenByte & 0x7f;\n        len = 0;\n        for (let i = 0; i < numLenBytes; i++) {\n            len = (len * 256) + bytes[offset + 2 + i];\n        }\n        lenBytesUsed = 1 + numLenBytes;\n    }\n    const contentStart = offset + 1 + lenBytesUsed;\n    return { tag, len, contentStart, nextOffset: contentStart + len };\n}\n\nfunction derToBigInt(bytes, start, len) {\n    let hex = '';\n    for (let i = 0; i < len; i++) {\n        const b = bytes[start + i];\n        hex += (b < 16 ? '0' : '') + b.toString(16);\n    }\n    if (hex === '') return BigInt(0);\n    return BigInt('0x' + hex);\n}\n\nfunction extractRsaPrivateKeyParams(pem) {\n    const bytes = pemToDerBytes(pem);\n    let tlv = readDerTlv(bytes, 0);\n    let cursor = tlv.contentStart;\n    let versionPeek = readDerTlv(bytes, cursor);\n    let second = readDerTlv(bytes, versionPeek.nextOffset);\n    let rsaKeyStart;\n    if (second.tag === 0x02) {\n        rsaKeyStart = cursor;\n    } else {\n        let versionTlv = readDerTlv(bytes, cursor);\n        cursor = versionTlv.nextOffset;\n        let algTlv = readDerTlv(bytes, cursor);\n        cursor = algTlv.nextOffset;\n        let octetTlv = readDerTlv(bytes, cursor);\n        let inner = readDerTlv(bytes, octetTlv.contentStart);\n        rsaKeyStart = inner.contentStart;\n    }\n    let p = rsaKeyStart;\n    let versionField = readDerTlv(bytes, p);\n    p = versionField.nextOffset;\n    let nField = readDerTlv(bytes, p);\n    const n = derToBigInt(bytes, nField.contentStart, nField.len);\n    p = nField.nextOffset;\n    let eField = readDerTlv(bytes, p);\n    p = eField.nextOffset;\n    let dField = readDerTlv(bytes, p);\n    const d = derToBigInt(bytes, dField.contentStart, dField.len);\n    const modulusByteLength = Math.ceil(n.toString(2).length / 8);\n    return { n, d, modulusByteLength };\n}\n\nfunction modPow(base, exp, mod) {\n    let result = BigInt(1);\n    base = base % mod;\n    while (exp > BigInt(0)) {\n        if (exp % BigInt(2) === BigInt(1)) {\n            result = (result * base) % mod;\n        }\n        exp = exp / BigInt(2);\n        base = (base * base) % mod;\n    }\n    return result;\n}\n\nconst SHA256_DIGEST_INFO_PREFIX_HEX =\n    '3031300d060960864801650304020105000420';\n\nfunction hexToBytes(hex) {\n    const arr = new Uint8Array(hex.length / 2);\n    for (let i = 0; i < arr.length; i++) {\n        arr[i] = parseInt(hex.substr(i * 2, 2), 16);\n    }\n    return arr;\n}\n\nfunction bytesToHex(bytes) {\n    let hex = '';\n    for (let i = 0; i < bytes.length; i++) {\n        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);\n    }\n    return hex;\n}\n\nfunction emsaPkcs1v15Encode(digestHex, k) {\n    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;\n    const digestInfo = hexToBytes(digestInfoHex);\n    const tLen = digestInfo.length;\n    if (k < tLen + 11) {\n        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');\n    }\n    const psLen = k - tLen - 3;\n    const em = new Uint8Array(k);\n    em[0] = 0x00;\n    em[1] = 0x01;\n    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;\n    em[2 + psLen] = 0x00;\n    em.set(digestInfo, 3 + psLen);\n    return em;\n}\n\nfunction bytesToBigInt(bytes) {\n    return BigInt('0x' + (bytesToHex(bytes) || '0'));\n}\n\nfunction bigIntToBytes(bi, length) {\n    let hex = bi.toString(16);\n    if (hex.length % 2 !== 0) hex = '0' + hex;\n    let bytes = hexToBytes(hex);\n    if (bytes.length < length) {\n        const padded = new Uint8Array(length);\n        padded.set(bytes, length - bytes.length);\n        bytes = padded;\n    }\n    return bytes;\n}\n\nfunction rs256Sign(signingInput, privateKeyPem) {\n    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);\n    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);\n    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);\n    const m = bytesToBigInt(em);\n    const s = modPow(m, d, n);\n    const sigBytes = bigIntToBytes(s, modulusByteLength);\n    return b64urlFromBytes(sigBytes);\n}\n\nfunction buildSignedJwt(headerObj, payloadObj, privateKeyPem) {\n    const headerB64 = b64urlFromString(JSON.stringify(headerObj));\n    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));\n    const signingInput = headerB64 + '.' + payloadB64;\n    const sig = rs256Sign(signingInput, privateKeyPem);\n    return signingInput + '.' + sig;\n}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv1_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv1_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('simswapv1_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('simswapv1_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv1_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to Sim Swap check.');",
                      "    postman.setNextRequest('[Sim Swap v1 / CIBA] 3) Sim Swap v1 - Check');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('simswapv1_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('simswapv1_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Sim Swap v1 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{simswapv1_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv1_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv1_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to the Sim Swap check request once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / CIBA] 3) Sim Swap v1 - Check",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv1_ciba_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/check",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "check"
                  ]
                },
                "description": "CAMARA Sim Swap v1 check. maxAge=240 hours matches this app's SEP_APIs.razor default. The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / CIBA] 4) Sim Swap v1 - Retrieve Date",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/retrieve-date",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v1 retrieve-date. No maxAge field here - matches SEPAPIs.cs RetrieveSimSwapDateEndpoint, which only ever sends phoneNumber (never maxAge) in the body."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / CIBA] 5) Sim Swap v1 - Health",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/health",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "health"
                  ]
                },
                "description": "Health check GET for Sim Swap v1 (no request body, matches SEPAPIs.cs ExecuteHealthGet)."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v1 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv1_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Sim Swap v1 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('simswapv1_auth_state')) {",
                      "    pm.collectionVariables.set('simswapv1_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('simswapv1_auth_nonce')) {",
                      "    pm.collectionVariables.set('simswapv1_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{simswapv1_auth_redirect_uri}}&scope={{simswapv1_auth_scope}}&state={{simswapv1_auth_state}}&nonce={{simswapv1_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{simswapv1_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{simswapv1_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{simswapv1_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{simswapv1_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "function b64urlFromBytes(bytes) {\n    let bin = '';\n    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n    let b64 = btoa(bin);\n    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlFromString(str) {\n    const utf8 = unescape(encodeURIComponent(str));\n    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));\n}\n\nfunction Uint8ArrayFromBinaryString(bin) {\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);\n    return arr;\n}\n\nfunction pemToDerBytes(pem) {\n    const b64 = pem\n        .replace(/-----BEGIN [^-]+-----/, '')\n        .replace(/-----END [^-]+-----/, '')\n        .replace(/\\s+/g, '');\n    const bin = atob(b64);\n    return Uint8ArrayFromBinaryString(bin);\n}\n\nfunction readDerTlv(bytes, offset) {\n    const tag = bytes[offset];\n    let lenByte = bytes[offset + 1];\n    let len, lenBytesUsed;\n    if ((lenByte & 0x80) === 0) {\n        len = lenByte;\n        lenBytesUsed = 1;\n    } else {\n        const numLenBytes = lenByte & 0x7f;\n        len = 0;\n        for (let i = 0; i < numLenBytes; i++) {\n            len = (len * 256) + bytes[offset + 2 + i];\n        }\n        lenBytesUsed = 1 + numLenBytes;\n    }\n    const contentStart = offset + 1 + lenBytesUsed;\n    return { tag, len, contentStart, nextOffset: contentStart + len };\n}\n\nfunction derToBigInt(bytes, start, len) {\n    let hex = '';\n    for (let i = 0; i < len; i++) {\n        const b = bytes[start + i];\n        hex += (b < 16 ? '0' : '') + b.toString(16);\n    }\n    if (hex === '') return BigInt(0);\n    return BigInt('0x' + hex);\n}\n\nfunction extractRsaPrivateKeyParams(pem) {\n    const bytes = pemToDerBytes(pem);\n    let tlv = readDerTlv(bytes, 0);\n    let cursor = tlv.contentStart;\n    let versionPeek = readDerTlv(bytes, cursor);\n    let second = readDerTlv(bytes, versionPeek.nextOffset);\n    let rsaKeyStart;\n    if (second.tag === 0x02) {\n        rsaKeyStart = cursor;\n    } else {\n        let versionTlv = readDerTlv(bytes, cursor);\n        cursor = versionTlv.nextOffset;\n        let algTlv = readDerTlv(bytes, cursor);\n        cursor = algTlv.nextOffset;\n        let octetTlv = readDerTlv(bytes, cursor);\n        let inner = readDerTlv(bytes, octetTlv.contentStart);\n        rsaKeyStart = inner.contentStart;\n    }\n    let p = rsaKeyStart;\n    let versionField = readDerTlv(bytes, p);\n    p = versionField.nextOffset;\n    let nField = readDerTlv(bytes, p);\n    const n = derToBigInt(bytes, nField.contentStart, nField.len);\n    p = nField.nextOffset;\n    let eField = readDerTlv(bytes, p);\n    p = eField.nextOffset;\n    let dField = readDerTlv(bytes, p);\n    const d = derToBigInt(bytes, dField.contentStart, dField.len);\n    const modulusByteLength = Math.ceil(n.toString(2).length / 8);\n    return { n, d, modulusByteLength };\n}\n\nfunction modPow(base, exp, mod) {\n    let result = BigInt(1);\n    base = base % mod;\n    while (exp > BigInt(0)) {\n        if (exp % BigInt(2) === BigInt(1)) {\n            result = (result * base) % mod;\n        }\n        exp = exp / BigInt(2);\n        base = (base * base) % mod;\n    }\n    return result;\n}\n\nconst SHA256_DIGEST_INFO_PREFIX_HEX =\n    '3031300d060960864801650304020105000420';\n\nfunction hexToBytes(hex) {\n    const arr = new Uint8Array(hex.length / 2);\n    for (let i = 0; i < arr.length; i++) {\n        arr[i] = parseInt(hex.substr(i * 2, 2), 16);\n    }\n    return arr;\n}\n\nfunction bytesToHex(bytes) {\n    let hex = '';\n    for (let i = 0; i < bytes.length; i++) {\n        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);\n    }\n    return hex;\n}\n\nfunction emsaPkcs1v15Encode(digestHex, k) {\n    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;\n    const digestInfo = hexToBytes(digestInfoHex);\n    const tLen = digestInfo.length;\n    if (k < tLen + 11) {\n        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');\n    }\n    const psLen = k - tLen - 3;\n    const em = new Uint8Array(k);\n    em[0] = 0x00;\n    em[1] = 0x01;\n    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;\n    em[2 + psLen] = 0x00;\n    em.set(digestInfo, 3 + psLen);\n    return em;\n}\n\nfunction bytesToBigInt(bytes) {\n    return BigInt('0x' + (bytesToHex(bytes) || '0'));\n}\n\nfunction bigIntToBytes(bi, length) {\n    let hex = bi.toString(16);\n    if (hex.length % 2 !== 0) hex = '0' + hex;\n    let bytes = hexToBytes(hex);\n    if (bytes.length < length) {\n        const padded = new Uint8Array(length);\n        padded.set(bytes, length - bytes.length);\n        bytes = padded;\n    }\n    return bytes;\n}\n\nfunction rs256Sign(signingInput, privateKeyPem) {\n    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);\n    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);\n    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);\n    const m = bytesToBigInt(em);\n    const s = modPow(m, d, n);\n    const sigBytes = bigIntToBytes(s, modulusByteLength);\n    return b64urlFromBytes(sigBytes);\n}\n\nfunction buildSignedJwt(headerObj, payloadObj, privateKeyPem) {\n    const headerB64 = b64urlFromString(JSON.stringify(headerObj));\n    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));\n    const signingInput = headerB64 + '.' + payloadB64;\n    const sig = rs256Sign(signingInput, privateKeyPem);\n    return signingInput + '.' + sig;\n}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv1_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv1_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('simswapv1_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv1_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{simswapv1_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{simswapv1_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv1_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv1_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / Authorization Code] 3) Sim Swap v1 - Check",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv1_auth_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/check",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "check"
                  ]
                },
                "description": "CAMARA Sim Swap v1 check. maxAge=240 hours matches this app's SEP_APIs.razor default. The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / Authorization Code] 4) Sim Swap v1 - Retrieve Date",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/retrieve-date",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v1 retrieve-date. No maxAge field here - matches SEPAPIs.cs RetrieveSimSwapDateEndpoint, which only ever sends phoneNumber (never maxAge) in the body."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v1 / Authorization Code] 5) Sim Swap v1 - Health",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv1_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v1/health",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "sim-swap",
                    "v1",
                    "health"
                  ]
                },
                "description": "Health check GET for Sim Swap v1 (no request body, matches SEPAPIs.cs ExecuteHealthGet)."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v1 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv1_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Sim Swap v1. NOTE: only one of Sim Swap v1 / Sim Swap v2 can be ordered onto a single Application - if your shared client_id/private_key were provisioned for Sim Swap v2 instead, this folder will fail with a 401 UNAUTHENTICATED/InvalidClaim error."
    },
    {
      "name": "Sim Swap v2",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Sim Swap v2 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv2_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv2_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('simswapv2_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('simswapv2_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('simswapv2_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv2_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{simswapv2_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{simswapv2_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv2_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv2_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / JWT Bearer] Sim Swap v2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv2_jwt_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/check",
                  "host": [
                    "{{base_url}}/sim-swap/v2/check"
                  ]
                },
                "description": "CAMARA Sim Swap v2 check. maxAge in hours matches this app's SEP_APIs.razor default (240). The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / JWT Bearer] Sim Swap v2 - Retrieve Date",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/retrieve-date",
                  "host": [
                    "{{base_url}}/sim-swap/v2/retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v2 retrieve-date."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/health",
                  "host": [
                    "{{base_url}}/sim-swap/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v2 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv2_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Sim Swap v2 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv2_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv2_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('simswapv2_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('simswapv2_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('simswapv2_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('simswapv2_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{simswapv2_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{simswapv2_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv2_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv2_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{simswapv2_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv2_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv2_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('simswapv2_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('simswapv2_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv2_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Sim Swap v2 / CIBA] Sim Swap v2 - Check');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('simswapv2_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('simswapv2_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Sim Swap v2 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{simswapv2_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv2_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv2_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Sim Swap v2 - Check' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / CIBA] Sim Swap v2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv2_ciba_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/check",
                  "host": [
                    "{{base_url}}/sim-swap/v2/check"
                  ]
                },
                "description": "CAMARA Sim Swap v2 check. maxAge in hours matches this app's SEP_APIs.razor default (240). The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / CIBA] Sim Swap v2 - Retrieve Date",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/retrieve-date",
                  "host": [
                    "{{base_url}}/sim-swap/v2/retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v2 retrieve-date."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/health",
                  "host": [
                    "{{base_url}}/sim-swap/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v2 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv2_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Sim Swap v2 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('simswapv2_auth_state')) {",
                      "    pm.collectionVariables.set('simswapv2_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('simswapv2_auth_nonce')) {",
                      "    pm.collectionVariables.set('simswapv2_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{simswapv2_auth_redirect_uri}}&scope={{simswapv2_auth_scope}}&state={{simswapv2_auth_state}}&nonce={{simswapv2_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{simswapv2_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{simswapv2_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{simswapv2_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{simswapv2_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('simswapv2_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('simswapv2_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('simswapv2_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('simswapv2_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{simswapv2_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{simswapv2_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{simswapv2_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{simswapv2_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / Authorization Code] Sim Swap v2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{simswapv2_auth_max_age}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/check",
                  "host": [
                    "{{base_url}}/sim-swap/v2/check"
                  ]
                },
                "description": "CAMARA Sim Swap v2 check. maxAge in hours matches this app's SEP_APIs.razor default (240). The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / Authorization Code] Sim Swap v2 - Retrieve Date",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/retrieve-date",
                  "host": [
                    "{{base_url}}/sim-swap/v2/retrieve-date"
                  ]
                },
                "description": "CAMARA Sim Swap v2 retrieve-date."
              },
              "response": []
            },
            {
              "name": "[Sim Swap v2 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{simswapv2_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/sim-swap/v2/health",
                  "host": [
                    "{{base_url}}/sim-swap/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Sim Swap v2 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"simswapv2_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Sim Swap v2. NOTE: only one of Sim Swap v2 / Sim Swap v1 can be ordered onto a single Application - if your shared client_id/private_key were provisioned for Sim Swap v1 instead, this folder will fail with a 401 UNAUTHENTICATED/InvalidClaim error."
    },
    {
      "name": "KYC Match v0.2",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[KYC Match v0.2 / JWT Bearer] 1. Get Access Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "function checkPrivateKeyConfigured(pk) {",
                      "    if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "        throw new Error('private_key collection variable is still the placeholder - paste your real PKCS8 PEM RSA private key before running requests.');",
                      "    }",
                      "}",
                      "",
                      "function buildClientAssertionJwt() {",
                      "    const clientId = pm.collectionVariables.get('client_id');",
                      "    const privateKey = pm.collectionVariables.get('private_key');",
                      "    const kid = pm.collectionVariables.get('kid');",
                      "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "",
                      "    if (!clientId) {",
                      "        throw new Error('client_id collection variable is empty - set it before running requests.');",
                      "    }",
                      "    checkPrivateKeyConfigured(privateKey);",
                      "",
                      "    const header = { alg: 'RS256', typ: 'JWT' };",
                      "    if (kid) {",
                      "        header.kid = kid;",
                      "    }",
                      "",
                      "    const nowSeconds = Math.floor(Date.now() / 1000);",
                      "    const payload = {",
                      "        iss: clientId,",
                      "        sub: clientId,",
                      "        aud: tokenEndpoint,",
                      "        iat: nowSeconds,",
                      "        exp: nowSeconds + 300,",
                      "        jti: pm.variables.replaceIn('{{$guid}}')",
                      "    };",
                      "",
                      "    const jwt = buildSignedJwt(header, payload, privateKey);",
                      "    pm.collectionVariables.set('kycv02_jwt_client_assertion', jwt);",
                      "    return jwt;",
                      "}",
                      "",
                      "function buildSubscriberAssertionJwt() {",
                      "    const clientId = pm.collectionVariables.get('client_id');",
                      "    const privateKey = pm.collectionVariables.get('private_key');",
                      "    const kid = pm.collectionVariables.get('kid');",
                      "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "    const scope = pm.collectionVariables.get('kycv02_jwt_scope');",
                      "",
                      "    checkPrivateKeyConfigured(privateKey);",
                      "    if (!clientId) {",
                      "        throw new Error('client_id collection variable is empty - set it before running requests.');",
                      "    }",
                      "",
                      "    const header = { alg: 'RS256', typ: 'JWT' };",
                      "    if (kid) {",
                      "        header.kid = kid;",
                      "    }",
                      "",
                      "    const nowSeconds = Math.floor(Date.now() / 1000);",
                      "    const payload = {",
                      "        iss: clientId,",
                      "        sub: ('tel:' + pm.collectionVariables.get('kycv02_jwt_phone_number')),",
                      "        aud: tokenEndpoint,",
                      "        iat: nowSeconds,",
                      "        exp: nowSeconds + 300,",
                      "        jti: pm.variables.replaceIn('{{$guid}}'),",
                      "        scope: scope",
                      "    };",
                      "",
                      "    const jwt = buildSignedJwt(header, payload, privateKey);",
                      "    pm.collectionVariables.set('kycv02_jwt_assertion', jwt);",
                      "    return jwt;",
                      "}",
                      "",
                      "buildClientAssertionJwt();",
                      "buildSubscriberAssertionJwt();"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const jsonData = pm.response.json();",
                      "if (jsonData.access_token) {",
                      "    pm.collectionVariables.set('kycv02_jwt_access_token', jsonData.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', pm.response.text());",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(jsonData.access_token).to.be.a('string').and.not.empty;",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{kycv02_jwt_assertion}}",
                      "type": "text",
                      "description": "Signed subscriber assertion JWT built by the pre-request script (iss=client_id, sub=tel:{{kycv02_jwt_phone_number}})."
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv02_jwt_client_assertion}}",
                      "type": "text",
                      "description": "Signed PRIVATE_KEY_JWT client authentication assertion built by the pre-request script."
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber assertion JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access_token, authenticating the client with PRIVATE_KEY_JWT (client_assertion), never client_secret."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / JWT Bearer] POST KYC Match v0.2",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// No signing needed here - just reuses {{kycv02_jwt_access_token}} captured by the token request(s) above."
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_jwt_access_token}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv02_jwt_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/match"
                  ]
                },
                "description": "Calls the KYC Match v0.2 resource (kyc-match/v0.2/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / JWT Bearer] GET KYC Match Health v0.2",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_jwt_access_token}}"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/health"
                  ]
                },
                "description": "Health check for the KYC Match v0.2 service. Run after obtaining access_token from any of the token requests in this collection."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.2 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv02_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[KYC Match v0.2 / CIBA] 1. BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "function checkPrivateKeyConfigured(pk) {",
                      "    if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "        throw new Error('private_key collection variable is still the placeholder - paste your real PKCS8 PEM RSA private key before running requests.');",
                      "    }",
                      "}",
                      "",
                      "function buildClientAssertionJwt() {",
                      "    const clientId = pm.collectionVariables.get('client_id');",
                      "    const privateKey = pm.collectionVariables.get('private_key');",
                      "    const kid = pm.collectionVariables.get('kid');",
                      "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "",
                      "    if (!clientId) {",
                      "        throw new Error('client_id collection variable is empty - set it before running requests.');",
                      "    }",
                      "    checkPrivateKeyConfigured(privateKey);",
                      "",
                      "    const header = { alg: 'RS256', typ: 'JWT' };",
                      "    if (kid) {",
                      "        header.kid = kid;",
                      "    }",
                      "",
                      "    const nowSeconds = Math.floor(Date.now() / 1000);",
                      "    const payload = {",
                      "        iss: clientId,",
                      "        sub: clientId,",
                      "        aud: tokenEndpoint,",
                      "        iat: nowSeconds,",
                      "        exp: nowSeconds + 300,",
                      "        jti: pm.variables.replaceIn('{{$guid}}')",
                      "    };",
                      "",
                      "    const jwt = buildSignedJwt(header, payload, privateKey);",
                      "    pm.collectionVariables.set('kycv02_ciba_client_assertion', jwt);",
                      "    return jwt;",
                      "}",
                      "",
                      "buildClientAssertionJwt();"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const jsonData = pm.response.json();",
                      "if (jsonData.auth_req_id) {",
                      "    pm.collectionVariables.set('kycv02_ciba_auth_req_id', jsonData.auth_req_id);",
                      "    pm.collectionVariables.set('kycv02_ciba_ciba_interval', String(jsonData.interval || 5));",
                      "    pm.collectionVariables.set('kycv02_ciba_ciba_expires_in', String(jsonData.expires_in || 300));",
                      "    pm.collectionVariables.set('kycv02_ciba_ciba_poll_count', '0');",
                      "    console.log('auth_req_id captured:', jsonData.auth_req_id);",
                      "} else {",
                      "    console.error('No auth_req_id in bc-authorize response:', pm.response.text());",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(jsonData.auth_req_id).to.be.a('string').and.not.empty;",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{kycv02_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{kycv02_ciba_phone_number}}",
                      "type": "text",
                      "description": "CIBA login_hint format tel:<E.164 number>, built from the phone_number variable."
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv02_ciba_client_assertion}}",
                      "type": "text",
                      "description": "Signed PRIVATE_KEY_JWT client authentication assertion built by the pre-request script."
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow. Captures auth_req_id, interval, and expires_in for polling in the next request."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / CIBA] 2. Poll Token (CIBA)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "function checkPrivateKeyConfigured(pk) {",
                      "    if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "        throw new Error('private_key collection variable is still the placeholder - paste your real PKCS8 PEM RSA private key before running requests.');",
                      "    }",
                      "}",
                      "",
                      "function buildClientAssertionJwt() {",
                      "    const clientId = pm.collectionVariables.get('client_id');",
                      "    const privateKey = pm.collectionVariables.get('private_key');",
                      "    const kid = pm.collectionVariables.get('kid');",
                      "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "",
                      "    if (!clientId) {",
                      "        throw new Error('client_id collection variable is empty - set it before running requests.');",
                      "    }",
                      "    checkPrivateKeyConfigured(privateKey);",
                      "",
                      "    const header = { alg: 'RS256', typ: 'JWT' };",
                      "    if (kid) {",
                      "        header.kid = kid;",
                      "    }",
                      "",
                      "    const nowSeconds = Math.floor(Date.now() / 1000);",
                      "    const payload = {",
                      "        iss: clientId,",
                      "        sub: clientId,",
                      "        aud: tokenEndpoint,",
                      "        iat: nowSeconds,",
                      "        exp: nowSeconds + 300,",
                      "        jti: pm.variables.replaceIn('{{$guid}}')",
                      "    };",
                      "",
                      "    const jwt = buildSignedJwt(header, payload, privateKey);",
                      "    pm.collectionVariables.set('kycv02_ciba_client_assertion', jwt);",
                      "    return jwt;",
                      "}",
                      "",
                      "buildClientAssertionJwt();"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const authReqId = pm.collectionVariables.get('kycv02_ciba_auth_req_id');",
                      "const interval = parseInt(pm.collectionVariables.get('kycv02_ciba_ciba_interval') || '5', 10);",
                      "let pollCount = parseInt(pm.collectionVariables.get('kycv02_ciba_ciba_poll_count') || '0', 10);",
                      "const MAX_POLLS = 24;",
                      "",
                      "let jsonData = {};",
                      "try {",
                      "    jsonData = pm.response.json();",
                      "} catch (e) {",
                      "    console.error('Non-JSON token response:', pm.response.text());",
                      "}",
                      "",
                      "if (jsonData.access_token) {",
                      "    pm.collectionVariables.set('kycv02_ciba_access_token', jsonData.access_token);",
                      "    console.log('access_token captured after', pollCount, 'poll(s).');",
                      "    pm.test('CIBA token obtained', function () {",
                      "        pm.expect(jsonData.access_token).to.be.a('string').and.not.empty;",
                      "    });",
                      "    postman.setNextRequest('3. GET KYC Match Health v0.2 -> then run POST KYC Match v0.2 manually');",
                      "} else if (jsonData.error === 'authorization_pending' || jsonData.error === 'slow_down') {",
                      "    pollCount += 1;",
                      "    pm.collectionVariables.set('kycv02_ciba_ciba_poll_count', String(pollCount));",
                      "    if (pollCount >= MAX_POLLS) {",
                      "        console.error('CIBA polling gave up after', MAX_POLLS, 'attempts. Last response:', pm.response.text());",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        const waitMs = (jsonData.error === 'slow_down' ? interval + 5 : interval) * 1000;",
                      "        console.log('CIBA pending (' + jsonData.error + '), retrying in', waitMs, 'ms. Attempt', pollCount, 'of', MAX_POLLS);",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[KYC Match v0.2 / CIBA] 2. Poll Token (CIBA)');",
                      "        }, waitMs);",
                      "        return;",
                      "    }",
                      "} else {",
                      "    console.error('CIBA token request failed:', pm.response.text());",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{kycv02_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv02_ciba_client_assertion}}",
                      "type": "text",
                      "description": "Re-signed for every poll attempt (fresh iat/exp/jti) by the pre-request script."
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA flow. Auto-retries on authorization_pending/slow_down (honoring the server's interval, capped at 24 attempts) via postman.setNextRequest + setTimeout, and auto-advances to the KYC Match request once access_token is captured."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / CIBA] POST KYC Match v0.2",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// No signing needed here - just reuses {{kycv02_ciba_access_token}} captured by the token request(s) above."
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_ciba_access_token}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv02_ciba_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/match"
                  ]
                },
                "description": "Calls the KYC Match v0.2 resource (kyc-match/v0.2/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / CIBA] GET KYC Match Health v0.2",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_ciba_access_token}}"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/health"
                  ]
                },
                "description": "Health check for the KYC Match v0.2 service. Run after obtaining access_token from any of the token requests in this collection."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.2 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv02_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[KYC Match v0.2 / Authorization Code] 1. Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('kycv02_auth_state')) {",
                      "    pm.collectionVariables.set('kycv02_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('kycv02_auth_nonce')) {",
                      "    pm.collectionVariables.set('kycv02_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{kycv02_auth_redirect_uri}}&scope={{kycv02_auth_scope}}&state={{kycv02_auth_state}}&nonce={{kycv02_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycv02_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycv02_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{kycv02_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{kycv02_auth_nonce}}"
                    }
                  ]
                },
                "description": "MANUAL STEP REQUIRED: Authorization Code is an interactive browser flow and cannot be completed inside Postman. Click 'Code' / copy the generated request URL (Postman's preview, or the Send button then check the browser-blocked redirect), open it in a real browser, log in and consent, then copy the `code` query parameter from the redirect back to redirect_uri and paste it into the `auth_code` collection variable. Then run request 2."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / Authorization Code] 2. Exchange auth_code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "function checkPrivateKeyConfigured(pk) {",
                      "    if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "        throw new Error('private_key collection variable is still the placeholder - paste your real PKCS8 PEM RSA private key before running requests.');",
                      "    }",
                      "}",
                      "",
                      "function buildClientAssertionJwt() {",
                      "    const clientId = pm.collectionVariables.get('client_id');",
                      "    const privateKey = pm.collectionVariables.get('private_key');",
                      "    const kid = pm.collectionVariables.get('kid');",
                      "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "",
                      "    if (!clientId) {",
                      "        throw new Error('client_id collection variable is empty - set it before running requests.');",
                      "    }",
                      "    checkPrivateKeyConfigured(privateKey);",
                      "",
                      "    const header = { alg: 'RS256', typ: 'JWT' };",
                      "    if (kid) {",
                      "        header.kid = kid;",
                      "    }",
                      "",
                      "    const nowSeconds = Math.floor(Date.now() / 1000);",
                      "    const payload = {",
                      "        iss: clientId,",
                      "        sub: clientId,",
                      "        aud: tokenEndpoint,",
                      "        iat: nowSeconds,",
                      "        exp: nowSeconds + 300,",
                      "        jti: pm.variables.replaceIn('{{$guid}}')",
                      "    };",
                      "",
                      "    const jwt = buildSignedJwt(header, payload, privateKey);",
                      "    pm.collectionVariables.set('kycv02_auth_client_assertion', jwt);",
                      "    return jwt;",
                      "}",
                      "",
                      "buildClientAssertionJwt();"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const jsonData = pm.response.json();",
                      "if (jsonData.access_token) {",
                      "    pm.collectionVariables.set('kycv02_auth_access_token', jsonData.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', pm.response.text());",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(jsonData.access_token).to.be.a('string').and.not.empty;",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{kycv02_auth_auth_code}}",
                      "type": "text",
                      "description": "Paste the code you received from the browser redirect after completing request 1."
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycv02_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv02_auth_client_assertion}}",
                      "type": "text",
                      "description": "Signed PRIVATE_KEY_JWT client authentication assertion built by the pre-request script."
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code for an access_token, authenticating the client with PRIVATE_KEY_JWT (client_assertion), never client_secret."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / Authorization Code] POST KYC Match v0.2",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// No signing needed here - just reuses {{kycv02_auth_access_token}} captured by the token request(s) above."
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_auth_access_token}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv02_auth_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/match"
                  ]
                },
                "description": "Calls the KYC Match v0.2 resource (kyc-match/v0.2/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.2 / Authorization Code] GET KYC Match Health v0.2",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv02_auth_access_token}}"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "description": "Unique correlation ID for tracing this request through the API gateway/backend."
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.2/health"
                  ]
                },
                "description": "Health check for the KYC Match v0.2 service. Run after obtaining access_token from any of the token requests in this collection."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.2 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv02_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for KYC Match v0.2. NOTE: only one of KYC Match v0.2 / KYC Match v0.3 can be ordered onto a single Application - if your shared client_id/private_key were provisioned for KYC Match v0.3 instead, this folder will fail with a 401 UNAUTHENTICATED/InvalidClaim error."
    },
    {
      "name": "KYC Match v0.3",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[KYC Match v0.3 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycv03_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycv03_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('kycv03_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('kycv03_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('kycv03_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycv03_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{kycv03_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycv03_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycv03_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv03_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / JWT Bearer] POST KYC Match v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv03_jwt_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/match"
                  ]
                },
                "description": "Calls the KYC Match v0.3 resource (kyc-match/v0.3/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.3 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv03_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[KYC Match v0.3 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycv03_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycv03_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('kycv03_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('kycv03_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('kycv03_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('kycv03_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{kycv03_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{kycv03_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycv03_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv03_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{kycv03_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycv03_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycv03_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('kycv03_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('kycv03_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycv03_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[KYC Match v0.3 / CIBA] POST KYC Match v0.3');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('kycv03_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('kycv03_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[KYC Match v0.3 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{kycv03_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycv03_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv03_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'POST KYC Match v0.3' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / CIBA] POST KYC Match v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv03_ciba_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/match"
                  ]
                },
                "description": "Calls the KYC Match v0.3 resource (kyc-match/v0.3/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.3 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv03_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[KYC Match v0.3 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('kycv03_auth_state')) {",
                      "    pm.collectionVariables.set('kycv03_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('kycv03_auth_nonce')) {",
                      "    pm.collectionVariables.set('kycv03_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{kycv03_auth_redirect_uri}}&scope={{kycv03_auth_scope}}&state={{kycv03_auth_state}}&nonce={{kycv03_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycv03_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycv03_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{kycv03_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{kycv03_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycv03_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycv03_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('kycv03_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycv03_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{kycv03_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycv03_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycv03_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycv03_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / Authorization Code] POST KYC Match v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{kycv03_auth_phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/match",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/match"
                  ]
                },
                "description": "Calls the KYC Match v0.3 resource (kyc-match/v0.3/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[KYC Match v0.3 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycv03_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-match/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-match/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Match v0.3 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycv03_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for KYC Match v0.3. NOTE: only one of KYC Match v0.3 / KYC Match v0.2 can be ordered onto a single Application - if your shared client_id/private_key were provisioned for KYC Match v0.2 instead, this folder will fail with a 401 UNAUTHENTICATED/InvalidClaim error."
    },
    {
      "name": "KYC Age Verification v0.2",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[KYC Age Verification v0.2 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycage_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycage_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('kycage_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('kycage_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('kycage_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycage_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{kycage_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycage_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycage_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycage_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / JWT Bearer] POST KYC Age Verification v0.2",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ageThreshold\": {{kycage_jwt_age_threshold}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/verify",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/verify"
                  ]
                },
                "description": "Calls the KYC Age Verification v0.2 resource with ageThreshold from the age_threshold variable. Field name copied verbatim from CheckKycAgeVerificationV02 in SEPAPIs.cs (which validates 0-125)."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Age Verification v0.2 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycage_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[KYC Age Verification v0.2 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycage_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycage_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('kycage_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('kycage_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('kycage_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('kycage_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{kycage_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{kycage_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycage_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycage_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{kycage_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycage_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycage_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('kycage_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('kycage_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycage_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[KYC Age Verification v0.2 / CIBA] POST KYC Age Verification v0.2');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('kycage_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('kycage_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[KYC Age Verification v0.2 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{kycage_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycage_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycage_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'POST KYC Age Verification v0.2' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / CIBA] POST KYC Age Verification v0.2",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ageThreshold\": {{kycage_ciba_age_threshold}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/verify",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/verify"
                  ]
                },
                "description": "Calls the KYC Age Verification v0.2 resource with ageThreshold from the age_threshold variable. Field name copied verbatim from CheckKycAgeVerificationV02 in SEPAPIs.cs (which validates 0-125)."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Age Verification v0.2 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycage_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[KYC Age Verification v0.2 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('kycage_auth_state')) {",
                      "    pm.collectionVariables.set('kycage_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('kycage_auth_nonce')) {",
                      "    pm.collectionVariables.set('kycage_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{kycage_auth_redirect_uri}}&scope={{kycage_auth_scope}}&state={{kycage_auth_state}}&nonce={{kycage_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycage_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycage_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{kycage_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{kycage_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycage_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycage_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('kycage_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycage_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{kycage_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycage_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycage_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycage_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / Authorization Code] POST KYC Age Verification v0.2",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ageThreshold\": {{kycage_auth_age_threshold}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/verify",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/verify"
                  ]
                },
                "description": "Calls the KYC Age Verification v0.2 resource with ageThreshold from the age_threshold variable. Field name copied verbatim from CheckKycAgeVerificationV02 in SEPAPIs.cs (which validates 0-125)."
              },
              "response": []
            },
            {
              "name": "[KYC Age Verification v0.2 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycage_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-age-verification/v0.2/health",
                  "host": [
                    "{{base_url}}/kyc-age-verification/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Age Verification v0.2 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycage_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for KYC Age Verification v0.2."
    },
    {
      "name": "KYC Fill-in v0.3",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[KYC Fill-in v0.3 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycfillin_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycfillin_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('kycfillin_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('kycfillin_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('kycfillin_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycfillin_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{kycfillin_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycfillin_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycfillin_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycfillin_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / JWT Bearer] POST KYC Fill-in v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/fill-in",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/fill-in"
                  ]
                },
                "description": "Calls the KYC Fill-in v0.3 resource with an EMPTY body - per this app's SEPAPIs.cs SetAllKycFillInV03, the subscriber's phone number is conveyed entirely via the access token (sub claim), not a request body field, so there is nothing else to fill in here."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Fill-in v0.3 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycfillin_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[KYC Fill-in v0.3 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycfillin_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycfillin_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('kycfillin_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('kycfillin_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('kycfillin_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('kycfillin_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{kycfillin_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{kycfillin_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycfillin_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycfillin_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{kycfillin_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycfillin_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycfillin_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('kycfillin_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('kycfillin_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycfillin_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[KYC Fill-in v0.3 / CIBA] POST KYC Fill-in v0.3');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('kycfillin_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('kycfillin_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[KYC Fill-in v0.3 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{kycfillin_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycfillin_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycfillin_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'POST KYC Fill-in v0.3' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / CIBA] POST KYC Fill-in v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/fill-in",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/fill-in"
                  ]
                },
                "description": "Calls the KYC Fill-in v0.3 resource with an EMPTY body - per this app's SEPAPIs.cs SetAllKycFillInV03, the subscriber's phone number is conveyed entirely via the access token (sub claim), not a request body field, so there is nothing else to fill in here."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Fill-in v0.3 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycfillin_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[KYC Fill-in v0.3 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('kycfillin_auth_state')) {",
                      "    pm.collectionVariables.set('kycfillin_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('kycfillin_auth_nonce')) {",
                      "    pm.collectionVariables.set('kycfillin_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{kycfillin_auth_redirect_uri}}&scope={{kycfillin_auth_scope}}&state={{kycfillin_auth_state}}&nonce={{kycfillin_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycfillin_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{kycfillin_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{kycfillin_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{kycfillin_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('kycfillin_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('kycfillin_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('kycfillin_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('kycfillin_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{kycfillin_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{kycfillin_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{kycfillin_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{kycfillin_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / Authorization Code] POST KYC Fill-in v0.3",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/fill-in",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/fill-in"
                  ]
                },
                "description": "Calls the KYC Fill-in v0.3 resource with an EMPTY body - per this app's SEPAPIs.cs SetAllKycFillInV03, the subscriber's phone number is conveyed entirely via the access token (sub claim), not a request body field, so there is nothing else to fill in here."
              },
              "response": []
            },
            {
              "name": "[KYC Fill-in v0.3 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{kycfillin_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/kyc-fill-in/v0.3/health",
                  "host": [
                    "{{base_url}}/kyc-fill-in/v0.3/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "KYC Fill-in v0.3 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"kycfillin_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for KYC Fill-in v0.3."
    },
    {
      "name": "Location Verification v2",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Location Verification v2 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locverify_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locverify_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('locverify_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('locverify_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('locverify_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locverify_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{locverify_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{locverify_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locverify_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locverify_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / JWT Bearer] Location Verification v2 - Verify",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"area\": {\n        \"areaType\": \"CIRCLE\",\n        \"center\": {\n            \"latitude\": {{locverify_jwt_latitude}},\n            \"longitude\": {{locverify_jwt_longitude}}\n        },\n        \"radius\": {{locverify_jwt_radius}}\n    }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/verify",
                  "host": [
                    "{{base_url}}/location-verification/v2/verify"
                  ]
                },
                "description": "CAMARA Location Verification v2 - verifies the subscriber's device is within the given circular area. Field shape copied verbatim from VerifyLocationV2 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/health",
                  "host": [
                    "{{base_url}}/location-verification/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Verification v2 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locverify_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Location Verification v2 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locverify_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locverify_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('locverify_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('locverify_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('locverify_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('locverify_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{locverify_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{locverify_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locverify_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locverify_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{locverify_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locverify_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locverify_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('locverify_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('locverify_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locverify_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Location Verification v2 / CIBA] Location Verification v2 - Verify');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('locverify_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('locverify_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Location Verification v2 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{locverify_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locverify_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locverify_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Location Verification v2 - Verify' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / CIBA] Location Verification v2 - Verify",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"area\": {\n        \"areaType\": \"CIRCLE\",\n        \"center\": {\n            \"latitude\": {{locverify_ciba_latitude}},\n            \"longitude\": {{locverify_ciba_longitude}}\n        },\n        \"radius\": {{locverify_ciba_radius}}\n    }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/verify",
                  "host": [
                    "{{base_url}}/location-verification/v2/verify"
                  ]
                },
                "description": "CAMARA Location Verification v2 - verifies the subscriber's device is within the given circular area. Field shape copied verbatim from VerifyLocationV2 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/health",
                  "host": [
                    "{{base_url}}/location-verification/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Verification v2 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locverify_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Location Verification v2 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('locverify_auth_state')) {",
                      "    pm.collectionVariables.set('locverify_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('locverify_auth_nonce')) {",
                      "    pm.collectionVariables.set('locverify_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{locverify_auth_redirect_uri}}&scope={{locverify_auth_scope}}&state={{locverify_auth_state}}&nonce={{locverify_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{locverify_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{locverify_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{locverify_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{locverify_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locverify_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locverify_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('locverify_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locverify_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{locverify_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{locverify_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locverify_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locverify_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / Authorization Code] Location Verification v2 - Verify",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"area\": {\n        \"areaType\": \"CIRCLE\",\n        \"center\": {\n            \"latitude\": {{locverify_auth_latitude}},\n            \"longitude\": {{locverify_auth_longitude}}\n        },\n        \"radius\": {{locverify_auth_radius}}\n    }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/verify",
                  "host": [
                    "{{base_url}}/location-verification/v2/verify"
                  ]
                },
                "description": "CAMARA Location Verification v2 - verifies the subscriber's device is within the given circular area. Field shape copied verbatim from VerifyLocationV2 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Verification v2 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locverify_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-verification/v2/health",
                  "host": [
                    "{{base_url}}/location-verification/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Verification v2 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locverify_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Location Verification v2."
    },
    {
      "name": "Location Retrieval v0.4",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Location Retrieval v0.4 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locretrieve_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locretrieve_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('locretrieve_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('locretrieve_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('locretrieve_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locretrieve_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{locretrieve_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{locretrieve_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locretrieve_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locretrieve_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / JWT Bearer] Location Retrieval v0.4 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{locretrieve_jwt_max_age_seconds}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/retrieve",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/retrieve"
                  ]
                },
                "description": "CAMARA Location Retrieval v0.4 - retrieves the last known location for the subscriber identified by the access token. maxAge is in SECONDS. Field name copied verbatim from RetrieveLocationV04 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/health",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Retrieval v0.4 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locretrieve_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Location Retrieval v0.4 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locretrieve_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locretrieve_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('locretrieve_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('locretrieve_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('locretrieve_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('locretrieve_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{locretrieve_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{locretrieve_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locretrieve_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locretrieve_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{locretrieve_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locretrieve_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locretrieve_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('locretrieve_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('locretrieve_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locretrieve_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Location Retrieval v0.4 / CIBA] Location Retrieval v0.4 - Retrieve');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('locretrieve_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('locretrieve_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Location Retrieval v0.4 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{locretrieve_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locretrieve_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locretrieve_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Location Retrieval v0.4 - Retrieve' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / CIBA] Location Retrieval v0.4 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{locretrieve_ciba_max_age_seconds}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/retrieve",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/retrieve"
                  ]
                },
                "description": "CAMARA Location Retrieval v0.4 - retrieves the last known location for the subscriber identified by the access token. maxAge is in SECONDS. Field name copied verbatim from RetrieveLocationV04 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/health",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Retrieval v0.4 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locretrieve_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Location Retrieval v0.4 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('locretrieve_auth_state')) {",
                      "    pm.collectionVariables.set('locretrieve_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('locretrieve_auth_nonce')) {",
                      "    pm.collectionVariables.set('locretrieve_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{locretrieve_auth_redirect_uri}}&scope={{locretrieve_auth_scope}}&state={{locretrieve_auth_state}}&nonce={{locretrieve_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{locretrieve_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{locretrieve_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{locretrieve_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{locretrieve_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('locretrieve_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('locretrieve_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('locretrieve_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('locretrieve_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{locretrieve_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{locretrieve_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{locretrieve_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{locretrieve_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / Authorization Code] Location Retrieval v0.4 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"maxAge\": {{locretrieve_auth_max_age_seconds}}\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/retrieve",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/retrieve"
                  ]
                },
                "description": "CAMARA Location Retrieval v0.4 - retrieves the last known location for the subscriber identified by the access token. maxAge is in SECONDS. Field name copied verbatim from RetrieveLocationV04 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Location Retrieval v0.4 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{locretrieve_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/location-retrieval/v0.4/health",
                  "host": [
                    "{{base_url}}/location-retrieval/v0.4/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Location Retrieval v0.4 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"locretrieve_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Location Retrieval v0.4."
    },
    {
      "name": "Device Roaming Status v1",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Device Roaming Status v1 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devroam_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devroam_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('devroam_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('devroam_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('devroam_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devroam_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{devroam_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{devroam_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devroam_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devroam_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / JWT Bearer] Device Roaming Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Roaming Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type. Note: this app's SEPAPIs.cs documents that Germany PRODUCTION rejects a direct JWT Bearer access token on this endpoint and requires CIBA (or another OpenID user-consent flow) with an 'openid' scope prefix instead - if you hit that, use the CIBA collection and/or add 'openid' to your scope variable."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/health",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Roaming Status v1 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devroam_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Device Roaming Status v1 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devroam_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devroam_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('devroam_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('devroam_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('devroam_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('devroam_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{devroam_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{devroam_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devroam_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devroam_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{devroam_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devroam_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devroam_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('devroam_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('devroam_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devroam_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Device Roaming Status v1 / CIBA] Device Roaming Status v1 - Retrieve');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('devroam_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('devroam_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Device Roaming Status v1 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{devroam_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devroam_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devroam_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Device Roaming Status v1 - Retrieve' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / CIBA] Device Roaming Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Roaming Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type. Note: this app's SEPAPIs.cs documents that Germany PRODUCTION rejects a direct JWT Bearer access token on this endpoint and requires CIBA (or another OpenID user-consent flow) with an 'openid' scope prefix instead - if you hit that, use the CIBA collection and/or add 'openid' to your scope variable."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/health",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Roaming Status v1 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devroam_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Device Roaming Status v1 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('devroam_auth_state')) {",
                      "    pm.collectionVariables.set('devroam_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('devroam_auth_nonce')) {",
                      "    pm.collectionVariables.set('devroam_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{devroam_auth_redirect_uri}}&scope={{devroam_auth_scope}}&state={{devroam_auth_state}}&nonce={{devroam_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{devroam_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{devroam_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{devroam_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{devroam_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devroam_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devroam_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('devroam_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devroam_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{devroam_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{devroam_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devroam_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devroam_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / Authorization Code] Device Roaming Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Roaming Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type. Note: this app's SEPAPIs.cs documents that Germany PRODUCTION rejects a direct JWT Bearer access token on this endpoint and requires CIBA (or another OpenID user-consent flow) with an 'openid' scope prefix instead - if you hit that, use the CIBA collection and/or add 'openid' to your scope variable."
              },
              "response": []
            },
            {
              "name": "[Device Roaming Status v1 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devroam_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-roaming-status/v1/health",
                  "host": [
                    "{{base_url}}/device-roaming-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Roaming Status v1 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devroam_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Device Roaming Status v1."
    },
    {
      "name": "Device Reachability Status v1",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Device Reachability Status v1 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devreach_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devreach_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('devreach_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('devreach_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('devreach_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devreach_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{devreach_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{devreach_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devreach_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devreach_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / JWT Bearer] Device Reachability Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Reachability Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/health",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Reachability Status v1 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devreach_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Device Reachability Status v1 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devreach_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devreach_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('devreach_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('devreach_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('devreach_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('devreach_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{devreach_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{devreach_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devreach_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devreach_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{devreach_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devreach_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devreach_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('devreach_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('devreach_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devreach_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Device Reachability Status v1 / CIBA] Device Reachability Status v1 - Retrieve');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('devreach_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('devreach_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Device Reachability Status v1 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{devreach_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devreach_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devreach_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Device Reachability Status v1 - Retrieve' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / CIBA] Device Reachability Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Reachability Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/health",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Reachability Status v1 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devreach_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Device Reachability Status v1 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('devreach_auth_state')) {",
                      "    pm.collectionVariables.set('devreach_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('devreach_auth_nonce')) {",
                      "    pm.collectionVariables.set('devreach_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{devreach_auth_redirect_uri}}&scope={{devreach_auth_scope}}&state={{devreach_auth_state}}&nonce={{devreach_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{devreach_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{devreach_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{devreach_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{devreach_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('devreach_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('devreach_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('devreach_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('devreach_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{devreach_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{devreach_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{devreach_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{devreach_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / Authorization Code] Device Reachability Status v1 - Retrieve",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/retrieve",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/retrieve"
                  ]
                },
                "description": "CAMARA Device Reachability Status v1 - the subscriber is identified entirely by the access token (sub claim); no device identifier is needed in the body for this grant type."
              },
              "response": []
            },
            {
              "name": "[Device Reachability Status v1 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{devreach_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/device-reachability-status/v1/health",
                  "host": [
                    "{{base_url}}/device-reachability-status/v1/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Device Reachability Status v1 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"devreach_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Device Reachability Status v1."
    },
    {
      "name": "Number Verification v1 (NV1)",
      "item": [
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Number Verification v1 (NV1) / Authorization Code] 1 - Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Generate fresh state/nonce for this authorize attempt.",
                      "function randomHex(len) {",
                      "    var chars = 'abcdef0123456789';",
                      "    var out = '';",
                      "    for (var i = 0; i < len; i++) out += chars.charAt(Math.floor(Math.random() * chars.length));",
                      "    return out;",
                      "}",
                      "var state = randomHex(24);",
                      "var nonce = randomHex(24);",
                      "pm.collectionVariables.set('nv1_auth_state', state);",
                      "pm.collectionVariables.set('nv1_auth_nonce', nonce);",
                      "",
                      "var base = (pm.collectionVariables.get('base_url') + '/authorize');",
                      "var clientId = pm.collectionVariables.get('client_id');",
                      "var redirectUri = pm.collectionVariables.get('nv1_auth_redirect_uri');",
                      "var scope = 'openid number-verification:verify dpv:FraudPreventionAndDetection';",
                      "",
                      "var url = base",
                      "    + '?response_type=code'",
                      "    + '&client_id=' + encodeURIComponent(clientId)",
                      "    + '&redirect_uri=' + encodeURIComponent(redirectUri)",
                      "    + '&scope=' + encodeURIComponent(scope)",
                      "    + '&state=' + encodeURIComponent(state)",
                      "    + '&nonce=' + encodeURIComponent(nonce);",
                      "",
                      "pm.collectionVariables.set('generated_authorize_url', url);",
                      "console.log('--- Number Verification v1: Authorize URL ---');",
                      "console.log('Open this URL in a REAL MOBILE BROWSER on CELLULAR DATA (not Wi-Fi, not inside Postman):');",
                      "console.log(url);",
                      "console.log('After it redirects to your redirect_uri, copy the \"code\" query parameter value into the auth_code collection variable.');"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{nv1_auth_redirect_uri}}&scope=openid%20number-verification%3Averify%20dpv%3AFraudPreventionAndDetection&state={{nv1_auth_state}}&nonce={{nv1_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{nv1_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "openid number-verification:verify dpv:FraudPreventionAndDetection"
                    },
                    {
                      "key": "state",
                      "value": "{{nv1_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{nv1_auth_nonce}}"
                    }
                  ]
                },
                "description": "This request is FOR REFERENCE / COPY-PASTE ONLY - it is not meant to succeed inside Postman. Number Verification v1 uses a NETWORK-BASED, silent authorize flow: it only works when opened in a real mobile browser on the subscriber's cellular data connection, so the mobile network operator can silently identify the subscriber's phone number and redirect back with an authorization code (there is no login/consent screen). Running this GET from Postman will likely fail, hang, or be rejected because Postman is not a mobile browser on a cellular connection.\n\nSteps:\n1. Send this request once so the pre-request script logs the full authorize URL to the Postman Console (View > Show Postman Console).\n2. Copy that URL from the console (or use the Send button's generated URL) and open it in a real mobile browser on cellular data.\n3. After the silent redirect completes, your browser will land on redirect_uri with a `code` query parameter (and the same `state` you generated).\n4. Copy the `code` value and paste it into this collection's `auth_code` variable, then run request 2."
              }
            },
            {
              "name": "[Number Verification v1 (NV1) / Authorization Code] 2 - Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "// --- Build the PRIVATE_KEY_JWT client_assertion for this token exchange ---",
                      "var privateKey = pm.collectionVariables.get('private_key');",
                      "if (!privateKey || privateKey.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable to your real PKCS8 PEM RSA private key before running this request.');",
                      "}",
                      "",
                      "var clientId = pm.collectionVariables.get('client_id');",
                      "if (!clientId) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "",
                      "var tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "var kid = pm.collectionVariables.get('kid');",
                      "var nowSeconds = Math.floor(Date.now() / 1000);",
                      "",
                      "var header = { alg: 'RS256' };",
                      "if (kid) { header.kid = kid; }",
                      "",
                      "var payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: nowSeconds,",
                      "    exp: nowSeconds + 300,",
                      "    jti: (function () {",
                      "        var chars = 'abcdef0123456789';",
                      "        var out = '';",
                      "        for (var i = 0; i < 32; i++) out += chars.charAt(Math.floor(Math.random() * chars.length));",
                      "        return out;",
                      "    })()",
                      "};",
                      "",
                      "var clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('client_assertion', clientAssertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "pm.test('Token exchange succeeded', function () {",
                      "    pm.response.to.have.status(200);",
                      "});",
                      "",
                      "var json = {};",
                      "try { json = pm.response.json(); } catch (e) {}",
                      "",
                      "if (json && json.access_token) {",
                      "    pm.collectionVariables.set('nv1_auth_access_token', json.access_token);",
                      "    console.log('Captured access_token into collection variable access_token.');",
                      "} else {",
                      "    console.log('No access_token found in response - check the response body for errors.');",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code"
                    },
                    {
                      "key": "code",
                      "value": "{{nv1_auth_auth_code}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{nv1_auth_redirect_uri}}"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{client_assertion}}"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the authorization code you pasted into auth_code for an access token, authenticating with PRIVATE_KEY_JWT (a locally-signed RS256 client_assertion) instead of a client secret. The pre-request script signs the JWT entirely in pure JavaScript (BigInt + CryptoJS.SHA256 + atob/btoa) - no Node crypto module is used, so this works in every Postman sandbox version. The signed access_token is captured into the access_token collection variable by the test script for use in request 3."
              }
            },
            {
              "name": "[Number Verification v1 (NV1) / Authorization Code] 3 - Verify Number",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "pm.test('Verify call succeeded', function () {",
                      "    pm.response.to.have.status(200);",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{nv1_auth_access_token}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{nv1_auth_phone_number}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/number-verification/v1/verify",
                  "host": [
                    "{{base_url}}"
                  ],
                  "path": [
                    "number-verification",
                    "v1",
                    "verify"
                  ]
                },
                "description": "Calls Number Verification v1's verify endpoint with the access token obtained via the network-based authorization_code flow in request 2. The phoneNumber field is included per the CAMARA NV1 spec/SEPAPIs.cs's ExecuteNumberVerificationV1Verify body shape - the API verifies this number against the subscriber identity implicitly established during the silent /authorize redirect (request 1), it is not a lookup by arbitrary number."
              }
            }
          ],
          "description": "Number Verification v1 (NV1) using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"nv1_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Number Verification v1 (NV1)."
    },
    {
      "name": "Number Verification v2.1 (NV2)",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Number Verification v2.1 (NV2) / JWT Bearer] 1) Get Token (JWT Bearer, TS.43 operator token)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "if (!pm.collectionVariables.get('nv21_jwt_operator_token')) {",
                      "    throw new Error('Set the operator_token collection variable (from the TS.43 Digital Credentials flow) before running this request.');",
                      "}",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('nv21_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('nv21_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('nv21_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('nv21_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('nv21_jwt_operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'operatortoken:' + operatorToken2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('nv21_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('nv21_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{nv21_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{nv21_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{nv21_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{nv21_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Number Verification v2.1 (NV2) is fundamentally different from the other JWT Bearer APIs in this app: the subscriber assertion's 'sub' claim is NOT a phone number - it is 'operatortoken:<token>', where <token> is a TS.43 operator token obtained out-of-band via the Android Digital Credentials API and this app's DCQL aggregator (see Aggregator/ in this repo). That operator token cannot be generated inside Postman - paste a real one into the operator_token collection variable first. This is the ONLY grant type this app registers for NumberVerificationV21 (no CIBA/Authorization Code variants are provided, since they would not carry an operator token)."
              },
              "response": []
            },
            {
              "name": "[Number Verification v2.1 (NV2) / JWT Bearer] Number Verification v2 - Verify",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{nv21_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"phoneNumber\": \"{{nv21_jwt_phone_number}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/number-verification/v2/verify",
                  "host": [
                    "{{base_url}}/number-verification/v2/verify"
                  ]
                },
                "description": "Verifies the given phoneNumber matches the subscriber identified by the operator-token-based access token. Field name/shape copied verbatim from VerifyNumberVerificationTs43 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Number Verification v2.1 (NV2) / JWT Bearer] Number Verification v2 - Device Phone Number",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{nv21_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/number-verification/v2/device-phone-number",
                  "host": [
                    "{{base_url}}/number-verification/v2/device-phone-number"
                  ]
                },
                "description": "Returns the phone number associated with the subscriber's device, identified by the operator-token-based access token. Copied verbatim from GetNumberVerificationDevicePhoneNumberTs43 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Number Verification v2.1 (NV2) / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{nv21_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/number-verification/v2/health",
                  "host": [
                    "{{base_url}}/number-verification/v2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Number Verification v2.1 (NV2) using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"nv21_jwt_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Number Verification v2.1 (NV2)."
    },
    {
      "name": "Number Recycling v0.2",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Number Recycling v0.2 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('numrecycle_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('numrecycle_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('numrecycle_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('numrecycle_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('numrecycle_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('numrecycle_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{numrecycle_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{numrecycle_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{numrecycle_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{numrecycle_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / JWT Bearer] Number Recycling v0.2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"specifiedDate\": \"{{numrecycle_jwt_specified_date}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/check",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/check"
                  ]
                },
                "description": "CAMARA Number Recycling v0.2 check - the subscriber is identified via the access token (sub claim); specifiedDate is the only body field, copied verbatim from CheckNumberRecyclingV02 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / JWT Bearer] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/health",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Number Recycling v0.2 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"numrecycle_jwt_\" prefix."
        },
        {
          "name": "CIBA",
          "item": [
            {
              "name": "[Number Recycling v0.2 / CIBA] 1) BC-Authorize (start CIBA flow)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('numrecycle_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('numrecycle_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.auth_req_id) {",
                      "    pm.collectionVariables.set('numrecycle_ciba_auth_req_id', json.auth_req_id);",
                      "    pm.collectionVariables.set('numrecycle_ciba_interval', String(json.interval || 5));",
                      "    pm.collectionVariables.set('numrecycle_ciba_expires_in', String(json.expires_in || 120));",
                      "    pm.collectionVariables.set('numrecycle_ciba_poll_attempts', '0');",
                      "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
                      "} else {",
                      "    console.error('No auth_req_id in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('bc-authorize response has auth_req_id', function () {",
                      "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "scope",
                      "value": "{{numrecycle_ciba_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "login_hint",
                      "value": "tel:{{numrecycle_ciba_phone_number}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{numrecycle_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{numrecycle_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/bc-authorize",
                  "host": [
                    "{{base_url}}/bc-authorize"
                  ]
                },
                "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{numrecycle_ciba_phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / CIBA] 2) Poll Token (CIBA, auto-retries)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('numrecycle_ciba_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('numrecycle_ciba_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
                      "let json = {};",
                      "try { json = pm.response.json(); } catch (e) { json = {}; }",
                      "const attempts = parseInt(pm.collectionVariables.get('numrecycle_ciba_poll_attempts') || '0', 10);",
                      "const intervalSeconds = parseInt(pm.collectionVariables.get('numrecycle_ciba_interval') || '5', 10);",
                      "",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('numrecycle_ciba_access_token', json.access_token);",
                      "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
                      "    postman.setNextRequest('[Number Recycling v0.2 / CIBA] Number Recycling v0.2 - Check');",
                      "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
                      "    const nextAttempts = attempts + 1;",
                      "    pm.collectionVariables.set('numrecycle_ciba_poll_attempts', String(nextAttempts));",
                      "    if (json.error === 'slow_down') {",
                      "        pm.collectionVariables.set('numrecycle_ciba_interval', String(intervalSeconds + 5));",
                      "    }",
                      "    if (nextAttempts >= MAX_ATTEMPTS) {",
                      "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
                      "        postman.setNextRequest(null);",
                      "    } else {",
                      "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
                      "        setTimeout(function () {",
                      "            postman.setNextRequest('[Number Recycling v0.2 / CIBA] 2) Poll Token (CIBA, auto-retries)');",
                      "        }, intervalSeconds * 1000);",
                      "    }",
                      "} else {",
                      "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
                      "    postman.setNextRequest(null);",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:openid:params:grant-type:ciba",
                      "type": "text"
                    },
                    {
                      "key": "auth_req_id",
                      "value": "{{numrecycle_ciba_auth_req_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{numrecycle_ciba_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{numrecycle_ciba_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'Number Recycling v0.2 - Check' once access_token is captured. Run request 1 first."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / CIBA] Number Recycling v0.2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"specifiedDate\": \"{{numrecycle_ciba_specified_date}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/check",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/check"
                  ]
                },
                "description": "CAMARA Number Recycling v0.2 check - the subscriber is identified via the access token (sub claim); specifiedDate is the only body field, copied verbatim from CheckNumberRecyclingV02 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / CIBA] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_ciba_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/health",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Number Recycling v0.2 using the CIBA grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"numrecycle_ciba_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Number Recycling v0.2 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('numrecycle_auth_state')) {",
                      "    pm.collectionVariables.set('numrecycle_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('numrecycle_auth_nonce')) {",
                      "    pm.collectionVariables.set('numrecycle_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{numrecycle_auth_redirect_uri}}&scope={{numrecycle_auth_scope}}&state={{numrecycle_auth_state}}&nonce={{numrecycle_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{numrecycle_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{numrecycle_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{numrecycle_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{numrecycle_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('numrecycle_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('numrecycle_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('numrecycle_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('numrecycle_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{numrecycle_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{numrecycle_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{numrecycle_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{numrecycle_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / Authorization Code] Number Recycling v0.2 - Check",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n    \"specifiedDate\": \"{{numrecycle_auth_specified_date}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/check",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/check"
                  ]
                },
                "description": "CAMARA Number Recycling v0.2 check - the subscriber is identified via the access token (sub claim); specifiedDate is the only body field, copied verbatim from CheckNumberRecyclingV02 in SEPAPIs.cs."
              },
              "response": []
            },
            {
              "name": "[Number Recycling v0.2 / Authorization Code] Health Check",
              "event": [],
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{numrecycle_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "url": {
                  "raw": "{{base_url}}/number-recycling/v0.2/health",
                  "host": [
                    "{{base_url}}/number-recycling/v0.2/health"
                  ]
                },
                "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
              },
              "response": []
            }
          ],
          "description": "Number Recycling v0.2 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"numrecycle_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Number Recycling v0.2."
    },
    {
      "name": "Quality on Demand v1.1",
      "item": [
        {
          "name": "JWT Bearer",
          "item": [
            {
              "name": "[Quality on Demand v1.1 / JWT Bearer] 1) Get Token (JWT Bearer)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('qod_jwt_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('qod_jwt_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "const clientId2 = pm.collectionVariables.get('client_id');",
                      "const privateKey2 = pm.collectionVariables.get('private_key');",
                      "const kid2 = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
                      "const scope2 = pm.collectionVariables.get('qod_jwt_scope');",
                      "const phoneNumber2 = pm.collectionVariables.get('qod_jwt_phone_number');",
                      "const operatorToken2 = pm.collectionVariables.get('operator_token');",
                      "const now2 = Math.floor(Date.now() / 1000);",
                      "const header2 = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid2) { header2.kid = kid2; }",
                      "const payload2 = {",
                      "    iss: clientId2,",
                      "    sub: 'tel:' + phoneNumber2,",
                      "    aud: tokenEndpoint2,",
                      "    iat: now2,",
                      "    exp: now2 + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}'),",
                      "    scope: scope2",
                      "};",
                      "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
                      "pm.collectionVariables.set('qod_jwt_assertion', assertion);"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('qod_jwt_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                      "type": "text"
                    },
                    {
                      "key": "assertion",
                      "value": "{{qod_jwt_assertion}}",
                      "type": "text"
                    },
                    {
                      "key": "scope",
                      "value": "{{qod_jwt_scope}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{qod_jwt_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{qod_jwt_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges a signed subscriber 'assertion' JWT for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Quality on Demand v1.1 / JWT Bearer] Quality on Demand v1.1 - Create Session",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{qod_jwt_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"duration\": {{qod_jwt_duration}},\n  \"applicationServer\": {\n    \"ipv4Address\": \"{{qod_jwt_application_server_ipv4}}\"\n  },\n  \"qosProfile\": \"{{qod_jwt_qos_profile}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/quality-on-demand/v1/sessions",
                  "host": [
                    "{{base_url}}/quality-on-demand/v1/sessions"
                  ]
                },
                "description": "Creates a QoD session WITH a device identifier (phoneNumber) in the body - matches CreateQualityOnDemandSessionV11 in SEPAPIs.cs (used for JWT Bearer / non-network-based flows). duration/qosProfile/applicationServer defaults match this app's own defaults."
              },
              "response": []
            }
          ],
          "description": "Quality on Demand v1.1 using the JWT Bearer grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"qod_jwt_\" prefix."
        },
        {
          "name": "Authorization Code",
          "item": [
            {
              "name": "[Quality on Demand v1.1 / Authorization Code] 1) Build Authorize URL (open in browser)",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('qod_auth_state')) {",
                      "    pm.collectionVariables.set('qod_auth_state', pm.variables.replaceIn('{{$guid}}'));",
                      "}",
                      "if (!pm.collectionVariables.get('qod_auth_nonce')) {",
                      "    pm.collectionVariables.set('qod_auth_nonce', pm.variables.replaceIn('{{$guid}}'));",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
                      "console.log(pm.request.url.toString());"
                    ]
                  }
                }
              ],
              "request": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{qod_auth_redirect_uri}}&scope={{qod_auth_scope}}&state={{qod_auth_state}}&nonce={{qod_auth_nonce}}",
                  "host": [
                    "{{base_url}}/authorize"
                  ],
                  "query": [
                    {
                      "key": "response_type",
                      "value": "code"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{qod_auth_redirect_uri}}"
                    },
                    {
                      "key": "scope",
                      "value": "{{qod_auth_scope}}"
                    },
                    {
                      "key": "state",
                      "value": "{{qod_auth_state}}"
                    },
                    {
                      "key": "nonce",
                      "value": "{{qod_auth_nonce}}"
                    }
                  ]
                },
                "description": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL. Copy it, open it in a real browser (on cellular data, since this is the network-based/no-device-identifier QoD flow), log in and consent, then copy the 'code' query parameter into the auth_code collection variable before running the next request."
              },
              "response": []
            },
            {
              "name": "[Quality on Demand v1.1 / Authorization Code] 2) Exchange Code for Token",
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('client_id')) {",
                      "    throw new Error('Set the client_id collection variable before running this request.');",
                      "}",
                      "const pk = pm.collectionVariables.get('private_key');",
                      "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
                      "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
                      "}",
                      "",
                      "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
                      "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
                      "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
                      "",
                      "function b64urlFromBytes(bytes) {",
                      "    let bin = '';",
                      "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
                      "    let b64 = btoa(bin);",
                      "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
                      "}",
                      "",
                      "function b64urlFromString(str) {",
                      "    // str is a JS string containing only ASCII/UTF-8 JSON text",
                      "    const utf8 = unescape(encodeURIComponent(str));",
                      "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
                      "}",
                      "",
                      "function Uint8ArrayFromBinaryString(bin) {",
                      "    const arr = new Uint8Array(bin.length);",
                      "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
                      "    return arr;",
                      "}",
                      "",
                      "function pemToDerBytes(pem) {",
                      "    const b64 = pem",
                      "        .replace(/-----BEGIN [^-]+-----/, '')",
                      "        .replace(/-----END [^-]+-----/, '')",
                      "        .replace(/\\s+/g, '');",
                      "    const bin = atob(b64);",
                      "    return Uint8ArrayFromBinaryString(bin);",
                      "}",
                      "",
                      "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
                      "function readDerTlv(bytes, offset) {",
                      "    const tag = bytes[offset];",
                      "    let lenByte = bytes[offset + 1];",
                      "    let len, lenBytesUsed;",
                      "    if ((lenByte & 0x80) === 0) {",
                      "        len = lenByte;",
                      "        lenBytesUsed = 1;",
                      "    } else {",
                      "        const numLenBytes = lenByte & 0x7f;",
                      "        len = 0;",
                      "        for (let i = 0; i < numLenBytes; i++) {",
                      "            len = (len * 256) + bytes[offset + 2 + i];",
                      "        }",
                      "        lenBytesUsed = 1 + numLenBytes;",
                      "    }",
                      "    const contentStart = offset + 1 + lenBytesUsed;",
                      "    return { tag, len, contentStart, nextOffset: contentStart + len };",
                      "}",
                      "",
                      "function derToBigInt(bytes, start, len) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < len; i++) {",
                      "        const b = bytes[start + i];",
                      "        hex += (b < 16 ? '0' : '') + b.toString(16);",
                      "    }",
                      "    if (hex === '') return BigInt(0);",
                      "    return BigInt('0x' + hex);",
                      "}",
                      "",
                      "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
                      "function extractRsaPrivateKeyParams(pem) {",
                      "    const bytes = pemToDerBytes(pem);",
                      "    // Top-level SEQUENCE",
                      "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
                      "    let cursor = tlv.contentStart;",
                      "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
                      "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
                      "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
                      "    let versionPeek = readDerTlv(bytes, cursor);",
                      "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
                      "    let rsaKeyStart;",
                      "    if (second.tag === 0x02) {",
                      "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
                      "        rsaKeyStart = cursor;",
                      "    } else {",
                      "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
                      "        // skip version INTEGER",
                      "        let versionTlv = readDerTlv(bytes, cursor);",
                      "        cursor = versionTlv.nextOffset;",
                      "        // skip AlgorithmIdentifier SEQUENCE",
                      "        let algTlv = readDerTlv(bytes, cursor);",
                      "        cursor = algTlv.nextOffset;",
                      "        // OCTET STRING wrapping the PKCS#1 key",
                      "        let octetTlv = readDerTlv(bytes, cursor);",
                      "        // Inside the octet string is the PKCS#1 SEQUENCE",
                      "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
                      "        rsaKeyStart = inner.contentStart;",
                      "    }",
                      "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
                      "    let p = rsaKeyStart;",
                      "    let versionField = readDerTlv(bytes, p);",
                      "    p = versionField.nextOffset;",
                      "    let nField = readDerTlv(bytes, p);",
                      "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
                      "    p = nField.nextOffset;",
                      "    let eField = readDerTlv(bytes, p);",
                      "    p = eField.nextOffset;",
                      "    let dField = readDerTlv(bytes, p);",
                      "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
                      "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
                      "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
                      "    // exclude that padding byte, so derive it from n's actual bit length.",
                      "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
                      "    return { n, d, modulusByteLength };",
                      "}",
                      "",
                      "// Modular exponentiation: base^exp mod m using BigInt",
                      "function modPow(base, exp, mod) {",
                      "    let result = BigInt(1);",
                      "    base = base % mod;",
                      "    while (exp > BigInt(0)) {",
                      "        if (exp % BigInt(2) === BigInt(1)) {",
                      "            result = (result * base) % mod;",
                      "        }",
                      "        exp = exp / BigInt(2);",
                      "        base = (base * base) % mod;",
                      "    }",
                      "    return result;",
                      "}",
                      "",
                      "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
                      "const SHA256_DIGEST_INFO_PREFIX_HEX =",
                      "    '3031300d060960864801650304020105000420';",
                      "",
                      "function hexToBytes(hex) {",
                      "    const arr = new Uint8Array(hex.length / 2);",
                      "    for (let i = 0; i < arr.length; i++) {",
                      "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
                      "    }",
                      "    return arr;",
                      "}",
                      "",
                      "function bytesToHex(bytes) {",
                      "    let hex = '';",
                      "    for (let i = 0; i < bytes.length; i++) {",
                      "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
                      "    }",
                      "    return hex;",
                      "}",
                      "",
                      "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
                      "function emsaPkcs1v15Encode(digestHex, k) {",
                      "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
                      "    const digestInfo = hexToBytes(digestInfoHex);",
                      "    const tLen = digestInfo.length;",
                      "    if (k < tLen + 11) {",
                      "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
                      "    }",
                      "    const psLen = k - tLen - 3;",
                      "    const em = new Uint8Array(k);",
                      "    em[0] = 0x00;",
                      "    em[1] = 0x01;",
                      "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
                      "    em[2 + psLen] = 0x00;",
                      "    em.set(digestInfo, 3 + psLen);",
                      "    return em;",
                      "}",
                      "",
                      "function bytesToBigInt(bytes) {",
                      "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
                      "}",
                      "",
                      "function bigIntToBytes(bi, length) {",
                      "    let hex = bi.toString(16);",
                      "    if (hex.length % 2 !== 0) hex = '0' + hex;",
                      "    let bytes = hexToBytes(hex);",
                      "    if (bytes.length < length) {",
                      "        const padded = new Uint8Array(length);",
                      "        padded.set(bytes, length - bytes.length);",
                      "        bytes = padded;",
                      "    }",
                      "    return bytes;",
                      "}",
                      "",
                      "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
                      "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
                      "function rs256Sign(signingInput, privateKeyPem) {",
                      "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
                      "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
                      "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
                      "    const m = bytesToBigInt(em);",
                      "    const s = modPow(m, d, n);",
                      "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
                      "    return b64urlFromBytes(sigBytes);",
                      "}",
                      "",
                      "// Build and sign a JWT given header/payload objects and a PEM private key.",
                      "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
                      "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
                      "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
                      "    const signingInput = headerB64 + '.' + payloadB64;",
                      "    const sig = rs256Sign(signingInput, privateKeyPem);",
                      "    return signingInput + '.' + sig;",
                      "}",
                      "",
                      "const clientId = pm.collectionVariables.get('client_id');",
                      "const privateKey = pm.collectionVariables.get('private_key');",
                      "const kid = pm.collectionVariables.get('kid');",
                      "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
                      "const now = Math.floor(Date.now() / 1000);",
                      "const header = { alg: 'RS256', typ: 'JWT' };",
                      "if (kid) { header.kid = kid; }",
                      "const payload = {",
                      "    iss: clientId,",
                      "    sub: clientId,",
                      "    aud: tokenEndpoint,",
                      "    iat: now,",
                      "    exp: now + 300,",
                      "    jti: pm.variables.replaceIn('{{$guid}}')",
                      "};",
                      "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
                      "pm.collectionVariables.set('qod_auth_client_assertion', clientAssertion);",
                      "pm.collectionVariables.set('qod_auth_client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
                      "",
                      "if (!pm.collectionVariables.get('qod_auth_auth_code')) {",
                      "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
                      "}"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "const json = pm.response.json();",
                      "if (json.access_token) {",
                      "    pm.collectionVariables.set('qod_auth_access_token', json.access_token);",
                      "    console.log('access_token captured.');",
                      "} else {",
                      "    console.error('No access_token in response:', JSON.stringify(json));",
                      "}",
                      "pm.test('Token response has access_token', function () {",
                      "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/x-www-form-urlencoded",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "urlencoded",
                  "urlencoded": [
                    {
                      "key": "grant_type",
                      "value": "authorization_code",
                      "type": "text"
                    },
                    {
                      "key": "code",
                      "value": "{{qod_auth_auth_code}}",
                      "type": "text"
                    },
                    {
                      "key": "redirect_uri",
                      "value": "{{qod_auth_redirect_uri}}",
                      "type": "text"
                    },
                    {
                      "key": "client_id",
                      "value": "{{client_id}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion_type",
                      "value": "{{qod_auth_client_assertion_type}}",
                      "type": "text"
                    },
                    {
                      "key": "client_assertion",
                      "value": "{{qod_auth_client_assertion}}",
                      "type": "text"
                    }
                  ]
                },
                "url": {
                  "raw": "{{base_url}}/token",
                  "host": [
                    "{{base_url}}/token"
                  ]
                },
                "description": "Exchanges the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
              },
              "response": []
            },
            {
              "name": "[Quality on Demand v1.1 / Authorization Code] Quality on Demand v1.1 - Create Session (network-based)",
              "event": [],
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Authorization",
                    "value": "Bearer {{qod_auth_access_token}}",
                    "type": "text"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/json",
                    "type": "text"
                  },
                  {
                    "key": "x-correlator",
                    "value": "{{$guid}}",
                    "type": "text"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"duration\": {{qod_auth_duration}},\n  \"applicationServer\": {\n    \"ipv4Address\": \"{{qod_auth_application_server_ipv4}}\"\n  },\n  \"qosProfile\": \"{{qod_auth_qos_profile}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{base_url}}/quality-on-demand/v1/sessions",
                  "host": [
                    "{{base_url}}/quality-on-demand/v1/sessions"
                  ]
                },
                "description": "Creates a QoD session WITHOUT a device identifier in the body - matches CreateQualityOnDemandSessionV11NetworkBased in SEPAPIs.cs, used for the Authorization Code / network-based flow where the subscriber is identified purely from the access token (no phoneNumber field needed or expected)."
              },
              "response": []
            }
          ],
          "description": "Quality on Demand v1.1 using the Authorization Code grant. Uses the shared client_id/private_key/kid/base_url variables entered once at the collection level; every other variable in this sub-folder is namespaced with the \"qod_auth_\" prefix."
        }
      ],
      "description": "All grant-type variants built for Quality on Demand v1.1."
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "type": "string",
      "description": "Germany staging by default. Change ONLY this variable to switch environment/country: production Germany = https://api.telekom.com; Austria staging = https://at.stg.api.telekom.com, Austria prod = https://at.api.telekom.com; Poland and Greece staging/prod hosts follow the same at.*/pl.*/gr.* pattern - see this app's /documentation/endpoints page for the full current list. Every request and script in this collection builds the full endpoint path directly from base_url (e.g. {{base_url}}/token) rather than through a separate derived variable, because Postman does not resolve nested variable references (a variable whose value itself contains {{...}}) when read inside pre-request/test scripts via pm.collectionVariables.get() - only base_url itself needs to be correct. "
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Your registered OAuth client_id for the SimSwapV1 credential profile. Never commit a real value here."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "REQUIRED. PKCS8 PEM RSA private key used to sign the private_key_jwt client_assertion (and, for the JWT Bearer variant, the subscriber assertion). Replace the placeholder text between the BEGIN/END markers with your real PKCS8 PEM key. Never commit a real private key."
    },
    {
      "key": "kid",
      "value": "",
      "type": "string",
      "description": "Optional key ID matching a key in your JWKS - leave blank if your JWKS has only one key."
    },
    {
      "key": "simswapv1_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "simswapv1_jwt_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv1_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication - auto-set by the pre-request script too."
    },
    {
      "key": "simswapv1_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the pre-request script on each token request. Do not edit."
    },
    {
      "key": "simswapv1_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script. Do not edit."
    },
    {
      "key": "simswapv1_jwt_scope",
      "value": "sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection - no 'openid' prefix, since JWT_BEARER does not use UsesOpenIdScope in SEPAPIs.cs."
    },
    {
      "key": "simswapv1_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated subscriber assertion JWT (sub=tel:+phoneNumber) by the pre-request script. Do not edit."
    },
    {
      "key": "simswapv1_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "simswapv1_ciba_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv1_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication - auto-set by the pre-request script too."
    },
    {
      "key": "simswapv1_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the pre-request script on each token request. Do not edit."
    },
    {
      "key": "simswapv1_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script. Do not edit."
    },
    {
      "key": "simswapv1_ciba_scope",
      "value": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection - 'openid' is prefixed because SEPAPIs.cs's UsesOpenIdScope returns true for GrantType == CIBA."
    },
    {
      "key": "simswapv1_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request's test script. Do not edit."
    },
    {
      "key": "simswapv1_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Auto-populated polling interval (seconds) from the bc-authorize response. Do not edit."
    },
    {
      "key": "simswapv1_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated auth_req_id validity window (seconds) from the bc-authorize response. Do not edit."
    },
    {
      "key": "simswapv1_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Internal poll counter used by the polling request's test script. Do not edit."
    },
    {
      "key": "simswapv1_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "simswapv1_auth_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv1_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication - auto-set by the pre-request script too."
    },
    {
      "key": "simswapv1_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the pre-request script on each token request. Do not edit."
    },
    {
      "key": "simswapv1_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script. Do not edit."
    },
    {
      "key": "simswapv1_auth_scope",
      "value": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection - 'openid' is prefixed because SEPAPIs.cs's UsesOpenIdScope returns true for GrantType == AUTHORIZATION_CODE."
    },
    {
      "key": "simswapv1_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Must match a redirect URI registered for your OAuth client."
    },
    {
      "key": "simswapv1_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated random state value by the pre-request script (CSRF protection). Do not edit."
    },
    {
      "key": "simswapv1_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated random nonce value by the pre-request script (replay protection). Do not edit."
    },
    {
      "key": "simswapv1_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Paste the 'code' query parameter from the browser redirect here after completing the login/consent step in request 1."
    },
    {
      "key": "simswapv2_jwt_scope",
      "value": "sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Sim Swap v2."
    },
    {
      "key": "simswapv2_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "simswapv2_jwt_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv2_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "simswapv2_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "simswapv2_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "simswapv2_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "simswapv2_ciba_scope",
      "value": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "simswapv2_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "simswapv2_ciba_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv2_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "simswapv2_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "simswapv2_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "simswapv2_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "simswapv2_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "simswapv2_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "simswapv2_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "simswapv2_auth_scope",
      "value": "sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Sim Swap v2."
    },
    {
      "key": "simswapv2_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "simswapv2_auth_max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "key": "simswapv2_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "simswapv2_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "simswapv2_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "simswapv2_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "simswapv2_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "simswapv2_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "simswapv2_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "kycv02_jwt_scope",
      "value": "dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "Scope is the exact string used by CheckKycMatchEndpoint in SEPAPIs.cs: \"dpv:FraudPreventionAndDetection kyc-match:match\". This app only prefixes scopes with \"openid\" when its UsesOpenIdScope setting is active for a given credential profile/environment (see SEPAPIs.cs ResolveScope calls) - that is environment-specific, not fixed for KYC Match, so this collection ships the base scope as-is. If your real OAuth server requires an \"openid\" scope for CIBA or Authorization Code token issuance, prepend it yourself in the `scope` variable (e.g. \"openid dpv:FraudPreventionAndDetection kyc-match:match\")."
    },
    {
      "key": "kycv02_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "kycv02_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by pre-request scripts - the signed RS256 client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv02_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script once a token is obtained."
    },
    {
      "key": "kycv02_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT (iss=client_id, sub=tel:{{phone_number}}). Do not edit manually."
    },
    {
      "key": "kycv02_ciba_scope",
      "value": "openid dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "openid dpv:FraudPreventionAndDetection kyc-match:match - CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed, so it is prefixed here by default (unlike the JWT Bearer variant, where openid is not required). See SEPAPIs.cs UsesOpenIdScope / ResolveScope for how this app itself decides when to prefix openid per grant type."
    },
    {
      "key": "kycv02_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "kycv02_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by pre-request scripts - the signed RS256 client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv02_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script once a token is obtained."
    },
    {
      "key": "kycv02_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the BC-Authorize request's test script."
    },
    {
      "key": "kycv02_ciba_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Auto-populated polling interval (seconds) returned by the BC-Authorize call."
    },
    {
      "key": "kycv02_ciba_ciba_expires_in",
      "value": "300",
      "type": "string",
      "description": "Auto-populated auth_req_id expiry (seconds) returned by the BC-Authorize call."
    },
    {
      "key": "kycv02_ciba_ciba_poll_count",
      "value": "0",
      "type": "string",
      "description": "Auto-incremented poll attempt counter; polling stops after ~24 attempts."
    },
    {
      "key": "kycv02_auth_scope",
      "value": "dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "Scope is the exact string used by CheckKycMatchEndpoint in SEPAPIs.cs: \"dpv:FraudPreventionAndDetection kyc-match:match\". This app only prefixes scopes with \"openid\" when its UsesOpenIdScope setting is active for a given credential profile/environment (see SEPAPIs.cs ResolveScope calls) - that is environment-specific, not fixed for KYC Match, so this collection ships the base scope as-is. If your real OAuth server requires an \"openid\" scope for CIBA or Authorization Code token issuance, prepend it yourself in the `scope` variable (e.g. \"openid dpv:FraudPreventionAndDetection kyc-match:match\")."
    },
    {
      "key": "kycv02_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "kycv02_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by pre-request scripts - the signed RS256 client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv02_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script once a token is obtained."
    },
    {
      "key": "kycv02_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your registered OAuth redirect URI. Change to match your client registration."
    },
    {
      "key": "kycv02_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated random state value (CSRF protection) by request 1's pre-request script."
    },
    {
      "key": "kycv02_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated random nonce value by request 1's pre-request script."
    },
    {
      "key": "kycv02_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "Paste the authorization code you receive from the browser redirect after completing request 1 in this variable, then run request 2."
    },
    {
      "key": "kycv03_jwt_scope",
      "value": "dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "Scope string used by this app for KYC Match v0.3."
    },
    {
      "key": "kycv03_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycv03_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycv03_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv03_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycv03_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv03_ciba_scope",
      "value": "openid dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "openid dpv:FraudPreventionAndDetection kyc-match:match - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "kycv03_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycv03_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycv03_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv03_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycv03_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "kycv03_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "kycv03_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "kycv03_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "kycv03_auth_scope",
      "value": "dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "Scope string used by this app for KYC Match v0.3."
    },
    {
      "key": "kycv03_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycv03_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycv03_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycv03_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycv03_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "kycv03_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycv03_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycv03_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "kycage_jwt_scope",
      "value": "kyc-age-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for KYC Age Verification v0.2."
    },
    {
      "key": "kycage_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycage_jwt_age_threshold",
      "value": "18",
      "type": "string",
      "description": "Age threshold to verify against (0-125). Matches this app's CheckKycAgeVerificationV02 default of 18."
    },
    {
      "key": "kycage_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycage_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycage_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycage_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "kycage_ciba_scope",
      "value": "openid kyc-age-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid kyc-age-verification:verify dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "kycage_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycage_ciba_age_threshold",
      "value": "18",
      "type": "string",
      "description": "Age threshold to verify against (0-125). Matches this app's CheckKycAgeVerificationV02 default of 18."
    },
    {
      "key": "kycage_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycage_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycage_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycage_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "kycage_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "kycage_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "kycage_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "kycage_auth_scope",
      "value": "kyc-age-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for KYC Age Verification v0.2."
    },
    {
      "key": "kycage_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycage_auth_age_threshold",
      "value": "18",
      "type": "string",
      "description": "Age threshold to verify against (0-125). Matches this app's CheckKycAgeVerificationV02 default of 18."
    },
    {
      "key": "kycage_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycage_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycage_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycage_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "kycage_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycage_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycage_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "kycfillin_jwt_scope",
      "value": "kyc-fill-in:set-all dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for KYC Fill-in v0.3."
    },
    {
      "key": "kycfillin_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycfillin_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycfillin_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycfillin_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycfillin_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "kycfillin_ciba_scope",
      "value": "openid kyc-fill-in:set-all dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid kyc-fill-in:set-all dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "kycfillin_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycfillin_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycfillin_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycfillin_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycfillin_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "kycfillin_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "kycfillin_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "kycfillin_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "kycfillin_auth_scope",
      "value": "kyc-fill-in:set-all dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for KYC Fill-in v0.3."
    },
    {
      "key": "kycfillin_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "kycfillin_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "kycfillin_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "kycfillin_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "kycfillin_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "kycfillin_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycfillin_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "kycfillin_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "locverify_jwt_scope",
      "value": "location-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Location Verification v2."
    },
    {
      "key": "locverify_jwt_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locverify_jwt_latitude",
      "value": "52.5200",
      "type": "string",
      "description": "Sample latitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.latitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_jwt_longitude",
      "value": "13.4050",
      "type": "string",
      "description": "Sample longitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.longitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_jwt_radius",
      "value": "10000",
      "type": "string",
      "description": "Verification radius in meters (1-200000 per SEPAPIs.cs validation). Increased from the app's UI default of 2000 to 10000 - some deployments reject radius values too close to the CAMARA spec's minimum boundary with an INVALID_ARGUMENT/'area.radius' validation error; increase further if you still see that error."
    },
    {
      "key": "locverify_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locverify_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locverify_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locverify_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "locverify_ciba_scope",
      "value": "openid location-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid location-verification:verify dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "locverify_ciba_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locverify_ciba_latitude",
      "value": "52.5200",
      "type": "string",
      "description": "Sample latitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.latitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_ciba_longitude",
      "value": "13.4050",
      "type": "string",
      "description": "Sample longitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.longitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_ciba_radius",
      "value": "10000",
      "type": "string",
      "description": "Verification radius in meters (1-200000 per SEPAPIs.cs validation). Increased from the app's UI default of 2000 to 10000 - some deployments reject radius values too close to the CAMARA spec's minimum boundary with an INVALID_ARGUMENT/'area.radius' validation error; increase further if you still see that error."
    },
    {
      "key": "locverify_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locverify_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locverify_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locverify_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "locverify_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "locverify_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "locverify_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "locverify_auth_scope",
      "value": "location-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Location Verification v2."
    },
    {
      "key": "locverify_auth_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locverify_auth_latitude",
      "value": "52.5200",
      "type": "string",
      "description": "Sample latitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.latitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_auth_longitude",
      "value": "13.4050",
      "type": "string",
      "description": "Sample longitude (Berlin Alexanderplatz - central, well-covered urban location). If you still get an INVALID_ARGUMENT/'area.center.longitude' error, verify with your account manager which coordinates are valid for your test subscriber's registered network location."
    },
    {
      "key": "locverify_auth_radius",
      "value": "10000",
      "type": "string",
      "description": "Verification radius in meters (1-200000 per SEPAPIs.cs validation). Increased from the app's UI default of 2000 to 10000 - some deployments reject radius values too close to the CAMARA spec's minimum boundary with an INVALID_ARGUMENT/'area.radius' validation error; increase further if you still see that error."
    },
    {
      "key": "locverify_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locverify_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locverify_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locverify_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "locverify_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "locverify_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "locverify_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "locretrieve_jwt_scope",
      "value": "location-retrieval:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Location Retrieval v0.4."
    },
    {
      "key": "locretrieve_jwt_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locretrieve_jwt_max_age_seconds",
      "value": "60",
      "type": "string",
      "description": "Location Retrieval maxAge in SECONDS (not hours) - 60 is this app's SEP_APIs.razor default (RetrieveLocationV04)."
    },
    {
      "key": "locretrieve_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locretrieve_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locretrieve_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locretrieve_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "locretrieve_ciba_scope",
      "value": "openid location-retrieval:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid location-retrieval:read dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "locretrieve_ciba_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locretrieve_ciba_max_age_seconds",
      "value": "60",
      "type": "string",
      "description": "Location Retrieval maxAge in SECONDS (not hours) - 60 is this app's SEP_APIs.razor default (RetrieveLocationV04)."
    },
    {
      "key": "locretrieve_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locretrieve_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locretrieve_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locretrieve_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "locretrieve_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "locretrieve_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "locretrieve_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "locretrieve_auth_scope",
      "value": "location-retrieval:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Location Retrieval v0.4."
    },
    {
      "key": "locretrieve_auth_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "locretrieve_auth_max_age_seconds",
      "value": "60",
      "type": "string",
      "description": "Location Retrieval maxAge in SECONDS (not hours) - 60 is this app's SEP_APIs.razor default (RetrieveLocationV04)."
    },
    {
      "key": "locretrieve_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "locretrieve_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "locretrieve_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "locretrieve_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "locretrieve_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "locretrieve_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "locretrieve_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "devroam_jwt_scope",
      "value": "device-roaming-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Device Roaming Status v1."
    },
    {
      "key": "devroam_jwt_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devroam_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devroam_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devroam_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devroam_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "devroam_ciba_scope",
      "value": "openid device-roaming-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid device-roaming-status:read dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "devroam_ciba_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devroam_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devroam_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devroam_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devroam_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "devroam_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "devroam_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "devroam_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "devroam_auth_scope",
      "value": "device-roaming-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Device Roaming Status v1."
    },
    {
      "key": "devroam_auth_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devroam_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devroam_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devroam_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devroam_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "devroam_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "devroam_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "devroam_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "devreach_jwt_scope",
      "value": "device-reachability-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Device Reachability Status v1."
    },
    {
      "key": "devreach_jwt_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devreach_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devreach_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devreach_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devreach_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "devreach_ciba_scope",
      "value": "openid device-reachability-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid device-reachability-status:read dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "devreach_ciba_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devreach_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devreach_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devreach_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devreach_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "devreach_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "devreach_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "devreach_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "devreach_auth_scope",
      "value": "device-reachability-status:read dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Device Reachability Status v1."
    },
    {
      "key": "devreach_auth_phone_number",
      "value": "+4915174466709",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "devreach_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "devreach_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "devreach_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "devreach_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "devreach_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "devreach_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "devreach_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "nv1_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "description": "The redirect URI registered for your client, used in the /authorize request and the token exchange."
    },
    {
      "key": "nv1_auth_auth_code",
      "value": "",
      "description": "Paste the `code` query parameter value here after completing the browser-based authorize step (request 1)."
    },
    {
      "key": "nv1_auth_phone_number",
      "value": "+491704042076",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "nv1_auth_state",
      "value": "",
      "description": "Randomly generated by request 1's pre-request script - CSRF protection value echoed back on redirect."
    },
    {
      "key": "nv1_auth_nonce",
      "value": "",
      "description": "Randomly generated by request 1's pre-request script - OIDC replay protection value."
    },
    {
      "key": "nv1_auth_access_token",
      "value": "",
      "description": "Populated automatically by request 2's test script after a successful token exchange."
    },
    {
      "key": "nv21_jwt_scope",
      "value": "number-verification:device-phone-number:read number-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope used by this app for Number Verification v2.1 (NumberVerificationV21), from SepStandardApiConfigurationService.cs."
    },
    {
      "key": "nv21_jwt_operator_token",
      "value": "",
      "type": "string",
      "description": "REQUIRED. The TS.43 operator token obtained out-of-band via the Android Digital Credentials API / this app's DCQL aggregator flow (see Aggregator/ in this repo) - this cannot be generated inside Postman."
    },
    {
      "key": "nv21_jwt_phone_number",
      "value": "+491704042076",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - used only in the /verify request body, NOT as the JWT subject (which uses operator_token instead)."
    },
    {
      "key": "nv21_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "nv21_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script. Do not edit manually."
    },
    {
      "key": "nv21_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT with sub=operatortoken:<operator_token>. Do not edit manually."
    },
    {
      "key": "nv21_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "numrecycle_jwt_scope",
      "value": "number-recycling:check dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Number Recycling v0.2."
    },
    {
      "key": "numrecycle_jwt_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "numrecycle_jwt_specified_date",
      "value": "2024-10-31",
      "type": "string",
      "description": "Date to check for number recycling (YYYY-MM-DD) - matches this app's StandardTestData.NumberRecyclingSpecifiedDate sample value."
    },
    {
      "key": "numrecycle_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "numrecycle_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "numrecycle_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "numrecycle_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "numrecycle_ciba_scope",
      "value": "openid number-recycling:check dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid number-recycling:check dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "numrecycle_ciba_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "numrecycle_ciba_specified_date",
      "value": "2024-10-31",
      "type": "string",
      "description": "Date to check for number recycling (YYYY-MM-DD) - matches this app's StandardTestData.NumberRecyclingSpecifiedDate sample value."
    },
    {
      "key": "numrecycle_ciba_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "numrecycle_ciba_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "numrecycle_ciba_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "numrecycle_ciba_auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "numrecycle_ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "numrecycle_ciba_expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "numrecycle_ciba_poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    },
    {
      "key": "numrecycle_auth_scope",
      "value": "number-recycling:check dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Number Recycling v0.2."
    },
    {
      "key": "numrecycle_auth_phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "numrecycle_auth_specified_date",
      "value": "2024-10-31",
      "type": "string",
      "description": "Date to check for number recycling (YYYY-MM-DD) - matches this app's StandardTestData.NumberRecyclingSpecifiedDate sample value."
    },
    {
      "key": "numrecycle_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "numrecycle_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "numrecycle_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "numrecycle_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "numrecycle_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "numrecycle_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "numrecycle_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    },
    {
      "key": "qod_jwt_scope",
      "value": "quality-on-demand:sessions:create quality-on-demand:sessions:read quality-on-demand:sessions:delete quality-on-demand:sessions:update dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "QualityOnDemandV11Scope from SEPAPIs.cs - uses dpv:FraudPreventionAndDetection."
    },
    {
      "key": "qod_jwt_phone_number",
      "value": "+4915174444126",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table)."
    },
    {
      "key": "qod_jwt_duration",
      "value": "10",
      "type": "string",
      "description": "QoD session duration in seconds (1-86400) - matches this app's StandardTestData.Duration."
    },
    {
      "key": "qod_jwt_qos_profile",
      "value": "QOS_L",
      "type": "string",
      "description": "QoS profile name - QOS_L matches this app's CreateQualityOnDemandSessionV11 default."
    },
    {
      "key": "qod_jwt_application_server_ipv4",
      "value": "0.0.0.0/0",
      "type": "string",
      "description": "Application server IPv4 address/CIDR - matches this app's CreateQualityOnDemandSessionV11(NetworkBased) default of 0.0.0.0/0."
    },
    {
      "key": "qod_jwt_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "qod_jwt_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script. Do not edit manually."
    },
    {
      "key": "qod_jwt_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "qod_jwt_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    },
    {
      "key": "qod_auth_scope",
      "value": "quality-on-demand:sessions:create quality-on-demand:sessions:read quality-on-demand:sessions:delete quality-on-demand:sessions:update dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "QualityOnDemandV11Scope from SEPAPIs.cs - uses dpv:FraudPreventionAndDetection."
    },
    {
      "key": "qod_auth_duration",
      "value": "10",
      "type": "string",
      "description": "QoD session duration in seconds (1-86400) - matches this app's StandardTestData.Duration."
    },
    {
      "key": "qod_auth_qos_profile",
      "value": "QOS_L",
      "type": "string",
      "description": "QoS profile name - QOS_L matches this app's CreateQualityOnDemandSessionV11 default."
    },
    {
      "key": "qod_auth_application_server_ipv4",
      "value": "0.0.0.0/0",
      "type": "string",
      "description": "Application server IPv4 address/CIDR - matches this app's CreateQualityOnDemandSessionV11(NetworkBased) default of 0.0.0.0/0."
    },
    {
      "key": "qod_auth_client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "qod_auth_client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script. Do not edit manually."
    },
    {
      "key": "qod_auth_access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "qod_auth_redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your OAuth redirect URI, registered on your TMF Application."
    },
    {
      "key": "qod_auth_state",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "qod_auth_nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated (random GUID) by request 1 if left blank."
    },
    {
      "key": "qod_auth_auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED before request 2 - paste the \"code\" query parameter from the browser redirect here."
    }
  ]
}
