create-ailk 0.19.0 → 0.20.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 +64 -7
- package/dist/add-module.js +7 -2
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/lib/scaffold-render-sweep.d.ts +23 -0
- package/dist/lib/scaffold-render-sweep.js +52 -0
- package/dist/module-architecture.js +64 -5
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +40 -3
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +98 -3
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/topology.js +29 -1
- package/dist/update.d.ts +48 -0
- package/dist/update.js +504 -0
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/_gitignore +162 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +18 -8
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +41 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +8 -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__/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/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/lib/stripe.ts +97 -5
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +13 -4
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +72 -14
- package/templates/apps/api/src/middleware/auth.ts +16 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +295 -82
- 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 +15 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- 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-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +201 -6
- package/templates/apps/api/src/server.ts +125 -18
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- 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 +115 -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/audit-log.ts +72 -10
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/lead-routing.ts +105 -12
- 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/public-site-resolver.ts +21 -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 +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +95 -8
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- 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__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +17 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +154 -53
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +116 -27
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- 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 +124 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +116 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +290 -20
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +56 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +74 -4
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +15 -9
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +70 -27
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +26 -3
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +65 -19
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +89 -37
- 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-frame.tsx +44 -34
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +1 -1
- package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +13 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/auth.ts +20 -1
- package/templates/apps/web/jest.config.cjs +3 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +96 -7
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/lib/topology.generated.ts +9 -0
- package/templates/apps/web/messages/en.json +121 -18
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +55 -0
- 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__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +114 -22
- 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/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/project.yaml +6 -3
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
|
@@ -74,6 +74,13 @@ export interface TenantContext {
|
|
|
74
74
|
* `@working-theory/validation`'s `allows`.
|
|
75
75
|
*/
|
|
76
76
|
grants?: readonly AgentGrantablePermission[];
|
|
77
|
+
/**
|
|
78
|
+
* Set ONLY by `agentTenant` (#8514 D4); a person's tenant never carries it.
|
|
79
|
+
* `@working-theory/validation`'s `roleAllows` / `allows` read it to withhold
|
|
80
|
+
* `AGENT_WITHHELD_PERMISSIONS` from an agent's role, and both tenant guards
|
|
81
|
+
* refuse an agent request whose tenant lacks it (fail closed).
|
|
82
|
+
*/
|
|
83
|
+
agent?: true;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type TenantErrorKind = "forbidden" | "not_found" | "conflict";
|
|
@@ -222,7 +229,9 @@ export async function resolveDefaultTenantForOperator(
|
|
|
222
229
|
* An agent's tenant (#3591 D5, #7864): its person's tenant with the role
|
|
223
230
|
* clamped to the agent's ceiling and, when any survive, the agent's grants
|
|
224
231
|
* that its person's own role holds. `grants` is omitted when none survive,
|
|
225
|
-
* so an agent with no effective grant is the same shape as before.
|
|
232
|
+
* so an agent with no effective grant is the same shape as before. It always
|
|
233
|
+
* carries `agent: true` (#8514 D4), which withholds the client-keeping rows a
|
|
234
|
+
* member holds by role.
|
|
226
235
|
*
|
|
227
236
|
* The one clamp both tenant guards apply — `resolveAgentTenant` (multi-tenant)
|
|
228
237
|
* and `resolveDefaultTenantForAgent` (base build) — so the two builds cannot
|
|
@@ -238,8 +247,8 @@ export function agentTenant(
|
|
|
238
247
|
(p) => agent.grants.includes(p) && can(personTenant.role, p),
|
|
239
248
|
);
|
|
240
249
|
return grants.length > 0
|
|
241
|
-
? { ...personTenant, role, grants }
|
|
242
|
-
: { ...personTenant, role };
|
|
250
|
+
? { ...personTenant, role, agent: true, grants }
|
|
251
|
+
: { ...personTenant, role, agent: true };
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
/**
|
|
@@ -114,7 +114,8 @@ function refuse<T = never>(
|
|
|
114
114
|
// keyed on an identity the request has already proved (the user the cookie
|
|
115
115
|
// session resolved to; the client that passed secret AND spoke key), so an
|
|
116
116
|
// invented key never gets one (SEC-7894-8). The shape of `middleware/auth.ts`'s
|
|
117
|
-
// per-agent limiter. SINGLE-REPLICA, like
|
|
117
|
+
// per-agent limiter. SINGLE-REPLICA, like the in-process limiters in this
|
|
118
|
+
// app (the per-IP middleware shares a database store, #5040).
|
|
118
119
|
|
|
119
120
|
interface Bucket {
|
|
120
121
|
windowStartMs: number;
|
|
@@ -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
|
|
@@ -72,6 +80,15 @@
|
|
|
72
80
|
* an update; an unmarked create carries no `spokeCheckout` key at all and
|
|
73
81
|
* relies on the column default. The plan rule above is unchanged: it still
|
|
74
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.
|
|
75
92
|
*/
|
|
76
93
|
|
|
77
94
|
import { randomUUID } from "node:crypto";
|
|
@@ -91,8 +108,15 @@ import { appendLeadRecord } from "../lib/lead-capture-store.js";
|
|
|
91
108
|
import { acceptsKind, resolveLeadDelivery } from "../lib/lead-delivery.js";
|
|
92
109
|
import { prisma } from "../lib/prisma.js";
|
|
93
110
|
|
|
111
|
+
import { linkConsentVisitor } from "./consent.js";
|
|
94
112
|
import { enrichLead } from "./enrichment-shim.js";
|
|
95
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
findOrCreatePerson,
|
|
115
|
+
normalizeCapturedContact,
|
|
116
|
+
normalizeTelephone,
|
|
117
|
+
setPersonPlanOffering,
|
|
118
|
+
} from "./person.js";
|
|
119
|
+
import type { LeadContact, PersonContact } from "./person.js";
|
|
96
120
|
import { requestConfirmSupersedesNotification } from "./request-confirm.js";
|
|
97
121
|
|
|
98
122
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -133,6 +157,12 @@ export type SourceMeta = {
|
|
|
133
157
|
* never grant it.
|
|
134
158
|
*/
|
|
135
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;
|
|
136
166
|
};
|
|
137
167
|
|
|
138
168
|
/** Where the lead landed: a database row, the local file fallback, or the owner's inbox by email (#6730). */
|
|
@@ -142,6 +172,12 @@ export type Persisted = "db" | "file" | "email";
|
|
|
142
172
|
export type RouteLeadOptions = {
|
|
143
173
|
/** The caller's validated `Idempotency-Key`; honoured for `submit` only. */
|
|
144
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;
|
|
145
181
|
};
|
|
146
182
|
|
|
147
183
|
export type RouteLeadResult =
|
|
@@ -154,6 +190,12 @@ export type RouteLeadResult =
|
|
|
154
190
|
*/
|
|
155
191
|
replayed?: true;
|
|
156
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;
|
|
157
199
|
persisted: Persisted;
|
|
158
200
|
email:
|
|
159
201
|
| { ok: true; messageId: string }
|
|
@@ -256,6 +298,8 @@ type PersistResult =
|
|
|
256
298
|
source: "db";
|
|
257
299
|
skipEmail: boolean;
|
|
258
300
|
skipReason?: string;
|
|
301
|
+
/** The Person a `submit` resolved (#8438 D11). */
|
|
302
|
+
personId?: string | null;
|
|
259
303
|
}
|
|
260
304
|
// #7869 D5 — a keyed create lost a race to a row it now answers with.
|
|
261
305
|
| { ok: "keyed"; outcome: KeyedOutcome }
|
|
@@ -267,25 +311,41 @@ type PersistResult =
|
|
|
267
311
|
* must never refuse, lose or delay a lead. Logs the outcome kind and the
|
|
268
312
|
* site only.
|
|
269
313
|
*/
|
|
270
|
-
async function
|
|
314
|
+
async function resolveCapturePersonId(
|
|
315
|
+
siteId: string,
|
|
316
|
+
capture: { email: string; name?: string | null; contact?: Partial<PersonContact> },
|
|
317
|
+
): Promise<string | null> {
|
|
271
318
|
try {
|
|
272
|
-
const
|
|
273
|
-
const person = await findOrCreatePerson(input.siteId, { email: input.email, name });
|
|
319
|
+
const person = await findOrCreatePerson(siteId, capture);
|
|
274
320
|
if (person.ok) return person.value.personId;
|
|
275
321
|
if (person.error.kind === "invalid_email") {
|
|
276
322
|
logger.warn(
|
|
277
|
-
{ event: "person-skipped", kind: person.error.kind, siteId
|
|
323
|
+
{ event: "person-skipped", kind: person.error.kind, siteId },
|
|
278
324
|
"lead person skipped",
|
|
279
325
|
);
|
|
280
326
|
} else {
|
|
281
|
-
logger.error({ event: "person-failed", siteId
|
|
327
|
+
logger.error({ event: "person-failed", siteId }, "lead person failed");
|
|
282
328
|
}
|
|
283
329
|
} catch {
|
|
284
|
-
logger.error({ event: "person-failed", siteId
|
|
330
|
+
logger.error({ event: "person-failed", siteId }, "lead person failed");
|
|
285
331
|
}
|
|
286
332
|
return null;
|
|
287
333
|
}
|
|
288
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
|
+
|
|
289
349
|
/** A LIVE-mode Checkout Session id (#7877 D10, SEC-7877-C). */
|
|
290
350
|
const LIVE_CHECKOUT_SESSION_KEY = /^cs_live_[A-Za-z0-9]{1,250}$/;
|
|
291
351
|
const SITE_SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
@@ -357,9 +417,10 @@ async function persistToDb(
|
|
|
357
417
|
idempotencyKey?: string,
|
|
358
418
|
viaSpoke = false,
|
|
359
419
|
relayed = false,
|
|
420
|
+
contact?: LeadContact,
|
|
360
421
|
): Promise<PersistResult> {
|
|
361
422
|
if (input.kind === "submit") {
|
|
362
|
-
const personId = await resolveLeadPersonId(input);
|
|
423
|
+
const personId = await resolveLeadPersonId(input, contact);
|
|
363
424
|
// #8185 D2 — the plan rule's own predicate (pure: input, key, flag),
|
|
364
425
|
// evaluated before the create so its result can be stored. A relayed
|
|
365
426
|
// request is never a spoke's server call (D3).
|
|
@@ -413,10 +474,12 @@ async function persistToDb(
|
|
|
413
474
|
if (candidate !== null && personId !== null) {
|
|
414
475
|
await applyCheckoutPlan(input, personId, candidate);
|
|
415
476
|
}
|
|
416
|
-
return { ok: true, id: row.id, source: "db", skipEmail: false };
|
|
477
|
+
return { ok: true, id: row.id, source: "db", skipEmail: false, personId };
|
|
417
478
|
}
|
|
418
479
|
|
|
419
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 });
|
|
420
483
|
try {
|
|
421
484
|
const row = (await prisma.subscriber.create({
|
|
422
485
|
data: {
|
|
@@ -430,6 +493,7 @@ async function persistToDb(
|
|
|
430
493
|
workspaceId: input.siteId,
|
|
431
494
|
email: input.email,
|
|
432
495
|
source: input.source,
|
|
496
|
+
personId,
|
|
433
497
|
},
|
|
434
498
|
select: { id: true },
|
|
435
499
|
})) as DbRow;
|
|
@@ -467,6 +531,18 @@ async function persistToDb(
|
|
|
467
531
|
}
|
|
468
532
|
|
|
469
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;
|
|
470
546
|
const row = (await prisma.callback.create({
|
|
471
547
|
data: {
|
|
472
548
|
siteId: input.siteId,
|
|
@@ -480,6 +556,7 @@ async function persistToDb(
|
|
|
480
556
|
preferredTime: input.preferredTime,
|
|
481
557
|
}),
|
|
482
558
|
payload: input.payload as Prisma.InputJsonValue,
|
|
559
|
+
...(callbackPersonId !== undefined && { personId: callbackPersonId }),
|
|
483
560
|
},
|
|
484
561
|
select: { id: true },
|
|
485
562
|
})) as DbRow;
|
|
@@ -750,6 +827,7 @@ export async function routeLead(
|
|
|
750
827
|
let persisted: Persisted;
|
|
751
828
|
let skipEmail = false;
|
|
752
829
|
let skipReason: string | undefined;
|
|
830
|
+
let personId: string | null = null;
|
|
753
831
|
|
|
754
832
|
try {
|
|
755
833
|
const dbResult = await persistToDb(
|
|
@@ -757,6 +835,7 @@ export async function routeLead(
|
|
|
757
835
|
idempotencyKey,
|
|
758
836
|
sourceMeta.viaSpoke === true,
|
|
759
837
|
sourceMeta.relayed === true,
|
|
838
|
+
input.kind === "submit" ? options.contact : undefined,
|
|
760
839
|
);
|
|
761
840
|
// #7869 D5 — a lost race answers like the lookup above: no capture
|
|
762
841
|
// record, no notification.
|
|
@@ -772,6 +851,20 @@ export async function routeLead(
|
|
|
772
851
|
persisted = "db";
|
|
773
852
|
skipEmail = dbResult.skipEmail;
|
|
774
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
|
+
}
|
|
775
868
|
} catch (dbErr) {
|
|
776
869
|
// #6254 AC-11 (spec D6, SEC-02) — a constraint violation is OUR OWN
|
|
777
870
|
// derivation producing a row Postgres itself refuses (an unresolved
|
|
@@ -891,5 +984,5 @@ export async function routeLead(
|
|
|
891
984
|
emailResult = await sendEmail(input, id!, persisted!, sourceMeta.viaSpoke);
|
|
892
985
|
}
|
|
893
986
|
|
|
894
|
-
return { ok: true, id: id!, persisted, email: emailResult };
|
|
987
|
+
return { ok: true, id: id!, personId, persisted, email: emailResult };
|
|
895
988
|
}
|