Skip to content

Commit 0878c21

Browse files
sametbasbugclaude
andcommitted
Bring the staging contract check up to today, and cover the new door
The staging verifier had not been run since consent moved into the OAuth start body on 8 August. It still posted `{}` and asserted 201, so the first staging deploy of this migration failed on `terms_not_accepted` — the script's own staleness, not the deployment's. It now sends the acceptance and imports `LEGAL_LAST_UPDATED` from the source rather than repeating the date. That is why it becomes a `.ts` file run through tsx: a hand-copied version string goes stale the day the legal text changes, and on that day the script verifies itself instead of staging. Google is now the door that matters, so it is what gets asserted: the authorization URL's origin and path, a client id actually present in this environment, PKCE S256, the `openid email profile` scope, and — the one most likely to be wrong after a deploy — that the redirect_uri handed to Google is the callback this deployment answers on. A missing-consent request is checked too, because consent being taken before the provider round trip rather than after is a sequence worth holding in place. The GitHub block stays, marked for removal along with the rest of the temporary path. Also drops two helpers the previous commit orphaned: `count` in the D1 test worker and `ownerSession` in the Slice 1 suite. Local proof: `npm run check` 0 errors 0 hints, `npm run build` end to end, and the verifier passing against the deployed staging Worker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ce129b2 commit 0878c21

5 files changed

Lines changed: 115 additions & 97 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"d1:migrate:local": "wrangler d1 migrations apply orbit-v6-local --local",
2828
"d1:migrate:staging": "wrangler d1 migrations apply DB --remote --config wrangler.staging.jsonc",
2929
"staging:deploy": "ORBIT_SITE_URL=https://orbit-v6-staging.samett33710.workers.dev ORBIT_NOINDEX=true astro build --config astro.worker.config.mjs && node scripts/orbit-staging-assets.mjs && wrangler deploy --config wrangler.staging.jsonc --message 'Orbit V6 staging gate'",
30-
"staging:verify": "node scripts/orbit-staging-verify.mjs",
30+
"staging:verify": "tsx scripts/orbit-staging-verify.ts",
3131
"staging:slice3:cutover-rehearsal": "tsx scripts/orbit-slice3-cutover-rehearsal.ts",
3232
"staging:slice4:e2e": "tsx scripts/orbit-slice4-staging-e2e.ts",
3333
"staging:slice4:backup-rehearsal": "tsx scripts/orbit-slice4-backup-rehearsal.ts",

‎scripts/orbit-d1-test-worker.ts‎

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,6 @@ function stringArrayValue(data: Record<string, unknown>, key: string): string[]
6161
return value as string[];
6262
}
6363

