create-ailk 0.15.0 → 0.16.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 +124 -0
- package/dist/add-module.js +52 -7
- package/dist/add-route.js +8 -0
- package/dist/adopt.d.ts +74 -0
- package/dist/adopt.js +314 -0
- package/dist/cli.js +82 -2
- package/dist/derive-architecture.d.ts +22 -0
- package/dist/derive-architecture.js +58 -1
- package/dist/lib/declared-env.d.ts +142 -0
- package/dist/lib/declared-env.js +985 -0
- package/dist/lib/env-catalog.d.ts +17 -0
- package/dist/lib/env-catalog.js +234 -0
- package/dist/lib/extract-module-bundle.d.ts +9 -0
- package/dist/lib/extract-module-bundle.js +18 -1
- package/dist/lib/module-tier.d.ts +2 -0
- package/dist/lib/module-tier.js +4 -0
- package/dist/module-architecture.js +76 -0
- package/dist/parse-args.d.ts +5 -0
- package/dist/parse-args.js +17 -1
- package/dist/programmatic.d.ts +8 -0
- package/dist/programmatic.js +78 -9
- package/dist/seed-flows.d.ts +65 -0
- package/dist/seed-flows.js +350 -0
- package/dist/site-manifest.d.ts +43 -0
- package/dist/site-manifest.js +98 -3
- package/dist/site-roster.d.ts +13 -0
- package/dist/site-roster.js +22 -8
- package/dist/spoke-checkout.d.ts +42 -0
- package/dist/spoke-checkout.js +359 -0
- package/dist/surfaces.js +7 -0
- package/dist/sync-routes.js +23 -8
- package/dist/topology.d.ts +124 -9
- package/dist/topology.js +449 -40
- package/package.json +3 -3
- package/templates/.env.example +10 -0
- package/templates/apps/api/CLAUDE.md +19 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
- package/templates/apps/api/src/bin/spokes.ts +62 -1
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +89 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
- package/templates/apps/api/src/lib/spoke-store.ts +25 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
- package/templates/apps/api/src/middleware/auth.ts +126 -0
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +7 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +72 -1
- package/templates/apps/api/src/server.ts +105 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +293 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
- package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
- package/templates/apps/api/src/services/lead-routing.ts +131 -2
- package/templates/apps/api/src/services/person.ts +541 -0
- package/templates/apps/web/.env.example +58 -5
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
- package/templates/apps/web/app/[locale]/layout.tsx +28 -15
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
- package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
- package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +15 -0
- package/templates/apps/web/app/openapi.json/route.ts +3 -6
- package/templates/apps/web/auth.ts +29 -2
- package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
- package/templates/apps/web/components/signed-in-nav.tsx +17 -3
- package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
- package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
- package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
- package/templates/apps/web/lib/__tests__/relay.test.ts +27 -0
- package/templates/apps/web/lib/account-config.ts +130 -1
- package/templates/apps/web/lib/api-origin.generated.ts +11 -0
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
- package/templates/apps/web/lib/hub-session/config.ts +104 -0
- package/templates/apps/web/lib/hub-session/index.ts +23 -0
- package/templates/apps/web/lib/hub-session/seal.ts +82 -0
- package/templates/apps/web/lib/hub-session/session.ts +180 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
- package/templates/apps/web/lib/listings-config.ts +31 -0
- package/templates/apps/web/lib/listings.tsx +7 -3
- package/templates/apps/web/lib/meeting-slugs.ts +11 -1
- package/templates/apps/web/lib/next-path.ts +35 -0
- package/templates/apps/web/lib/relay.ts +3 -1
- package/templates/apps/web/lib/server-api.ts +11 -1
- package/templates/apps/web/lib/topology.generated.ts +15 -5
- package/templates/apps/web/messages/en.json +32 -3
- package/templates/database/CHANGELOG.md +26 -0
- package/templates/database/__tests__/agents.db.test.ts +25 -5
- package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
- package/templates/database/__tests__/member-area.db.test.ts +245 -0
- package/templates/database/__tests__/person.db.test.ts +584 -0
- package/templates/database/auth/schema.prisma +3 -2
- package/templates/database/config/schema.prisma +13 -0
- package/templates/database/inbox/schema.prisma +98 -0
- package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
- package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/person-backfill.sql +132 -0
- package/templates/database/tenancy.ts +53 -0
- package/templates/tools/check-declared-env.ts +37 -0
- package/templates/tools/declared-env.ts +1163 -0
- package/versions.json +35 -35
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
leadCallbackRoute,
|
|
66
66
|
} from "../index";
|
|
67
67
|
import {
|
|
68
|
+
MOCK_PERSON_ID,
|
|
68
69
|
mockNoWorkspace,
|
|
69
70
|
mockSentinelWorkspaceExists,
|
|
70
71
|
} from "../../../lib/__mocks__/prisma.js";
|
|
@@ -308,6 +309,8 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
308
309
|
source: CONTACT_FORM_LEAD_BODY.source,
|
|
309
310
|
email: CONTACT_FORM_LEAD_BODY.email,
|
|
310
311
|
payload: CONTACT_FORM_LEAD_BODY.payload,
|
|
312
|
+
// #852 D6 — the Person the shared mock's `person.findFirst` resolves.
|
|
313
|
+
personId: MOCK_PERSON_ID,
|
|
311
314
|
},
|
|
312
315
|
select: { id: true },
|
|
313
316
|
});
|
|
@@ -627,6 +630,8 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
627
630
|
source: "step-flow",
|
|
628
631
|
email: "envelope@example.com",
|
|
629
632
|
payload: envelopePayload,
|
|
633
|
+
// #852 D6 — the Person the shared mock's `person.findFirst` resolves.
|
|
634
|
+
personId: MOCK_PERSON_ID,
|
|
630
635
|
},
|
|
631
636
|
select: { id: true },
|
|
632
637
|
});
|
|
@@ -731,6 +736,8 @@ describe("POST /api/leads — id isolation", () => {
|
|
|
731
736
|
source: "step-flow",
|
|
732
737
|
email: "top-level@example.com",
|
|
733
738
|
payload: { email: "shadowed-payload@example.com", message: "hi" },
|
|
739
|
+
// #852 D6 — the Person the shared mock's `person.findFirst` resolves.
|
|
740
|
+
personId: MOCK_PERSON_ID,
|
|
734
741
|
},
|
|
735
742
|
select: { id: true },
|
|
736
743
|
});
|
|
@@ -328,3 +328,108 @@ describe("AC-5 — PII boundary", () => {
|
|
|
328
328
|
}
|
|
329
329
|
});
|
|
330
330
|
});
|
|
331
|
+
|
|
332
|
+
// ── #852 D6 — the hub checkout's Person (AC-3) ──────────────────────────────
|
|
333
|
+
//
|
|
334
|
+
// After the stamp, `recordCheckoutPerson` reads the checkout row THIS session
|
|
335
|
+
// completed, then its lead, then finds or creates the Person — with
|
|
336
|
+
// `session.customer` as the customer id only on a live-mode event. Observed
|
|
337
|
+
// through the shared mock's delegates: the `person.findFirst` keyed on a
|
|
338
|
+
// customer id is the customer-id step, so its absence means no id was passed.
|
|
339
|
+
describe("the hub checkout's Person (#852 D6, AC-3)", () => {
|
|
340
|
+
const WORKSPACE = "wksp_00000000-0000-7000-8000-000000000852";
|
|
341
|
+
const LEAD = "lead_00000000-0000-7000-8000-000000000852";
|
|
342
|
+
const personFindFirst = () => jest.mocked(prisma.person.findFirst);
|
|
343
|
+
const personCalls = () =>
|
|
344
|
+
[prisma.person.findFirst, prisma.person.createMany, prisma.person.updateMany].reduce(
|
|
345
|
+
(n, fn) => n + jest.mocked(fn).mock.calls.length,
|
|
346
|
+
0,
|
|
347
|
+
);
|
|
348
|
+
const personLines = () =>
|
|
349
|
+
logCalls.filter(
|
|
350
|
+
(c) => typeof c[0] === "object" && c[0] !== null && String((c[0] as { outcome?: string }).outcome).startsWith("person_"),
|
|
351
|
+
);
|
|
352
|
+
const liveEvent = (livemode: boolean, object: Record<string, unknown> = {}) => ({
|
|
353
|
+
...sessionEvent("checkout.session.completed", { customer: "cus_9", ...object }),
|
|
354
|
+
livemode,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
(prisma.flowCheckout.findFirst as jest.Mock).mockResolvedValue({ leadId: LEAD, workspaceId: WORKSPACE });
|
|
359
|
+
(prisma.lead.findFirst as jest.Mock).mockResolvedValue({ email: EMAIL });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("a paid live-mode hub session finds the Person of its checkout's lead with session.customer", async () => {
|
|
363
|
+
personFindFirst().mockResolvedValueOnce(null); // no Person carries cus_9 yet
|
|
364
|
+
mockVerifyWebhookSignature.mockReturnValue(liveEvent(true));
|
|
365
|
+
expect((await post()).statusCode).toBe(200);
|
|
366
|
+
|
|
367
|
+
expect(prisma.flowCheckout.findFirst).toHaveBeenCalledWith({
|
|
368
|
+
where: { id: FLOW_CHECKOUT_ID, stripeSessionId: SESSION_ID, status: "completed" },
|
|
369
|
+
select: { leadId: true, workspaceId: true },
|
|
370
|
+
});
|
|
371
|
+
expect(prisma.lead.findFirst).toHaveBeenCalledWith({
|
|
372
|
+
where: { id: LEAD, workspaceId: WORKSPACE },
|
|
373
|
+
select: { email: true },
|
|
374
|
+
});
|
|
375
|
+
expect(personFindFirst()).toHaveBeenCalledWith(
|
|
376
|
+
expect.objectContaining({ where: { workspaceId: WORKSPACE, billingCustomerId: "cus_9" } }),
|
|
377
|
+
);
|
|
378
|
+
expect(personFindFirst()).toHaveBeenCalledWith(
|
|
379
|
+
expect.objectContaining({ where: { workspaceId: WORKSPACE, email: EMAIL } }),
|
|
380
|
+
);
|
|
381
|
+
// The stamp settles before the Person step reads the row it completed.
|
|
382
|
+
expect((prisma.flowCheckout.updateMany as jest.Mock).mock.invocationCallOrder[0]!).toBeLessThan(
|
|
383
|
+
(prisma.flowCheckout.findFirst as jest.Mock).mock.invocationCallOrder[0]!,
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("the same session in test mode passes no customer id", async () => {
|
|
388
|
+
mockVerifyWebhookSignature.mockReturnValue(liveEvent(false));
|
|
389
|
+
expect((await post()).statusCode).toBe(200);
|
|
390
|
+
|
|
391
|
+
expect(personFindFirst()).toHaveBeenCalledWith(
|
|
392
|
+
expect.objectContaining({ where: { workspaceId: WORKSPACE, email: EMAIL } }),
|
|
393
|
+
);
|
|
394
|
+
for (const [args] of personFindFirst().mock.calls) {
|
|
395
|
+
expect((args as { where: Record<string, unknown> }).where).not.toHaveProperty("billingCustomerId");
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("a session naming a checkout another session completed makes no person call and logs person_skipped", async () => {
|
|
400
|
+
// The read is keyed on THIS session's id, so another session's row is no row.
|
|
401
|
+
(prisma.flowCheckout.findFirst as jest.Mock).mockResolvedValue(null);
|
|
402
|
+
mockVerifyWebhookSignature.mockReturnValue(liveEvent(true));
|
|
403
|
+
expect((await post()).statusCode).toBe(200);
|
|
404
|
+
|
|
405
|
+
expect(personCalls()).toBe(0);
|
|
406
|
+
expect(personLines().map((c) => (c[0] as { outcome: string }).outcome)).toEqual(["person_skipped"]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it.each([
|
|
410
|
+
["a foreign-site session", { metadata: { flowCheckoutId: FLOW_CHECKOUT_ID, site: "wksp_other-spoke" } }],
|
|
411
|
+
["an unpaid session", { payment_status: "unpaid" }],
|
|
412
|
+
["a session with no flowCheckoutId", { metadata: { flowSlug: "ailk-reco" } }],
|
|
413
|
+
])("%s makes no person call", async (_label, object) => {
|
|
414
|
+
mockVerifyWebhookSignature.mockReturnValue(liveEvent(true, object));
|
|
415
|
+
expect((await post()).statusCode).toBe(200);
|
|
416
|
+
expect(prisma.flowCheckout.findFirst).not.toHaveBeenCalled();
|
|
417
|
+
expect(personCalls()).toBe(0);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("a Person step that throws still answers 200 and logs person_failed with no email or customer id", async () => {
|
|
421
|
+
personFindFirst().mockRejectedValueOnce(new Error(`relation people down for ${EMAIL} cus_9`));
|
|
422
|
+
mockVerifyWebhookSignature.mockReturnValue(liveEvent(true));
|
|
423
|
+
expect((await post()).statusCode).toBe(200);
|
|
424
|
+
|
|
425
|
+
const lines = personLines();
|
|
426
|
+
expect(lines.map((c) => c[0])).toEqual([
|
|
427
|
+
{ eventIdPrefix: "evt_test", flowCheckoutIdPrefix: FLOW_CHECKOUT_ID.substring(0, 8), outcome: "person_failed" },
|
|
428
|
+
]);
|
|
429
|
+
for (const line of logCalls) {
|
|
430
|
+
const text = JSON.stringify(line);
|
|
431
|
+
expect(text).not.toContain(EMAIL);
|
|
432
|
+
expect(text).not.toContain("cus_9");
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
* `"no_payment_required"` (a 100%-off promo code, #4862) — anything else
|
|
71
71
|
* logs `outcome: "unpaid"` and leaves the row `created`.
|
|
72
72
|
*
|
|
73
|
+
* Checkout Person (checkout.session.completed, #852 D6):
|
|
74
|
+
* Chained after that stamp, `recordFlowCheckoutPerson` finds or creates the
|
|
75
|
+
* Person of the checkout row THIS session completed (matched on its own
|
|
76
|
+
* `stripeSessionId`), with `session.customer` as the customer id only on a
|
|
77
|
+
* live-mode event. Logs an outcome kind only; never fails the 200.
|
|
78
|
+
*
|
|
73
79
|
* Dunning (invoice.payment_failed / customer.subscription.updated, #6034):
|
|
74
80
|
* Records a per-subscriber lifecycle state in a `UserData` row behind a
|
|
75
81
|
* reserved-key guard (routes/auth/user.ts). `invoice.payment_failed` arms
|
|
@@ -111,6 +117,10 @@ import {
|
|
|
111
117
|
type DunningEntry,
|
|
112
118
|
type DunningRecord,
|
|
113
119
|
} from "../../services/dunning-policy.js";
|
|
120
|
+
import {
|
|
121
|
+
recordCheckoutPerson,
|
|
122
|
+
type CheckoutPersonOutcome,
|
|
123
|
+
} from "../../services/person.js";
|
|
114
124
|
|
|
115
125
|
const TELEMETRY_TIMEOUT_MS = 2000;
|
|
116
126
|
const DUNNING_WRITE_MAX_ATTEMPTS = 3;
|
|
@@ -359,11 +369,19 @@ export const stripeWebhookHandler = async (
|
|
|
359
369
|
await Promise.all([
|
|
360
370
|
fireTelemetryWithTimeout(request, "purchase-completed", props),
|
|
361
371
|
billingTask,
|
|
372
|
+
// #852 D6 — the Person step runs only after the stamp has settled,
|
|
373
|
+
// so it reads the row this very session completed.
|
|
362
374
|
stampFlowCheckout(request, {
|
|
363
375
|
session,
|
|
364
376
|
eventIdPrefix,
|
|
365
377
|
outcome: "completed",
|
|
366
|
-
})
|
|
378
|
+
}).then(() =>
|
|
379
|
+
recordFlowCheckoutPerson(request, {
|
|
380
|
+
session,
|
|
381
|
+
livemode: event.livemode === true,
|
|
382
|
+
eventIdPrefix,
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
367
385
|
stampRequestPaid(request, { session, eventIdPrefix }),
|
|
368
386
|
]);
|
|
369
387
|
break;
|
|
@@ -623,6 +641,59 @@ async function stampFlowCheckout(
|
|
|
623
641
|
}
|
|
624
642
|
}
|
|
625
643
|
|
|
644
|
+
/**
|
|
645
|
+
* The hub checkout's Person (#852 D6): after `stampFlowCheckout`, find or
|
|
646
|
+
* create the Person of the completed checkout's lead
|
|
647
|
+
* (`services/person.ts`'s `recordCheckoutPerson`). Runs only for a paid (or
|
|
648
|
+
* nothing-due) session of THIS site — the foreign-site filter has already
|
|
649
|
+
* returned — that carries `metadata.flowCheckoutId`. The customer id's ONE
|
|
650
|
+
* source is `session.customer`, written onto the Person only for a
|
|
651
|
+
* live-mode event (SEC-852-A). A replay matches the same completed row, so a
|
|
652
|
+
* delivery whose Person step failed is repaired by the next one.
|
|
653
|
+
*
|
|
654
|
+
* Logs `{ eventIdPrefix, flowCheckoutIdPrefix, outcome }` only — never the
|
|
655
|
+
* email or the customer id. Never throws: the response stays 200.
|
|
656
|
+
*/
|
|
657
|
+
async function recordFlowCheckoutPerson(
|
|
658
|
+
request: FastifyRequest,
|
|
659
|
+
{
|
|
660
|
+
session,
|
|
661
|
+
livemode,
|
|
662
|
+
eventIdPrefix,
|
|
663
|
+
}: {
|
|
664
|
+
session: CheckoutSessionObject;
|
|
665
|
+
livemode: boolean;
|
|
666
|
+
eventIdPrefix: string;
|
|
667
|
+
},
|
|
668
|
+
): Promise<void> {
|
|
669
|
+
const flowCheckoutId = session.metadata?.["flowCheckoutId"];
|
|
670
|
+
if (!flowCheckoutId) return;
|
|
671
|
+
if (
|
|
672
|
+
session.payment_status !== "paid" &&
|
|
673
|
+
session.payment_status !== "no_payment_required"
|
|
674
|
+
) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const flowCheckoutIdPrefix = flowCheckoutId.substring(0, 8);
|
|
678
|
+
let outcome: CheckoutPersonOutcome;
|
|
679
|
+
try {
|
|
680
|
+
outcome = await recordCheckoutPerson({
|
|
681
|
+
flowCheckoutId,
|
|
682
|
+
stripeSessionId: session.id,
|
|
683
|
+
customer: session.customer,
|
|
684
|
+
livemode,
|
|
685
|
+
});
|
|
686
|
+
} catch {
|
|
687
|
+
outcome = "person_failed";
|
|
688
|
+
}
|
|
689
|
+
const line = { eventIdPrefix, flowCheckoutIdPrefix, outcome };
|
|
690
|
+
if (outcome === "person_failed") {
|
|
691
|
+
request.log.error(line, "flow checkout person failed — continuing");
|
|
692
|
+
} else {
|
|
693
|
+
request.log.info(line, "flow checkout person");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
626
697
|
/**
|
|
627
698
|
* Stamp `paidAt` on a request-and-confirm callback row (#5970 AC-3).
|
|
628
699
|
*
|
|
@@ -48,6 +48,10 @@ import {
|
|
|
48
48
|
} from "./openapi/spec.js";
|
|
49
49
|
import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
50
50
|
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
51
|
+
import {
|
|
52
|
+
hubSignInClientRoutes,
|
|
53
|
+
hubSignInSessionRoutes,
|
|
54
|
+
} from "./routes/auth/hub-sign-in.js";
|
|
51
55
|
import { tokenRoutes } from "./routes/auth/tokens.js";
|
|
52
56
|
import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
|
|
53
57
|
import { userRoutes } from "./routes/auth/user.js";
|
|
@@ -217,6 +221,16 @@ const WORKSPACES_MESSAGES = "./routes/workspaces/messages.js";
|
|
|
217
221
|
// WORKSPACES_MESSAGES in the same workspace/multi-tenant scope.
|
|
218
222
|
const WORKSPACES_SUBMISSIONS = "./routes/workspaces/submissions.js";
|
|
219
223
|
const WORKSPACES_SUBSCRIBERS = "./routes/workspaces/subscribers.js";
|
|
224
|
+
// #852 D8 — the People reads (the workspace's clients), the SAME scope.
|
|
225
|
+
const WORKSPACES_PEOPLE = "./routes/workspaces/people.js";
|
|
226
|
+
// #7877 D9 — a client's pieces, plan and outbound messages, and the
|
|
227
|
+
// workspace's piece stages: the operator's writes, the SAME scope.
|
|
228
|
+
const WORKSPACES_PIECES = "./routes/workspaces/pieces.js";
|
|
229
|
+
const WORKSPACES_PLAN = "./routes/workspaces/plan.js";
|
|
230
|
+
const WORKSPACES_MESSAGES_OUT = "./routes/workspaces/messages-out.js";
|
|
231
|
+
// #7877 D11 — the member reads: auth only, under the SAME module predicate.
|
|
232
|
+
const MEMBER_PIECES = "./routes/member/pieces.js";
|
|
233
|
+
const MEMBER_MESSAGES = "./routes/member/messages.js";
|
|
220
234
|
// #3591 PR-B — the agent seat's management routes and an agent's own
|
|
221
235
|
// context, siblings of WORKSPACES_MEMBERS in the same workspace/multi-tenant
|
|
222
236
|
// scope.
|
|
@@ -292,6 +306,33 @@ const SLICE_PLUGINS = {
|
|
|
292
306
|
specifier: WORKSPACES_SUBSCRIBERS,
|
|
293
307
|
load: () => import(WORKSPACES_SUBSCRIBERS),
|
|
294
308
|
},
|
|
309
|
+
// #852 D8 — the People list and Person record, the SAME scope.
|
|
310
|
+
workspacePeopleRoutes: {
|
|
311
|
+
specifier: WORKSPACES_PEOPLE,
|
|
312
|
+
load: () => import(WORKSPACES_PEOPLE),
|
|
313
|
+
},
|
|
314
|
+
// #7877 D9 — the member area's operator routes, the SAME scope.
|
|
315
|
+
workspacePieceRoutes: {
|
|
316
|
+
specifier: WORKSPACES_PIECES,
|
|
317
|
+
load: () => import(WORKSPACES_PIECES),
|
|
318
|
+
},
|
|
319
|
+
workspacePlanRoutes: {
|
|
320
|
+
specifier: WORKSPACES_PLAN,
|
|
321
|
+
load: () => import(WORKSPACES_PLAN),
|
|
322
|
+
},
|
|
323
|
+
workspaceOutboundMessageRoutes: {
|
|
324
|
+
specifier: WORKSPACES_MESSAGES_OUT,
|
|
325
|
+
load: () => import(WORKSPACES_MESSAGES_OUT),
|
|
326
|
+
},
|
|
327
|
+
// #7877 D11 — the member reads, in authScope under the same predicate.
|
|
328
|
+
memberPieceRoutes: {
|
|
329
|
+
specifier: MEMBER_PIECES,
|
|
330
|
+
load: () => import(MEMBER_PIECES),
|
|
331
|
+
},
|
|
332
|
+
memberMessageRoutes: {
|
|
333
|
+
specifier: MEMBER_MESSAGES,
|
|
334
|
+
load: () => import(MEMBER_MESSAGES),
|
|
335
|
+
},
|
|
295
336
|
// #3591 PR-B — the agent seat, the SAME scope.
|
|
296
337
|
workspaceAgentRoutes: {
|
|
297
338
|
specifier: WORKSPACES_AGENTS,
|
|
@@ -365,6 +406,12 @@ const SLICE_PLUGINS = {
|
|
|
365
406
|
specifier: FLOWS_INDEX,
|
|
366
407
|
load: () => import(FLOWS_INDEX),
|
|
367
408
|
},
|
|
409
|
+
// #7876 D1 — a spoke seeds its declared flows; off until
|
|
410
|
+
// AILK_SPOKE_FLOW_SEED (the route answers 403 flow_seed_disabled).
|
|
411
|
+
putFlowDefinitionRoute: {
|
|
412
|
+
specifier: FLOWS_INDEX,
|
|
413
|
+
load: () => import(FLOWS_INDEX),
|
|
414
|
+
},
|
|
368
415
|
// #6157 — the tenant-scoped magnet Admin authoring surface. ONE plugin,
|
|
369
416
|
// ONE specifier (D2): all four routes register through this one entry.
|
|
370
417
|
magnetRoutes: {
|
|
@@ -791,6 +838,14 @@ export async function build() {
|
|
|
791
838
|
await authScope.register(tokenRoutes);
|
|
792
839
|
await authScope.register(userRoutes);
|
|
793
840
|
|
|
841
|
+
// The hub signs in its spokes (#7894 D5) — `codes` and `me`. Auth only,
|
|
842
|
+
// NOT tenant-guarded: a person signing in to a spoke has no tenant here.
|
|
843
|
+
// Unconditional, like the leads inbox read below: a spoke's sign-in is not
|
|
844
|
+
// a module. `codes` answers a cookie session only; `me` the hub credential
|
|
845
|
+
// only (both enforced in the route). The credential's own admission is the
|
|
846
|
+
// preHandler's hub-credential branch (D6).
|
|
847
|
+
await authScope.register(hubSignInSessionRoutes);
|
|
848
|
+
|
|
794
849
|
// Tenant-context routes (multi-tenant module) — auth only. They
|
|
795
850
|
// establish/inspect the active workspace, so the tenant guard does not
|
|
796
851
|
// apply to them; each enforces membership in the service layer.
|
|
@@ -862,6 +917,18 @@ export async function build() {
|
|
|
862
917
|
await authScope.register(await sliceRoutePlugin("projectListingMeRoute"));
|
|
863
918
|
}
|
|
864
919
|
|
|
920
|
+
// The member reads (#7877 D11) — auth only, NOT tenant-guarded: a client
|
|
921
|
+
// holds no membership (and an agent, which reaches only tenant-guarded
|
|
922
|
+
// routes, is refused 403 by the preHandler). Each handler resolves the
|
|
923
|
+
// caller through `memberCaller` and the workspace from the calling
|
|
924
|
+
// spoke's key, never a request value. Registered under the SAME module
|
|
925
|
+
// predicate as every routes/workspaces/* route, at the exact URLs the
|
|
926
|
+
// preHandler's `HUB_SESSION_ROUTES` admits a hub credential on.
|
|
927
|
+
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
928
|
+
await authScope.register(await sliceRoutePlugin("memberPieceRoutes"));
|
|
929
|
+
await authScope.register(await sliceRoutePlugin("memberMessageRoutes"));
|
|
930
|
+
}
|
|
931
|
+
|
|
865
932
|
// Tenant-guarded domain routes (multi-tenant module) — the tenant
|
|
866
933
|
// preHandler (default-deny guard, see middleware/tenant.ts) runs after
|
|
867
934
|
// auth for every route in this inner scope. No domain handler executes
|
|
@@ -1024,6 +1091,25 @@ export async function build() {
|
|
|
1024
1091
|
await tenantScope.register(
|
|
1025
1092
|
await sliceRoutePlugin("workspaceSubscriberRoutes"),
|
|
1026
1093
|
);
|
|
1094
|
+
// The People reads (#852 D8) — the SAME scope and guard: the
|
|
1095
|
+
// workspace's clients and one Person with its derived timeline,
|
|
1096
|
+
// behind `people-read` (owner, admin, a granted agent).
|
|
1097
|
+
await tenantScope.register(
|
|
1098
|
+
await sliceRoutePlugin("workspacePeopleRoutes"),
|
|
1099
|
+
);
|
|
1100
|
+
// The member area's operator routes (#7877 D9) — the SAME scope and
|
|
1101
|
+
// guard: a client's pieces, plan and outbound messages, and the
|
|
1102
|
+
// workspace's declared piece stages. Writes need `people-write`
|
|
1103
|
+
// (owner, admin; never an agent).
|
|
1104
|
+
await tenantScope.register(
|
|
1105
|
+
await sliceRoutePlugin("workspacePieceRoutes"),
|
|
1106
|
+
);
|
|
1107
|
+
await tenantScope.register(
|
|
1108
|
+
await sliceRoutePlugin("workspacePlanRoutes"),
|
|
1109
|
+
);
|
|
1110
|
+
await tenantScope.register(
|
|
1111
|
+
await sliceRoutePlugin("workspaceOutboundMessageRoutes"),
|
|
1112
|
+
);
|
|
1027
1113
|
// The agent seat (#3591 PR-B) — the SAME scope and guard: owners and
|
|
1028
1114
|
// admins list, add, re-key and switch off agents (`agents-manage`),
|
|
1029
1115
|
// and an agent reads its own context at GET /v1/agent/context.
|
|
@@ -1249,6 +1335,22 @@ export async function build() {
|
|
|
1249
1335
|
await authEmailScope.register(transactionalEmailRoute);
|
|
1250
1336
|
});
|
|
1251
1337
|
|
|
1338
|
+
// The hub signs in its spokes (#7894 D5) — `token` and `revoke`. Their OWN
|
|
1339
|
+
// scope, deliberately OUTSIDE `authScope`: the caller is a spoke's server,
|
|
1340
|
+
// authenticated by its client secret AND its spoke key inside the service,
|
|
1341
|
+
// not by a person's session — an auth preHandler in front would reject every
|
|
1342
|
+
// legitimate call. `scope: "hubsignin"` namespaces the buckets (the scope
|
|
1343
|
+
// name admits no hyphen). The per-IP limit keys on `req.ip` under
|
|
1344
|
+
// `trustProxy`, so it is a backstop only; the control is the per-client
|
|
1345
|
+
// bucket the service keys on the VERIFIED client (SEC-7894-8).
|
|
1346
|
+
await server.register(async (hubSignInScope) => {
|
|
1347
|
+
hubSignInScope.register(rateLimitMiddleware, {
|
|
1348
|
+
perIpPerMinute: 60,
|
|
1349
|
+
scope: "hubsignin",
|
|
1350
|
+
});
|
|
1351
|
+
await hubSignInScope.register(hubSignInClientRoutes);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1252
1354
|
// Lead-capture status (#6730 D4) — a cheap read the web page makes before
|
|
1253
1355
|
// choosing the form or the contact email. Top level, NOT in `leadsScope`:
|
|
1254
1356
|
// the web server's single egress IP already spends that scope's strict
|
|
@@ -1360,6 +1462,9 @@ export async function build() {
|
|
|
1360
1462
|
await flowsScope.register(
|
|
1361
1463
|
await sliceRoutePlugin("getFlowAggregateRoute"),
|
|
1362
1464
|
);
|
|
1465
|
+
await flowsScope.register(
|
|
1466
|
+
await sliceRoutePlugin("putFlowDefinitionRoute"),
|
|
1467
|
+
);
|
|
1363
1468
|
}
|
|
1364
1469
|
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
1365
1470
|
// rate-limited posture as the three above. Module-conditional (commerce,
|
|
@@ -354,7 +354,7 @@ describe("fulfillDeliverable", () => {
|
|
|
354
354
|
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
355
355
|
workspaceId: WORKSPACE_ID,
|
|
356
356
|
});
|
|
357
|
-
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID });
|
|
357
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID, tags: [] });
|
|
358
358
|
|
|
359
359
|
await fulfillDeliverable(baseInput());
|
|
360
360
|
|
|
@@ -366,7 +366,7 @@ describe("fulfillDeliverable", () => {
|
|
|
366
366
|
);
|
|
367
367
|
expect(prisma.asset.findFirst).toHaveBeenCalledWith({
|
|
368
368
|
where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
|
|
369
|
-
select: { id: true },
|
|
369
|
+
select: { id: true, tags: true },
|
|
370
370
|
});
|
|
371
371
|
expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
|
|
372
372
|
expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
|
|
@@ -421,7 +421,7 @@ describe("fulfillDeliverable", () => {
|
|
|
421
421
|
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
422
422
|
workspaceId: WORKSPACE_ID,
|
|
423
423
|
});
|
|
424
|
-
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID });
|
|
424
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID, tags: [] });
|
|
425
425
|
// beforeEach's default deliverableAsset.findUnique mock already resolves
|
|
426
426
|
// a row at this key — it must never even be consulted.
|
|
427
427
|
|
|
@@ -482,7 +482,7 @@ describe("fulfillDeliverable", () => {
|
|
|
482
482
|
|
|
483
483
|
expect(prisma.asset.findFirst).toHaveBeenCalledWith({
|
|
484
484
|
where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
|
|
485
|
-
select: { id: true },
|
|
485
|
+
select: { id: true, tags: true },
|
|
486
486
|
});
|
|
487
487
|
expect(prisma.asset.findFirst).not.toHaveBeenCalledWith(
|
|
488
488
|
expect.objectContaining({
|