create-ailk 0.23.0 → 0.25.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/README.md +68 -33
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +10 -3
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +38 -4
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +75 -22
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +1 -1
- package/dist/lib/fetch-template-snapshot.js +7 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +34 -12
- package/dist/lib/scaffold-render-sweep.d.ts +33 -0
- package/dist/lib/scaffold-render-sweep.js +45 -0
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +122 -538
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- package/dist/sync-routes.d.ts +30 -4
- package/dist/sync-routes.js +271 -18
- package/dist/template-record.d.ts +14 -4
- package/dist/template-record.js +23 -5
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +31 -19
- package/dist/update.d.ts +10 -1
- package/dist/update.js +540 -16
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +9 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +15 -3
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +9 -2
- package/templates/apps/api/CLAUDE.md +122 -82
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1857
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +77 -24
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -12
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +21 -5
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +2 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
- package/templates/apps/web/lib/auth-email.ts +1 -1
- package/templates/apps/web/lib/magnet-routes.ts +3 -1
- package/templates/apps/web/lib/newsletter.actions.ts +9 -6
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +32 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +757 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +48 -35
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -254
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -318
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -1815
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -268
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -129
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -573
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -313
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1188
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2073
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -342
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -526
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -137
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -75
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
- package/templates/apps/mcp/src/tools/search_content.ts +0 -67
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -961
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -240
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /v1/auth/transactional-email — the signed auth-email relay (#5962 § D3).
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS ROUTE EXISTS. `architecture.yaml`'s `apps/web` node carries an
|
|
5
|
-
* explicit `may_not_import_from` entry for `packages/email` ("email send is
|
|
6
|
-
* server-side only via apps/api"), and `tools/lint-architecture.ts` evaluates
|
|
7
|
-
* deny BEFORE allow — so adding an allow entry would not clear it. Better
|
|
8
|
-
* Auth's password-reset and email-verification callbacks live in `apps/web`
|
|
9
|
-
* and need to send mail. This is where that crosses.
|
|
10
|
-
*
|
|
11
|
-
* API ↔ MCP PARITY — EXEMPT, DECLARED RATHER THAN SKIPPED (§ D19).
|
|
12
|
-
* `CLAUDE.md`'s parity contract binds "every HTTP endpoint an agent could
|
|
13
|
-
* reasonably call" to a 1:1 MCP tool. This is not one: it is an internal,
|
|
14
|
-
* HMAC-signed, machine-to-machine hop whose only caller is `apps/web`'s auth
|
|
15
|
-
* callbacks, and an MCP twin would be an anonymous agent tool that sends
|
|
16
|
-
* password-reset mail to an arbitrary address — a credential-reset oracle on
|
|
17
|
-
* the free surface. The precedent for an `apps/api` route with no twin is
|
|
18
|
-
* `routes/webhooks/stripe.ts`, a signed receiver in the same shape.
|
|
19
|
-
*
|
|
20
|
-
* THE HONEST ACCOUNTING. This endpoint takes a caller-chosen recipient and a
|
|
21
|
-
* caller-chosen URL: it is mail-relay-shaped. What makes it tolerable is the
|
|
22
|
-
* whole set, not the HMAC alone — the secret is deployment-only and never
|
|
23
|
-
* `NEXT_PUBLIC_`; the timestamp is inside the signed material and bounds
|
|
24
|
-
* replay to 300 s; the route sits behind `rateLimitMiddleware` at
|
|
25
|
-
* DEFAULT_PER_IP_PER_MINUTE_STRICT; the per-recipient budget bounds volume per
|
|
26
|
-
* victim; `kind` selects from FIXED templates so a caller chooses neither
|
|
27
|
-
* subject nor copy; and `packages/email`'s CR/LF guard rejects header
|
|
28
|
-
* injection on every string field. Anyone who can sign a body can already read
|
|
29
|
-
* AUTH_EMAIL_WEBHOOK_SECRET off the web deployment — at which point they can
|
|
30
|
-
* read BETTER_AUTH_SECRET too and forge sessions directly.
|
|
31
|
-
*
|
|
32
|
-
* ORDER: 503 → 401 → 400, signature verified over the RAW body BEFORE it is
|
|
33
|
-
* parsed. The reason is `apps/web/app/api/revalidate/route.ts`'s, verbatim: "a
|
|
34
|
-
* 400 before 401 would hand an unauthenticated caller a JSON-parser oracle".
|
|
35
|
-
*
|
|
36
|
-
* PII BOUNDARY (§ D12): a log line carries `userId`, `kind`, `reason` and an
|
|
37
|
-
* error KIND. Never the recipient, never the token, never the assembled link —
|
|
38
|
-
* the link IS the credential, so a log line quoting it hands anyone with log
|
|
39
|
-
* access a live reset for an arbitrary account.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
43
|
-
|
|
44
|
-
import {
|
|
45
|
-
sendEmailVerificationEmail,
|
|
46
|
-
sendMagicLink,
|
|
47
|
-
sendPasswordChangedEmail,
|
|
48
|
-
sendPasswordResetEmail,
|
|
49
|
-
} from "@working-theory/email";
|
|
50
|
-
import type { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
51
|
-
|
|
52
|
-
import {
|
|
53
|
-
consumeRecipientBudget,
|
|
54
|
-
siteVolumeAlert,
|
|
55
|
-
type AuthEmailKind,
|
|
56
|
-
} from "../../lib/auth-email-budget.js";
|
|
57
|
-
|
|
58
|
-
const SIGNATURE_HEADER = "x-ailk-auth-email-signature";
|
|
59
|
-
const TIMESTAMP_HEADER = "x-ailk-auth-email-timestamp";
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* A FORMAT guard, not a secret-dependent one — it compares the header against
|
|
63
|
-
* a constant pattern, never against the digest.
|
|
64
|
-
*/
|
|
65
|
-
const SIGNATURE_SHAPE = /^sha256=[0-9a-f]{64}$/;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* How stale a signed body may be. #5851's revalidate pair has NO replay window
|
|
69
|
-
* and #5877 § D7 accepts that, because a replayed `revalidateTag` is a no-op.
|
|
70
|
-
* A replayed EMAIL is not a no-op, so this pair closes the gap at the one seam
|
|
71
|
-
* where it matters.
|
|
72
|
-
*/
|
|
73
|
-
const MAX_SKEW_SECONDS = 300;
|
|
74
|
-
|
|
75
|
-
const KINDS = new Set<AuthEmailKind>([
|
|
76
|
-
"password-reset",
|
|
77
|
-
"email-verification",
|
|
78
|
-
"password-changed",
|
|
79
|
-
"magic-link",
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
type RelayBody = {
|
|
83
|
-
kind: AuthEmailKind;
|
|
84
|
-
userId: string;
|
|
85
|
-
to: string;
|
|
86
|
-
url?: string;
|
|
87
|
-
expiresInMinutes?: number;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Line-for-line the repo's shipped verifier shape
|
|
92
|
-
* (`apps/web/app/api/revalidate/route.ts`), with the base string changed to
|
|
93
|
-
* `<timestamp>.<rawBody>` so the timestamp cannot be swapped independently of
|
|
94
|
-
* the body it authenticates. The length guard is required, not decorative:
|
|
95
|
-
* `timingSafeEqual` throws on unequal lengths. The `sha256=` prefix stays
|
|
96
|
-
* inside the compared material so a same-length `sha512=` header is rejected
|
|
97
|
-
* outright rather than compared against a sha256 digest.
|
|
98
|
-
*/
|
|
99
|
-
function isValidSignature(
|
|
100
|
-
rawBody: string,
|
|
101
|
-
timestamp: string,
|
|
102
|
-
provided: string,
|
|
103
|
-
secret: string,
|
|
104
|
-
): boolean {
|
|
105
|
-
const expected = `sha256=${createHmac("sha256", secret)
|
|
106
|
-
.update(`${timestamp}.${rawBody}`, "utf8")
|
|
107
|
-
.digest("hex")}`;
|
|
108
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
109
|
-
const providedBuf = Buffer.from(provided, "utf8");
|
|
110
|
-
if (expectedBuf.length !== providedBuf.length) return false;
|
|
111
|
-
return timingSafeEqual(expectedBuf, providedBuf);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The parsed body, or null if the payload is not one this relay will send. */
|
|
115
|
-
function parseBody(rawBody: string): RelayBody | null {
|
|
116
|
-
let parsed: unknown;
|
|
117
|
-
try {
|
|
118
|
-
parsed = JSON.parse(rawBody);
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
const { kind, userId, to, url, expiresInMinutes } = parsed as Record<
|
|
126
|
-
string,
|
|
127
|
-
unknown
|
|
128
|
-
>;
|
|
129
|
-
if (
|
|
130
|
-
typeof kind !== "string" ||
|
|
131
|
-
!KINDS.has(kind as AuthEmailKind) ||
|
|
132
|
-
typeof userId !== "string" ||
|
|
133
|
-
userId.length === 0 ||
|
|
134
|
-
typeof to !== "string" ||
|
|
135
|
-
to.length === 0
|
|
136
|
-
) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
// The three link-carrying kinds need a url; the notice must NOT have one
|
|
140
|
-
// acted on even if a caller supplies it (§ D17 — the notice carries no
|
|
141
|
-
// credential, and that is enforced by the sender's input type too).
|
|
142
|
-
const needsUrl =
|
|
143
|
-
kind === "password-reset" ||
|
|
144
|
-
kind === "email-verification" ||
|
|
145
|
-
kind === "magic-link";
|
|
146
|
-
if (needsUrl && (typeof url !== "string" || url.length === 0)) return null;
|
|
147
|
-
if (expiresInMinutes !== undefined && typeof expiresInMinutes !== "number") {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
kind: kind as AuthEmailKind,
|
|
153
|
-
userId,
|
|
154
|
-
to,
|
|
155
|
-
...(typeof url === "string" ? { url } : {}),
|
|
156
|
-
...(typeof expiresInMinutes === "number" ? { expiresInMinutes } : {}),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* One fixed template per kind. Exhaustive: a kind added to `AuthEmailKind`
|
|
162
|
-
* without a branch here fails the type check.
|
|
163
|
-
*/
|
|
164
|
-
function sendFor(
|
|
165
|
-
kind: AuthEmailKind,
|
|
166
|
-
to: string,
|
|
167
|
-
linkInput: { to: string; url: string; fromAddress: string; expiresInMinutes?: number },
|
|
168
|
-
fromAddress: string,
|
|
169
|
-
) {
|
|
170
|
-
switch (kind) {
|
|
171
|
-
case "password-changed":
|
|
172
|
-
return sendPasswordChangedEmail({ to, fromAddress });
|
|
173
|
-
case "email-verification":
|
|
174
|
-
return sendEmailVerificationEmail(linkInput);
|
|
175
|
-
case "password-reset":
|
|
176
|
-
return sendPasswordResetEmail(linkInput);
|
|
177
|
-
case "magic-link":
|
|
178
|
-
// The template's copy fixes its own lifetime ("expires in 5 minutes")
|
|
179
|
-
// and its input has no expiry field, so none is forwarded (#7435 § D4).
|
|
180
|
-
return sendMagicLink({ to, url: linkInput.url, fromAddress });
|
|
181
|
-
default: {
|
|
182
|
-
const unreachable: never = kind;
|
|
183
|
-
throw new Error(`unhandled auth email kind: ${String(unreachable)}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export const transactionalEmailRoute: FastifyPluginAsync = async (fastify) => {
|
|
189
|
-
fastify.post(
|
|
190
|
-
"/v1/auth/transactional-email",
|
|
191
|
-
async (request: FastifyRequest, reply) => {
|
|
192
|
-
// Read per request, never at module load: a module-scope const latches
|
|
193
|
-
// the value at cold start, so a secret added to a running deployment
|
|
194
|
-
// would keep returning 503 until the lambda recycles.
|
|
195
|
-
const secret = process.env.AUTH_EMAIL_WEBHOOK_SECRET;
|
|
196
|
-
const fromAddress = process.env.AUTH_EMAIL_FROM;
|
|
197
|
-
|
|
198
|
-
if (!secret || !fromAddress) {
|
|
199
|
-
request.log.warn(
|
|
200
|
-
{ reason: "not_configured" },
|
|
201
|
-
"auth-email.not_configured",
|
|
202
|
-
);
|
|
203
|
-
return reply.status(503).send({ error: "not_configured" });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Kept as the signed material — never re-serialized. The route's scope
|
|
207
|
-
// installs a buffer parser for exactly this reason.
|
|
208
|
-
const raw = request.body;
|
|
209
|
-
const rawBody = Buffer.isBuffer(raw)
|
|
210
|
-
? raw.toString("utf8")
|
|
211
|
-
: typeof raw === "string"
|
|
212
|
-
? raw
|
|
213
|
-
: "";
|
|
214
|
-
|
|
215
|
-
const provided = request.headers[SIGNATURE_HEADER];
|
|
216
|
-
const timestamp = request.headers[TIMESTAMP_HEADER];
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
typeof provided !== "string" ||
|
|
220
|
-
typeof timestamp !== "string" ||
|
|
221
|
-
!SIGNATURE_SHAPE.test(provided) ||
|
|
222
|
-
!isValidSignature(rawBody, timestamp, provided, secret)
|
|
223
|
-
) {
|
|
224
|
-
request.log.warn({ reason: "bad_signature" }, "auth-email.unauthorized");
|
|
225
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Checked AFTER the signature so an unauthenticated caller learns
|
|
229
|
-
// nothing from the skew answer, and the timestamp it bounds is one the
|
|
230
|
-
// signature already authenticated.
|
|
231
|
-
const tsSeconds = Number(timestamp);
|
|
232
|
-
const skew = Math.abs(Math.floor(Date.now() / 1000) - tsSeconds);
|
|
233
|
-
if (!Number.isFinite(tsSeconds) || skew > MAX_SKEW_SECONDS) {
|
|
234
|
-
request.log.warn({ reason: "stale_timestamp" }, "auth-email.unauthorized");
|
|
235
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const body = parseBody(rawBody);
|
|
239
|
-
if (body === null) {
|
|
240
|
-
request.log.warn(
|
|
241
|
-
{ reason: "validation_failed" },
|
|
242
|
-
"auth-email.validation_failed",
|
|
243
|
-
);
|
|
244
|
-
return reply.status(400).send({ error: "validation_failed" });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (siteVolumeAlert()) {
|
|
248
|
-
request.log.warn(
|
|
249
|
-
{ reason: "site_volume_high" },
|
|
250
|
-
"auth-email.site-volume-high",
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Over budget → skip the send, log one line, and answer exactly what a
|
|
255
|
-
// sent message answers. Suppression is NEVER observable (§ D8c).
|
|
256
|
-
if (!consumeRecipientBudget(body.to, body.kind)) {
|
|
257
|
-
request.log.warn(
|
|
258
|
-
{ userId: body.userId, kind: body.kind, reason: "budget_recipient" },
|
|
259
|
-
"auth-email.suppressed",
|
|
260
|
-
);
|
|
261
|
-
return reply.status(202).send({ ok: true });
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// `kind` selects from FIXED templates. A caller supplies no subject and
|
|
265
|
-
// no copy — only a recipient and, for the three link kinds, a URL.
|
|
266
|
-
const linkInput = {
|
|
267
|
-
to: body.to,
|
|
268
|
-
url: body.url ?? "",
|
|
269
|
-
fromAddress,
|
|
270
|
-
...(body.expiresInMinutes !== undefined
|
|
271
|
-
? { expiresInMinutes: body.expiresInMinutes }
|
|
272
|
-
: {}),
|
|
273
|
-
};
|
|
274
|
-
const result = await sendFor(body.kind, body.to, linkInput, fromAddress);
|
|
275
|
-
|
|
276
|
-
if (!result.ok) {
|
|
277
|
-
// `error.kind` only — `error.message` can quote an address (§ D12).
|
|
278
|
-
request.log.warn(
|
|
279
|
-
{
|
|
280
|
-
userId: body.userId,
|
|
281
|
-
kind: body.kind,
|
|
282
|
-
reason: "send_failed",
|
|
283
|
-
errorKind: result.error.kind,
|
|
284
|
-
},
|
|
285
|
-
"auth-email.send_failed",
|
|
286
|
-
);
|
|
287
|
-
} else {
|
|
288
|
-
request.log.info(
|
|
289
|
-
{ userId: body.userId, kind: body.kind, reason: "sent" },
|
|
290
|
-
"auth-email.sent",
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 202 on every accepted outcome, a failed send included: the visitor's
|
|
295
|
-
// answer must not distinguish "this address exists" from "it does not",
|
|
296
|
-
// and a send failure is the operator's signal (the log line above), not
|
|
297
|
-
// the caller's.
|
|
298
|
-
return reply.status(202).send({ ok: true });
|
|
299
|
-
},
|
|
300
|
-
);
|
|
301
|
-
};
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User Data Routes
|
|
3
|
-
*
|
|
4
|
-
* Handles user profile and preferences (stored locally).
|
|
5
|
-
*/
|
|
6
|
-
import type { Prisma } from '@working-theory/database';
|
|
7
|
-
import { logger } from '@working-theory/observability';
|
|
8
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
|
|
11
|
-
import { prisma } from '../../lib/prisma.js';
|
|
12
|
-
|
|
13
|
-
const updateProfileSchema = z.object({
|
|
14
|
-
name: z.string().optional(),
|
|
15
|
-
image: z.string().url().optional(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const userDataSchema = z.object({
|
|
19
|
-
key: z.string().min(1),
|
|
20
|
-
value: z.unknown(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Key namespaces owned by the server. A caller may never read or write these
|
|
25
|
-
* through /v1/user/data/:key — the rows are written by trusted server paths
|
|
26
|
-
* (e.g. the Stripe webhook's dunning record, #6034 § D12) and are the
|
|
27
|
-
* SUBJECT of decisions about the caller, not the caller's own storage.
|
|
28
|
-
* Without this guard the subject of an access decision is also its author:
|
|
29
|
-
* a subscriber whose renewal has failed could read their own dunning record,
|
|
30
|
-
* PUT it back as a self-granted "active" state, and poison the record's
|
|
31
|
-
* ordering watermark so every future genuine Stripe event for that
|
|
32
|
-
* subscription is rejected as stale forever.
|
|
33
|
-
*/
|
|
34
|
-
const RESERVED_USER_DATA_KEY_PREFIXES = ['commerce.', 'system.'] as const;
|
|
35
|
-
|
|
36
|
-
/** #6034 § D12 — a serialized `value` above this bound is refused on PUT. */
|
|
37
|
-
const USER_DATA_MAX_VALUE_BYTES = 64 * 1024;
|
|
38
|
-
|
|
39
|
-
/** Case-insensitive prefix match on the raw key (after Fastify's own param
|
|
40
|
-
* decoding) — `Commerce.` and `COMMERCE.` are refused too. */
|
|
41
|
-
function isReservedUserDataKey(key: string): boolean {
|
|
42
|
-
const lower = key.toLowerCase();
|
|
43
|
-
return RESERVED_USER_DATA_KEY_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const userRoutes: FastifyPluginAsync = async (fastify) => {
|
|
47
|
-
/**
|
|
48
|
-
* GET /v1/user/profile
|
|
49
|
-
* Get current user's profile
|
|
50
|
-
*/
|
|
51
|
-
fastify.get('/v1/user/profile', async (request, reply) => {
|
|
52
|
-
try {
|
|
53
|
-
const userId = (request as unknown as { userId?: string }).userId;
|
|
54
|
-
if (!userId) {
|
|
55
|
-
return reply.status(401).send({
|
|
56
|
-
success: false,
|
|
57
|
-
error: 'Authentication required',
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const user = await prisma.user.findUnique({
|
|
62
|
-
where: { id: userId },
|
|
63
|
-
select: {
|
|
64
|
-
id: true,
|
|
65
|
-
email: true,
|
|
66
|
-
name: true,
|
|
67
|
-
image: true,
|
|
68
|
-
role: true,
|
|
69
|
-
createdAt: true,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (!user) {
|
|
74
|
-
return reply.status(404).send({
|
|
75
|
-
success: false,
|
|
76
|
-
error: 'User not found',
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return reply.send({
|
|
81
|
-
success: true,
|
|
82
|
-
data: user,
|
|
83
|
-
});
|
|
84
|
-
} catch (error) {
|
|
85
|
-
logger.error({ err: error }, '[User] Error fetching profile');
|
|
86
|
-
return reply.status(500).send({
|
|
87
|
-
success: false,
|
|
88
|
-
error: 'Internal server error',
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* PATCH /v1/user/profile
|
|
95
|
-
* Update current user's profile
|
|
96
|
-
*/
|
|
97
|
-
fastify.patch('/v1/user/profile', async (request, reply) => {
|
|
98
|
-
try {
|
|
99
|
-
const userId = (request as unknown as { userId?: string }).userId;
|
|
100
|
-
if (!userId) {
|
|
101
|
-
return reply.status(401).send({
|
|
102
|
-
success: false,
|
|
103
|
-
error: 'Authentication required',
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const data = updateProfileSchema.parse(request.body);
|
|
108
|
-
|
|
109
|
-
const user = await prisma.user.update({
|
|
110
|
-
where: { id: userId },
|
|
111
|
-
data: {
|
|
112
|
-
...(data.name !== undefined && { name: data.name }),
|
|
113
|
-
...(data.image !== undefined && { image: data.image }),
|
|
114
|
-
},
|
|
115
|
-
select: {
|
|
116
|
-
id: true,
|
|
117
|
-
email: true,
|
|
118
|
-
name: true,
|
|
119
|
-
image: true,
|
|
120
|
-
role: true,
|
|
121
|
-
updatedAt: true,
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return reply.send({
|
|
126
|
-
success: true,
|
|
127
|
-
data: user,
|
|
128
|
-
});
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (error instanceof z.ZodError) {
|
|
131
|
-
return reply.status(400).send({
|
|
132
|
-
success: false,
|
|
133
|
-
error: 'Validation failed',
|
|
134
|
-
details: error.issues,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
logger.error({ err: error }, '[User] Error updating profile');
|
|
139
|
-
return reply.status(500).send({
|
|
140
|
-
success: false,
|
|
141
|
-
error: 'Internal server error',
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* GET /v1/user/data/:key
|
|
148
|
-
* Get user data by key
|
|
149
|
-
*/
|
|
150
|
-
fastify.get<{ Params: { key: string } }>('/v1/user/data/:key', async (request, reply) => {
|
|
151
|
-
try {
|
|
152
|
-
const userId = (request as unknown as { userId?: string }).userId;
|
|
153
|
-
if (!userId) {
|
|
154
|
-
return reply.status(401).send({
|
|
155
|
-
success: false,
|
|
156
|
-
error: 'Authentication required',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const { key } = request.params;
|
|
161
|
-
|
|
162
|
-
if (isReservedUserDataKey(key)) {
|
|
163
|
-
return reply.status(403).send({
|
|
164
|
-
success: false,
|
|
165
|
-
error: 'reserved-key',
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const userData = await prisma.userData.findUnique({
|
|
170
|
-
where: {
|
|
171
|
-
userId_key: { userId, key },
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
if (!userData) {
|
|
176
|
-
return reply.status(404).send({
|
|
177
|
-
success: false,
|
|
178
|
-
error: 'Data not found',
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return reply.send({
|
|
183
|
-
success: true,
|
|
184
|
-
data: userData.value,
|
|
185
|
-
});
|
|
186
|
-
} catch (error) {
|
|
187
|
-
logger.error({ err: error }, '[User] Error fetching data');
|
|
188
|
-
return reply.status(500).send({
|
|
189
|
-
success: false,
|
|
190
|
-
error: 'Internal server error',
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* PUT /v1/user/data/:key
|
|
197
|
-
* Set user data by key
|
|
198
|
-
*/
|
|
199
|
-
fastify.put<{ Params: { key: string } }>('/v1/user/data/:key', async (request, reply) => {
|
|
200
|
-
try {
|
|
201
|
-
const userId = (request as unknown as { userId?: string }).userId;
|
|
202
|
-
if (!userId) {
|
|
203
|
-
return reply.status(401).send({
|
|
204
|
-
success: false,
|
|
205
|
-
error: 'Authentication required',
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const { key } = request.params;
|
|
210
|
-
|
|
211
|
-
if (isReservedUserDataKey(key)) {
|
|
212
|
-
return reply.status(403).send({
|
|
213
|
-
success: false,
|
|
214
|
-
error: 'reserved-key',
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const { value } = userDataSchema.parse({ key, value: request.body });
|
|
219
|
-
|
|
220
|
-
const jsonValue = value as Prisma.InputJsonValue;
|
|
221
|
-
|
|
222
|
-
// `JSON.stringify` returns `undefined` (not the string "undefined")
|
|
223
|
-
// for an `undefined` input — never pass that to `Buffer.byteLength`,
|
|
224
|
-
// which throws on a non-string. An undefined value serializes to
|
|
225
|
-
// nothing on the wire, so it never exceeds the bound.
|
|
226
|
-
const serializedValue = JSON.stringify(jsonValue);
|
|
227
|
-
if (
|
|
228
|
-
serializedValue !== undefined &&
|
|
229
|
-
Buffer.byteLength(serializedValue) > USER_DATA_MAX_VALUE_BYTES
|
|
230
|
-
) {
|
|
231
|
-
return reply.status(400).send({
|
|
232
|
-
success: false,
|
|
233
|
-
error: 'value-too-large',
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const userData = await prisma.userData.upsert({
|
|
238
|
-
where: {
|
|
239
|
-
userId_key: { userId, key },
|
|
240
|
-
},
|
|
241
|
-
update: { value: jsonValue },
|
|
242
|
-
create: { userId, key, value: jsonValue },
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
return reply.send({
|
|
246
|
-
success: true,
|
|
247
|
-
data: userData.value,
|
|
248
|
-
});
|
|
249
|
-
} catch (error) {
|
|
250
|
-
if (error instanceof z.ZodError) {
|
|
251
|
-
return reply.status(400).send({
|
|
252
|
-
success: false,
|
|
253
|
-
error: 'Validation failed',
|
|
254
|
-
details: error.issues,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
logger.error({ err: error }, '[User] Error saving data');
|
|
259
|
-
return reply.status(500).send({
|
|
260
|
-
success: false,
|
|
261
|
-
error: 'Internal server error',
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* DELETE /v1/user/data/:key
|
|
268
|
-
* Delete user data by key
|
|
269
|
-
*/
|
|
270
|
-
fastify.delete<{ Params: { key: string } }>('/v1/user/data/:key', async (request, reply) => {
|
|
271
|
-
try {
|
|
272
|
-
const userId = (request as unknown as { userId?: string }).userId;
|
|
273
|
-
if (!userId) {
|
|
274
|
-
return reply.status(401).send({
|
|
275
|
-
success: false,
|
|
276
|
-
error: 'Authentication required',
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const { key } = request.params;
|
|
281
|
-
|
|
282
|
-
if (isReservedUserDataKey(key)) {
|
|
283
|
-
return reply.status(403).send({
|
|
284
|
-
success: false,
|
|
285
|
-
error: 'reserved-key',
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
await prisma.userData.deleteMany({
|
|
290
|
-
where: { userId, key },
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
return reply.send({
|
|
294
|
-
success: true,
|
|
295
|
-
message: 'Data deleted',
|
|
296
|
-
});
|
|
297
|
-
} catch (error) {
|
|
298
|
-
logger.error({ err: error }, '[User] Error deleting data');
|
|
299
|
-
return reply.status(500).send({
|
|
300
|
-
success: false,
|
|
301
|
-
error: 'Internal server error',
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
};
|