create-ailk 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +19 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +218 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +135 -14
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -48,8 +48,16 @@
|
|
|
48
48
|
* throws, never refuses the lead and never changes `routeLead`'s result: an
|
|
49
49
|
* unstorable address or a failed step writes the lead with `personId: null`
|
|
50
50
|
* and logs one bare line (`person-skipped` / `person-failed`, never the
|
|
51
|
-
* email). `
|
|
52
|
-
*
|
|
51
|
+
* email). The `email`, `local` and `none` deliveries never reach
|
|
52
|
+
* `persistToDb`.
|
|
53
|
+
*
|
|
54
|
+
* CONTACT AND LINKS (#8435 D5). A `submit` may carry `options.contact` (the
|
|
55
|
+
* magnet's tagged answers): `findOrCreatePerson` fills the Person's BLANK
|
|
56
|
+
* contact fields from it while the Person is uncurated, and the lead's own
|
|
57
|
+
* `payload` is unchanged. A `subscribe` and a `callback` with a storable email
|
|
58
|
+
* (a callback's is `payload.email`) now also resolve a Person and stamp its id
|
|
59
|
+
* as `personId`; a callback with no email makes no Person call and writes no
|
|
60
|
+
* `personId`. Every one of these steps is best-effort exactly like the lead's.
|
|
53
61
|
*
|
|
54
62
|
* CHECKOUT PLAN (#7877 D10). A spoke's checkout fulfilment posts one
|
|
55
63
|
* `submit` with the Checkout Session id as its `Idempotency-Key`. When that
|
|
@@ -63,6 +71,24 @@
|
|
|
63
71
|
* `routeLead`'s result, and logs one bare `checkout-plan` line — never the
|
|
64
72
|
* offering, the email or the key. A lead that does not qualify makes no
|
|
65
73
|
* Person call at all.
|
|
74
|
+
*
|
|
75
|
+
* SPOKE CHECKOUT MARKER (#8185 D2). The same predicate, plus "not relayed
|
|
76
|
+
* for a visitor" (`SourceMeta.relayed`), is evaluated once BEFORE the lead's
|
|
77
|
+
* create and stored as `Lead.spokeCheckout` — the hub's conclusion at write
|
|
78
|
+
* time, which `GET /v1/member/purchases` serves as a purchase
|
|
79
|
+
* (`lib/member-store.ts`). Written only in the submit create below, never by
|
|
80
|
+
* an update; an unmarked create carries no `spokeCheckout` key at all and
|
|
81
|
+
* relies on the column default. The plan rule above is unchanged: it still
|
|
82
|
+
* runs after the write, from the same candidate.
|
|
83
|
+
*
|
|
84
|
+
* VISITOR LINK (#8438 D11). When the request carried a valid consent visitor
|
|
85
|
+
* id (`SourceMeta.consentVisitorId`, set by the leads and flows routes from
|
|
86
|
+
* `x-ailk-consent-visitor` only when it is a v4 UUID) and the submit resolved
|
|
87
|
+
* a Person, `linkConsentVisitor` joins the two — source `magnet` for a
|
|
88
|
+
* `flow:` source, else `api`. A link never fails a lead and is never
|
|
89
|
+
* returned: `RouteLeadResult.personId` is internal, for the flow engine's
|
|
90
|
+
* consent write. A subscribe or callback resolves no Person, so it links
|
|
91
|
+
* nothing. The header is never echoed and never logged.
|
|
66
92
|
*/
|
|
67
93
|
|
|
68
94
|
import { randomUUID } from "node:crypto";
|
|
@@ -82,8 +108,15 @@ import { appendLeadRecord } from "../lib/lead-capture-store.js";
|
|
|
82
108
|
import { acceptsKind, resolveLeadDelivery } from "../lib/lead-delivery.js";
|
|
83
109
|
import { prisma } from "../lib/prisma.js";
|
|
84
110
|
|
|
111
|
+
import { linkConsentVisitor } from "./consent.js";
|
|
85
112
|
import { enrichLead } from "./enrichment-shim.js";
|
|
86
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
findOrCreatePerson,
|
|
115
|
+
normalizeCapturedContact,
|
|
116
|
+
normalizeTelephone,
|
|
117
|
+
setPersonPlanOffering,
|
|
118
|
+
} from "./person.js";
|
|
119
|
+
import type { LeadContact, PersonContact } from "./person.js";
|
|
87
120
|
import { requestConfirmSupersedesNotification } from "./request-confirm.js";
|
|
88
121
|
|
|
89
122
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -117,6 +150,19 @@ export type SourceMeta = {
|
|
|
117
150
|
* `false`. Never persisted — `captureRecordFor` copies fields by name.
|
|
118
151
|
*/
|
|
119
152
|
viaSpoke?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* The request carried `x-ailk-relayed`, i.e. it was forwarded for a
|
|
155
|
+
* visitor by a spoke's same-origin relay (#8185 D3). Absent reads as
|
|
156
|
+
* `false`. Never persisted. It can only REMOVE the spoke-checkout marker,
|
|
157
|
+
* never grant it.
|
|
158
|
+
*/
|
|
159
|
+
relayed?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* The cookie banner's visitor id from the request's `x-ailk-consent-visitor`
|
|
162
|
+
* header, set only when it is a v4 UUID (#8438 D11). Used to link the
|
|
163
|
+
* visitor to the lead's Person; never persisted, echoed or logged.
|
|
164
|
+
*/
|
|
165
|
+
consentVisitorId?: string;
|
|
120
166
|
};
|
|
121
167
|
|
|
122
168
|
/** Where the lead landed: a database row, the local file fallback, or the owner's inbox by email (#6730). */
|
|
@@ -126,6 +172,12 @@ export type Persisted = "db" | "file" | "email";
|
|
|
126
172
|
export type RouteLeadOptions = {
|
|
127
173
|
/** The caller's validated `Idempotency-Key`; honoured for `submit` only. */
|
|
128
174
|
idempotencyKey?: string;
|
|
175
|
+
/**
|
|
176
|
+
* The contact the capture offers (#8435 D5) — the magnet's tagged answers —
|
|
177
|
+
* honoured for `submit` only and never persisted as such: the lead's
|
|
178
|
+
* `payload` stays the raw answers.
|
|
179
|
+
*/
|
|
180
|
+
contact?: LeadContact;
|
|
129
181
|
};
|
|
130
182
|
|
|
131
183
|
export type RouteLeadResult =
|
|
@@ -138,6 +190,12 @@ export type RouteLeadResult =
|
|
|
138
190
|
*/
|
|
139
191
|
replayed?: true;
|
|
140
192
|
id: string;
|
|
193
|
+
/**
|
|
194
|
+
* The Person a `submit` resolved (#8438 D11), `null`/absent when none.
|
|
195
|
+
* Internal only — the route builds its 201 body from `id`, `persisted`
|
|
196
|
+
* and `email`, so this never reaches a wire.
|
|
197
|
+
*/
|
|
198
|
+
personId?: string | null;
|
|
141
199
|
persisted: Persisted;
|
|
142
200
|
email:
|
|
143
201
|
| { ok: true; messageId: string }
|
|
@@ -240,6 +298,8 @@ type PersistResult =
|
|
|
240
298
|
source: "db";
|
|
241
299
|
skipEmail: boolean;
|
|
242
300
|
skipReason?: string;
|
|
301
|
+
/** The Person a `submit` resolved (#8438 D11). */
|
|
302
|
+
personId?: string | null;
|
|
243
303
|
}
|
|
244
304
|
// #7869 D5 — a keyed create lost a race to a row it now answers with.
|
|
245
305
|
| { ok: "keyed"; outcome: KeyedOutcome }
|
|
@@ -251,25 +311,41 @@ type PersistResult =
|
|
|
251
311
|
* must never refuse, lose or delay a lead. Logs the outcome kind and the
|
|
252
312
|
* site only.
|
|
253
313
|
*/
|
|
254
|
-
async function
|
|
314
|
+
async function resolveCapturePersonId(
|
|
315
|
+
siteId: string,
|
|
316
|
+
capture: { email: string; name?: string | null; contact?: Partial<PersonContact> },
|
|
317
|
+
): Promise<string | null> {
|
|
255
318
|
try {
|
|
256
|
-
const
|
|
257
|
-
const person = await findOrCreatePerson(input.siteId, { email: input.email, name });
|
|
319
|
+
const person = await findOrCreatePerson(siteId, capture);
|
|
258
320
|
if (person.ok) return person.value.personId;
|
|
259
321
|
if (person.error.kind === "invalid_email") {
|
|
260
322
|
logger.warn(
|
|
261
|
-
{ event: "person-skipped", kind: person.error.kind, siteId
|
|
323
|
+
{ event: "person-skipped", kind: person.error.kind, siteId },
|
|
262
324
|
"lead person skipped",
|
|
263
325
|
);
|
|
264
326
|
} else {
|
|
265
|
-
logger.error({ event: "person-failed", siteId
|
|
327
|
+
logger.error({ event: "person-failed", siteId }, "lead person failed");
|
|
266
328
|
}
|
|
267
329
|
} catch {
|
|
268
|
-
logger.error({ event: "person-failed", siteId
|
|
330
|
+
logger.error({ event: "person-failed", siteId }, "lead person failed");
|
|
269
331
|
}
|
|
270
332
|
return null;
|
|
271
333
|
}
|
|
272
334
|
|
|
335
|
+
async function resolveLeadPersonId(input: SubmitInput, contact?: LeadContact): Promise<string | null> {
|
|
336
|
+
const payloadName = typeof input.payload["name"] === "string" ? input.payload["name"] : null;
|
|
337
|
+
if (contact === undefined) {
|
|
338
|
+
return resolveCapturePersonId(input.siteId, { email: input.email, name: payloadName });
|
|
339
|
+
}
|
|
340
|
+
// #8435 D5 — an explicit contact name wins over the payload's.
|
|
341
|
+
const name = typeof contact.name === "string" ? contact.name : payloadName;
|
|
342
|
+
return resolveCapturePersonId(input.siteId, {
|
|
343
|
+
email: input.email,
|
|
344
|
+
name,
|
|
345
|
+
contact: normalizeCapturedContact(contact),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
273
349
|
/** A LIVE-mode Checkout Session id (#7877 D10, SEC-7877-C). */
|
|
274
350
|
const LIVE_CHECKOUT_SESSION_KEY = /^cs_live_[A-Za-z0-9]{1,250}$/;
|
|
275
351
|
const SITE_SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
@@ -340,9 +416,16 @@ async function persistToDb(
|
|
|
340
416
|
input: RouteLeadInput,
|
|
341
417
|
idempotencyKey?: string,
|
|
342
418
|
viaSpoke = false,
|
|
419
|
+
relayed = false,
|
|
420
|
+
contact?: LeadContact,
|
|
343
421
|
): Promise<PersistResult> {
|
|
344
422
|
if (input.kind === "submit") {
|
|
345
|
-
const personId = await resolveLeadPersonId(input);
|
|
423
|
+
const personId = await resolveLeadPersonId(input, contact);
|
|
424
|
+
// #8185 D2 — the plan rule's own predicate (pure: input, key, flag),
|
|
425
|
+
// evaluated before the create so its result can be stored. A relayed
|
|
426
|
+
// request is never a spoke's server call (D3).
|
|
427
|
+
const candidate = checkoutPlanCandidate(input, idempotencyKey, viaSpoke);
|
|
428
|
+
const spokeCheckout = candidate !== null && relayed !== true;
|
|
346
429
|
let row: DbRow;
|
|
347
430
|
try {
|
|
348
431
|
row = (await prisma.lead.create({
|
|
@@ -371,6 +454,8 @@ async function persistToDb(
|
|
|
371
454
|
// #852 D6 — the Person `resolveLeadPersonId` found or created.
|
|
372
455
|
personId,
|
|
373
456
|
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
457
|
+
// #8185 D4 — only a marked create names the column.
|
|
458
|
+
...(spokeCheckout && { spokeCheckout: true }),
|
|
374
459
|
},
|
|
375
460
|
select: { id: true },
|
|
376
461
|
})) as DbRow;
|
|
@@ -386,14 +471,15 @@ async function persistToDb(
|
|
|
386
471
|
return { ok: "keyed", outcome: classifyKeyedRow(winner, input) };
|
|
387
472
|
}
|
|
388
473
|
// #7877 D10 — only after THIS request's own write, so a replay sets nothing.
|
|
389
|
-
const candidate = checkoutPlanCandidate(input, idempotencyKey, viaSpoke);
|
|
390
474
|
if (candidate !== null && personId !== null) {
|
|
391
475
|
await applyCheckoutPlan(input, personId, candidate);
|
|
392
476
|
}
|
|
393
|
-
return { ok: true, id: row.id, source: "db", skipEmail: false };
|
|
477
|
+
return { ok: true, id: row.id, source: "db", skipEmail: false, personId };
|
|
394
478
|
}
|
|
395
479
|
|
|
396
480
|
if (input.kind === "subscribe") {
|
|
481
|
+
// #8435 D5 — the subscriber's Person; `null` when the step skips or fails.
|
|
482
|
+
const personId = await resolveCapturePersonId(input.siteId, { email: input.email });
|
|
397
483
|
try {
|
|
398
484
|
const row = (await prisma.subscriber.create({
|
|
399
485
|
data: {
|
|
@@ -407,6 +493,7 @@ async function persistToDb(
|
|
|
407
493
|
workspaceId: input.siteId,
|
|
408
494
|
email: input.email,
|
|
409
495
|
source: input.source,
|
|
496
|
+
personId,
|
|
410
497
|
},
|
|
411
498
|
select: { id: true },
|
|
412
499
|
})) as DbRow;
|
|
@@ -444,6 +531,18 @@ async function persistToDb(
|
|
|
444
531
|
}
|
|
445
532
|
|
|
446
533
|
// kind === "callback"
|
|
534
|
+
// #8435 D5 — a callback with a string `payload.email` resolves a Person; one
|
|
535
|
+
// without makes no call and writes no `personId`.
|
|
536
|
+
const callbackEmail = input.payload["email"];
|
|
537
|
+
const telephone = normalizeTelephone(input.phone);
|
|
538
|
+
const callbackPersonId =
|
|
539
|
+
typeof callbackEmail === "string"
|
|
540
|
+
? await resolveCapturePersonId(input.siteId, {
|
|
541
|
+
email: callbackEmail,
|
|
542
|
+
name: input.name,
|
|
543
|
+
contact: telephone === null ? {} : { telephone },
|
|
544
|
+
})
|
|
545
|
+
: undefined;
|
|
447
546
|
const row = (await prisma.callback.create({
|
|
448
547
|
data: {
|
|
449
548
|
siteId: input.siteId,
|
|
@@ -457,6 +556,7 @@ async function persistToDb(
|
|
|
457
556
|
preferredTime: input.preferredTime,
|
|
458
557
|
}),
|
|
459
558
|
payload: input.payload as Prisma.InputJsonValue,
|
|
559
|
+
...(callbackPersonId !== undefined && { personId: callbackPersonId }),
|
|
460
560
|
},
|
|
461
561
|
select: { id: true },
|
|
462
562
|
})) as DbRow;
|
|
@@ -727,9 +827,16 @@ export async function routeLead(
|
|
|
727
827
|
let persisted: Persisted;
|
|
728
828
|
let skipEmail = false;
|
|
729
829
|
let skipReason: string | undefined;
|
|
830
|
+
let personId: string | null = null;
|
|
730
831
|
|
|
731
832
|
try {
|
|
732
|
-
const dbResult = await persistToDb(
|
|
833
|
+
const dbResult = await persistToDb(
|
|
834
|
+
input,
|
|
835
|
+
idempotencyKey,
|
|
836
|
+
sourceMeta.viaSpoke === true,
|
|
837
|
+
sourceMeta.relayed === true,
|
|
838
|
+
input.kind === "submit" ? options.contact : undefined,
|
|
839
|
+
);
|
|
733
840
|
// #7869 D5 — a lost race answers like the lookup above: no capture
|
|
734
841
|
// record, no notification.
|
|
735
842
|
if (dbResult.ok === "keyed") return keyedResult(dbResult.outcome);
|
|
@@ -744,6 +851,20 @@ export async function routeLead(
|
|
|
744
851
|
persisted = "db";
|
|
745
852
|
skipEmail = dbResult.skipEmail;
|
|
746
853
|
skipReason = dbResult.skipReason;
|
|
854
|
+
personId = dbResult.personId ?? null;
|
|
855
|
+
// #8438 D11 — only after THIS request's own write (a keyed replay
|
|
856
|
+
// returned above), and only when a Person was resolved.
|
|
857
|
+
if (sourceMeta.consentVisitorId !== undefined && personId !== null) {
|
|
858
|
+
await linkConsentVisitor({
|
|
859
|
+
workspaceId: input.siteId,
|
|
860
|
+
visitorId: sourceMeta.consentVisitorId,
|
|
861
|
+
personId,
|
|
862
|
+
source:
|
|
863
|
+
input.kind === "submit" && input.source.startsWith("flow:")
|
|
864
|
+
? "magnet"
|
|
865
|
+
: "api",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
747
868
|
} catch (dbErr) {
|
|
748
869
|
// #6254 AC-11 (spec D6, SEC-02) — a constraint violation is OUR OWN
|
|
749
870
|
// derivation producing a row Postgres itself refuses (an unresolved
|
|
@@ -863,5 +984,5 @@ export async function routeLead(
|
|
|
863
984
|
emailResult = await sendEmail(input, id!, persisted!, sourceMeta.viaSpoke);
|
|
864
985
|
}
|
|
865
986
|
|
|
866
|
-
return { ok: true, id: id!, persisted, email: emailResult };
|
|
987
|
+
return { ok: true, id: id!, personId, persisted, email: emailResult };
|
|
867
988
|
}
|