create-ailk 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/env-catalog.js +5 -9
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +15 -2
- package/dist/topology.js +83 -4
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +58 -7
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/app-entry.test.ts +325 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +280 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/index.test.ts +86 -47
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +5 -6
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +93 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/server.test.ts +63 -6
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/app.ts +32 -0
- package/templates/apps/mcp/src/boot.ts +167 -0
- package/templates/apps/mcp/src/http-transport.ts +132 -88
- package/templates/apps/mcp/src/index.ts +19 -126
- package/templates/apps/mcp/src/lib/audit-single-page.ts +7 -6
- package/templates/apps/mcp/src/lib/content-adapter.ts +34 -5
- package/templates/apps/mcp/src/lib/topology.generated.ts +3 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/server.ts +10 -2
- package/templates/apps/mcp/src/serverless-app.ts +88 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/get_business_hours.ts +6 -5
- package/templates/apps/mcp/src/tools/get_faq.ts +6 -5
- package/templates/apps/mcp/src/tools/get_locations.ts +6 -5
- package/templates/apps/mcp/src/tools/get_offerings.ts +6 -5
- package/templates/apps/mcp/src/tools/get_pricing.ts +6 -5
- package/templates/apps/mcp/src/tools/get_site_info.ts +6 -5
- package/templates/apps/mcp/src/tools/list_pages.ts +8 -7
- package/templates/apps/mcp/src/tools/read_content.ts +7 -7
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/search_content.ts +6 -5
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/mcp/tsconfig.json +2 -0
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +16 -4
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +3 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +25 -32
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +42 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/site-brand.tsx +22 -6
- package/templates/apps/web/lib/topology.generated.ts +27 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the hub's continue step (#8146, spec
|
|
7
|
+
* docs/specs/8146-email-link-sign-in-from-spec.md D4; AC-1(c), AC-3(b)).
|
|
8
|
+
*
|
|
9
|
+
* `auth.ts` (the Better Auth singleton: `resolveSecret`, the session read) and
|
|
10
|
+
* `@working-theory/api-client`'s `hubSignIn` are the external boundaries; both
|
|
11
|
+
* are mocked, exactly as the authorize suite does. The route's own decisions
|
|
12
|
+
* are under test: where a ref sends the visitor, and what it never does.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// --- Mocks set up BEFORE imports ---
|
|
16
|
+
|
|
17
|
+
const mockGetSession = jest.fn();
|
|
18
|
+
const mockSecret = "k".repeat(43);
|
|
19
|
+
// eslint-disable-next-line no-restricted-syntax -- auth.ts is the Better Auth singleton (its adapter needs a live database); this stand-in for the secret and the session read is the repo-standard double for that boundary (see the authorize suite beside this one).
|
|
20
|
+
jest.mock("../../../../../../../auth", () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
auth: {
|
|
23
|
+
api: { getSession: (...args: unknown[]) => mockGetSession(...args) },
|
|
24
|
+
},
|
|
25
|
+
resolveSecret: () => mockSecret,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const mockCreateCode = jest.fn();
|
|
29
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
30
|
+
__esModule: true,
|
|
31
|
+
hubSignIn: { createCode: (...args: unknown[]) => mockCreateCode(...args) },
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { seal } from "../../../../../../../lib/hub-session/seal";
|
|
35
|
+
import { GET as authorize } from "../../../authorize/route";
|
|
36
|
+
import { mintContinuation } from "../../../continuation";
|
|
37
|
+
import { GET } from "../route";
|
|
38
|
+
|
|
39
|
+
const HUB = "https://hub.example.com";
|
|
40
|
+
const SPOKE_CALLBACK = "https://spoke.example.com/api/hub-session/callback";
|
|
41
|
+
const STATE = "s".repeat(43);
|
|
42
|
+
const CODE = "k".repeat(43);
|
|
43
|
+
|
|
44
|
+
const ORIGINAL_ENV = process.env;
|
|
45
|
+
|
|
46
|
+
const QUERY = new URLSearchParams({
|
|
47
|
+
response_type: "code",
|
|
48
|
+
client_id: "spoke_abc",
|
|
49
|
+
redirect_uri: SPOKE_CALLBACK,
|
|
50
|
+
state: STATE,
|
|
51
|
+
code_challenge: "c".repeat(43),
|
|
52
|
+
code_challenge_method: "S256",
|
|
53
|
+
}).toString();
|
|
54
|
+
|
|
55
|
+
function continueTo(ref: string): Promise<Response> {
|
|
56
|
+
return GET(new Request(`${HUB}/api/auth/hub/continue/${ref}`), {
|
|
57
|
+
params: Promise.resolve({ ref }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
jest.resetAllMocks();
|
|
63
|
+
process.env = { ...ORIGINAL_ENV, BETTER_AUTH_URL: HUB };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
process.env = ORIGINAL_ENV;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("AC-1(c) — a valid ref returns the visitor to the authorize step", () => {
|
|
71
|
+
it("answers 303 to /api/auth/hub/authorize with the same six-key query", async () => {
|
|
72
|
+
const res = await continueTo(mintContinuation(QUERY, mockSecret));
|
|
73
|
+
|
|
74
|
+
expect(res.status).toBe(303);
|
|
75
|
+
expect(res.headers.get("location")).toBe(
|
|
76
|
+
`/api/auth/hub/authorize?${QUERY}`,
|
|
77
|
+
);
|
|
78
|
+
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
79
|
+
expect(res.headers.get("referrer-policy")).toBe("no-referrer");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("chained into the authorize step with a session: 303 to the registered spoke URI with the code and the original state", async () => {
|
|
83
|
+
mockGetSession.mockResolvedValue({ user: { id: "u1" } });
|
|
84
|
+
mockCreateCode.mockResolvedValue({
|
|
85
|
+
ok: true,
|
|
86
|
+
body: { code: CODE, redirectUri: SPOKE_CALLBACK },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const step = await continueTo(mintContinuation(QUERY, mockSecret));
|
|
90
|
+
const res = await authorize(
|
|
91
|
+
new Request(`${HUB}${step.headers.get("location")}`, {
|
|
92
|
+
headers: { cookie: "better-auth.session_token=tok" },
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(res.status).toBe(303);
|
|
97
|
+
const target = new URL(res.headers.get("location") ?? "");
|
|
98
|
+
expect(`${target.origin}${target.pathname}`).toBe(SPOKE_CALLBACK);
|
|
99
|
+
expect(target.searchParams.get("code")).toBe(CODE);
|
|
100
|
+
expect(target.searchParams.get("state")).toBe(STATE);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("reads no session itself: the authorize step decides every code", async () => {
|
|
104
|
+
await continueTo(mintContinuation(QUERY, mockSecret));
|
|
105
|
+
expect(mockGetSession).not.toHaveBeenCalled();
|
|
106
|
+
expect(mockCreateCode).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("AC-3(b) — any other ref answers 303 /account", () => {
|
|
111
|
+
const good = mintContinuation(QUERY, mockSecret);
|
|
112
|
+
const T = Date.now();
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
[
|
|
116
|
+
"the first character flipped",
|
|
117
|
+
(good[0] === "A" ? "B" : "A") + good.slice(1),
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"sealed under another secret",
|
|
121
|
+
mintContinuation(QUERY, "x".repeat(43)),
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'a valid payload sealed with purpose "flow" and the SAME name',
|
|
125
|
+
seal(
|
|
126
|
+
{ v: 1, q: QUERY, exp: T + 600_000 },
|
|
127
|
+
mockSecret,
|
|
128
|
+
"flow",
|
|
129
|
+
"hub-continue",
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
["exp in the past", mintContinuation(QUERY, mockSecret, T - 601_000)],
|
|
133
|
+
["4097 characters", "A".repeat(4097)],
|
|
134
|
+
["a character outside base64url", `${good}%2Fevil`],
|
|
135
|
+
[
|
|
136
|
+
"plain base64url JSON, unsealed",
|
|
137
|
+
Buffer.from(
|
|
138
|
+
JSON.stringify({ v: 1, q: QUERY, exp: T + 600_000 }),
|
|
139
|
+
).toString("base64url"),
|
|
140
|
+
],
|
|
141
|
+
["an empty-looking ref", "-"],
|
|
142
|
+
])("%s", async (_name, ref) => {
|
|
143
|
+
const res = await continueTo(ref);
|
|
144
|
+
|
|
145
|
+
expect(res.status).toBe(303);
|
|
146
|
+
expect(res.headers.get("location")).toBe("/account");
|
|
147
|
+
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("a ref sealed under the test secret whose q holds CR/LF answers 303 without throwing, with no CR or LF in Location", async () => {
|
|
151
|
+
const ref = seal(
|
|
152
|
+
{ v: 1, q: "a=1\r\nX-Injected: 1", exp: T + 600_000 },
|
|
153
|
+
mockSecret,
|
|
154
|
+
"continue",
|
|
155
|
+
"hub-continue",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const res = await continueTo(ref);
|
|
159
|
+
|
|
160
|
+
expect(res.status).toBe(303);
|
|
161
|
+
expect(res.headers.get("location")).not.toMatch(/[\r\n]/);
|
|
162
|
+
expect(res.headers.get("x-injected")).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("every Location is /account or starts with /api/auth/hub/authorize? — never `//` or a scheme", async () => {
|
|
166
|
+
const refs = [
|
|
167
|
+
good,
|
|
168
|
+
"garbage",
|
|
169
|
+
mintContinuation("redirect_uri=https://evil.example/x", mockSecret),
|
|
170
|
+
mintContinuation("//evil.example/x", mockSecret),
|
|
171
|
+
seal(
|
|
172
|
+
{ v: 1, q: "https://evil.example/x", exp: T + 600_000 },
|
|
173
|
+
mockSecret,
|
|
174
|
+
"continue",
|
|
175
|
+
"hub-continue",
|
|
176
|
+
),
|
|
177
|
+
];
|
|
178
|
+
for (const ref of refs) {
|
|
179
|
+
const location = (await continueTo(ref)).headers.get("location") ?? "";
|
|
180
|
+
expect(
|
|
181
|
+
location === "/account" ||
|
|
182
|
+
location.startsWith("/api/auth/hub/authorize?"),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
expect(location.startsWith("//")).toBe(false);
|
|
185
|
+
expect(location).not.toMatch(/^[a-z][a-z0-9+.-]*:/i);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub's sign-in continuation (#8146, spec
|
|
3
|
+
* docs/specs/8146-email-link-sign-in-from-spec.md D4).
|
|
4
|
+
*
|
|
5
|
+
* GET /api/auth/hub/continue/<ref>
|
|
6
|
+
*
|
|
7
|
+
* The authorize step, for a signed-out visitor, seals the validated authorize
|
|
8
|
+
* query into `<ref>` and sends the visitor to sign in with THIS path as the
|
|
9
|
+
* destination (Better Auth decodes a callback twice, so the path must hold
|
|
10
|
+
* nothing a decode can change). After sign-in — by the emailed link or by
|
|
11
|
+
* Google — the visitor lands here, and this route sends them back to the
|
|
12
|
+
* authorize step with the original query.
|
|
13
|
+
*
|
|
14
|
+
* - a valid, unexpired ref: 303 to `/api/auth/hub/authorize?<the query>`;
|
|
15
|
+
* - any other ref (forged, expired, replayed past its life, unreadable): 303
|
|
16
|
+
* to the hub's own `/account`. By then the spoke's flow cookie (600 s) has
|
|
17
|
+
* expired too, so returning to the spoke could not finish.
|
|
18
|
+
*
|
|
19
|
+
* Both targets are fixed relative paths: this route redirects nowhere else,
|
|
20
|
+
* and never to a URL carried in the ref. It reads no session — the authorize
|
|
21
|
+
* step does that and mints the code, so every decision about a code stays in
|
|
22
|
+
* one place. It logs nothing.
|
|
23
|
+
*/
|
|
24
|
+
import { resolveSecret } from "../../../../../../auth";
|
|
25
|
+
import { openContinuation } from "../../continuation";
|
|
26
|
+
|
|
27
|
+
/** Where an unreadable ref goes: the destination the hub's own sign-in uses. */
|
|
28
|
+
const FALLBACK_PATH = "/account";
|
|
29
|
+
|
|
30
|
+
const AUTHORIZE_PATH = "/api/auth/hub/authorize";
|
|
31
|
+
|
|
32
|
+
/** A 303 to a fixed same-origin path. */
|
|
33
|
+
function see(location: string): Response {
|
|
34
|
+
return new Response(null, {
|
|
35
|
+
status: 303,
|
|
36
|
+
headers: {
|
|
37
|
+
Location: location,
|
|
38
|
+
"Cache-Control": "no-store",
|
|
39
|
+
// The authorize query rides this URL; no page may leak it through a Referer.
|
|
40
|
+
"Referrer-Policy": "no-referrer",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// platform-decisions-waiver: api-versioning — sign-in redirect step, not a versioned API contract (spec 8146 §7)
|
|
46
|
+
export async function GET(
|
|
47
|
+
_request: Request,
|
|
48
|
+
{ params }: { params: Promise<{ ref: string }> },
|
|
49
|
+
): Promise<Response> {
|
|
50
|
+
const { ref } = await params;
|
|
51
|
+
const query = openContinuation(ref, resolveSecret());
|
|
52
|
+
return see(query === null ? FALLBACK_PATH : `${AUTHORIZE_PATH}?${query}`);
|
|
53
|
+
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* - Sitemap location declared
|
|
7
7
|
* - Optional path disallow via AILK_ROBOTS_DISALLOW env var
|
|
8
8
|
* (comma-separated paths, e.g. "/admin/,/private/")
|
|
9
|
+
* - STEALTH=true disallows every path for every crawler — layer 1 of the
|
|
10
|
+
* three-layer stealth posture (#884, #7374); it subsumes any
|
|
11
|
+
* AILK_ROBOTS_DISALLOW paths
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
@@ -14,10 +17,13 @@ export function GET(): Response {
|
|
|
14
17
|
const baseUrl = resolveBaseUrl();
|
|
15
18
|
const disallowEnv = process.env["AILK_ROBOTS_DISALLOW"] ?? "";
|
|
16
19
|
|
|
17
|
-
const disallowPaths =
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const disallowPaths =
|
|
21
|
+
process.env["STEALTH"] === "true"
|
|
22
|
+
? ["/"]
|
|
23
|
+
: disallowEnv
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((p) => p.trim())
|
|
26
|
+
.filter((p) => p.length > 0);
|
|
21
27
|
|
|
22
28
|
const lines = [
|
|
23
29
|
"User-agent: *",
|
|
@@ -194,8 +194,12 @@ function buildSocialProviders(): SocialProviders {
|
|
|
194
194
|
* "up" — and `apps/web/.env.example` has always documented the variable as
|
|
195
195
|
* REQUIRED in production. The change makes the file honest rather than adding
|
|
196
196
|
* a requirement.
|
|
197
|
+
*
|
|
198
|
+
* Exported (#8146 D5) so the hub's sign-in continuation seals its ref under the
|
|
199
|
+
* same resolved secret instead of re-reading the variable: the second consumer
|
|
200
|
+
* after `betterAuth({ secret })`.
|
|
197
201
|
*/
|
|
198
|
-
function resolveSecret(): string {
|
|
202
|
+
export function resolveSecret(): string {
|
|
199
203
|
const configured = process.env.BETTER_AUTH_SECRET;
|
|
200
204
|
if (configured) return configured;
|
|
201
205
|
|