64-
async function count(db: TestDatabase, table: string, column: string, value: string): Promise<number> {
65-
const allowed = new Set([
66-
'accounts:id',
67-
'sessions:account_id',
68-
'audit_events:id',
69-
'invitation_redemptions:invitation_id',
70-
'agent_credentials:agent_id',
71-
]);
72-
if (!allowed.has(`${table}:${column}`)) {
73-
throw new Error('unsupported_count');
74-
}
75-
const row = await db.prepare(`SELECT COUNT(*) AS value FROM ${table} WHERE ${column} = ?`)
76-
.bind(value)
77-
.first<{ value: number }>();
78-
return row?.value ?? 0;
79-
}
80-
8164
async function seedOwner(db: TestDatabase, accountId: string, now: number): Promise<void> {
8265
await db.prepare(`
8366
INSERT OR IGNORE INTO accounts (

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,16 +2402,6 @@ let firstCredentialToken = '';
24022402
);
24032403
});
24042404

2405-
/* Paketin başındaki `ownerCookies` bu noktada artık geçerli değil: çıkış
2406-
* testi o oturumu bilerek iptal ediyor. Buradaki testler kendi taze
2407-
* oturumunu açıyor — bir öncekinin artığına yaslanmak, sıraları değiştiği
2408-
* gün sessizce bozulacak bir bağımlılık olurdu. */
2409-
async function ownerSession(now: number): Promise<Map<string, string>> {
2410-
const response = await callback('owner', await startOAuth(now - 1), now);
2411-
assert.equal(response.status, 302, await response.clone().text());
2412-
return cookieValues(response);
2413-
}
2414-
24152405
/* Kayıt hız tavanı. Bu testler davet kapısı KAPALIYKEN de anlamlı, ama asıl
24162406
* sebepleri kapının açılacağı gün: o gün kayıt hacmini bir insanın kararı
24172407
* değil internet belirleyecek. */

‎scripts/orbit-staging-verify.mjs‎

Lines changed: 0 additions & 69 deletions
This file was deleted.

‎scripts/orbit-staging-verify.ts‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import assert from 'node:assert/strict';
2+
3+
import { LEGAL_LAST_UPDATED } from '../src/data/legal';
4+
5+
const ORIGIN = 'https://orbit-v6-staging.samett33710.workers.dev';
6+
const CRAWLER_POLICY = 'noindex, nofollow, noarchive';
7+
8+
async function get(pathname: string, init: RequestInit = {}): Promise<Response> {
9+
const response = await fetch(`${ORIGIN}${pathname}`, {
10+
...init,
11+
redirect: 'manual',
12+
});
13+
assert.equal(
14+
response.headers.get('x-robots-tag'),
15+
CRAWLER_POLICY,
16+
`${pathname} must deny crawler indexing`,
17+
);
18+
return response;
19+
}
20+
21+
/* Onay, /start'ın gövdesinde isteniyor ve sürümü karşılaştırılıyor. Sürüm
22+
* buraya elle yazılmıyor: kaynaktan içe aktarılıyor, çünkü elle yazılan bir
23+
* tarih metin güncellendiği gün sessizce eskiyor ve bu betik o gün staging'i
24+
* değil kendini doğrulamış olur. Betiğin `.ts` olmasının sebebi de bu. */
25+
async function startOAuth(provider: 'github' | 'google'): Promise<Response> {
26+
return await get(`/v1/auth/${provider}/start`, {
27+
method: 'POST',
28+
headers: {
29+
'content-type': 'application/json',
30+
origin: ORIGIN,
31+
},
32+
body: JSON.stringify({ acceptedTerms: true, termsVersion: LEGAL_LAST_UPDATED }),
33+
});
34+
}
35+
36+
const health = await get('/healthz');
37+
assert.equal(health.status, 200);
38+
assert.deepEqual(await health.json(), {
39+
ok: true,
40+
service: 'orbit-v6',
41+
environment: 'staging',
42+
});
43+
44+
const home = await get('/');
45+
assert.equal(home.status, 200);
46+
const html = await home.text();
47+
assert.match(
48+
html,
49+
/<meta name="robots" content="noindex, nofollow, noarchive"/u,
50+
'staging HTML must deny crawler indexing without relying on Worker routing',
51+
);
52+
assert.match(
53+
html,
54+
/<link rel="canonical" href="https:\/\/orbit-v6-staging\.samett33710\.workers\.dev\/"/u,
55+
'staging build must not advertise the production canonical origin',
56+
);
57+
58+
const forbiddenOrigin = await get('/v1/auth/google/start', {
59+
method: 'POST',
60+
headers: {
61+
'content-type': 'application/json',
62+
origin: 'https://evil.example',
63+
},
64+
body: JSON.stringify({ acceptedTerms: true, termsVersion: LEGAL_LAST_UPDATED }),
65+
});
66+
assert.equal(forbiddenOrigin.status, 403);
67+
const forbiddenBody = await forbiddenOrigin.json() as { error?: { code?: string } };
68+
assert.equal(forbiddenBody.error?.code, 'origin_forbidden');
69+
70+
/* Onaysız istek reddedilmeli. Bu satır bir kapının değil, bir sıranın
71+
* testi: onay sağlayıcıya gitmeden önce alınıyor, dönüşte değil. */
72+
const withoutConsent = await get('/v1/auth/google/start', {
73+
method: 'POST',
74+
headers: {
75+
'content-type': 'application/json',
76+
origin: ORIGIN,
77+
},
78+
body: '{}',
79+
});
80+
assert.equal(withoutConsent.status, 400);
81+
const withoutConsentBody = await withoutConsent.json() as { error?: { code?: string } };
82+
assert.equal(withoutConsentBody.error?.code, 'terms_not_accepted');
83+
84+
const googleStart = await startOAuth('google');
85+
assert.equal(googleStart.status, 201, await googleStart.clone().text());
86+
const googleBody = await googleStart.json() as { authorizationUrl: string };
87+
const googleUrl = new URL(googleBody.authorizationUrl);
88+
assert.equal(googleUrl.origin, 'https://accounts.google.com');
89+
assert.equal(googleUrl.pathname, '/o/oauth2/v2/auth');
90+
assert.ok(googleUrl.searchParams.get('client_id'), 'staging must carry a Google client id');
91+
assert.ok(googleUrl.searchParams.get('state'));
92+
assert.equal(googleUrl.searchParams.get('code_challenge_method'), 'S256');
93+
assert.ok(googleUrl.searchParams.get('code_challenge'));
94+
assert.equal(googleUrl.searchParams.get('scope'), 'openid email profile');
95+
assert.equal(
96+
googleUrl.searchParams.get('redirect_uri'),
97+
`${ORIGIN}/v1/auth/google/callback`,
98+
'the callback Google is told about must be the one this deployment answers on',
99+
);
100+
101+
/* GitHub hâlâ açık ve GEÇİCİ: yalnız mevcut hesapların girip Google'ı
102+
* bağlaması için duruyor. Kayıt kapısı olmadığını buradan doğrulayamıyoruz —
103+
* o cevap ancak gerçek bir GitHub kimliğiyle dönüşte veriliyor. Doğrulanan
104+
* şey kapının hâlâ açık olduğu; kapandığı gün bu blok da silinecek. */
105+
const githubStart = await startOAuth('github');
106+
assert.equal(githubStart.status, 201, await githubStart.clone().text());
107+
const githubBody = await githubStart.json() as { authorizationUrl: string };
108+
const githubUrl = new URL(githubBody.authorizationUrl);
109+
assert.equal(githubUrl.origin, 'https://github.com');
110+
assert.equal(githubUrl.pathname, '/login/oauth/authorize');
111+
assert.ok(githubUrl.searchParams.get('client_id'));
112+
assert.ok(githubUrl.searchParams.get('state'));
113+
114+
process.stdout.write('Orbit V6 staging HTTP contract: PASS\n');

0 commit comments

Comments
 (0)