← 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
M lib/apple.jsA test/apple-route.test.jsM test/apple.test.js
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 →