[object Object]

← back to Costa Rica

costa-rica: Cody-gate fixes on Apple tests — fix real exp:0 falsy-zero bug in apple.js (payload.exp && -> !== undefined, an epoch-exp token was wrongly accepted); add route-level test (apple-route.test.js) proving /auth/apple only auto-links on email_verified=true (protects c2 guard 0689909 at the actual gate, not just the verifier); add strict-equality (email_verified 1/'TRUE'->false) + exp:0 edges; honest test comment. Suite 50/50 green — TK-10346

55c95571624350cb2d445f89241ec3ea3c6f5ee1 · 2026-08-07 13:31:53 -0700 · Steve

Files touched

Diff

commit 55c95571624350cb2d445f89241ec3ea3c6f5ee1
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 13:31:53 2026 -0700

    costa-rica: Cody-gate fixes on Apple tests — fix real exp:0 falsy-zero bug in apple.js (payload.exp && -> !== undefined, an epoch-exp token was wrongly accepted); add route-level test (apple-route.test.js) proving /auth/apple only auto-links on email_verified=true (protects c2 guard 0689909 at the actual gate, not just the verifier); add strict-equality (email_verified 1/'TRUE'->false) + exp:0 edges; honest test comment. Suite 50/50 green — TK-10346
---
 lib/apple.js             |  2 +-
 test/apple-route.test.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++++
 test/apple.test.js       | 22 +++++++++++----
 3 files changed, 88 insertions(+), 6 deletions(-)

diff --git a/lib/apple.js b/lib/apple.js
index ebdf092..c896956 100644
--- a/lib/apple.js
+++ b/lib/apple.js
@@ -37,7 +37,7 @@ async function verifyIdentityToken(idToken) {
 
   if (payload.iss !== ISS) throw new Error('bad iss');
   if (!AUD.includes(payload.aud)) throw new Error(`bad aud ${payload.aud}`);
-  if (payload.exp && Math.floor(Date.now() / 1000) > payload.exp) throw new Error('token expired');
+  if (payload.exp !== undefined && Math.floor(Date.now() / 1000) > payload.exp) throw new Error('token expired');
 
   return { sub: payload.sub, email: payload.email || null, email_verified: payload.email_verified === 'true' || payload.email_verified === true };
 }
diff --git a/test/apple-route.test.js b/test/apple-route.test.js
new file mode 100644
index 0000000..d156419
--- /dev/null
+++ b/test/apple-route.test.js
@@ -0,0 +1,70 @@
+'use strict';
+// Route-level regression test for the c2 account-takeover guard (commit 0689909).
+// The verifier-layer test (apple.test.js) proves email_verified is a real boolean;
+// THIS proves the /auth/apple route only auto-links an Apple identity to an existing
+// account when that boolean is true. No DB / no network: pool.query + apple verifier
+// are mocked via their shared module objects. Run: node --test
+const { test, before, after } = require('node:test');
+const assert = require('node:assert');
+const http = require('node:http');
+const express = require('express');
+
+const db = require('../lib/db');
+const apple = require('../lib/apple');
+
+let sqls = [];              // every SQL the route runs, in order
+let appleClaims = null;     // what the mocked verifier returns
+
+// Mock pool.query: record SQL, return sensible rows for the /auth/apple flow.
+db.pool.query = async (sql) => {
+  sqls.push(sql);
+  if (/SELECT \* FROM app_users WHERE apple_sub/.test(sql)) return { rows: [] };        // no existing apple account
+  if (/UPDATE app_users SET apple_sub/.test(sql)) return { rows: [{ id: 1, email: 'a@b.com', role: 'guest', is_host: false }] };
+  if (/INSERT INTO app_users \(apple_sub/.test(sql)) return { rows: [{ id: 2, email: 'a@b.com', role: 'guest', is_host: false }] };
+  return { rows: [] };
+};
+apple.verifyIdentityToken = async () => appleClaims;
+
+const { router } = require('../routes/app');
+let server, base;
+before(async () => {
+  const app = express();
+  app.use(express.json());
+  app.use('/api/app', router);
+  await new Promise(r => { server = app.listen(0, r); });
+  base = `http://127.0.0.1:${server.address().port}`;
+});
+after(() => server && server.close());
+
+function post(path, body) {
+  return new Promise((resolve, reject) => {
+    const data = JSON.stringify(body);
+    const req = http.request(base + path, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } },
+      res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); });
+    req.on('error', reject); req.end(data);
+  });
+}
+const linked = () => sqls.some(s => /UPDATE app_users SET apple_sub/.test(s));
+
+test('SECURITY: email_verified=false → does NOT auto-link, creates a fresh Apple account instead', async () => {
+  sqls = []; appleClaims = { sub: 'apple-x', email: 'a@b.com', email_verified: false };
+  const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
+  assert.equal(r.status, 200);
+  assert.equal(linked(), false, 'unverified email must NOT link to the existing account');
+  assert.ok(sqls.some(s => /INSERT INTO app_users \(apple_sub/.test(s)), 'a new Apple-owned account should be created');
+});
+
+test('email_verified=true → DOES auto-link the Apple identity to the existing account', async () => {
+  sqls = []; appleClaims = { sub: 'apple-x', email: 'a@b.com', email_verified: true };
+  const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
+  assert.equal(r.status, 200);
+  assert.equal(linked(), true, 'verified email should link');
+  assert.ok(!sqls.some(s => /INSERT INTO app_users \(apple_sub/.test(s)), 'link path should skip the INSERT');
+});
+
+test('no email present → no link even if email_verified is somehow true', async () => {
+  sqls = []; appleClaims = { sub: 'apple-x', email: null, email_verified: true };
+  const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
+  assert.equal(r.status, 200);
+  assert.equal(linked(), false, 'no email → nothing to link against');
+});
diff --git a/test/apple.test.js b/test/apple.test.js
index a623b41..97bb748 100644
--- a/test/apple.test.js
+++ b/test/apple.test.js
@@ -44,10 +44,15 @@ test('email_verified normalizes both the string "true" and boolean true → true
   assert.equal((await verifyIdentityToken(signToken({ sub: 's', email_verified: true }))).email_verified, true);
 });
 
-test('SECURITY: email_verified "false"/missing is false (blocks c2 account-takeover auto-link)', async () => {
+// Verifier-layer half of the c2 guard: the boolean the route gate depends on is
+// only ever true for a genuine Apple-verified email. (The route gate itself is
+// exercised in apple-route.test.js.)
+test('email_verified is strictly true only for Apple "true"/true — false/missing/truthy-junk → false', async () => {
   assert.equal((await verifyIdentityToken(signToken({ sub: 's', email_verified: 'false' }))).email_verified, false);
   assert.equal((await verifyIdentityToken(signToken({ sub: 's', email_verified: false }))).email_verified, false);
-  assert.equal((await verifyIdentityToken(signToken({ sub: 's' }))).email_verified, false); // absent
+  assert.equal((await verifyIdentityToken(signToken({ sub: 's' }))).email_verified, false);        // absent
+  assert.equal((await verifyIdentityToken(signToken({ sub: 's', email_verified: 1 }))).email_verified, false); // strict: truthy 1 is NOT verified
+  assert.equal((await verifyIdentityToken(signToken({ sub: 's', email_verified: 'TRUE' }))).email_verified, false); // strict: wrong case
 });
 
 test('rejects a tampered signature', async () => {
@@ -70,11 +75,18 @@ test('rejects a wrong issuer', async () => {
   await assert.rejects(() => verifyIdentityToken(`${h}.${p}.${sig}`), /bad iss/);
 });
 
-test('rejects an expired token', async () => {
+async function signRaw(payload) {
   const h = b64u({ alg: 'RS256', kid: KID });
-  const p = b64u({ iss: ISS, aud: AUD, exp: Math.floor(Date.now() / 1000) - 60, sub: 's' });
+  const p = b64u(payload);
   const sig = crypto.sign('RSA-SHA256', Buffer.from(`${h}.${p}`), privateKey).toString('base64url');
-  await assert.rejects(() => verifyIdentityToken(`${h}.${p}.${sig}`), /expired/);
+  return `${h}.${p}.${sig}`;
+}
+
+test('rejects an expired token (incl. the exp:0 falsy-zero edge)', async () => {
+  await assert.rejects(() => verifyIdentityToken(signToken({ sub: 's', exp: Math.floor(Date.now() / 1000) - 60 })), /expired/);
+  // exp:0 (Unix epoch) is clearly expired — must NOT be accepted via a `payload.exp &&` short-circuit.
+  const epochTok = await signRaw({ iss: ISS, aud: AUD, exp: 0, sub: 's' });
+  await assert.rejects(() => verifyIdentityToken(epochTok), /expired/);
 });
 
 test('rejects a malformed token and an unknown signing key', async () => {

← c04da8f costa-rica: payouts settlement integration test — sandbox ra  ·  back to Costa Rica  ·  yoloforever: cycle 4 ledger — security test coverage (Apple 1828f55 →