create-ailk 0.32.0 → 0.33.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/README.md +9 -3
- package/component-catalog.json +12 -1
- package/component-catalog.md +6 -5
- package/dist/export-content.d.ts +6 -3
- package/dist/export-content.js +79 -10
- package/dist/lib/import-mdx.d.ts +42 -0
- package/dist/lib/import-mdx.js +531 -45
- package/dist/seed-flows.d.ts +1 -1
- package/dist/seed-flows.js +9 -4
- package/dist/surfaces.js +1 -0
- package/package.json +3 -3
- package/templates/apps/api/CLAUDE.md +7 -5
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +28 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/__tests__/actions.test.ts +94 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/actions.ts +77 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +79 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +21 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +15 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +38 -105
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +72 -58
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/verify.test.tsx +249 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +164 -20
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/by-result.test.tsx +269 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/score.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +108 -40
- package/templates/apps/web/app/[locale]/flows/[slug]/verify/page.tsx +153 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/verify.actions.ts +151 -0
- package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -20
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +2 -9
- package/templates/apps/web/auth.ts +22 -6
- package/templates/apps/web/lib/__tests__/account-config.test.ts +69 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +99 -1
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +20 -0
- package/templates/apps/web/lib/account-config.ts +52 -14
- package/templates/apps/web/lib/auth-email.ts +87 -5
- package/templates/apps/web/lib/telemetry-scrub.ts +6 -0
- package/templates/apps/web/messages/en.json +26 -1
- package/templates/database/seed.ts +7 -7
- package/versions.json +48 -48
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +0 -89
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +0 -104
|
@@ -319,13 +319,30 @@ export function makeSendResetPassword(deps: ResetPasswordDeps) {
|
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
interface VerificationDeps extends DispatchDeps {
|
|
323
|
+
/**
|
|
324
|
+
* The site's own confirm page — `new URL("/verify-email", BETTER_AUTH_URL)`
|
|
325
|
+
* — which the API's email-verification service appends its single-use nonce
|
|
326
|
+
* to (#10820 D4).
|
|
327
|
+
*/
|
|
328
|
+
confirmBase: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Better Auth's `sendVerificationEmail` (§ D9, #10820 D4).
|
|
333
|
+
*
|
|
334
|
+
* Keeps the library's signature and IGNORES its `url`: that link is Better
|
|
335
|
+
* Auth's stateless JWT, which is neither single-use nor revocable, and its
|
|
336
|
+
* `/verify-email` endpoint is disabled in `auth.ts`. The relay's
|
|
337
|
+
* `email-verification` kind mints the service's own nonce for this user and
|
|
338
|
+
* mails `confirmBase?verify=<nonce>`; a click on that page confirms through
|
|
339
|
+
* `confirmSignUpEmail` below.
|
|
340
|
+
*/
|
|
341
|
+
export function makeSendVerificationEmail(deps: VerificationDeps) {
|
|
342
|
+
const { send, confirmBase } = deps;
|
|
325
343
|
const log = deps.log ?? defaultLog;
|
|
326
344
|
return async function sendVerificationEmail({
|
|
327
345
|
user,
|
|
328
|
-
url,
|
|
329
346
|
}: {
|
|
330
347
|
user: { id: string; email: string };
|
|
331
348
|
url: string;
|
|
@@ -335,7 +352,7 @@ export function makeSendVerificationEmail(deps: DispatchDeps) {
|
|
|
335
352
|
kind: "email-verification",
|
|
336
353
|
userId: user.id,
|
|
337
354
|
to: user.email,
|
|
338
|
-
url,
|
|
355
|
+
url: confirmBase,
|
|
339
356
|
expiresInMinutes: 24 * 60,
|
|
340
357
|
},
|
|
341
358
|
{ send, log },
|
|
@@ -343,6 +360,71 @@ export function makeSendVerificationEmail(deps: DispatchDeps) {
|
|
|
343
360
|
};
|
|
344
361
|
}
|
|
345
362
|
|
|
363
|
+
/** The confirm hop's answer, as the `/verify-email` action reads it. */
|
|
364
|
+
export type SignUpConfirmOutcome =
|
|
365
|
+
| "verified"
|
|
366
|
+
| "already_verified"
|
|
367
|
+
| "expired"
|
|
368
|
+
| "invalid"
|
|
369
|
+
| "failed";
|
|
370
|
+
|
|
371
|
+
/** The confirm hop's transport seam — `authEmail.confirm`'s shape. */
|
|
372
|
+
export type ConfirmTransport = (
|
|
373
|
+
rawBody: string,
|
|
374
|
+
headers: Record<string, string>,
|
|
375
|
+
) => Promise<{ ok: boolean; body?: unknown }>;
|
|
376
|
+
|
|
377
|
+
const CONFIRM_OUTCOMES = new Set(["verified", "already_verified", "expired", "invalid"]);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Confirm a sign-up link's nonce through the API's signed hop,
|
|
381
|
+
* `POST /v1/auth/email-verification/confirm` (#10820 D2, D7).
|
|
382
|
+
*
|
|
383
|
+
* ONE serialization of `{ nonce }`, signed over the DOMAIN-SEPARATED base
|
|
384
|
+
* string `email-verification-confirm.<timestamp>.<rawBody>` — so a body signed
|
|
385
|
+
* for the mail relay can never be replayed to the confirm route, or the
|
|
386
|
+
* reverse — and the same bytes handed to the transport.
|
|
387
|
+
*
|
|
388
|
+
* NEVER THROWS, and logs only `{ kind, outcome | errorKind }`: never the
|
|
389
|
+
* nonce, which is the credential. Anything but a 2xx carrying a known outcome
|
|
390
|
+
* is `failed`.
|
|
391
|
+
*/
|
|
392
|
+
export async function confirmSignUpEmail(
|
|
393
|
+
nonce: string,
|
|
394
|
+
deps: { confirm?: ConfirmTransport; log?: AuthEmailLog } = {},
|
|
395
|
+
): Promise<SignUpConfirmOutcome> {
|
|
396
|
+
const confirm: ConfirmTransport =
|
|
397
|
+
deps.confirm ?? ((rawBody, headers) => authEmail.confirm(rawBody, headers));
|
|
398
|
+
const log = deps.log ?? defaultLog;
|
|
399
|
+
const base = { kind: "email-verification-confirm" };
|
|
400
|
+
const secret = process.env.AUTH_EMAIL_WEBHOOK_SECRET;
|
|
401
|
+
if (!secret) {
|
|
402
|
+
log.warn({ ...base, errorKind: "not_configured" });
|
|
403
|
+
return "failed";
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const rawBody = JSON.stringify({ nonce });
|
|
407
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
408
|
+
const signature = `sha256=${createHmac("sha256", secret)
|
|
409
|
+
.update(`email-verification-confirm.${timestamp}.${rawBody}`, "utf8")
|
|
410
|
+
.digest("hex")}`;
|
|
411
|
+
const result = await confirm(rawBody, {
|
|
412
|
+
"x-ailk-auth-email-signature": signature,
|
|
413
|
+
"x-ailk-auth-email-timestamp": timestamp,
|
|
414
|
+
});
|
|
415
|
+
const outcome = (result.body as { outcome?: unknown } | null | undefined)?.outcome;
|
|
416
|
+
if (!result.ok || typeof outcome !== "string" || !CONFIRM_OUTCOMES.has(outcome)) {
|
|
417
|
+
log.warn({ ...base, errorKind: "relay_rejected" });
|
|
418
|
+
return "failed";
|
|
419
|
+
}
|
|
420
|
+
log.info({ ...base, outcome });
|
|
421
|
+
return outcome as SignUpConfirmOutcome;
|
|
422
|
+
} catch {
|
|
423
|
+
log.warn({ ...base, errorKind: "transport_error" });
|
|
424
|
+
return "failed";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
346
428
|
/**
|
|
347
429
|
* The completed-reset notice (§ D17).
|
|
348
430
|
*
|
|
@@ -19,12 +19,18 @@
|
|
|
19
19
|
// Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
|
|
20
20
|
// since apps/* must not import @working-theory/* packages — keep both lists
|
|
21
21
|
// in sync (#5613).
|
|
22
|
+
// `verify` joined by #10820 (D2): the email-verification nonce on a
|
|
23
|
+
// `/verify-email` or listing resume link.
|
|
24
|
+
// `lead` joined by #10826 (D5, SEC-10826-7): a magnet Lead's id on its
|
|
25
|
+
// `/flows/<slug>/verify?lead=…&verify=…` confirm link.
|
|
22
26
|
export const TOKEN_QUERY_PARAMS = [
|
|
23
27
|
"g",
|
|
24
28
|
"session_id",
|
|
25
29
|
"email",
|
|
26
30
|
"login_hint",
|
|
27
31
|
"ref",
|
|
32
|
+
"verify",
|
|
33
|
+
"lead",
|
|
28
34
|
];
|
|
29
35
|
|
|
30
36
|
// The same grant id rides a PATH too: `deliverables.openGrant` fetches
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"heading": "Blog",
|
|
81
81
|
"emptyState": "No posts yet. Drop MDX files into",
|
|
82
82
|
"emptyStateTrailing": "to populate this list.",
|
|
83
|
+
"emptyStateLive": "No posts yet.",
|
|
83
84
|
"allTopics": "All",
|
|
84
85
|
"topicsNav": "Blog topics",
|
|
85
86
|
"latestPosts": "Latest posts",
|
|
@@ -254,6 +255,20 @@
|
|
|
254
255
|
},
|
|
255
256
|
"questions": {}
|
|
256
257
|
},
|
|
258
|
+
"flowVerify": {
|
|
259
|
+
"title": "Confirm your email",
|
|
260
|
+
"confirmHeading": "Confirm your email",
|
|
261
|
+
"confirmSubhead": "One click and your address is confirmed.",
|
|
262
|
+
"confirmBody": "Select the button below to confirm this email address.",
|
|
263
|
+
"confirmButton": "Confirm my email",
|
|
264
|
+
"confirmedHeading": "Your email is confirmed",
|
|
265
|
+
"confirmedSubhead": "Thanks — your address is verified.",
|
|
266
|
+
"confirmedBody": "You can close this tab once you're done.",
|
|
267
|
+
"errorHeading": "This confirmation link has expired",
|
|
268
|
+
"errorSubhead": "Confirmation links last 24 hours.",
|
|
269
|
+
"errorBody": "Answer the form again and we'll send you a fresh confirmation email.",
|
|
270
|
+
"back": "Back to the form"
|
|
271
|
+
},
|
|
257
272
|
"how-to": {
|
|
258
273
|
"heading": "How-To Guides",
|
|
259
274
|
"description": "Step-by-step guides for building with this site.",
|
|
@@ -665,6 +680,13 @@
|
|
|
665
680
|
"verifyExpiredHeading": "This confirmation link has expired",
|
|
666
681
|
"verifyExpiredSubhead": "Confirmation links last 24 hours.",
|
|
667
682
|
"verifyExpiredBody": "Sign in again and we'll send you a fresh confirmation email.",
|
|
683
|
+
"confirmHeading": "Confirm your email",
|
|
684
|
+
"confirmSubhead": "One click and your address is confirmed.",
|
|
685
|
+
"confirmBody": "Select the button below to confirm this email address for your account.",
|
|
686
|
+
"confirmButton": "Confirm my email",
|
|
687
|
+
"verifiedHeading": "Your email is confirmed",
|
|
688
|
+
"verifiedSubhead": "Thanks — your address is verified.",
|
|
689
|
+
"verifiedBody": "You can sign in now. You can close this tab once you're done.",
|
|
668
690
|
"sentHeading": "Check your email",
|
|
669
691
|
"sentSubhead": "Follow the link we just sent to continue.",
|
|
670
692
|
"sentBody": "Check your inbox and follow the link to continue. You can close this tab once you're done.",
|
|
@@ -969,7 +991,10 @@
|
|
|
969
991
|
"tier": "Tier",
|
|
970
992
|
"score": "Score",
|
|
971
993
|
"completedAt": "Completed",
|
|
972
|
-
"unavailable": "—"
|
|
994
|
+
"unavailable": "—",
|
|
995
|
+
"magnet": "Form",
|
|
996
|
+
"submittedAt": "Submitted",
|
|
997
|
+
"open": "Open"
|
|
973
998
|
},
|
|
974
999
|
"purchases": {
|
|
975
1000
|
"heading": "My purchases",
|
|
@@ -149,8 +149,8 @@ export async function provisionDefaultWorkspace(
|
|
|
149
149
|
kind: "contact",
|
|
150
150
|
title: REQUEST_QUOTE_FLOW_TITLE,
|
|
151
151
|
status: "draft",
|
|
152
|
-
|
|
153
|
-
completion: "
|
|
152
|
+
who: "user",
|
|
153
|
+
completion: "user",
|
|
154
154
|
tags: [STANDARD_MAGNET_TAGS[REQUEST_QUOTE_FLOW_SLUG]!],
|
|
155
155
|
},
|
|
156
156
|
update: {},
|
|
@@ -171,7 +171,7 @@ export async function provisionDefaultWorkspace(
|
|
|
171
171
|
kind: "contact",
|
|
172
172
|
title: CONTACT_FORM_FLOW_TITLE,
|
|
173
173
|
status: "live",
|
|
174
|
-
|
|
174
|
+
who: "visitor",
|
|
175
175
|
completion: "lead",
|
|
176
176
|
tags: [STANDARD_MAGNET_TAGS[CONTACT_FORM_FLOW_SLUG]!],
|
|
177
177
|
landingPage: CONTACT_FORM_LANDING_PAGE,
|
|
@@ -194,8 +194,8 @@ export async function provisionDefaultWorkspace(
|
|
|
194
194
|
kind: "diagnostic",
|
|
195
195
|
title: JOB_APPLICATION_FLOW_TITLE,
|
|
196
196
|
status: "live",
|
|
197
|
-
|
|
198
|
-
completion: "
|
|
197
|
+
who: "visitor",
|
|
198
|
+
completion: "user",
|
|
199
199
|
tags: [STANDARD_MAGNET_TAGS[JOB_APPLICATION_FLOW_SLUG]!],
|
|
200
200
|
landingPage: JOB_APPLICATION_LANDING_PAGE,
|
|
201
201
|
},
|
|
@@ -218,7 +218,7 @@ export async function provisionDefaultWorkspace(
|
|
|
218
218
|
kind: "newsletter",
|
|
219
219
|
title: NEWSLETTER_FLOW_TITLE,
|
|
220
220
|
status: "live",
|
|
221
|
-
|
|
221
|
+
who: "visitor",
|
|
222
222
|
completion: "lead",
|
|
223
223
|
tags: [STANDARD_MAGNET_TAGS[NEWSLETTER_FLOW_SLUG]!],
|
|
224
224
|
landingPage: NEWSLETTER_LANDING_PAGE,
|
|
@@ -242,7 +242,7 @@ export async function provisionDefaultWorkspace(
|
|
|
242
242
|
kind: "diagnostic",
|
|
243
243
|
title: TRIAL_FLOW_TITLE,
|
|
244
244
|
status: "live",
|
|
245
|
-
|
|
245
|
+
who: "visitor",
|
|
246
246
|
completion: "lead",
|
|
247
247
|
tags: [STANDARD_MAGNET_TAGS[TRIAL_FLOW_SLUG]!],
|
|
248
248
|
landingPage: TRIAL_FLOW_LANDING_PAGE,
|
package/versions.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@working-theory/admin-ui": "0.
|
|
3
|
-
"@working-theory/aeo": "0.
|
|
4
|
-
"@working-theory/agent-context": "0.
|
|
2
|
+
"@working-theory/admin-ui": "0.56.0",
|
|
3
|
+
"@working-theory/aeo": "0.56.0",
|
|
4
|
+
"@working-theory/agent-context": "0.56.0",
|
|
5
5
|
"@working-theory/agent-contract": "0.1.0",
|
|
6
6
|
"@working-theory/agent-memory": "0.0.0",
|
|
7
7
|
"@working-theory/ai": "0.1.0",
|
|
8
|
-
"@working-theory/analytics": "0.
|
|
9
|
-
"@working-theory/api": "0.
|
|
10
|
-
"@working-theory/api-client": "0.
|
|
8
|
+
"@working-theory/analytics": "0.56.0",
|
|
9
|
+
"@working-theory/api": "0.56.0",
|
|
10
|
+
"@working-theory/api-client": "0.56.0",
|
|
11
11
|
"@working-theory/cda-filesystem": "0.0.0",
|
|
12
|
-
"@working-theory/cli": "0.
|
|
13
|
-
"@working-theory/common": "0.
|
|
12
|
+
"@working-theory/cli": "0.56.0",
|
|
13
|
+
"@working-theory/common": "0.56.0",
|
|
14
14
|
"@working-theory/consent": "0.1.0",
|
|
15
|
-
"@working-theory/content-adapters": "0.
|
|
16
|
-
"@working-theory/content-projection": "0.
|
|
17
|
-
"@working-theory/email": "0.
|
|
15
|
+
"@working-theory/content-adapters": "0.56.0",
|
|
16
|
+
"@working-theory/content-projection": "0.56.0",
|
|
17
|
+
"@working-theory/email": "0.56.0",
|
|
18
18
|
"@working-theory/env": "0.0.0",
|
|
19
|
-
"@working-theory/eslint-config": "0.
|
|
20
|
-
"@working-theory/fumadocs-schema": "0.
|
|
21
|
-
"@working-theory/ids": "0.
|
|
22
|
-
"@working-theory/jsonld": "0.
|
|
23
|
-
"@working-theory/license": "0.
|
|
24
|
-
"@working-theory/mcp-server": "0.
|
|
25
|
-
"@working-theory/metadata": "0.
|
|
26
|
-
"@working-theory/observability": "0.
|
|
27
|
-
"@working-theory/page-renderer": "0.
|
|
19
|
+
"@working-theory/eslint-config": "0.56.0",
|
|
20
|
+
"@working-theory/fumadocs-schema": "0.56.0",
|
|
21
|
+
"@working-theory/ids": "0.20.0",
|
|
22
|
+
"@working-theory/jsonld": "0.56.0",
|
|
23
|
+
"@working-theory/license": "0.56.0",
|
|
24
|
+
"@working-theory/mcp-server": "0.56.0",
|
|
25
|
+
"@working-theory/metadata": "0.56.0",
|
|
26
|
+
"@working-theory/observability": "0.56.0",
|
|
27
|
+
"@working-theory/page-renderer": "0.56.0",
|
|
28
28
|
"@working-theory/plugin-mcp": "0.1.3",
|
|
29
|
-
"@working-theory/schema": "0.
|
|
30
|
-
"@working-theory/schema-pack-registry": "0.
|
|
31
|
-
"@working-theory/schema-validator": "0.
|
|
32
|
-
"@working-theory/sections-registry": "0.
|
|
33
|
-
"@working-theory/site-config": "0.
|
|
34
|
-
"@working-theory/spoke-checkout": "0.7.
|
|
35
|
-
"@working-theory/stripe-client": "0.
|
|
36
|
-
"@working-theory/templates": "0.
|
|
37
|
-
"@working-theory/theme": "0.
|
|
38
|
-
"@working-theory/ui": "0.
|
|
39
|
-
"@working-theory/validation": "0.
|
|
40
|
-
"@working-theory/web-i18n": "0.
|
|
41
|
-
"@working-theory/admin": "0.
|
|
42
|
-
"@working-theory/api-commerce": "0.1.
|
|
43
|
-
"@working-theory/api-content-write": "0.
|
|
44
|
-
"@working-theory/api-hub": "0.4.
|
|
45
|
-
"@working-theory/api-multi-tenant": "0.2.
|
|
46
|
-
"@working-theory/api-scheduling": "0.4.
|
|
47
|
-
"@working-theory/api-signal-collectors": "0.
|
|
48
|
-
"@working-theory/api-workspace": "0.
|
|
29
|
+
"@working-theory/schema": "0.56.0",
|
|
30
|
+
"@working-theory/schema-pack-registry": "0.56.0",
|
|
31
|
+
"@working-theory/schema-validator": "0.56.0",
|
|
32
|
+
"@working-theory/sections-registry": "0.56.0",
|
|
33
|
+
"@working-theory/site-config": "0.56.0",
|
|
34
|
+
"@working-theory/spoke-checkout": "0.7.12",
|
|
35
|
+
"@working-theory/stripe-client": "0.56.0",
|
|
36
|
+
"@working-theory/templates": "0.56.0",
|
|
37
|
+
"@working-theory/theme": "0.56.0",
|
|
38
|
+
"@working-theory/ui": "0.56.0",
|
|
39
|
+
"@working-theory/validation": "0.56.0",
|
|
40
|
+
"@working-theory/web-i18n": "0.56.0",
|
|
41
|
+
"@working-theory/admin": "0.11.0",
|
|
42
|
+
"@working-theory/api-commerce": "0.1.10",
|
|
43
|
+
"@working-theory/api-content-write": "0.6.0",
|
|
44
|
+
"@working-theory/api-hub": "0.4.2",
|
|
45
|
+
"@working-theory/api-multi-tenant": "0.2.5",
|
|
46
|
+
"@working-theory/api-scheduling": "0.4.7",
|
|
47
|
+
"@working-theory/api-signal-collectors": "0.4.0",
|
|
48
|
+
"@working-theory/api-workspace": "0.6.0",
|
|
49
49
|
"@working-theory/billing-client": "0.0.0",
|
|
50
50
|
"@working-theory/branding": "0.0.0",
|
|
51
51
|
"@working-theory/license-client": "0.0.0",
|
|
52
52
|
"@working-theory/license-manifest": "0.0.0",
|
|
53
|
-
"@working-theory/mcp-scheduling": "0.3.
|
|
54
|
-
"@working-theory/mcp-signal-collectors": "0.2.
|
|
55
|
-
"@working-theory/mcp-workspace": "0.6.
|
|
53
|
+
"@working-theory/mcp-scheduling": "0.3.9",
|
|
54
|
+
"@working-theory/mcp-signal-collectors": "0.2.10",
|
|
55
|
+
"@working-theory/mcp-workspace": "0.6.2",
|
|
56
56
|
"@working-theory/partner-session": "0.0.0",
|
|
57
57
|
"@working-theory/preview-protocol": "0.0.0",
|
|
58
|
-
"@working-theory/ui-commerce": "0.23.
|
|
59
|
-
"@working-theory/ui-docs-blog": "0.23.
|
|
60
|
-
"@working-theory/ui-scheduling": "0.25.
|
|
61
|
-
"@working-theory/ui-signal-collectors": "0.
|
|
58
|
+
"@working-theory/ui-commerce": "0.23.34",
|
|
59
|
+
"@working-theory/ui-docs-blog": "0.23.34",
|
|
60
|
+
"@working-theory/ui-scheduling": "0.25.8",
|
|
61
|
+
"@working-theory/ui-signal-collectors": "0.28.0",
|
|
62
62
|
"@working-theory/web": "0.0.0",
|
|
63
63
|
"@working-theory/app-api": "1.0.0",
|
|
64
64
|
"@working-theory/mcp": "0.0.0",
|
|
65
|
-
"@working-theory/database": "0.
|
|
65
|
+
"@working-theory/database": "0.56.0"
|
|
66
66
|
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use client"; // Reads sessionStorage (a browser API) once on mount — a server page cannot.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AccountInviteLink — the /thanks page's "Track this in your account" link
|
|
5
|
-
* (#8771 D6). The magnet walk writes one `sessionStorage` item,
|
|
6
|
-
* `ailk:account-invite`, before it navigates here; this shows the link only
|
|
7
|
-
* for the thank-you page that walk was headed to, and only if the stored href
|
|
8
|
-
* is a same-site sign-in path. It reads the item once and removes it, so the
|
|
9
|
-
* email in the href does not stay in the tab. A refresh shows no link (the
|
|
10
|
-
* invite email still carries it).
|
|
11
|
-
*
|
|
12
|
-
* Base tree: it imports nothing from `flows/**`, so a build without the walk
|
|
13
|
-
* still compiles this page.
|
|
14
|
-
*/
|
|
15
|
-
import { Button, Container, Link } from "@working-theory/ui";
|
|
16
|
-
import type { ReactElement } from "react";
|
|
17
|
-
import { useEffect, useState } from "react";
|
|
18
|
-
|
|
19
|
-
/** Must equal `ACCOUNT_INVITE_STORAGE_KEY` in the walk's `FlowStepperClient`. */
|
|
20
|
-
const STORAGE_KEY = "ailk:account-invite";
|
|
21
|
-
/** A reserved name that never resolves: only used to test an href stays on-site. */
|
|
22
|
-
const PLACEHOLDER_ORIGIN = "https://placeholder.invalid";
|
|
23
|
-
const HUB_SIGN_IN_PATH = "/api/hub-session/sign-in";
|
|
24
|
-
|
|
25
|
-
interface Props {
|
|
26
|
-
/** The route's `[slug]` — the hand-off must have been written for this page. */
|
|
27
|
-
thanksSlug: string;
|
|
28
|
-
/** `contact.trackInAccount`, read on the server. */
|
|
29
|
-
label: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The stored href when it is a same-site sign-in path, else `null`. */
|
|
33
|
-
function safeSignInHref(raw: unknown, thanks: unknown, thanksSlug: string): string | null {
|
|
34
|
-
if (typeof raw !== "string" || thanks !== thanksSlug) return null;
|
|
35
|
-
let url: URL;
|
|
36
|
-
try {
|
|
37
|
-
url = new URL(raw, PLACEHOLDER_ORIGIN);
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
if (url.origin !== PLACEHOLDER_ORIGIN) return null;
|
|
42
|
-
if (url.pathname !== HUB_SIGN_IN_PATH && !url.pathname.endsWith("/sign-in")) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
return raw;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Read the hand-off once, remove it, and answer the link to show (or `null`). */
|
|
49
|
-
function takeHandOff(thanksSlug: string): string | null {
|
|
50
|
-
try {
|
|
51
|
-
const stored = window.sessionStorage.getItem(STORAGE_KEY);
|
|
52
|
-
if (stored === null) return null;
|
|
53
|
-
window.sessionStorage.removeItem(STORAGE_KEY);
|
|
54
|
-
const parsed: unknown = JSON.parse(stored);
|
|
55
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
56
|
-
const { thanks, href } = parsed as { thanks?: unknown; href?: unknown };
|
|
57
|
-
return safeSignInHref(href, thanks, thanksSlug);
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function AccountInviteLink({ thanksSlug, label }: Props): ReactElement | null {
|
|
64
|
-
const [href, setHref] = useState<string | null>(null);
|
|
65
|
-
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
// Only ever sets the link: React's development double-mount runs this
|
|
68
|
-
// twice, and the second run finds the item already removed.
|
|
69
|
-
const found = takeHandOff(thanksSlug);
|
|
70
|
-
if (found !== null) setHref(found);
|
|
71
|
-
}, [thanksSlug]);
|
|
72
|
-
|
|
73
|
-
if (href === null) return null;
|
|
74
|
-
return (
|
|
75
|
-
// Session Replay records the DOM, so the link (it carries the email) is
|
|
76
|
-
// blocked from it; `Link` does not forward an undeclared data-* attribute.
|
|
77
|
-
// The page shell sets no gutters, so the link sits in its own Container,
|
|
78
|
-
// lined up with the thank-you content above it.
|
|
79
|
-
<Container className="pb-section">
|
|
80
|
-
<div data-sentry-block>
|
|
81
|
-
<Button asChild variant="outline" className="min-h-11">
|
|
82
|
-
<Link href={href} variant="bare" data-testid="thanks-account-invite">
|
|
83
|
-
{label}
|
|
84
|
-
</Link>
|
|
85
|
-
</Button>
|
|
86
|
-
</div>
|
|
87
|
-
</Container>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #8771 AC-5 — /thanks shows only its own hand-off: the walk's sessionStorage
|
|
3
|
-
* item, for this page's slug, when its href is a same-site sign-in path.
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
import { render, screen } from "@testing-library/react";
|
|
9
|
-
import React, { StrictMode } from "react";
|
|
10
|
-
|
|
11
|
-
import { AccountInviteLink } from "../AccountInviteLink";
|
|
12
|
-
|
|
13
|
-
const KEY = "ailk:account-invite";
|
|
14
|
-
const LABEL = "Track this in your account";
|
|
15
|
-
const SITE_HREF = "/sign-in?next=%2Faccount&email=jane%40example.com";
|
|
16
|
-
const HUB_HREF = "/api/hub-session/sign-in?next=%2Faccount&email=x%40y.example";
|
|
17
|
-
const WALK = join(__dirname, "..", "..", "..", "flows", "[slug]", "FlowStepperClient.tsx");
|
|
18
|
-
// A Free scaffold ships no magnet walk, and the scaffold matrix runs this suite
|
|
19
|
-
// inside one (#8799), so the key check skips where the walk does not exist.
|
|
20
|
-
const itWithWalk = existsSync(WALK) ? it : it.skip;
|
|
21
|
-
|
|
22
|
-
function store(value: unknown): void {
|
|
23
|
-
window.sessionStorage.setItem(KEY, typeof value === "string" ? value : JSON.stringify(value));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function show(slug = "flow-thanks") {
|
|
27
|
-
return render(<AccountInviteLink thanksSlug={slug} label={LABEL} />);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
beforeEach(() => window.sessionStorage.clear());
|
|
31
|
-
|
|
32
|
-
describe("AccountInviteLink", () => {
|
|
33
|
-
it("renders the stored link for its own slug, blocked from Replay, and removes the item", () => {
|
|
34
|
-
store({ thanks: "flow-thanks", href: SITE_HREF });
|
|
35
|
-
|
|
36
|
-
show();
|
|
37
|
-
|
|
38
|
-
const link = screen.getByTestId("thanks-account-invite");
|
|
39
|
-
expect(link).toHaveAttribute("href", SITE_HREF);
|
|
40
|
-
expect(link).toHaveTextContent(LABEL);
|
|
41
|
-
expect(link.closest("[data-sentry-block]")).not.toBeNull();
|
|
42
|
-
expect(window.sessionStorage.getItem(KEY)).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("renders a hub spoke's sign-in path under the matching slug", () => {
|
|
46
|
-
store({ thanks: "flow-thanks", href: HUB_HREF });
|
|
47
|
-
show();
|
|
48
|
-
expect(screen.getByTestId("thanks-account-invite")).toHaveAttribute("href", HUB_HREF);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("renders a locale-prefixed site sign-in path", () => {
|
|
52
|
-
store({ thanks: "flow-thanks", href: "/fr/sign-in?email=a%40b.example" });
|
|
53
|
-
show();
|
|
54
|
-
expect(screen.getByTestId("thanks-account-invite")).toBeInTheDocument();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it.each([
|
|
58
|
-
["another slug", { thanks: "other", href: SITE_HREF }, "flow-thanks"],
|
|
59
|
-
["an off-site href", { thanks: "flow-thanks", href: "https://evil.example/sign-in" }, "flow-thanks"],
|
|
60
|
-
["a protocol-relative href", { thanks: "flow-thanks", href: "//evil.example/sign-in" }, "flow-thanks"],
|
|
61
|
-
["a non-sign-in path", { thanks: "flow-thanks", href: "/account" }, "flow-thanks"],
|
|
62
|
-
["a hub href under another thanks", { thanks: "flow-thanks-qualified", href: HUB_HREF }, "flow-thanks"],
|
|
63
|
-
["a value that is not JSON", "not json", "flow-thanks"],
|
|
64
|
-
["a JSON null", "null", "flow-thanks"],
|
|
65
|
-
])("renders nothing for %s", (_name, value, slug) => {
|
|
66
|
-
store(value);
|
|
67
|
-
show(slug);
|
|
68
|
-
expect(screen.queryByTestId("thanks-account-invite")).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("renders nothing when sessionStorage throws", () => {
|
|
72
|
-
const spy = jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
|
73
|
-
throw new Error("denied");
|
|
74
|
-
});
|
|
75
|
-
show();
|
|
76
|
-
expect(screen.queryByTestId("thanks-account-invite")).toBeNull();
|
|
77
|
-
spy.mockRestore();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("shows the link once: a fresh mount after it (a refresh) renders nothing", () => {
|
|
81
|
-
store({ thanks: "flow-thanks", href: SITE_HREF });
|
|
82
|
-
const first = show();
|
|
83
|
-
expect(screen.getByTestId("thanks-account-invite")).toBeInTheDocument();
|
|
84
|
-
first.unmount();
|
|
85
|
-
|
|
86
|
-
show();
|
|
87
|
-
expect(screen.queryByTestId("thanks-account-invite")).toBeNull();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("keeps the link under React.StrictMode's double effect", () => {
|
|
91
|
-
store({ thanks: "flow-thanks", href: SITE_HREF });
|
|
92
|
-
render(
|
|
93
|
-
<StrictMode>
|
|
94
|
-
<AccountInviteLink thanksSlug="flow-thanks" label={LABEL} />
|
|
95
|
-
</StrictMode>,
|
|
96
|
-
);
|
|
97
|
-
expect(screen.getByTestId("thanks-account-invite")).toBeInTheDocument();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
itWithWalk("reads the same storage key the walk writes (the two constants are tied only by this test)", () => {
|
|
101
|
-
const walk = readFileSync(WALK, "utf8");
|
|
102
|
-
expect(walk).toContain(`ACCOUNT_INVITE_STORAGE_KEY = "${KEY}"`);
|
|
103
|
-
});
|
|
104
|
-
});
|