kn-apple-native-hosted-exchange.test.mjs
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago
| 1 | /** |
| 2 | * KN-APPLE-NATIVE-HOSTED-EXCHANGE — seven-tier suite (freeze §KNA.6). |
| 3 | * |
| 4 | * Covers Apple identity-assertion exchange → hosted session mint + C7 introspect. |
| 5 | * No live calls to appleid.apple.com — JWKS and tokens are fixtures under test keys. |
| 6 | */ |
| 7 | |
| 8 | import assert from 'node:assert/strict'; |
| 9 | import { createHash, generateKeyPairSync } from 'node:crypto'; |
| 10 | import fs from 'node:fs'; |
| 11 | import http from 'node:http'; |
| 12 | import os from 'node:os'; |
| 13 | import path from 'node:path'; |
| 14 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 15 | import { after, before, describe, test } from 'node:test'; |
| 16 | import jwt from 'jsonwebtoken'; |
| 17 | import { |
| 18 | APPLE_ISS, |
| 19 | appleNonceMatches, |
| 20 | appleProviderAdvertised, |
| 21 | createAppleIdentityVerifier, |
| 22 | jwtExpiryToSeconds, |
| 23 | parseAppleExchangeBody, |
| 24 | } from '../hub/gateway/apple-identity-token.mjs'; |
| 25 | |
| 26 | const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 27 | const SECRET = 'kn-apple-exchange-test-session-secret-not-production'; |
| 28 | const APPLE_AUD = 'com.example.knowtation.test'; |
| 29 | const PERF_P95_MS = 250; |
| 30 | |
| 31 | const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); |
| 32 | const FIXTURE_JWK = { |
| 33 | ...publicKey.export({ format: 'jwk' }), |
| 34 | kid: 'test-apple-kid-1', |
| 35 | use: 'sig', |
| 36 | alg: 'RS256', |
| 37 | }; |
| 38 | |
| 39 | function b64urlJson(obj) { |
| 40 | return Buffer.from(JSON.stringify(obj)).toString('base64url'); |
| 41 | } |
| 42 | |
| 43 | function signAppleFixture(claims, headerExtra = {}) { |
| 44 | return jwt.sign(claims, privateKey, { |
| 45 | algorithm: 'RS256', |
| 46 | header: { kid: FIXTURE_JWK.kid, ...headerExtra }, |
| 47 | }); |
| 48 | } |
| 49 | |
| 50 | function validAppleClaims(overrides = {}) { |
| 51 | const now = Math.floor(Date.now() / 1000); |
| 52 | return { |
| 53 | iss: APPLE_ISS, |
| 54 | aud: APPLE_AUD, |
| 55 | sub: overrides.sub ?? `apple-fixture-sub-${now}`, |
| 56 | exp: now + 600, |
| 57 | iat: now, |
| 58 | ...overrides, |
| 59 | }; |
| 60 | } |
| 61 | |
| 62 | function startServer(app) { |
| 63 | return new Promise((resolve, reject) => { |
| 64 | const srv = http.createServer(app); |
| 65 | srv.listen(0, '127.0.0.1', () => { |
| 66 | const { port } = srv.address(); |
| 67 | resolve({ |
| 68 | baseUrl: `http://127.0.0.1:${port}`, |
| 69 | close: () => |
| 70 | new Promise((res, rej) => { |
| 71 | srv.close((err) => (err ? rej(err) : res())); |
| 72 | }), |
| 73 | }); |
| 74 | }); |
| 75 | srv.on('error', reject); |
| 76 | }); |
| 77 | } |
| 78 | |
| 79 | function request(baseUrl, method, urlPath, { body, token, headers } = {}) { |
| 80 | const raw = body === undefined ? null : JSON.stringify(body); |
| 81 | return new Promise((resolve, reject) => { |
| 82 | const u = new URL(baseUrl + urlPath); |
| 83 | const req = http.request( |
| 84 | { |
| 85 | hostname: u.hostname, |
| 86 | port: u.port, |
| 87 | path: u.pathname + u.search, |
| 88 | method, |
| 89 | headers: { |
| 90 | ...(raw != null |
| 91 | ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(raw) } |
| 92 | : {}), |
| 93 | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| 94 | ...(headers || {}), |
| 95 | }, |
| 96 | }, |
| 97 | (res) => { |
| 98 | let data = ''; |
| 99 | res.on('data', (c) => { |
| 100 | data += c; |
| 101 | }); |
| 102 | res.on('end', () => { |
| 103 | let parsed = data; |
| 104 | try { |
| 105 | parsed = data ? JSON.parse(data) : null; |
| 106 | } catch { |
| 107 | /* keep raw */ |
| 108 | } |
| 109 | resolve({ status: res.statusCode, body: parsed, headers: res.headers, raw: data }); |
| 110 | }); |
| 111 | }, |
| 112 | ); |
| 113 | req.on('error', reject); |
| 114 | if (raw != null) req.write(raw); |
| 115 | req.end(); |
| 116 | }); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Dishonest stub: accepts unsigned JWT payload without verifying signature. |
| 121 | * Security tier must fail against production code if this behavior were mounted. |
| 122 | */ |
| 123 | function createUnverifiedAcceptStub() { |
| 124 | return { |
| 125 | async verifyIdentityToken(identityToken) { |
| 126 | const parts = String(identityToken || '').split('.'); |
| 127 | if (parts.length < 2) { |
| 128 | return { ok: false, code: 'APPLE_ASSERTION_INVALID', error: 'malformed' }; |
| 129 | } |
| 130 | const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')); |
| 131 | return { ok: true, claims: { appleSub: String(payload.sub || 'forged') } }; |
| 132 | }, |
| 133 | }; |
| 134 | } |
| 135 | |
| 136 | let gw; |
| 137 | let verifier; |
| 138 | |
| 139 | async function loadGateway({ offlineLocked = false, appleClientId = APPLE_AUD, sessionSecret = SECRET } = {}) { |
| 140 | const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-apple-exchange-')); |
| 141 | process.env.SESSION_SECRET = sessionSecret || ''; |
| 142 | if (!sessionSecret) delete process.env.SESSION_SECRET; |
| 143 | delete process.env.HUB_JWT_SECRET; |
| 144 | process.env.APPLE_CLIENT_ID = appleClientId || ''; |
| 145 | if (!appleClientId) delete process.env.APPLE_CLIENT_ID; |
| 146 | process.env.NETLIFY = '1'; |
| 147 | process.env.CANISTER_URL = ''; |
| 148 | process.env.BRIDGE_URL = ''; |
| 149 | process.env.BILLING_ENFORCE = 'false'; |
| 150 | process.env.KNOWTATION_GATEWAY_DATA_DIR = tmpDir; |
| 151 | if (offlineLocked) { |
| 152 | process.env.KNOWTATION_OFFLINE_LOCKED_AUTH = 'enabled'; |
| 153 | } else { |
| 154 | delete process.env.KNOWTATION_OFFLINE_LOCKED_AUTH; |
| 155 | } |
| 156 | delete process.env.GOOGLE_CLIENT_ID; |
| 157 | delete process.env.GOOGLE_CLIENT_SECRET; |
| 158 | delete process.env.GITHUB_CLIENT_ID; |
| 159 | delete process.env.GITHUB_CLIENT_SECRET; |
| 160 | |
| 161 | verifier = createAppleIdentityVerifier({ |
| 162 | fetchImpl: async () => { |
| 163 | throw new Error('live Apple JWKS forbidden in CI'); |
| 164 | }, |
| 165 | }); |
| 166 | verifier.seedJwksCache([FIXTURE_JWK]); |
| 167 | globalThis.__knowtation_apple_identity_verifier = verifier; |
| 168 | |
| 169 | const entry = pathToFileURL(path.join(ROOT, 'hub', 'gateway', 'server.mjs')).href; |
| 170 | const { app } = await import(`${entry}?kn-apple=${Date.now()}-${Math.random()}`); |
| 171 | const srv = await startServer(app); |
| 172 | return { |
| 173 | ...srv, |
| 174 | tmpDir, |
| 175 | close: async () => { |
| 176 | await srv.close(); |
| 177 | delete globalThis.__knowtation_apple_identity_verifier; |
| 178 | try { |
| 179 | fs.rmSync(tmpDir, { recursive: true, force: true }); |
| 180 | } catch { |
| 181 | /* ignore */ |
| 182 | } |
| 183 | }, |
| 184 | }; |
| 185 | } |
| 186 | |
| 187 | // ─── unit ─────────────────────────────────────────────────────────────────── |
| 188 | |
| 189 | describe('KN-APPLE unit', () => { |
| 190 | test('parseAppleExchangeBody allowlist + forbidden identity fields', () => { |
| 191 | assert.equal(parseAppleExchangeBody(null).ok, false); |
| 192 | assert.equal(parseAppleExchangeBody({}).code, 'BAD_REQUEST'); |
| 193 | assert.equal(parseAppleExchangeBody({ identity_token: 'x', extra: 1 }).code, 'BAD_REQUEST'); |
| 194 | assert.equal(parseAppleExchangeBody({ identity_token: 'x', role: 'admin' }).code, 'BAD_REQUEST'); |
| 195 | assert.equal(parseAppleExchangeBody({ identity_token: 'x', scooling_uid: 'abc' }).code, 'BAD_REQUEST'); |
| 196 | const ok = parseAppleExchangeBody({ |
| 197 | identity_token: ' tok ', |
| 198 | nonce: 'n1', |
| 199 | full_name: 'A'.repeat(200), |
| 200 | }); |
| 201 | assert.equal(ok.ok, true); |
| 202 | assert.equal(ok.identityToken, 'tok'); |
| 203 | assert.equal(ok.nonce, 'n1'); |
| 204 | assert.equal(ok.fullName.length, 128); |
| 205 | }); |
| 206 | |
| 207 | test('appleProviderAdvertised boolean logic', () => { |
| 208 | assert.equal(appleProviderAdvertised({ appleClientId: APPLE_AUD, offlineLocked: false }), true); |
| 209 | assert.equal(appleProviderAdvertised({ appleClientId: ' ', offlineLocked: false }), false); |
| 210 | assert.equal(appleProviderAdvertised({ appleClientId: APPLE_AUD, offlineLocked: true }), false); |
| 211 | assert.equal(appleProviderAdvertised({ offlineLocked: false }), false); |
| 212 | }); |
| 213 | |
| 214 | test('userId shape apple:<sub> via mint claims', () => { |
| 215 | const sub = '001234.abcdef'; |
| 216 | assert.equal(`apple:${sub}`, `apple:${sub}`); |
| 217 | }); |
| 218 | |
| 219 | test('aud/iss/exp reject matrix + alg=none', async () => { |
| 220 | const v = createAppleIdentityVerifier({ |
| 221 | fetchImpl: async () => ({ ok: true, json: async () => ({ keys: [FIXTURE_JWK] }) }), |
| 222 | }); |
| 223 | const good = signAppleFixture(validAppleClaims({ sub: 'u-matrix' })); |
| 224 | const ok = await v.verifyIdentityToken(good, { audience: APPLE_AUD }); |
| 225 | assert.equal(ok.ok, true); |
| 226 | assert.equal(ok.claims.appleSub, 'u-matrix'); |
| 227 | |
| 228 | const wrongAud = signAppleFixture(validAppleClaims({ aud: 'other.bundle', sub: 'u2' })); |
| 229 | assert.equal((await v.verifyIdentityToken(wrongAud, { audience: APPLE_AUD })).code, 'APPLE_ASSERTION_INVALID'); |
| 230 | |
| 231 | const wrongIss = signAppleFixture(validAppleClaims({ iss: 'https://evil.example', sub: 'u3' })); |
| 232 | assert.equal((await v.verifyIdentityToken(wrongIss, { audience: APPLE_AUD })).code, 'APPLE_ASSERTION_INVALID'); |
| 233 | |
| 234 | const expired = signAppleFixture(validAppleClaims({ sub: 'u4', exp: Math.floor(Date.now() / 1000) - 120 })); |
| 235 | assert.equal((await v.verifyIdentityToken(expired, { audience: APPLE_AUD })).code, 'APPLE_ASSERTION_INVALID'); |
| 236 | |
| 237 | const noneTok = `${b64urlJson({ alg: 'none', typ: 'JWT' })}.${b64urlJson({ |
| 238 | iss: APPLE_ISS, |
| 239 | aud: APPLE_AUD, |
| 240 | sub: 'u5', |
| 241 | exp: Math.floor(Date.now() / 1000) + 600, |
| 242 | })}.`; |
| 243 | assert.equal((await v.verifyIdentityToken(noneTok, { audience: APPLE_AUD })).code, 'APPLE_ASSERTION_INVALID'); |
| 244 | }); |
| 245 | |
| 246 | test('nonce match raw or sha256 hex', () => { |
| 247 | const raw = 'client-nonce-1'; |
| 248 | const hex = createHash('sha256').update(raw, 'utf8').digest('hex'); |
| 249 | assert.equal(appleNonceMatches(raw, raw), true); |
| 250 | assert.equal(appleNonceMatches(hex, raw), true); |
| 251 | assert.equal(appleNonceMatches('other', raw), false); |
| 252 | assert.equal(appleNonceMatches(undefined, undefined), true); |
| 253 | }); |
| 254 | |
| 255 | test('jwtExpiryToSeconds', () => { |
| 256 | assert.equal(jwtExpiryToSeconds('24h'), 86400); |
| 257 | assert.equal(jwtExpiryToSeconds('15m'), 900); |
| 258 | assert.equal(jwtExpiryToSeconds(120), 120); |
| 259 | }); |
| 260 | }); |
| 261 | |
| 262 | // ─── integration / e2e / stress / data-integrity / performance / security ─── |
| 263 | |
| 264 | describe('KN-APPLE gateway tiers', () => { |
| 265 | before(async () => { |
| 266 | gw = await loadGateway(); |
| 267 | }); |
| 268 | after(async () => { |
| 269 | if (gw) await gw.close(); |
| 270 | delete process.env.APPLE_CLIENT_ID; |
| 271 | delete process.env.KNOWTATION_OFFLINE_LOCKED_AUTH; |
| 272 | }); |
| 273 | |
| 274 | test('integration: exchange → 200 JWT + C7 provider apple', async () => { |
| 275 | const sub = 'int-apple-sub-1'; |
| 276 | const identity_token = signAppleFixture(validAppleClaims({ sub })); |
| 277 | const ex = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 278 | body: { identity_token, full_name: 'Ada' }, |
| 279 | }); |
| 280 | assert.equal(ex.status, 200); |
| 281 | assert.equal(ex.body.schema_version, 1); |
| 282 | assert.equal(ex.body.token_type, 'Bearer'); |
| 283 | assert.equal(typeof ex.body.access_token, 'string'); |
| 284 | assert.ok(ex.body.expires_in > 0); |
| 285 | assert.equal(ex.body.scooling_uid, undefined); |
| 286 | assert.equal(ex.body.refresh_token, undefined); |
| 287 | |
| 288 | const payload = jwt.verify(ex.body.access_token, SECRET); |
| 289 | assert.equal(payload.sub, `apple:${sub}`); |
| 290 | assert.equal(payload.provider, 'apple'); |
| 291 | assert.equal(payload.id, sub); |
| 292 | assert.equal(payload.type, 'session'); |
| 293 | assert.equal(payload.name, 'Ada'); |
| 294 | |
| 295 | const sess = await request(gw.baseUrl, 'GET', '/api/v1/auth/session', { |
| 296 | token: ex.body.access_token, |
| 297 | }); |
| 298 | assert.equal(sess.status, 200); |
| 299 | assert.equal(sess.body.provider, 'apple'); |
| 300 | assert.equal(sess.body.sub, `apple:${sub}`); |
| 301 | assert.equal(sess.body.id, sub); |
| 302 | assert.ok(Array.isArray(sess.body.scopes)); |
| 303 | }); |
| 304 | |
| 305 | test('integration: unconfigured APPLE_CLIENT_ID → 503 NOT_CONFIGURED', async () => { |
| 306 | const local = await loadGateway({ appleClientId: '' }); |
| 307 | try { |
| 308 | const ex = await request(local.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 309 | body: { identity_token: signAppleFixture(validAppleClaims({ sub: 'x' })) }, |
| 310 | }); |
| 311 | assert.equal(ex.status, 503); |
| 312 | assert.equal(ex.body.code, 'NOT_CONFIGURED'); |
| 313 | } finally { |
| 314 | await local.close(); |
| 315 | } |
| 316 | }); |
| 317 | |
| 318 | test('e2e: providers.apple + exchange → session round-trip; offline-locked → 403', async () => { |
| 319 | const providers = await request(gw.baseUrl, 'GET', '/api/v1/auth/providers'); |
| 320 | assert.equal(providers.status, 200); |
| 321 | assert.equal(providers.body.apple, true); |
| 322 | assert.equal(typeof providers.body.google, 'boolean'); |
| 323 | assert.equal(typeof providers.body.github, 'boolean'); |
| 324 | |
| 325 | const sub = 'e2e-apple-sub'; |
| 326 | const ex = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 327 | body: { identity_token: signAppleFixture(validAppleClaims({ sub })) }, |
| 328 | }); |
| 329 | assert.equal(ex.status, 200); |
| 330 | const sess = await request(gw.baseUrl, 'GET', '/api/v1/auth/session', { |
| 331 | token: ex.body.access_token, |
| 332 | }); |
| 333 | assert.equal(sess.status, 200); |
| 334 | assert.equal(sess.body.sub, `apple:${sub}`); |
| 335 | |
| 336 | const offline = await loadGateway({ offlineLocked: true }); |
| 337 | try { |
| 338 | const blocked = await request(offline.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 339 | body: { identity_token: signAppleFixture(validAppleClaims({ sub: 'off' })) }, |
| 340 | }); |
| 341 | assert.equal(blocked.status, 403); |
| 342 | assert.equal(blocked.body.code, 'OAUTH_DISABLED'); |
| 343 | const p = await request(offline.baseUrl, 'GET', '/api/v1/auth/providers'); |
| 344 | assert.equal(p.body.apple, false); |
| 345 | } finally { |
| 346 | await offline.close(); |
| 347 | } |
| 348 | }); |
| 349 | |
| 350 | test('stress: parallel exchanges no cross-user mix; JWKS cache stable', async () => { |
| 351 | const N = 24; |
| 352 | const results = await Promise.all( |
| 353 | Array.from({ length: N }, async (_, i) => { |
| 354 | const sub = `stress-sub-${i}`; |
| 355 | const ex = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 356 | body: { identity_token: signAppleFixture(validAppleClaims({ sub })) }, |
| 357 | }); |
| 358 | assert.equal(ex.status, 200); |
| 359 | const payload = jwt.verify(ex.body.access_token, SECRET); |
| 360 | return payload.sub; |
| 361 | }), |
| 362 | ); |
| 363 | const unique = new Set(results); |
| 364 | assert.equal(unique.size, N); |
| 365 | for (let i = 0; i < N; i++) { |
| 366 | assert.ok(unique.has(`apple:stress-sub-${i}`)); |
| 367 | } |
| 368 | }); |
| 369 | |
| 370 | test('data-integrity: no scooling_uid / identity_token echo / durable identity row', async () => { |
| 371 | const sub = 'di-apple-sub'; |
| 372 | const identity_token = signAppleFixture(validAppleClaims({ sub })); |
| 373 | const ex = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 374 | body: { identity_token }, |
| 375 | }); |
| 376 | assert.equal(ex.status, 200); |
| 377 | assert.equal(ex.body.scooling_uid, undefined); |
| 378 | assert.equal(ex.body.identity_token, undefined); |
| 379 | assert.ok(!JSON.stringify(ex.body).includes(identity_token)); |
| 380 | assert.ok(!ex.raw.includes(['BEGIN', 'PRIVATE', 'KEY'].join(' '))); |
| 381 | const payload = jwt.verify(ex.body.access_token, SECRET); |
| 382 | assert.equal(payload.sub, `apple:${sub}`); |
| 383 | |
| 384 | const files = fs.readdirSync(gw.tmpDir); |
| 385 | assert.ok(!files.some((f) => /identity|apple-map|scooling/i.test(f))); |
| 386 | }); |
| 387 | |
| 388 | test('performance: single exchange p95 under fixture JWKS bound', async () => { |
| 389 | const samples = []; |
| 390 | for (let i = 0; i < 20; i++) { |
| 391 | const t0 = performance.now(); |
| 392 | const ex = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 393 | body: { identity_token: signAppleFixture(validAppleClaims({ sub: `perf-${i}` })) }, |
| 394 | }); |
| 395 | samples.push(performance.now() - t0); |
| 396 | assert.equal(ex.status, 200); |
| 397 | } |
| 398 | samples.sort((a, b) => a - b); |
| 399 | const p95 = samples[Math.floor(samples.length * 0.95) - 1]; |
| 400 | assert.ok( |
| 401 | p95 < PERF_P95_MS, |
| 402 | `p95 ${p95.toFixed(1)}ms exceeds bound ${PERF_P95_MS}ms (fixture JWKS only)`, |
| 403 | ); |
| 404 | }); |
| 405 | |
| 406 | test('security: forged / wrong aud / client role → reject; fixtures ban prod shapes', async () => { |
| 407 | const forged = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 408 | body: { |
| 409 | identity_token: jwt.sign(validAppleClaims({ sub: 'forged' }), 'not-apple-key', { |
| 410 | algorithm: 'HS256', |
| 411 | }), |
| 412 | }, |
| 413 | }); |
| 414 | assert.equal(forged.status, 401); |
| 415 | assert.equal(forged.body.code, 'APPLE_ASSERTION_INVALID'); |
| 416 | |
| 417 | const wrongAud = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 418 | body: { |
| 419 | identity_token: signAppleFixture(validAppleClaims({ aud: 'com.evil.app', sub: 'w' })), |
| 420 | }, |
| 421 | }); |
| 422 | assert.equal(wrongAud.status, 401); |
| 423 | |
| 424 | const clientRole = await request(gw.baseUrl, 'POST', '/api/v1/auth/native-apple-exchange', { |
| 425 | body: { |
| 426 | identity_token: signAppleFixture(validAppleClaims({ sub: 'r' })), |
| 427 | role: 'admin', |
| 428 | scooling_uid: 'deadbeef', |
| 429 | }, |
| 430 | }); |
| 431 | assert.equal(clientRole.status, 400); |
| 432 | assert.equal(clientRole.body.code, 'BAD_REQUEST'); |
| 433 | |
| 434 | // Fixture / product sources must not embed PEM private-key material or Team ID assignments. |
| 435 | const pemNeedle = ['BEGIN', 'PRIVATE', 'KEY'].join(' '); |
| 436 | const teamIdAssign = ['TEAM', '_ID', '='].join(''); |
| 437 | for (const rel of [ |
| 438 | 'test/kn-apple-native-hosted-exchange.test.mjs', |
| 439 | 'hub/gateway/apple-identity-token.mjs', |
| 440 | 'hub/gateway/server.mjs', |
| 441 | '.env.example', |
| 442 | ]) { |
| 443 | const src = fs.readFileSync(path.join(ROOT, rel), 'utf8'); |
| 444 | assert.ok(!src.includes(pemNeedle), `${rel} must not embed PEM private key material`); |
| 445 | assert.ok( |
| 446 | !new RegExp(`${teamIdAssign}\\s*['"][A-Z0-9]{10}['"]`).test(src), |
| 447 | `${rel} must not embed Apple Team ID assignments`, |
| 448 | ); |
| 449 | } |
| 450 | // Ban pasted production-looking Apple JWTs (long eyJ…eyJ triples in string literals). |
| 451 | const suiteSrc = fs.readFileSync(fileURLToPath(import.meta.url), 'utf8'); |
| 452 | assert.ok( |
| 453 | !/['"`]eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}['"`]/.test(suiteSrc), |
| 454 | 'committed suite must not embed production-shaped Apple JWT string literals', |
| 455 | ); |
| 456 | |
| 457 | // Regression discriminator: unverified-accept stub must diverge from real verifier. |
| 458 | const stub = createUnverifiedAcceptStub(); |
| 459 | const unsignedPayload = `${b64urlJson({ alg: 'none' })}.${b64urlJson({ |
| 460 | sub: 'forged-via-stub', |
| 461 | iss: APPLE_ISS, |
| 462 | aud: APPLE_AUD, |
| 463 | exp: Math.floor(Date.now() / 1000) + 600, |
| 464 | })}.`; |
| 465 | const stubOk = await stub.verifyIdentityToken(unsignedPayload); |
| 466 | assert.equal(stubOk.ok, true, 'stub must accept unverified payloads (discriminator)'); |
| 467 | const realReject = await verifier.verifyIdentityToken(unsignedPayload, { audience: APPLE_AUD }); |
| 468 | assert.equal(realReject.ok, false, 'production verifier must reject unverified assertion'); |
| 469 | |
| 470 | // Passport / native PKCE paths still declared (not equated to SIWA). |
| 471 | const serverSrc = fs.readFileSync(path.join(ROOT, 'hub', 'gateway', 'server.mjs'), 'utf8'); |
| 472 | assert.ok(serverSrc.includes("app.use('/api/v1/auth/native'")); |
| 473 | assert.ok(serverSrc.includes("passport.authenticate('google'")); |
| 474 | assert.ok(serverSrc.includes('/api/v1/auth/native-apple-exchange')); |
| 475 | assert.ok( |
| 476 | !/PKCE\s*=\s*SIWA|native PKCE equals|equals Sign in with Apple/i.test(serverSrc), |
| 477 | 'must not claim PKCE equals SIWA', |
| 478 | ); |
| 479 | }); |
| 480 | }); |
File History
1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago