{
  "info": {
    "name": "KYC Match v0.2 - CIBA",
    "_postman_id": "kycmatch-v02-de-staging-ciba",
    "description": "KYC Match v0.2 (kyc-match/v0.2/match) using the CIBA grant (urn:openid:params:grant-type:ciba). Client authentication is PRIVATE_KEY_JWT only (RS256-signed client_assertion) - never client_secret. See the base_url variable description for the default environment and how to switch country/environment.\n\nScope 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\").\n\nCurrently listed for: 🇩🇪 Germany, 🇦🇹 Austria. (Source: this app's live product-catalog grant-type configuration, staging environment - see the /documentation/postman-collections page for the current matrix.)\n\nIMPORTANT: only one version of this API family can be ordered onto a single TMF Application - if your client_id/private_key were provisioned for KYC Match v0.3 instead of this one (e.g. you ordered Sim Swap v1 but not v2, or vice versa), authentication will fail here with a 401 UNAUTHENTICATED/InvalidClaim error even though your credentials are otherwise valid and correctly entered. You need a client_id/private_key from an Application that specifically ordered THIS version to use this collection.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "type": "string",
      "description": "Single variable driving every request in this collection. Default is Germany STAGING (https://stg.api.telekom.com). Change ONLY this variable to switch environment/country:\n- Germany production: https://api.telekom.com\n- Austria staging: https://at.stg.api.telekom.com | production: https://at.api.telekom.com\n- Poland staging: https://pl.stg.api.telekom.com | production: https://pl.api.telekom.com\n- Greece staging: https://gr.stg.api.telekom.com | production: https://gr.api.telekom.com\nSee the /documentation/endpoints page in the MBAPI app for the full country/environment reference table. 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": "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": "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": "client_id",
      "value": "",
      "type": "string",
      "description": "Your registered OAuth client ID (PRIVATE_KEY_JWT client authentication). Required - never ships with a real value."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "Your PKCS8 PEM RSA private key (matching the public key/JWK registered for your client). Replace the placeholder body between the BEGIN/END markers with your real key. Never commit or share this value."
    },
    {
      "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": "client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by pre-request scripts - the signed RS256 client_assertion JWT. Do not edit manually."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script once a token is obtained."
    },
    {
      "key": "auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the BC-Authorize request's test script."
    },
    {
      "key": "ciba_interval",
      "value": "5",
      "type": "string",
      "description": "Auto-populated polling interval (seconds) returned by the BC-Authorize call."
    },
    {
      "key": "ciba_expires_in",
      "value": "300",
      "type": "string",
      "description": "Auto-populated auth_req_id expiry (seconds) returned by the BC-Authorize call."
    },
    {
      "key": "ciba_poll_count",
      "value": "0",
      "type": "string",
      "description": "Auto-incremented poll attempt counter; polling stops after ~24 attempts."
    }
  ],
  "item": [
    {
      "name": "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('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('auth_req_id', jsonData.auth_req_id);",
              "    pm.collectionVariables.set('ciba_interval', String(jsonData.interval || 5));",
              "    pm.collectionVariables.set('ciba_expires_in', String(jsonData.expires_in || 300));",
              "    pm.collectionVariables.set('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": "{{scope}}",
              "type": "text"
            },
            {
              "key": "login_hint",
              "value": "tel:{{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": "{{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": "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('client_assertion', jwt);",
              "    return jwt;",
              "}",
              "",
              "buildClientAssertionJwt();"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const authReqId = pm.collectionVariables.get('auth_req_id');",
              "const interval = parseInt(pm.collectionVariables.get('ciba_interval') || '5', 10);",
              "let pollCount = parseInt(pm.collectionVariables.get('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('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('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('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": "{{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": "{{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": "POST KYC Match v0.2",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// No signing needed here - just reuses {{access_token}} captured by the token request(s) above."
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{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\": \"{{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": "GET KYC Match Health v0.2",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{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": []
    }
  ]
}
