create-ailk 0.20.0 → 0.22.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 +21 -1
- package/component-catalog.json +220 -8
- package/component-catalog.md +21 -9
- package/dist/add-module.js +9 -5
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +63 -25
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +30 -14
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +124 -1
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +125 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +20 -7
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +139 -3
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +192 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +40 -12
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/database/CHANGELOG.md +47 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/seed.test.ts +72 -2
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +60 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
|
@@ -96,6 +96,16 @@
|
|
|
96
96
|
* `stripeSessionId`), with `session.customer` as the customer id only on a
|
|
97
97
|
* live-mode event. Logs an outcome kind only; never fails the 200.
|
|
98
98
|
*
|
|
99
|
+
* Checkout terms grant (checkout.session.completed, #8765 D14):
|
|
100
|
+
* Chained after the Person step, `recordCheckoutTermsGrant` records the
|
|
101
|
+
* buyer's acceptance of the session's terms box as one `checkout` row of
|
|
102
|
+
* the consent ledger — only for a session whose `consent.terms_of_service`
|
|
103
|
+
* is `accepted` and whose server-written `metadata.ailk_terms` names
|
|
104
|
+
* `account_terms` at its current version (a Payment Link, invoice or older
|
|
105
|
+
* session carries none), for the Person `checkoutBuyerPerson` resolves.
|
|
106
|
+
* Logs `{ event: "checkout-consent", outcome, eventIdPrefix }` only; never
|
|
107
|
+
* fails the 200.
|
|
108
|
+
*
|
|
99
109
|
* Dunning (invoice.payment_failed / customer.subscription.updated, #6034):
|
|
100
110
|
* Records a per-subscriber lifecycle state in a `UserData` row behind a
|
|
101
111
|
* reserved-key guard (routes/auth/user.ts). `invoice.payment_failed` arms
|
|
@@ -108,6 +118,11 @@
|
|
|
108
118
|
import { track } from "@working-theory/analytics/server";
|
|
109
119
|
import type { Prisma } from "@working-theory/database";
|
|
110
120
|
import { isId } from "@working-theory/ids";
|
|
121
|
+
import {
|
|
122
|
+
ACCOUNT_TERMS_PURPOSE_ID,
|
|
123
|
+
consentPurposeById,
|
|
124
|
+
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
125
|
+
} from "@working-theory/validation";
|
|
111
126
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
112
127
|
|
|
113
128
|
import { config } from "../../config/index.js";
|
|
@@ -133,6 +148,8 @@ import {
|
|
|
133
148
|
} from "../../lib/stripe.js";
|
|
134
149
|
import { hasSeen, markSeen } from "../../lib/webhook-idempotency.js";
|
|
135
150
|
import { recordWorkspaceAuditAction } from "../../services/audit-log.js";
|
|
151
|
+
import { checkoutBuyerPerson } from "../../services/checkout-buyer.js";
|
|
152
|
+
import { recordCheckoutConsent } from "../../services/consent.js";
|
|
136
153
|
import { isUniqueViolation } from "../../services/default-tenant.js";
|
|
137
154
|
import {
|
|
138
155
|
DUNNING_MAX_SUBSCRIPTIONS,
|
|
@@ -142,8 +159,8 @@ import {
|
|
|
142
159
|
type DunningRecord,
|
|
143
160
|
} from "../../services/dunning-policy.js";
|
|
144
161
|
import {
|
|
145
|
-
|
|
146
|
-
type
|
|
162
|
+
resolveCheckoutPerson,
|
|
163
|
+
type CheckoutPersonResult,
|
|
147
164
|
} from "../../services/person.js";
|
|
148
165
|
|
|
149
166
|
const TELEMETRY_TIMEOUT_MS = 2000;
|
|
@@ -429,13 +446,24 @@ export const stripeWebhookHandler = async (
|
|
|
429
446
|
session,
|
|
430
447
|
eventIdPrefix,
|
|
431
448
|
outcome: "completed",
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
449
|
+
})
|
|
450
|
+
.then(() =>
|
|
451
|
+
recordFlowCheckoutPerson(request, {
|
|
452
|
+
session,
|
|
453
|
+
livemode: event.livemode === true,
|
|
454
|
+
eventIdPrefix,
|
|
455
|
+
}),
|
|
456
|
+
)
|
|
457
|
+
// #8765 D14 — after the Person step, whose Person is a flow
|
|
458
|
+
// checkout's grant subject.
|
|
459
|
+
.then((flowPerson) =>
|
|
460
|
+
recordCheckoutTermsGrant(request, {
|
|
461
|
+
session,
|
|
462
|
+
livemode: event.livemode === true,
|
|
463
|
+
eventIdPrefix,
|
|
464
|
+
flowPerson,
|
|
465
|
+
}),
|
|
466
|
+
),
|
|
439
467
|
stampRequestPaid(request, { session, eventIdPrefix }),
|
|
440
468
|
stampProposalPaid(request, { session, eventId: event.id, eventIdPrefix }),
|
|
441
469
|
]);
|
|
@@ -531,6 +559,8 @@ type CheckoutSessionObject = {
|
|
|
531
559
|
currency?: string | null;
|
|
532
560
|
customer?: string | null;
|
|
533
561
|
customer_details?: { email?: string | null } | null;
|
|
562
|
+
/** The terms box's answer (#8765 D14): `accepted` when the buyer ticked it. */
|
|
563
|
+
consent?: { terms_of_service?: string | null } | null;
|
|
534
564
|
metadata?: Record<string, string> | null;
|
|
535
565
|
};
|
|
536
566
|
|
|
@@ -750,7 +780,7 @@ async function stampFlowCheckout(
|
|
|
750
780
|
/**
|
|
751
781
|
* The hub checkout's Person (#852 D6): after `stampFlowCheckout`, find or
|
|
752
782
|
* create the Person of the completed checkout's lead
|
|
753
|
-
* (`services/person.ts`'s `
|
|
783
|
+
* (`services/person.ts`'s `resolveCheckoutPerson`). Runs only for a paid (or
|
|
754
784
|
* nothing-due) session of THIS site — the foreign-site filter has already
|
|
755
785
|
* returned — that carries `metadata.flowCheckoutId`. The customer id's ONE
|
|
756
786
|
* source is `session.customer`, written onto the Person only for a
|
|
@@ -758,7 +788,8 @@ async function stampFlowCheckout(
|
|
|
758
788
|
* delivery whose Person step failed is repaired by the next one.
|
|
759
789
|
*
|
|
760
790
|
* Logs `{ eventIdPrefix, flowCheckoutIdPrefix, outcome }` only — never the
|
|
761
|
-
* email or the customer id.
|
|
791
|
+
* email or the customer id. Answers the Person, or `null` when the step did
|
|
792
|
+
* not run or found none (#8765 D14). Never throws: the response stays 200.
|
|
762
793
|
*/
|
|
763
794
|
async function recordFlowCheckoutPerson(
|
|
764
795
|
request: FastifyRequest,
|
|
@@ -771,33 +802,137 @@ async function recordFlowCheckoutPerson(
|
|
|
771
802
|
livemode: boolean;
|
|
772
803
|
eventIdPrefix: string;
|
|
773
804
|
},
|
|
774
|
-
): Promise<
|
|
805
|
+
): Promise<CheckoutPersonResult["person"]> {
|
|
775
806
|
const flowCheckoutId = session.metadata?.["flowCheckoutId"];
|
|
776
|
-
if (!flowCheckoutId) return;
|
|
807
|
+
if (!flowCheckoutId) return null;
|
|
777
808
|
if (
|
|
778
809
|
session.payment_status !== "paid" &&
|
|
779
810
|
session.payment_status !== "no_payment_required"
|
|
780
811
|
) {
|
|
781
|
-
return;
|
|
812
|
+
return null;
|
|
782
813
|
}
|
|
783
814
|
const flowCheckoutIdPrefix = flowCheckoutId.substring(0, 8);
|
|
784
|
-
let
|
|
815
|
+
let result: CheckoutPersonResult;
|
|
785
816
|
try {
|
|
786
|
-
|
|
817
|
+
result = await resolveCheckoutPerson({
|
|
787
818
|
flowCheckoutId,
|
|
788
819
|
stripeSessionId: session.id,
|
|
789
820
|
customer: session.customer,
|
|
790
821
|
livemode,
|
|
791
822
|
});
|
|
792
823
|
} catch {
|
|
793
|
-
|
|
824
|
+
result = { outcome: "person_failed", person: null };
|
|
794
825
|
}
|
|
795
|
-
const line = { eventIdPrefix, flowCheckoutIdPrefix, outcome };
|
|
796
|
-
if (outcome === "person_failed") {
|
|
826
|
+
const line = { eventIdPrefix, flowCheckoutIdPrefix, outcome: result.outcome };
|
|
827
|
+
if (result.outcome === "person_failed") {
|
|
797
828
|
request.log.error(line, "flow checkout person failed — continuing");
|
|
798
829
|
} else {
|
|
799
830
|
request.log.info(line, "flow checkout person");
|
|
800
831
|
}
|
|
832
|
+
return result.person;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** The metadata key the Checkout creators stamp with the terms the box showed (#8765 D12). */
|
|
836
|
+
const TERMS_STAMP_KEY = "ailk_terms";
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* `<purposeId>@<policyVersion>` of the account terms as the registry reads
|
|
840
|
+
* them now — the only stamp a grant is recorded for (SEC-8765-E) — and that
|
|
841
|
+
* version, which `recordCheckoutConsent` checks against the site's registry.
|
|
842
|
+
* Derived, never written down, so a version bump retires every older stamp.
|
|
843
|
+
*/
|
|
844
|
+
function currentTermsStamp(): { stamp: string; policyVersion: number } | null {
|
|
845
|
+
const purpose = consentPurposeById(
|
|
846
|
+
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
847
|
+
ACCOUNT_TERMS_PURPOSE_ID,
|
|
848
|
+
);
|
|
849
|
+
return purpose
|
|
850
|
+
? {
|
|
851
|
+
stamp: `${purpose.id}@${purpose.policyVersion}`,
|
|
852
|
+
policyVersion: purpose.policyVersion,
|
|
853
|
+
}
|
|
854
|
+
: null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The buyer's terms grant (#8765 D14). Writes nothing — and logs why — for a
|
|
859
|
+
* session whose box was not accepted (`consent_absent`: Stripe enforced the
|
|
860
|
+
* box, so the order completes regardless), one that does not carry the
|
|
861
|
+
* current `ailk_terms` stamp (`terms_unstamped`: a Payment Link, an invoice,
|
|
862
|
+
* an older session or other terms), or one whose buyer has no Person
|
|
863
|
+
* (`person_unresolved`). Otherwise `recordCheckoutConsent` writes at most one
|
|
864
|
+
* row per Person and session (`recorded` / `unchanged`), or none when the
|
|
865
|
+
* site's registry has moved past the stamped version (`version_changed`).
|
|
866
|
+
*
|
|
867
|
+
* Logs `{ event, outcome, eventIdPrefix }` only (plus `errName` on `failed`)
|
|
868
|
+
* — never an email or a Person id. Never throws: the response stays 200.
|
|
869
|
+
*/
|
|
870
|
+
async function recordCheckoutTermsGrant(
|
|
871
|
+
request: FastifyRequest,
|
|
872
|
+
{
|
|
873
|
+
session,
|
|
874
|
+
livemode,
|
|
875
|
+
eventIdPrefix,
|
|
876
|
+
flowPerson,
|
|
877
|
+
}: {
|
|
878
|
+
session: CheckoutSessionObject;
|
|
879
|
+
livemode: boolean;
|
|
880
|
+
eventIdPrefix: string;
|
|
881
|
+
/** A flow checkout's Person, from `recordFlowCheckoutPerson`. */
|
|
882
|
+
flowPerson: CheckoutPersonResult["person"];
|
|
883
|
+
},
|
|
884
|
+
): Promise<void> {
|
|
885
|
+
const line = (outcome: string) => ({
|
|
886
|
+
event: "checkout-consent",
|
|
887
|
+
outcome,
|
|
888
|
+
eventIdPrefix,
|
|
889
|
+
});
|
|
890
|
+
try {
|
|
891
|
+
if (session.consent?.terms_of_service !== "accepted") {
|
|
892
|
+
request.log.info(
|
|
893
|
+
line("consent_absent"),
|
|
894
|
+
"checkout terms grant: box not accepted",
|
|
895
|
+
);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const stamp: unknown = session.metadata?.[TERMS_STAMP_KEY];
|
|
899
|
+
const expected = currentTermsStamp();
|
|
900
|
+
if (expected === null || stamp !== expected.stamp) {
|
|
901
|
+
request.log.info(
|
|
902
|
+
line("terms_unstamped"),
|
|
903
|
+
"checkout terms grant: session carries no current terms stamp",
|
|
904
|
+
);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const sessionId = session.id;
|
|
908
|
+
const buyer =
|
|
909
|
+
typeof sessionId === "string" && sessionId !== ""
|
|
910
|
+
? await checkoutBuyerPerson(session, livemode, flowPerson)
|
|
911
|
+
: null;
|
|
912
|
+
if (buyer === null || typeof sessionId !== "string") {
|
|
913
|
+
request.log.info(
|
|
914
|
+
line("person_unresolved"),
|
|
915
|
+
"checkout terms grant: no Person for this buyer",
|
|
916
|
+
);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const outcome = await recordCheckoutConsent({
|
|
920
|
+
workspaceId: buyer.workspaceId,
|
|
921
|
+
personId: buyer.personId,
|
|
922
|
+
sessionId,
|
|
923
|
+
policyVersion: expected.policyVersion,
|
|
924
|
+
});
|
|
925
|
+
request.log.info(line(outcome), "checkout terms grant");
|
|
926
|
+
} catch (err) {
|
|
927
|
+
// The error's class only (a Prisma error's message can carry values).
|
|
928
|
+
request.log.error(
|
|
929
|
+
{
|
|
930
|
+
...line("failed"),
|
|
931
|
+
errName: err instanceof Error ? err.name : "UnknownError",
|
|
932
|
+
},
|
|
933
|
+
"checkout terms grant failed — continuing",
|
|
934
|
+
);
|
|
935
|
+
}
|
|
801
936
|
}
|
|
802
937
|
|
|
803
938
|
/**
|
|
@@ -50,13 +50,16 @@ import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
|
50
50
|
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
51
51
|
import {
|
|
52
52
|
hubSignInClientRoutes,
|
|
53
|
+
hubSignInPublicRoutes,
|
|
53
54
|
hubSignInSessionRoutes,
|
|
54
55
|
} from "./routes/auth/hub-sign-in.js";
|
|
55
56
|
import { tokenRoutes } from "./routes/auth/tokens.js";
|
|
56
57
|
import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
|
|
57
58
|
import { userRoutes } from "./routes/auth/user.js";
|
|
58
59
|
import {
|
|
60
|
+
consentAccountRoutes,
|
|
59
61
|
consentCookieRoute,
|
|
62
|
+
consentSignUpRoute,
|
|
60
63
|
consentUnsubscribeRoute,
|
|
61
64
|
} from "./routes/consent/index.js";
|
|
62
65
|
import { getPageRoute } from "./routes/content/get-page.js";
|
|
@@ -980,6 +983,14 @@ export async function build() {
|
|
|
980
983
|
// preHandler's hub-credential branch (D6).
|
|
981
984
|
await authScope.register(hubSignInSessionRoutes);
|
|
982
985
|
|
|
986
|
+
// #8765 D5 — the caller's account-level sign-up agreements. Auth only,
|
|
987
|
+
// NOT tenant-guarded: the agreement is about the `User` row, read and
|
|
988
|
+
// written in the deployment's own site workspace whatever site asks.
|
|
989
|
+
// Unconditional — auth and consent are never-gate surfaces — and both
|
|
990
|
+
// are in `HUB_SESSION_ROUTES`, so a spoke reaches them with its member's
|
|
991
|
+
// credential (the account layout's interstitial, D7).
|
|
992
|
+
await authScope.register(consentAccountRoutes);
|
|
993
|
+
|
|
983
994
|
// Tenant-context routes (multi-tenant module) — auth only. They
|
|
984
995
|
// establish/inspect the active workspace, so the tenant guard does not
|
|
985
996
|
// apply to them; each enforces membership in the service layer.
|
|
@@ -1378,6 +1389,10 @@ export async function build() {
|
|
|
1378
1389
|
await server.register(searchContentRoute);
|
|
1379
1390
|
await server.register(listPagesRoute);
|
|
1380
1391
|
await server.register(getPageRoute);
|
|
1392
|
+
// The hub's sign-in page names the spoke that sent a visitor (#8790 D2):
|
|
1393
|
+
// a public read of one client's name + origin from the registry. No auth —
|
|
1394
|
+
// the visitor is signed out by definition when the page asks.
|
|
1395
|
+
await server.register(hubSignInPublicRoutes);
|
|
1381
1396
|
// Checkout launcher + reader — commerce PRIMITIVES (D-64, #5796; the GET
|
|
1382
1397
|
// added by #6084). Registered UNCONDITIONALLY and gated at CALL time on
|
|
1383
1398
|
// the JWT `commerce` claim, the same posture content-write has
|
|
@@ -1570,6 +1585,19 @@ export async function build() {
|
|
|
1570
1585
|
});
|
|
1571
1586
|
await unsubscribeScope.register(consentUnsubscribeRoute);
|
|
1572
1587
|
});
|
|
1588
|
+
// #8765 D2 — the sign-up agreements' evidence, beside the consent floor:
|
|
1589
|
+
// anonymous, keyless, outside every module gate (sign-up and consent are
|
|
1590
|
+
// never-gate surfaces). Its own scope, `signupevidence`, so an anonymous
|
|
1591
|
+
// burst never shares a bucket with the cookie banner's log (SEC-8765-D).
|
|
1592
|
+
// Its main caller is the web's own server, whose cache keeps it to one call
|
|
1593
|
+
// a minute per enabled locale per instance.
|
|
1594
|
+
await server.register(async (signUpEvidenceScope) => {
|
|
1595
|
+
signUpEvidenceScope.register(rateLimitMiddleware, {
|
|
1596
|
+
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1597
|
+
scope: "signupevidence",
|
|
1598
|
+
});
|
|
1599
|
+
await signUpEvidenceScope.register(consentSignUpRoute);
|
|
1600
|
+
});
|
|
1573
1601
|
|
|
1574
1602
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
1575
1603
|
// default (10/min, pre-#4730) rather than inheriting the wider
|