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
package/dist/spoke-checkout.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The spoke checkout slice (#7838) — what a spoke carrying `commerce`
|
|
3
|
-
* `apps/web`: the
|
|
4
|
-
* the
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The spoke checkout slice (#7838, #8748) — what a spoke carrying `commerce`
|
|
3
|
+
* gets in `apps/web`: the standard spoke checkout, copied from the template
|
|
4
|
+
* tree. That is the buy route and the Stripe webhook route (allowed to import
|
|
5
|
+
* the checkout package, #7873), `lib/checkout.ts` (the standard fulfilment),
|
|
6
|
+
* the site's own `lib/checkout-config.ts`, the thank-you and unavailable
|
|
7
|
+
* return pages and their tests (`SPOKE_CHECKOUT_TEMPLATE_PATHS`). A spoke that
|
|
8
|
+
* signs in through its hub also gets the billing action, the third file
|
|
9
|
+
* allowed to import the package, and the web seam render that reaches it
|
|
10
|
+
* (#8217 D6–D8); that action and the checkout e2e workflow are still rendered
|
|
11
|
+
* here.
|
|
8
12
|
*
|
|
9
13
|
* Every guarantee lives in `@working-theory/spoke-checkout` and arrives by a
|
|
10
|
-
* package bump;
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* package bump; the copied files are `apps/web` source, tested in the
|
|
15
|
+
* monorepo and kept current by `create-ailk update`, and apart from the
|
|
16
|
+
* config's one stamp line they are byte-identical across spokes at one
|
|
17
|
+
* create-ailk version (roadmap D3, AC-SPOKE-02). A hub never gets them: its
|
|
18
|
+
* checkout is `apps/api`'s.
|
|
13
19
|
*
|
|
14
|
-
* Spec: docs/specs/7838-spoke-web-checkout-slice-spec.md (D3–D7, D9)
|
|
20
|
+
* Spec: docs/specs/7838-spoke-web-checkout-slice-spec.md (D3–D7, D9);
|
|
21
|
+
* docs/specs/8748-a-standard-spoke-checkout-route-spec.md (D2, D3).
|
|
15
22
|
*
|
|
16
|
-
* Pure module: no `import.meta.url`, so jest imports it directly
|
|
17
|
-
* package specifier in a rendered file is
|
|
18
|
-
* monorepo's own architecture lint never
|
|
19
|
-
* create-ailk makes (the
|
|
23
|
+
* Pure module: no `import.meta.url`, so jest imports it directly — the caller
|
|
24
|
+
* passes `templatesDir`. Every package specifier in a rendered file is
|
|
25
|
+
* interpolated from a constant, so the monorepo's own architecture lint never
|
|
26
|
+
* reads a rendered line as an import create-ailk makes (the
|
|
27
|
+
* `CONTENT_ADAPTERS_PACKAGE` idiom in topology.ts).
|
|
20
28
|
*/
|
|
21
29
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
-
import { dirname, join } from "node:path";
|
|
30
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
|
|
31
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
32
|
+
import { buildScaffoldMarkers, renderTemplateFile, templateDestination } from "./copy.js";
|
|
24
33
|
import { SPOKE_CHECKOUT_ROUTE_FILES } from "./derive-architecture.js";
|
|
25
34
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
26
35
|
import { diffTwoStrings, } from "./lib/extract-module-bundle.js";
|
|
@@ -29,9 +38,7 @@ import { TopologyError, WEB_SEAM_PATH, renderWebSeam, } from "./topology.js";
|
|
|
29
38
|
/** The checkout package a spoke's two route files import. */
|
|
30
39
|
export const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
|
|
31
40
|
const API_CLIENT_PACKAGE = "@working-theory/api-client";
|
|
32
|
-
|
|
33
|
-
const VALIDATION_PACKAGE = "@working-theory/validation";
|
|
34
|
-
/** The site's checkout data — written once, the site's from then on. */
|
|
41
|
+
/** The site's checkout data — written once, the site's from then on (#8748 D2). */
|
|
35
42
|
export const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
|
|
36
43
|
/** The two return pages: Stripe's success URL and every refused buy's target. */
|
|
37
44
|
export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
|
|
@@ -39,12 +46,19 @@ export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
|
|
|
39
46
|
"apps/web/app/[locale]/checkout/unavailable/page.tsx",
|
|
40
47
|
]);
|
|
41
48
|
/**
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
49
|
+
* The standard spoke checkout's template paths, scaffold-relative (#8748 D3).
|
|
50
|
+
* A directory entry covers every file under it, so the tests that sit beside
|
|
51
|
+
* the routes and pages travel with them. `copyTemplate` skips all of them for
|
|
52
|
+
* every scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
|
|
46
53
|
*/
|
|
47
|
-
export const
|
|
54
|
+
export const SPOKE_CHECKOUT_TEMPLATE_PATHS = Object.freeze([
|
|
55
|
+
"apps/web/app/api/checkout",
|
|
56
|
+
"apps/web/app/api/webhooks/stripe",
|
|
57
|
+
"apps/web/app/[locale]/checkout",
|
|
58
|
+
"apps/web/lib/checkout.ts",
|
|
59
|
+
SPOKE_CHECKOUT_CONFIG_PATH,
|
|
60
|
+
"apps/web/lib/__tests__/checkout.test.ts",
|
|
61
|
+
]);
|
|
48
62
|
/** The variables a commerce spoke depends on (D9). The package reads three of them. */
|
|
49
63
|
export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
|
|
50
64
|
STRIPE_SECRET_KEY: {
|
|
@@ -59,10 +73,9 @@ export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
|
|
|
59
73
|
description: `Read by ${SPOKE_CHECKOUT_PACKAGE}: test (the default) or live. Live is the operator's switch, per site.`,
|
|
60
74
|
secret: false,
|
|
61
75
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
76
|
+
// Read by the standard spoke checkout's fulfilment, so the base catalog
|
|
77
|
+
// carries it too (#8748).
|
|
78
|
+
AILK_SPOKE_KEY: ENV_CATALOG["AILK_SPOKE_KEY"],
|
|
66
79
|
});
|
|
67
80
|
/** The names a commerce spoke declares although only the package reads them (D9). */
|
|
68
81
|
export const SPOKE_CHECKOUT_DECLARED_ENV = Object.freeze([
|
|
@@ -112,34 +125,6 @@ export function spokeCheckoutSite(projectName) {
|
|
|
112
125
|
const digest = createHash("sha256").update(projectName).digest("hex");
|
|
113
126
|
return `site-${digest.slice(0, 8)}`;
|
|
114
127
|
}
|
|
115
|
-
/** One route file (D4): three lines of wiring, every guarantee in the package. */
|
|
116
|
-
function renderRoute(opts) {
|
|
117
|
-
return [
|
|
118
|
-
`// ${opts.title} — GENERATED by create-ailk (#7838). Do not edit: every`,
|
|
119
|
-
"// guarantee lives in the spoke-checkout package and arrives by a package",
|
|
120
|
-
"// bump. This site's data is lib/checkout-config.ts.",
|
|
121
|
-
`import { createSpokeCheckout } from "${SPOKE_CHECKOUT_PACKAGE}";`,
|
|
122
|
-
"",
|
|
123
|
-
`import { checkoutConfig } from "../../../../lib/checkout-config";`,
|
|
124
|
-
"",
|
|
125
|
-
`export const runtime = "nodejs";`,
|
|
126
|
-
`export const dynamic = "force-dynamic";`,
|
|
127
|
-
"",
|
|
128
|
-
`// platform-decisions-waiver: api-versioning — ${opts.waiver} (spec 7838 §7)`,
|
|
129
|
-
opts.exportLine,
|
|
130
|
-
"",
|
|
131
|
-
].join("\n");
|
|
132
|
-
}
|
|
133
|
-
const BUY_ROUTE = renderRoute({
|
|
134
|
-
title: "The buy route",
|
|
135
|
-
waiver: "a browser buy link that redirects to Stripe Checkout, not a versioned API contract",
|
|
136
|
-
exportLine: "export const { GET, POST } = createSpokeCheckout(checkoutConfig).buy;",
|
|
137
|
-
});
|
|
138
|
-
const WEBHOOK_ROUTE = renderRoute({
|
|
139
|
-
title: "The Stripe webhook route",
|
|
140
|
-
waiver: "the endpoint URL registered in Stripe, not a versioned API contract",
|
|
141
|
-
exportLine: "export const { POST } = createSpokeCheckout(checkoutConfig).webhook;",
|
|
142
|
-
});
|
|
143
128
|
/**
|
|
144
129
|
* The spoke billing action (#8217 D6): wiring only, byte-identical across
|
|
145
130
|
* sites, so every guarantee arrives by a package bump. A plain server module
|
|
@@ -156,10 +141,10 @@ const BILLING_ACTION = [
|
|
|
156
141
|
`import { member } from "${API_CLIENT_PACKAGE}";`,
|
|
157
142
|
`import { createSpokeCheckout } from "${SPOKE_CHECKOUT_PACKAGE}";`,
|
|
158
143
|
"",
|
|
159
|
-
`import {
|
|
144
|
+
`import { spokeCheckoutConfig } from "../../../../../lib/checkout";`,
|
|
160
145
|
`import { forwardedCookieInit } from "../../../../../lib/server-api";`,
|
|
161
146
|
"",
|
|
162
|
-
"const billing = createSpokeCheckout(
|
|
147
|
+
"const billing = createSpokeCheckout(spokeCheckoutConfig).billing.forMember(",
|
|
163
148
|
" async () => member.billing(await forwardedCookieInit()),",
|
|
164
149
|
");",
|
|
165
150
|
"",
|
|
@@ -167,162 +152,6 @@ const BILLING_ACTION = [
|
|
|
167
152
|
"export const spokeBillingPortalUrl = billing.portalUrl;",
|
|
168
153
|
"",
|
|
169
154
|
].join("\n");
|
|
170
|
-
/** The site's checkout data (D5). Only the `const SITE` line varies by site. */
|
|
171
|
-
function renderConfig(site) {
|
|
172
|
-
return [
|
|
173
|
-
"// checkout-config.ts — this spoke's checkout, as data (#7838).",
|
|
174
|
-
"//",
|
|
175
|
-
"// Written once by create-ailk and yours from then on: create-ailk never",
|
|
176
|
-
"// rewrites it. The buy route and the Stripe webhook route build their",
|
|
177
|
-
"// handlers from it, and so does the billing action on a spoke that signs",
|
|
178
|
-
"// in through its hub. Only those files may import the checkout package",
|
|
179
|
-
"// (architecture.yaml), so this file never imports it, stripe-client or the",
|
|
180
|
-
"// stripe SDK. Keep it to data and the fulfil callback.",
|
|
181
|
-
"//",
|
|
182
|
-
"// site Stamped on every Checkout Session as metadata.site. This site's",
|
|
183
|
-
"// webhook acts only on its own stamp, and the hub ignores any stamp",
|
|
184
|
-
"// that is not its own, so the stamp must be unique on the Stripe",
|
|
185
|
-
'// account this site shares. Never "default". Change it only before',
|
|
186
|
-
"// any price exists: every lookup key starts with it.",
|
|
187
|
-
"// offerings slug -> Stripe lookup_key. The buy link is /api/checkout/<slug>.",
|
|
188
|
-
"// scaffold-commerce replaces the placeholder below.",
|
|
189
|
-
"// fulfil Runs for each paid session, at least once. It posts one lead to",
|
|
190
|
-
"// the hub with the session id as its Idempotency-Key, so a repeat",
|
|
191
|
-
"// writes nothing new. The hub dedupes only an identical body: change",
|
|
192
|
-
"// the payload's shape only when no webhook is failing.",
|
|
193
|
-
`import { leads } from "${API_CLIENT_PACKAGE}";`,
|
|
194
|
-
"",
|
|
195
|
-
`import { resolveBaseUrl } from "../src/lib/routes";`,
|
|
196
|
-
"",
|
|
197
|
-
`const SITE = "${site}";`,
|
|
198
|
-
"",
|
|
199
|
-
"/** What this site's fulfil callback reads from a completed checkout. */",
|
|
200
|
-
"interface CompletedCheckout {",
|
|
201
|
-
" idempotencyKey: string;",
|
|
202
|
-
" sessionId: string;",
|
|
203
|
-
" offering: string;",
|
|
204
|
-
' kind: "fixed" | "quoted";',
|
|
205
|
-
' mode: "test" | "live";',
|
|
206
|
-
" amountTotal: number | null;",
|
|
207
|
-
" currency: string | null;",
|
|
208
|
-
" email: string | null;",
|
|
209
|
-
" customerId: string | null;",
|
|
210
|
-
"}",
|
|
211
|
-
"",
|
|
212
|
-
"export const checkoutConfig = {",
|
|
213
|
-
" site: SITE,",
|
|
214
|
-
" siteBaseUrl: resolveBaseUrl(),",
|
|
215
|
-
" offerings: {",
|
|
216
|
-
" // A placeholder until scaffold-commerce creates this site's prices.",
|
|
217
|
-
' example: { kind: "fixed", lookupKey: `${SITE}_example`, cancelPath: "/" },',
|
|
218
|
-
" },",
|
|
219
|
-
" async fulfil(checkout: CompletedCheckout): Promise<void> {",
|
|
220
|
-
" if (checkout.email === null) return;",
|
|
221
|
-
" // No spoke key: the hub cannot place the lead in this site's workspace.",
|
|
222
|
-
" // Fail, so Stripe retries and the dashboard shows a failed webhook.",
|
|
223
|
-
" if (!process.env.AILK_SPOKE_KEY)",
|
|
224
|
-
' throw new Error("AILK_SPOKE_KEY is not set");',
|
|
225
|
-
" const result = await leads.submit(",
|
|
226
|
-
" {",
|
|
227
|
-
" siteId: SITE,",
|
|
228
|
-
' source: "web",',
|
|
229
|
-
" email: checkout.email,",
|
|
230
|
-
" payload: {",
|
|
231
|
-
" site: SITE,",
|
|
232
|
-
' page: "checkout",',
|
|
233
|
-
" offering: checkout.offering,",
|
|
234
|
-
" checkoutKind: checkout.kind,",
|
|
235
|
-
" mode: checkout.mode,",
|
|
236
|
-
" amountTotal: checkout.amountTotal,",
|
|
237
|
-
" currency: checkout.currency,",
|
|
238
|
-
" stripeCustomerId: checkout.customerId,",
|
|
239
|
-
" checkoutSession: checkout.sessionId,",
|
|
240
|
-
" },",
|
|
241
|
-
" },",
|
|
242
|
-
' { headers: { "Idempotency-Key": checkout.idempotencyKey } },',
|
|
243
|
-
" );",
|
|
244
|
-
" // A replay under the same key answers 201 with the first lead; any",
|
|
245
|
-
" // failure (422 idempotency_key_reused included) makes Stripe retry.",
|
|
246
|
-
" if (!result.ok) throw new Error(`hub lead write answered ${result.status}`);",
|
|
247
|
-
" },",
|
|
248
|
-
"};",
|
|
249
|
-
"",
|
|
250
|
-
].join("\n");
|
|
251
|
-
}
|
|
252
|
-
/** One return page (D7): a server component that reads nothing but its copy. */
|
|
253
|
-
function renderPage(opts) {
|
|
254
|
-
return [
|
|
255
|
-
`// ${opts.title} — ${SPOKE_CHECKOUT_PAGE_MARKER}.`,
|
|
256
|
-
...opts.about.map((line) => `// ${line}`),
|
|
257
|
-
`import { SectionRenderer } from "${UI_SECTIONS_PACKAGE}";`,
|
|
258
|
-
`import type { PageSection } from "${VALIDATION_PACKAGE}";`,
|
|
259
|
-
`import type { Metadata } from "next";`,
|
|
260
|
-
`import { getTranslations } from "next-intl/server";`,
|
|
261
|
-
`import type { ReactElement } from "react";`,
|
|
262
|
-
"",
|
|
263
|
-
"interface Props {",
|
|
264
|
-
" params: Promise<{ locale: string }>;",
|
|
265
|
-
"}",
|
|
266
|
-
"",
|
|
267
|
-
"export async function generateMetadata({ params }: Props): Promise<Metadata> {",
|
|
268
|
-
" const { locale } = await params;",
|
|
269
|
-
` const t = await getTranslations({ locale, namespace: "${opts.namespace}" });`,
|
|
270
|
-
` return { title: t("title"), robots: { index: false, follow: false } };`,
|
|
271
|
-
"}",
|
|
272
|
-
"",
|
|
273
|
-
"export default async function Page({ params }: Props): Promise<ReactElement> {",
|
|
274
|
-
" const { locale } = await params;",
|
|
275
|
-
` const t = await getTranslations({ locale, namespace: "${opts.namespace}" });`,
|
|
276
|
-
" const sections: PageSection[] = [",
|
|
277
|
-
...opts.sections.map((line) => ` ${line}`),
|
|
278
|
-
" ];",
|
|
279
|
-
" return (",
|
|
280
|
-
' <main id="main">',
|
|
281
|
-
" <SectionRenderer sections={sections} locale={locale} leadingSectionOwnsTitle />",
|
|
282
|
-
" </main>",
|
|
283
|
-
" );",
|
|
284
|
-
"}",
|
|
285
|
-
"",
|
|
286
|
-
].join("\n");
|
|
287
|
-
}
|
|
288
|
-
const THANK_YOU_PAGE = renderPage({
|
|
289
|
-
title: "The checkout thank-you page",
|
|
290
|
-
about: [
|
|
291
|
-
"Stripe returns a buyer here after paying. It reads no search parameter and",
|
|
292
|
-
"calls neither Stripe nor the hub: the webhook is what fulfils the order.",
|
|
293
|
-
],
|
|
294
|
-
namespace: "checkoutThankYou",
|
|
295
|
-
sections: [
|
|
296
|
-
"{",
|
|
297
|
-
' id: "checkout-thank-you",',
|
|
298
|
-
' type: "content-block",',
|
|
299
|
-
' data: { headline: t("title"), body: t("body") },',
|
|
300
|
-
"},",
|
|
301
|
-
],
|
|
302
|
-
});
|
|
303
|
-
const UNAVAILABLE_PAGE = renderPage({
|
|
304
|
-
title: "The checkout unavailable page",
|
|
305
|
-
about: [
|
|
306
|
-
"Every refused buy lands here. It never says why: a reason is a probe's",
|
|
307
|
-
"answer, and the buyer needs only that nothing was charged.",
|
|
308
|
-
],
|
|
309
|
-
namespace: "checkoutUnavailable",
|
|
310
|
-
sections: [
|
|
311
|
-
"{",
|
|
312
|
-
' id: "checkout-unavailable",',
|
|
313
|
-
' type: "content-block",',
|
|
314
|
-
' data: { headline: t("title"), body: t("body") },',
|
|
315
|
-
"},",
|
|
316
|
-
"{",
|
|
317
|
-
' id: "checkout-unavailable-cta",',
|
|
318
|
-
' type: "cta",',
|
|
319
|
-
" data: {",
|
|
320
|
-
' headline: t("ctaHeadline"),',
|
|
321
|
-
' primaryCta: { label: t("ctaLabel"), href: "/contact" },',
|
|
322
|
-
" },",
|
|
323
|
-
"},",
|
|
324
|
-
],
|
|
325
|
-
});
|
|
326
155
|
// ── #7839 D8 — the checkout e2e workflow ─────────────────────────────────────
|
|
327
156
|
/** Where a commerce spoke's end-to-end checkout workflow lives. */
|
|
328
157
|
export const SPOKE_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
|
|
@@ -497,28 +326,69 @@ jobs:
|
|
|
497
326
|
retention-days: 7
|
|
498
327
|
`;
|
|
499
328
|
/**
|
|
500
|
-
* The slice for `site
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
329
|
+
* The rendered part of the slice for `site` (#8748 D3): with `signIn: "hub"`
|
|
330
|
+
* the billing action (#8217 D8), then the checkout e2e workflow (#7839 D8).
|
|
331
|
+
* Neither depends on `site`. The rest of the slice is template source,
|
|
332
|
+
* copied by `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid
|
|
333
|
+
* stamp (SEC-7838-10).
|
|
505
334
|
*/
|
|
506
335
|
export function renderSpokeCheckoutFiles(site, opts = {}) {
|
|
507
336
|
assertSite(site);
|
|
508
|
-
const
|
|
509
|
-
const [thankYouPath, unavailablePath] = SPOKE_CHECKOUT_PAGE_FILES;
|
|
337
|
+
const billingActionPath = SPOKE_CHECKOUT_ROUTE_FILES[2];
|
|
510
338
|
return [
|
|
511
|
-
[buyPath, BUY_ROUTE],
|
|
512
|
-
[webhookPath, WEBHOOK_ROUTE],
|
|
513
339
|
...(opts.signIn === "hub"
|
|
514
340
|
? [[billingActionPath, BILLING_ACTION]]
|
|
515
341
|
: []),
|
|
516
|
-
[SPOKE_CHECKOUT_CONFIG_PATH, renderConfig(site)],
|
|
517
|
-
[thankYouPath, THANK_YOU_PAGE],
|
|
518
|
-
[unavailablePath, UNAVAILABLE_PAGE],
|
|
519
342
|
[SPOKE_CHECKOUT_E2E_WORKFLOW_PATH, CHECKOUT_E2E_WORKFLOW],
|
|
520
343
|
];
|
|
521
344
|
}
|
|
345
|
+
/** The one line of the template's config the writer stamps (#8748 D2). */
|
|
346
|
+
const SITE_LINE = /^const SITE = "[a-z0-9-]+";$/gm;
|
|
347
|
+
/**
|
|
348
|
+
* Every template file under `SPOKE_CHECKOUT_TEMPLATE_PATHS`, sorted, as its
|
|
349
|
+
* destination path and its source. A symlink is skipped, never followed, as
|
|
350
|
+
* `copyTemplate` skips one; a `.gitkeep` has no destination.
|
|
351
|
+
*/
|
|
352
|
+
function templateCheckoutFiles(templatesDir) {
|
|
353
|
+
const out = [];
|
|
354
|
+
const visit = (rel) => {
|
|
355
|
+
const abs = join(templatesDir, ...rel.split("/"));
|
|
356
|
+
if (!existsSync(abs))
|
|
357
|
+
return;
|
|
358
|
+
const stat = lstatSync(abs);
|
|
359
|
+
if (stat.isSymbolicLink())
|
|
360
|
+
return;
|
|
361
|
+
if (stat.isDirectory()) {
|
|
362
|
+
for (const name of readdirSync(abs))
|
|
363
|
+
visit(`${rel}/${name}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const dest = templateDestination(rel);
|
|
367
|
+
if (dest !== undefined)
|
|
368
|
+
out.push([dest, abs]);
|
|
369
|
+
};
|
|
370
|
+
for (const entry of SPOKE_CHECKOUT_TEMPLATE_PATHS)
|
|
371
|
+
visit(entry);
|
|
372
|
+
return out.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
373
|
+
}
|
|
374
|
+
/** Whether anything — a dangling symlink included — sits at `abs`. */
|
|
375
|
+
function lstatExists(abs) {
|
|
376
|
+
try {
|
|
377
|
+
lstatSync(abs);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Whether a path, or (when absent) its nearest existing ancestor, resolves inside `targetReal`. */
|
|
385
|
+
function resolvesInside(targetReal, abs) {
|
|
386
|
+
let probe = abs;
|
|
387
|
+
while (!existsSync(probe) && dirname(probe) !== probe)
|
|
388
|
+
probe = dirname(probe);
|
|
389
|
+
const real = realpathSync(probe);
|
|
390
|
+
return real === targetReal || real.startsWith(targetReal + sep);
|
|
391
|
+
}
|
|
522
392
|
const WEB_PACKAGE_JSON = "apps/web/package.json";
|
|
523
393
|
/** `deps` with `name: version` added in sorted position, every other key kept in place. */
|
|
524
394
|
function withDependency(deps, name, version) {
|
|
@@ -556,28 +426,78 @@ function isOlderPin(current, wanted) {
|
|
|
556
426
|
return false;
|
|
557
427
|
}
|
|
558
428
|
/**
|
|
559
|
-
* Write the slice into `targetDir` (D3).
|
|
560
|
-
*
|
|
429
|
+
* Write the slice into `targetDir` (#7838 D3, #8748 D3). The standard's
|
|
430
|
+
* template files (`SPOKE_CHECKOUT_TEMPLATE_PATHS`) are copied from
|
|
431
|
+
* `templatesDir`, each rendered with `copyTemplate`'s own renderer and
|
|
432
|
+
* markers, so a recorded hash equals what `create-ailk update` renders; the
|
|
433
|
+
* config's one `const SITE` line is stamped with `site`. `onWrite` is called
|
|
434
|
+
* once per template file written, as `copyTemplate` calls it. Then come the
|
|
435
|
+
* rendered files (`renderSpokeCheckoutFiles`). A file that already exists is
|
|
436
|
+
* left byte-for-byte and reported `skipped` — the config is the site's once
|
|
561
437
|
* written. With `signIn: "hub"` (#8217 D8) the slice carries the billing
|
|
562
438
|
* action, and the web seam — generated, so always rewritten — becomes the
|
|
563
|
-
* billing render: reported `patched` when it changed, not at all when equal.
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
439
|
+
* billing render: reported `patched` when it changed, not at all when equal.
|
|
440
|
+
* Adds the package to `apps/web/package.json` when absent, and raises a plain
|
|
441
|
+
* pin older than the one it writes (#7839): the generated checkout e2e
|
|
442
|
+
* workflow runs a command a `^0.2.0` pin never reaches. Any other range —
|
|
443
|
+
* newer, `workspace:`, a tag — is the site's and is kept.
|
|
567
444
|
* Every refusal (an invalid stamp, a version map without the package, a
|
|
568
|
-
* missing or malformed `apps/web/package.json
|
|
445
|
+
* missing or malformed `apps/web/package.json`, a template missing any
|
|
446
|
+
* `SPOKE_CHECKOUT_TEMPLATE_PATHS` entry, a template config without exactly
|
|
447
|
+
* one `const SITE` line, a destination to write that resolves outside
|
|
448
|
+
* `targetDir`) throws before the first write.
|
|
569
449
|
*/
|
|
570
450
|
export function writeSpokeCheckoutSlice(targetDir, opts) {
|
|
571
|
-
const
|
|
451
|
+
const rendered = renderSpokeCheckoutFiles(opts.site, { signIn: opts.signIn });
|
|
572
452
|
const range = JSON.parse(substituteWorkspaceVersions(`{"${SPOKE_CHECKOUT_PACKAGE}": "workspace:*"}`, opts.versions));
|
|
573
453
|
const version = range[SPOKE_CHECKOUT_PACKAGE];
|
|
574
454
|
const pkgPath = join(targetDir, WEB_PACKAGE_JSON);
|
|
575
455
|
const before = readFileSync(pkgPath, "utf8");
|
|
576
456
|
const pkg = JSON.parse(before);
|
|
457
|
+
const markers = buildScaffoldMarkers(opts.projectName);
|
|
458
|
+
const copied = [];
|
|
459
|
+
for (const [path, source] of templateCheckoutFiles(opts.templatesDir)) {
|
|
460
|
+
let bytes = Buffer.from(renderTemplateFile(readFileSync(source), path, opts.versions, markers));
|
|
461
|
+
if (path === SPOKE_CHECKOUT_CONFIG_PATH) {
|
|
462
|
+
const text = bytes.toString("utf8");
|
|
463
|
+
const lines = text.match(SITE_LINE) ?? [];
|
|
464
|
+
if (lines.length !== 1) {
|
|
465
|
+
throw new TopologyError(`spoke checkout: the template's ${SPOKE_CHECKOUT_CONFIG_PATH} must hold exactly one const SITE line, found ${lines.length}`);
|
|
466
|
+
}
|
|
467
|
+
bytes = Buffer.from(text.replace(SITE_LINE, `const SITE = "${opts.site}";`), "utf8");
|
|
468
|
+
}
|
|
469
|
+
copied.push([path, bytes]);
|
|
470
|
+
}
|
|
471
|
+
const missing = SPOKE_CHECKOUT_TEMPLATE_PATHS.filter((entry) => !copied.some(([path]) => path === entry || path.startsWith(`${entry}/`)));
|
|
472
|
+
if (missing.length > 0) {
|
|
473
|
+
throw new TopologyError(`spoke checkout: the template has no ${missing.join(", ")}`);
|
|
474
|
+
}
|
|
475
|
+
// A file already there is skipped below, never written, so only a path
|
|
476
|
+
// that will be written must resolve inside the site.
|
|
477
|
+
const targetReal = realpathSync(targetDir);
|
|
478
|
+
for (const [path] of [...copied, ...rendered]) {
|
|
479
|
+
const abs = resolve(targetDir, path);
|
|
480
|
+
if (lstatExists(abs))
|
|
481
|
+
continue;
|
|
482
|
+
if (!resolvesInside(targetReal, abs)) {
|
|
483
|
+
throw new TopologyError(`spoke checkout: refusing to write outside the site: ${path}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
577
486
|
const rows = [];
|
|
578
|
-
for (const [path,
|
|
487
|
+
for (const [path, bytes] of copied) {
|
|
488
|
+
const abs = join(targetDir, path);
|
|
489
|
+
if (lstatExists(abs)) {
|
|
490
|
+
rows.push({ path, action: "skipped", wanted: bytes.toString("utf8") });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
494
|
+
writeFileSync(abs, bytes);
|
|
495
|
+
opts.onWrite?.(path, bytes);
|
|
496
|
+
rows.push({ path, action: "written" });
|
|
497
|
+
}
|
|
498
|
+
for (const [path, text] of rendered) {
|
|
579
499
|
const abs = join(targetDir, path);
|
|
580
|
-
if (
|
|
500
|
+
if (lstatExists(abs)) {
|
|
581
501
|
rows.push({ path, action: "skipped", wanted: text });
|
|
582
502
|
continue;
|
|
583
503
|
}
|
|
@@ -617,3 +537,24 @@ export function writeSpokeCheckoutSlice(targetDir, opts) {
|
|
|
617
537
|
}
|
|
618
538
|
return rows;
|
|
619
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Drop the checkout package from a scaffold that does not get the slice — a
|
|
542
|
+
* hub, or a spoke without `commerce` (#8748 D3). The template's
|
|
543
|
+
* `apps/web/package.json` depends on it because the monorepo's `apps/web`
|
|
544
|
+
* carries the standard. Every other key is kept in place; a site without the
|
|
545
|
+
* dependency, or without `apps/web/package.json`, is left as it is.
|
|
546
|
+
*/
|
|
547
|
+
export function dropSpokeCheckoutDependency(targetDir) {
|
|
548
|
+
const pkgPath = join(targetDir, WEB_PACKAGE_JSON);
|
|
549
|
+
if (!existsSync(pkgPath))
|
|
550
|
+
return;
|
|
551
|
+
const before = readFileSync(pkgPath, "utf8");
|
|
552
|
+
const pkg = JSON.parse(before);
|
|
553
|
+
const deps = pkg.dependencies;
|
|
554
|
+
if (deps === undefined ||
|
|
555
|
+
!Object.prototype.hasOwnProperty.call(deps, SPOKE_CHECKOUT_PACKAGE)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
delete deps[SPOKE_CHECKOUT_PACKAGE];
|
|
559
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${before.endsWith("\n") ? "\n" : ""}`);
|
|
560
|
+
}
|
package/dist/starter-grid.js
CHANGED
|
@@ -117,6 +117,10 @@ const TEMPLATE_BASE_ROSTER = [
|
|
|
117
117
|
{ route: "/sign-up" },
|
|
118
118
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
119
119
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
120
|
+
{
|
|
121
|
+
route: "/working-agreement",
|
|
122
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
123
|
+
},
|
|
120
124
|
{
|
|
121
125
|
route: "/purchase-policy",
|
|
122
126
|
content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
|
|
@@ -235,6 +239,10 @@ const ORGANIZATION_SERVICE_ROWS = [
|
|
|
235
239
|
{ route: "/sign-up" },
|
|
236
240
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
237
241
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
242
|
+
{
|
|
243
|
+
route: "/working-agreement",
|
|
244
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
245
|
+
},
|
|
238
246
|
{
|
|
239
247
|
route: "/purchase-policy",
|
|
240
248
|
content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
|
|
@@ -267,6 +275,10 @@ const LOCAL_BUSINESS_SERVICE_ROWS = [
|
|
|
267
275
|
},
|
|
268
276
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
269
277
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
278
|
+
{
|
|
279
|
+
route: "/working-agreement",
|
|
280
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
281
|
+
},
|
|
270
282
|
];
|
|
271
283
|
/**
|
|
272
284
|
* `person-service`'s own rows (#6758 D1, D3), derived from
|
|
@@ -302,6 +314,10 @@ const PERSON_SERVICE_ROWS = [
|
|
|
302
314
|
},
|
|
303
315
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
304
316
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
317
|
+
{
|
|
318
|
+
route: "/working-agreement",
|
|
319
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
320
|
+
},
|
|
305
321
|
];
|
|
306
322
|
/**
|
|
307
323
|
* Each buildable shape's standard roster (#6753 D3) — the rows an alias starts
|
package/dist/surfaces.js
CHANGED
|
@@ -331,6 +331,13 @@ export const GATEABLE_SURFACES = [
|
|
|
331
331
|
routes: [`${ROUTE_ROOT}/whitepapers`],
|
|
332
332
|
content: ["content/<locale>/Whitepaper"],
|
|
333
333
|
},
|
|
334
|
+
{
|
|
335
|
+
// #8661 D10 — the working agreement, module-less like `privacy` and
|
|
336
|
+
// `terms`: every shape roster names it right after /terms.
|
|
337
|
+
id: "working-agreement",
|
|
338
|
+
routes: [`${ROUTE_ROOT}/working-agreement`],
|
|
339
|
+
content: ["content/<locale>/WorkingAgreementPage"],
|
|
340
|
+
},
|
|
334
341
|
];
|
|
335
342
|
/** The recognized `--surfaces` vocabulary, sorted. */
|
|
336
343
|
export const RECOGNIZED_SURFACES = GATEABLE_SURFACES.map((s) => s.id);
|
|
@@ -552,6 +559,7 @@ export function isUnservedSurfaceContent(path, enabled) {
|
|
|
552
559
|
*/
|
|
553
560
|
export const ROUTE_SINCE = {
|
|
554
561
|
about: "0.0.0",
|
|
562
|
+
"about/[slug]": "0.42.0",
|
|
555
563
|
"blog/[[...slug]]": "0.0.0",
|
|
556
564
|
careers: "0.0.0",
|
|
557
565
|
"careers/[slug]": "0.0.0",
|
|
@@ -638,6 +646,9 @@ export const ROUTE_SINCE = {
|
|
|
638
646
|
"vs/[slug]": "0.28.0",
|
|
639
647
|
whitepapers: "0.0.0",
|
|
640
648
|
"whitepapers/[slug]": "0.0.0",
|
|
649
|
+
// #8661 D10 — the working-agreement page. Genuinely new, so it attaches at
|
|
650
|
+
// the fixed group's next release line, the same rule #7430 applied.
|
|
651
|
+
"working-agreement": "0.42.0",
|
|
641
652
|
// #5014 — the (authed) route GROUP: layout.tsx + sign-in-gate.tsx sit
|
|
642
653
|
// directly under "(authed)" (a Next.js route group — its parens are not a
|
|
643
654
|
// URL segment); account/ and waitlist/ are its two syncable sub-trees.
|
|
@@ -787,38 +798,42 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
787
798
|
"apps/web/lib/auth-routes.ts",
|
|
788
799
|
],
|
|
789
800
|
"(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
|
|
790
|
-
// #
|
|
791
|
-
// carried-cookie mint
|
|
792
|
-
//
|
|
801
|
+
// #8765 D8/D9 — the sign-up page links its agreements through the one
|
|
802
|
+
// localized policy map; the carried-cookie mint (`sign-up.actions.ts`) is
|
|
803
|
+
// deleted, since the server now refuses and records the agreements.
|
|
793
804
|
"(auth)/sign-up": [
|
|
794
805
|
"apps/web/lib/auth-env.ts",
|
|
795
|
-
|
|
806
|
+
"apps/web/lib/consent-policies.ts",
|
|
796
807
|
],
|
|
797
808
|
"(auth)/forgot-password": ["apps/web/lib/auth-env.ts"],
|
|
798
809
|
"(auth)/reset-password": ["apps/web/lib/auth-env.ts"],
|
|
799
810
|
// #7430 — the group's layout reads `isAccountPath()` to give the account
|
|
800
|
-
// area its member-facing signed-out gate.
|
|
811
|
+
// area its member-facing signed-out gate. #8765 D3/D4 — `auth.ts` imports
|
|
812
|
+
// the sign-up agreements plugin and hooks, so that file travels beside it.
|
|
801
813
|
"(authed)": [
|
|
802
814
|
"apps/web/auth.ts",
|
|
803
815
|
"apps/web/lib/auth-env.ts",
|
|
804
816
|
"apps/web/lib/auth-routes.ts",
|
|
817
|
+
"apps/web/lib/sign-up-agreements.ts",
|
|
805
818
|
],
|
|
806
819
|
"(authed)/account": [
|
|
807
820
|
"apps/web/lib/account-config.ts",
|
|
821
|
+
"apps/web/lib/consent-policies.ts",
|
|
808
822
|
"apps/web/lib/server-api.ts",
|
|
809
823
|
"apps/web/lib/site-brand.tsx",
|
|
810
824
|
"apps/web/lib/site-logo.tsx",
|
|
811
|
-
// #8438 D13/D21 —
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// (
|
|
815
|
-
// `
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
// seed a sign-in-less spoke drops, so a
|
|
819
|
-
// into it would dangle.
|
|
825
|
+
// #8438 D13/D21, #8765 D7 — the agreements interstitial the layout shows
|
|
826
|
+
// a member with no current grant (`agreements-prompt.tsx`), its accept
|
|
827
|
+
// action (`agreements-actions.ts`) and the read the layout and the Inbox
|
|
828
|
+
// share (`agreements-gate.ts`). Listed because a scaffold
|
|
829
|
+
// whose `(authed)/account` predates #8438 needs them added, which the
|
|
830
|
+
// directory-level sync alone would not do (sync never overwrites or adds
|
|
831
|
+
// to a directory it already has). Both live in this directory, never in
|
|
832
|
+
// `(auth)/**` — a sign-in seed a sign-in-less spoke drops, so a
|
|
833
|
+
// cross-directory reach from here into it would dangle.
|
|
820
834
|
`${ROUTE_ROOT}/(authed)/account/agreements-prompt.tsx`,
|
|
821
835
|
`${ROUTE_ROOT}/(authed)/account/agreements-actions.ts`,
|
|
836
|
+
`${ROUTE_ROOT}/(authed)/account/agreements-gate.ts`,
|
|
822
837
|
],
|
|
823
838
|
// #7430 — the three child routes also import files in their PARENT's
|
|
824
839
|
// directory. A scaffold whose `(authed)/account` predates #7430 keeps its
|
|
@@ -923,6 +938,7 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
923
938
|
// D11 applied to Notifications' cookie-settings link.
|
|
924
939
|
"(authed)/account/privacy": [
|
|
925
940
|
"apps/web/lib/account-config.ts",
|
|
941
|
+
"apps/web/lib/consent-policies.ts",
|
|
926
942
|
"apps/web/lib/server-api.ts",
|
|
927
943
|
`${ROUTE_ROOT}/(authed)/account/account-data.ts`,
|
|
928
944
|
`${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
|