@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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/package.json +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.389.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.389.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"./billing-foundation": "./src/billing-foundation/index.ts",
|
|
64
64
|
"./billing-foundation/web": "./src/billing-foundation/web/index.ts",
|
|
65
65
|
"./subscription-stripe": "./src/subscription-stripe/index.ts",
|
|
66
|
+
"./subscription-dodo": "./src/subscription-dodo/index.ts",
|
|
66
67
|
"./subscription-mollie": "./src/subscription-mollie/index.ts",
|
|
67
68
|
"./foundation-shared": "./src/foundation-shared/index.ts",
|
|
68
69
|
"./auth-foundation": "./src/auth-foundation/index.ts",
|
|
@@ -359,6 +360,10 @@
|
|
|
359
360
|
"types": "./dist/subscription-stripe/index.d.ts",
|
|
360
361
|
"default": "./dist/subscription-stripe/index.js"
|
|
361
362
|
},
|
|
363
|
+
"./subscription-dodo": {
|
|
364
|
+
"types": "./dist/subscription-dodo/index.d.ts",
|
|
365
|
+
"default": "./dist/subscription-dodo/index.js"
|
|
366
|
+
},
|
|
362
367
|
"./subscription-mollie": {
|
|
363
368
|
"types": "./dist/subscription-mollie/index.d.ts",
|
|
364
369
|
"default": "./dist/subscription-mollie/index.js"
|
|
@@ -728,6 +733,8 @@
|
|
|
728
733
|
"@testing-library/user-event": "^14.6.1",
|
|
729
734
|
"@cosmicdrift/kumiko-locale-de": "workspace:*",
|
|
730
735
|
"@cosmicdrift/kumiko-locale-es": "workspace:*",
|
|
736
|
+
"@cosmicdrift/kumiko-locale-fr": "workspace:*",
|
|
737
|
+
"@cosmicdrift/kumiko-locale-it": "workspace:*",
|
|
731
738
|
"@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
|
|
732
739
|
"jsqr": "^1.4.0",
|
|
733
740
|
"unpdf": "^1.8.1",
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Local agent tool errors carry a code",
|
|
6
|
+
"detail": "dispatchToolCall returns code invalid_tool_call for validation errors and unknown_tool for an unknown tool name, instead of a result without code. TOOL_CALL_ERROR_CODES names both."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "dispatchToolCall documents which failures carry a code",
|
|
12
|
+
"detail": "Only handler and dispatcher failures carry a code; local validation errors such as an unknown tool or bad arguments come back without one, and the JSDoc now says so."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.366.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -9,7 +9,7 @@ export { buildAgentManifest } from "./agent-manifest.ts";
|
|
|
9
9
|
export { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "./feature.ts";
|
|
10
10
|
export { buildToolCatalog, OPEN_FORM_TOOL_NAME, toolNameForQn } from "./tool-catalog.ts";
|
|
11
11
|
export type { ToolCallRequest, ToolCallResult, ToolDispatcher } from "./tool-dispatch.ts";
|
|
12
|
-
export { dispatchToolCall } from "./tool-dispatch.ts";
|
|
12
|
+
export { dispatchToolCall, TOOL_CALL_ERROR_CODES } from "./tool-dispatch.ts";
|
|
13
13
|
export type {
|
|
14
14
|
AgentManifest,
|
|
15
15
|
AgentManifestEntity,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { isKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import {
|
|
3
|
+
import { runAsAgentToolCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
|
|
4
4
|
import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
5
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
6
|
import { FILTER_OPS, isRecord } from "./filter-ops.ts";
|
|
@@ -58,6 +58,21 @@ type ToolFailure = {
|
|
|
58
58
|
|
|
59
59
|
type FailureClassifier = (failure: ToolFailure, cause: unknown) => ToolCallResult;
|
|
60
60
|
|
|
61
|
+
export const TOOL_CALL_ERROR_CODES = {
|
|
62
|
+
invalidToolCall: "invalid_tool_call",
|
|
63
|
+
unknownTool: "unknown_tool",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
type ToolCallFailure = Extract<ToolCallResult, { readonly ok: false }>;
|
|
67
|
+
|
|
68
|
+
function invalidToolCall(error: string): ToolCallFailure {
|
|
69
|
+
return { ok: false, error, code: TOOL_CALL_ERROR_CODES.invalidToolCall };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unknownTool(error: string): ToolCallFailure {
|
|
73
|
+
return { ok: false, error, code: TOOL_CALL_ERROR_CODES.unknownTool };
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
const HTTP_TOO_MANY_REQUESTS = 429;
|
|
62
77
|
const HTTP_SERVER_ERROR = 500;
|
|
63
78
|
const UNEXPECTED_FAILURE_MESSAGE = "Tool failed unexpectedly";
|
|
@@ -108,7 +123,7 @@ type FilterOp = (typeof FILTER_OPS)[number];
|
|
|
108
123
|
|
|
109
124
|
type PayloadResult =
|
|
110
125
|
| { readonly ok: true; readonly value: Readonly<Record<string, unknown>> }
|
|
111
|
-
|
|
|
126
|
+
| ToolCallFailure;
|
|
112
127
|
|
|
113
128
|
function isFilterOp(value: unknown): value is FilterOp {
|
|
114
129
|
return isOneOf(FILTER_OPS, value);
|
|
@@ -120,7 +135,7 @@ function buildSearchPayload(
|
|
|
120
135
|
): PayloadResult {
|
|
121
136
|
const query = toolInput["query"];
|
|
122
137
|
if (typeof query !== "string" || query.length === 0) {
|
|
123
|
-
return
|
|
138
|
+
return invalidToolCall(`Tool "${toolName}" requires a non-empty string "query" argument`);
|
|
124
139
|
}
|
|
125
140
|
return { ok: true, value: { search: query, limit: SEARCH_RESULT_LIMIT } };
|
|
126
141
|
}
|
|
@@ -132,7 +147,7 @@ function buildFindByPayload(
|
|
|
132
147
|
): PayloadResult {
|
|
133
148
|
const value = toolInput[fieldName];
|
|
134
149
|
if (value === undefined) {
|
|
135
|
-
return
|
|
150
|
+
return invalidToolCall(`Tool "${toolName}" requires a "${fieldName}" argument`);
|
|
136
151
|
}
|
|
137
152
|
return {
|
|
138
153
|
ok: true,
|
|
@@ -156,10 +171,10 @@ function buildEntityListPayload(
|
|
|
156
171
|
const search = input["search"];
|
|
157
172
|
if (search !== undefined) {
|
|
158
173
|
if (typeof search !== "string") {
|
|
159
|
-
return
|
|
174
|
+
return invalidToolCall(`"search" must be a string`);
|
|
160
175
|
}
|
|
161
176
|
if (list.searchableFields.length === 0) {
|
|
162
|
-
return
|
|
177
|
+
return invalidToolCall("This entity has no searchable fields");
|
|
163
178
|
}
|
|
164
179
|
payload["search"] = search;
|
|
165
180
|
}
|
|
@@ -167,18 +182,18 @@ function buildEntityListPayload(
|
|
|
167
182
|
const filters = input["filters"];
|
|
168
183
|
if (filters !== undefined) {
|
|
169
184
|
if (!Array.isArray(filters)) {
|
|
170
|
-
return
|
|
185
|
+
return invalidToolCall(`"filters" must be an array`);
|
|
171
186
|
}
|
|
172
187
|
const validated: Array<{ field: string; op: FilterOp; value: unknown }> = [];
|
|
173
188
|
for (const entry of filters) {
|
|
174
189
|
if (!isRecord(entry) || typeof entry["field"] !== "string" || !("value" in entry)) {
|
|
175
|
-
return
|
|
190
|
+
return invalidToolCall(`Each filter must be { field, op, value }`);
|
|
176
191
|
}
|
|
177
192
|
if (!list.filterableFields.includes(entry["field"])) {
|
|
178
|
-
return
|
|
193
|
+
return invalidToolCall(`Field "${entry["field"]}" is not filterable on this entity`);
|
|
179
194
|
}
|
|
180
195
|
if (!isFilterOp(entry["op"])) {
|
|
181
|
-
return
|
|
196
|
+
return invalidToolCall(`Unsupported filter operator "${String(entry["op"])}"`);
|
|
182
197
|
}
|
|
183
198
|
validated.push({ field: entry["field"], op: entry["op"], value: entry["value"] });
|
|
184
199
|
}
|
|
@@ -250,7 +265,7 @@ async function dispatchServerQuery(
|
|
|
250
265
|
if (descriptor.detail) {
|
|
251
266
|
const id = input["id"];
|
|
252
267
|
if (typeof id !== "string" || id.length === 0) {
|
|
253
|
-
return
|
|
268
|
+
return invalidToolCall(`This tool requires a non-empty string "id" argument`);
|
|
254
269
|
}
|
|
255
270
|
const data = await dispatcher.query(descriptor.qn, { id }, user);
|
|
256
271
|
return { ok: true, data };
|
|
@@ -285,16 +300,16 @@ async function dispatchServerWrite(
|
|
|
285
300
|
if (descriptor.injectsVersion) {
|
|
286
301
|
const id = input["id"];
|
|
287
302
|
if (typeof id !== "string" || id.length === 0) {
|
|
288
|
-
return
|
|
303
|
+
return invalidToolCall(`This tool requires a non-empty string "id" argument`);
|
|
289
304
|
}
|
|
290
305
|
if (!descriptor.detailQn) {
|
|
291
|
-
return
|
|
306
|
+
return invalidToolCall("Tool is misconfigured: no detail handler to read version from");
|
|
292
307
|
}
|
|
293
308
|
const current = await dispatcher.query(descriptor.detailQn, { id }, user);
|
|
294
309
|
const version =
|
|
295
310
|
isRecord(current) && typeof current["version"] === "number" ? current["version"] : undefined;
|
|
296
311
|
if (version === undefined) {
|
|
297
|
-
return
|
|
312
|
+
return invalidToolCall(`Could not read the current "version" for id "${id}"`);
|
|
298
313
|
}
|
|
299
314
|
payload = { ...input, version };
|
|
300
315
|
}
|
|
@@ -336,10 +351,10 @@ function dispatchClientNavigate(
|
|
|
336
351
|
const entity = input["entity"];
|
|
337
352
|
if (typeof entity === "string") {
|
|
338
353
|
const screenId = descriptor.entityScreens.get(entity);
|
|
339
|
-
if (!screenId) return
|
|
354
|
+
if (!screenId) return invalidToolCall(`Unknown entity "${entity}" for navigation`);
|
|
340
355
|
const id = input["id"];
|
|
341
356
|
if (typeof id !== "string" || id.length === 0) {
|
|
342
|
-
return
|
|
357
|
+
return invalidToolCall(`Navigating to "${entity}" requires a non-empty string "id"`);
|
|
343
358
|
}
|
|
344
359
|
return { ok: true, data: { kind: "navigate", screenId, params: { id } } };
|
|
345
360
|
}
|
|
@@ -347,19 +362,19 @@ function dispatchClientNavigate(
|
|
|
347
362
|
const screenId = input["screenId"];
|
|
348
363
|
if (typeof screenId === "string") {
|
|
349
364
|
if (!descriptor.screenIds.has(screenId)) {
|
|
350
|
-
return
|
|
365
|
+
return invalidToolCall(`Unknown screen "${screenId}"`);
|
|
351
366
|
}
|
|
352
367
|
const params = input["params"];
|
|
353
368
|
if (params !== undefined) {
|
|
354
369
|
if (!isRecord(params) || Object.values(params).some((value) => typeof value !== "string")) {
|
|
355
|
-
return
|
|
370
|
+
return invalidToolCall(`"params" must be an object of string values`);
|
|
356
371
|
}
|
|
357
372
|
return { ok: true, data: { kind: "navigate", screenId, params } };
|
|
358
373
|
}
|
|
359
374
|
return { ok: true, data: { kind: "navigate", screenId, params: {} } };
|
|
360
375
|
}
|
|
361
376
|
|
|
362
|
-
return
|
|
377
|
+
return invalidToolCall(`navigate requires either { entity, id } or { screenId }`);
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
function dispatchClientOpenForm(
|
|
@@ -368,10 +383,10 @@ function dispatchClientOpenForm(
|
|
|
368
383
|
): ToolCallResult {
|
|
369
384
|
const handlerQn = input["handlerQn"];
|
|
370
385
|
if (typeof handlerQn !== "string") {
|
|
371
|
-
return
|
|
386
|
+
return invalidToolCall(`open_form requires a string "handlerQn" argument`);
|
|
372
387
|
}
|
|
373
388
|
const screenId = descriptor.formScreens.get(handlerQn);
|
|
374
|
-
if (!screenId) return
|
|
389
|
+
if (!screenId) return invalidToolCall(`No form screen mounted for handler "${handlerQn}"`);
|
|
375
390
|
return {
|
|
376
391
|
ok: true,
|
|
377
392
|
data: { kind: "open_form", screenId, handlerQn, prefill: input["prefill"] ?? {} },
|
|
@@ -385,14 +400,14 @@ function isStringArray(value: unknown): value is readonly string[] {
|
|
|
385
400
|
function dispatchClientAskUser(input: Readonly<Record<string, unknown>>): ToolCallResult {
|
|
386
401
|
const question = input["question"];
|
|
387
402
|
if (typeof question !== "string" || question.length === 0) {
|
|
388
|
-
return
|
|
403
|
+
return invalidToolCall(`ask_user requires a non-empty string "question" argument`);
|
|
389
404
|
}
|
|
390
405
|
const options = input["options"];
|
|
391
406
|
if (options === undefined) {
|
|
392
407
|
return { ok: true, data: { kind: "ask_user", question } };
|
|
393
408
|
}
|
|
394
409
|
if (!isStringArray(options)) {
|
|
395
|
-
return
|
|
410
|
+
return invalidToolCall(`ask_user "options" argument must be an array of strings`);
|
|
396
411
|
}
|
|
397
412
|
return { ok: true, data: { kind: "ask_user", question, options } };
|
|
398
413
|
}
|
|
@@ -411,23 +426,24 @@ function dispatchClient(
|
|
|
411
426
|
}
|
|
412
427
|
}
|
|
413
428
|
|
|
414
|
-
/** Executes one LLM-issued tool call. Never throws
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
429
|
+
/** Executes one LLM-issued tool call. Never throws: every failure comes back as `{ ok: false }`
|
|
430
|
+
* so the agent loop can feed the error back to the model instead of crashing.
|
|
431
|
+
* Every failure carries a `code`: the handler's own code for handler and dispatcher failures
|
|
432
|
+
* (429 adds `retryable`), `invalid_tool_call` for bad arguments and `unknown_tool` for a tool
|
|
433
|
+
* that is not in the table. Client errors (< 500) keep their message, server failures (>= 500,
|
|
434
|
+
* unknown throws) are logged and returned as a generic message.
|
|
435
|
+
* `request.user` must be the caller's real identity: server-kind tools run through the same
|
|
436
|
+
* permission-checked pipeline as an HTTP request. */
|
|
421
437
|
export async function dispatchToolCall(request: ToolCallRequest): Promise<ToolCallResult> {
|
|
422
438
|
const { dispatcher, user, toolName, input, dispatchTable, runId, toolCallId } = request;
|
|
423
439
|
const classifyFailure = createFailureClassifier(toolName, request.log ?? fallbackLog);
|
|
424
440
|
const descriptor = dispatchTable.get(toolName);
|
|
425
441
|
if (!descriptor) {
|
|
426
|
-
return
|
|
442
|
+
return unknownTool(`Unknown tool "${toolName}"`);
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
try {
|
|
430
|
-
return await
|
|
446
|
+
return await runAsAgentToolCallEntry(async () => {
|
|
431
447
|
switch (descriptor.kind) {
|
|
432
448
|
case "search":
|
|
433
449
|
return await dispatchSearch(descriptor, dispatcher, user, toolName, input);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Oversized audit cursor is documented as a 400 validation error",
|
|
6
|
+
"detail": "The cursor pattern comment now names the validation error (400) the API actually returns instead of 422."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Tests cover nested agent grants, erased subjects and anonymous change-password",
|
|
12
|
+
"detail": "Integration tests now check that a delegating tool handler keeps the cross-tenant-read marker closed, that an erased subject gets no consumer-protection mail, and that the anonymous change-password check runs once; the audit seed helper uses withEnv."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.384.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -49,6 +49,7 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
49
49
|
return defineFeature(
|
|
50
50
|
"audit",
|
|
51
51
|
(r) => {
|
|
52
|
+
r.adoptsBundledLists();
|
|
52
53
|
r.describe(
|
|
53
54
|
"Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection \u2014 the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. A SystemAdmin can pass `scope: \"system\"` to read the app-instance system events such as `app.started`, and reads the events of every tenant (system tenant excluded) through `audit:query:list-all-tenants` and the `audit-log-all-tenants` screen. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range. Also records `audit:event:escape-hatch-used` whenever a handler uses one of the framework's escape hatches (unsafeRaw, acknowledgeCrossTenant, db.global() writes, or a granted identity switch).",
|
|
54
55
|
);
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
|
|
21
21
|
const MAX_LIMIT = 100;
|
|
22
22
|
|
|
23
|
-
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a
|
|
23
|
+
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a validation error (400), not a 500.
|
|
24
24
|
const CURSOR_PATTERN = /^\d{1,18}$/;
|
|
25
25
|
|
|
26
26
|
export const auditListPayloadShape = {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Optional returnTo for password reset and signup",
|
|
6
|
+
"detail": "Password reset and signup accept an optional returnTo path. A safe value (same-origin path, no dot segments) is appended to the mailed link as a query parameter, the reset and signup-complete screens follow it, and the login screen forwards its next to the forgot-password and signup links. Invalid values are dropped silently and never block the mail. isSafeReturnPath and readReturnToFromSearch are exported from the web barrel. isSafeNextPath now uses the same check, so a next with a dot segment such as \"/a/../b\" is no longer followed."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "CurrentUser carries tenantId, logout clears remembered list state",
|
|
12
|
+
"detail": "The session CurrentUser has an optional tenantId used to scope remembered list state; logout removes all kumiko.list.* storage entries."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.385.0",
|
|
4
16
|
"type": "improvement",
|
package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function createRequestAccountUnlockHandler(opts: RequestAccountUnlockOpti
|
|
|
31
31
|
// empty) lockout state — harmless no-op if they weren't locked.
|
|
32
32
|
extraSilentSkip: () => false,
|
|
33
33
|
notificationType: AuthMailNotificationTypes.accountUnlock,
|
|
34
|
+
forwardsReturnTo: false,
|
|
34
35
|
},
|
|
35
36
|
opts,
|
|
36
37
|
);
|
|
@@ -28,6 +28,7 @@ export function createRequestEmailVerificationHandler(opts: RequestEmailVerifica
|
|
|
28
28
|
// caller sees the same 200 regardless of whether a token was minted.
|
|
29
29
|
extraSilentSkip: (user) => user.emailVerified === true,
|
|
30
30
|
notificationType: AuthMailNotificationTypes.emailVerification,
|
|
31
|
+
forwardsReturnTo: false,
|
|
31
32
|
},
|
|
32
33
|
opts,
|
|
33
34
|
);
|
package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts
CHANGED
|
@@ -30,6 +30,7 @@ export function createRequestPasswordResetHandler(opts: RequestPasswordResetOpti
|
|
|
30
30
|
// state. The sessions feature handles the post-change revocation.
|
|
31
31
|
extraSilentSkip: () => false,
|
|
32
32
|
notificationType: AuthMailNotificationTypes.passwordReset,
|
|
33
|
+
forwardsReturnTo: true,
|
|
33
34
|
},
|
|
34
35
|
opts,
|
|
35
36
|
);
|
|
@@ -31,6 +31,7 @@ import { resolveHandlerMailLocale } from "../magic-link-mail.ts";
|
|
|
31
31
|
import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.ts";
|
|
32
32
|
import { issueSignupActivation } from "../signup-activation.ts";
|
|
33
33
|
import { storeSignupHandover, takeOverSignupHandoverForResend } from "../signup-token-store.ts";
|
|
34
|
+
import { MAX_RETURN_PATH_LENGTH } from "../web/return-path.ts";
|
|
34
35
|
|
|
35
36
|
const SignupRequestSchema = z.object({
|
|
36
37
|
email: z.email(),
|
|
@@ -43,6 +44,8 @@ const SignupRequestSchema = z.object({
|
|
|
43
44
|
token: z.string().min(1),
|
|
44
45
|
})
|
|
45
46
|
.optional(),
|
|
47
|
+
// A bad returnTo must not void the signup; the mail builder drops unsafe values.
|
|
48
|
+
returnTo: z.string().max(MAX_RETURN_PATH_LENGTH).optional().catch(undefined),
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
const HANDOVER_GRANT_VERIFY_REASON =
|
|
@@ -133,6 +136,7 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
|
|
|
133
136
|
email,
|
|
134
137
|
appUrl: opts.appUrl,
|
|
135
138
|
timeZone: ctx.tz.user,
|
|
139
|
+
...(event.payload.returnTo !== undefined && { returnTo: event.payload.returnTo }),
|
|
136
140
|
...(opts.tokenTtlMinutes !== undefined && { tokenTtlMinutes: opts.tokenTtlMinutes }),
|
|
137
141
|
...(opts.appName !== undefined && { appName: opts.appName }),
|
|
138
142
|
...(locale !== undefined && { locale }),
|
|
@@ -22,9 +22,12 @@ import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row.ts";
|
|
|
22
22
|
import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.ts";
|
|
23
23
|
import type { AuthMailLocale } from "../email-templates.ts";
|
|
24
24
|
import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.ts";
|
|
25
|
+
import { MAX_RETURN_PATH_LENGTH } from "../web/return-path.ts";
|
|
25
26
|
|
|
26
27
|
const RequestTokenSchema = z.object({
|
|
27
28
|
email: z.email(),
|
|
29
|
+
// A bad returnTo must not void the request; the mail builder drops unsafe values.
|
|
30
|
+
returnTo: z.string().max(MAX_RETURN_PATH_LENGTH).optional().catch(undefined),
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
// What the route layer reads off `result.data` after dispatching. Identical
|
|
@@ -67,6 +70,9 @@ export type TokenRequestSpec<TName extends string, TSuccessKind extends string>
|
|
|
67
70
|
// Trigger-less r.notification dispatched via ctx.notify on success; the
|
|
68
71
|
// recipient is an anonymous email, so route:{email} delivers directly.
|
|
69
72
|
readonly notificationType: string;
|
|
73
|
+
// Only flows whose landing page can continue to a page (password reset) put the request's
|
|
74
|
+
// returnTo into the mail link; verification and unlock ignore it.
|
|
75
|
+
readonly forwardsReturnTo: boolean;
|
|
70
76
|
};
|
|
71
77
|
|
|
72
78
|
export type TokenRequestOptions = {
|
|
@@ -159,6 +165,8 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
|
|
|
159
165
|
timeZone: recipientTimeZone(user, ctx.tz.user),
|
|
160
166
|
...(opts.appName !== undefined && { appName: opts.appName }),
|
|
161
167
|
locale,
|
|
168
|
+
...(spec.forwardsReturnTo &&
|
|
169
|
+
event.payload.returnTo !== undefined && { returnTo: event.payload.returnTo }),
|
|
162
170
|
},
|
|
163
171
|
);
|
|
164
172
|
|
|
@@ -12,6 +12,7 @@ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
12
12
|
import { mailT, resolveMailLocale } from "@cosmicdrift/kumiko-framework/i18n";
|
|
13
13
|
import { type AuthMailLocale, tokenMailExpiry } from "./email-templates.ts";
|
|
14
14
|
import { authMailTemplateLocale } from "./mail-notifications.ts";
|
|
15
|
+
import { isSafeReturnPath, RETURN_TO_QUERY_PARAM } from "./web/return-path.ts";
|
|
15
16
|
|
|
16
17
|
// Per-flow constants: which notification type to dispatch. fallbackAppName
|
|
17
18
|
// replaces the translated default when the flow has no app name of its own.
|
|
@@ -38,15 +39,23 @@ export type MagicLinkMailParams = {
|
|
|
38
39
|
readonly timeZone?: string;
|
|
39
40
|
// Invite mails name the role the recipient is invited as.
|
|
40
41
|
readonly role?: string;
|
|
42
|
+
/** Page the landing page should lead to afterwards; an unsafe value is left out of the link. */
|
|
43
|
+
readonly returnTo?: string;
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
function resolveAppUrl(appUrl: string | ((locale: string) => string), locale: string): string {
|
|
44
47
|
return typeof appUrl === "function" ? appUrl(locale) : appUrl;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
function
|
|
50
|
+
function appendTokenAndReturnTo(
|
|
51
|
+
appUrl: string,
|
|
52
|
+
token: string,
|
|
53
|
+
returnTo: string | undefined,
|
|
54
|
+
): string {
|
|
55
|
+
const query = new URLSearchParams({ token });
|
|
56
|
+
if (isSafeReturnPath(returnTo)) query.set(RETURN_TO_QUERY_PARAM, returnTo);
|
|
48
57
|
const sep = appUrl.includes("?") ? "&" : "?";
|
|
49
|
-
return `${appUrl}${sep}
|
|
58
|
+
return `${appUrl}${sep}${query.toString()}`;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
// X-Locale on the request wins over the handler's static opts.locale, then
|
|
@@ -93,7 +102,11 @@ export async function dispatchMagicLinkMail(
|
|
|
93
102
|
data: {
|
|
94
103
|
appName:
|
|
95
104
|
params.appName ?? spec.fallbackAppName ?? mailT(templateLocale, "auth.mail.appNameDefault"),
|
|
96
|
-
actionUrl:
|
|
105
|
+
actionUrl: appendTokenAndReturnTo(
|
|
106
|
+
resolveAppUrl(params.appUrl, urlLocale),
|
|
107
|
+
params.token,
|
|
108
|
+
params.returnTo,
|
|
109
|
+
),
|
|
97
110
|
validFor,
|
|
98
111
|
validUntil,
|
|
99
112
|
...(params.role !== undefined && { role: params.role }),
|
|
@@ -28,6 +28,7 @@ export type IssueSignupActivationArgs = {
|
|
|
28
28
|
readonly locale?: string;
|
|
29
29
|
readonly timeZone?: string;
|
|
30
30
|
readonly tokenTtlMinutes?: number;
|
|
31
|
+
readonly returnTo?: string;
|
|
31
32
|
/** Runs after the token is stored and before the mail goes out. */
|
|
32
33
|
readonly onTokenStored?: (stored: {
|
|
33
34
|
readonly token: string;
|
|
@@ -75,6 +76,7 @@ export async function issueSignupActivation(
|
|
|
75
76
|
...(args.timeZone !== undefined && { timeZone: args.timeZone }),
|
|
76
77
|
...(args.appName !== undefined && { appName: args.appName }),
|
|
77
78
|
...(args.locale !== undefined && { locale: args.locale }),
|
|
79
|
+
...(args.returnTo !== undefined && { returnTo: args.returnTo }),
|
|
78
80
|
},
|
|
79
81
|
);
|
|
80
82
|
|
|
@@ -226,12 +226,13 @@ async function parseTokenFailure(res: Response): Promise<AuthTokenFailure> {
|
|
|
226
226
|
// found. 429 → typed rate-limit failure. 5xx → unknown-error.
|
|
227
227
|
export async function requestPasswordReset(
|
|
228
228
|
email: string,
|
|
229
|
+
returnTo?: string,
|
|
229
230
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
230
231
|
const res = await fetch("/api/auth/request-password-reset", {
|
|
231
232
|
method: "POST",
|
|
232
233
|
credentials: "same-origin",
|
|
233
234
|
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
234
|
-
body: JSON.stringify({ email }),
|
|
235
|
+
body: JSON.stringify({ email, ...(returnTo !== undefined && { returnTo }) }),
|
|
235
236
|
});
|
|
236
237
|
if (res.ok) return { ok: true };
|
|
237
238
|
return { ok: false, error: await parseTokenFailure(res) };
|
|
@@ -332,12 +333,17 @@ export async function requestSignup(
|
|
|
332
333
|
// travels any further than this request — never in the URL, the mail,
|
|
333
334
|
// or the confirm response.
|
|
334
335
|
handover?: { readonly entityType: string; readonly token: string },
|
|
336
|
+
returnTo?: string,
|
|
335
337
|
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
336
338
|
const res = await fetch("/api/auth/signup-request", {
|
|
337
339
|
method: "POST",
|
|
338
340
|
credentials: "same-origin",
|
|
339
341
|
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
340
|
-
body: JSON.stringify({
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
email,
|
|
344
|
+
...(handover !== undefined && { handover }),
|
|
345
|
+
...(returnTo !== undefined && { returnTo }),
|
|
346
|
+
}),
|
|
341
347
|
});
|
|
342
348
|
if (res.ok) return { ok: true };
|
|
343
349
|
return { ok: false, error: await parseTokenFailure(res) };
|
|
@@ -2,35 +2,37 @@
|
|
|
2
2
|
// string, so anything read back from it is untrusted: only same-origin
|
|
3
3
|
// root-relative paths pass (no "//host", no "/\host", no scheme).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { isSafeReturnPath, RETURN_TO_QUERY_PARAM, readReturnToFromSearch } from "./return-path.ts";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
9
|
-
const code = candidate.charCodeAt(i);
|
|
10
|
-
if (code <= 0x20 || code === 0x7f) return true;
|
|
11
|
-
}
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
7
|
+
export const NEXT_QUERY_PARAM = "next";
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
// would otherwise normalize to "//evil.com" past the leading-slash check.
|
|
17
|
-
export function isSafeNextPath(candidate: unknown): candidate is string {
|
|
18
|
-
if (typeof candidate !== "string" || !candidate.startsWith("/")) return false;
|
|
19
|
-
if (candidate[1] === "/" || candidate.includes("\\")) return false;
|
|
20
|
-
if (hasControlOrWhitespace(candidate)) return false;
|
|
21
|
-
try {
|
|
22
|
-
const parsed = new URL(candidate, "https://next.invalid");
|
|
23
|
-
return parsed.origin === "https://next.invalid" && !parsed.pathname.startsWith("//");
|
|
24
|
-
} catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
9
|
+
export const isSafeNextPath = isSafeReturnPath;
|
|
28
10
|
|
|
29
11
|
export function readNextFromSearch(search: string): string | null {
|
|
30
12
|
const candidate = new URLSearchParams(search).get(NEXT_QUERY_PARAM);
|
|
31
13
|
return isSafeNextPath(candidate) ? candidate : null;
|
|
32
14
|
}
|
|
33
15
|
|
|
16
|
+
// The returnTo a mail link or sibling auth page carried in this page's own URL; null on native, where there is no window.
|
|
17
|
+
export function readReturnToFromWindow(): string | null {
|
|
18
|
+
return typeof window === "undefined" ? null : readReturnToFromSearch(window.location.search);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Adds returnTo to an href of a sibling auth page; a missing value leaves the href untouched.
|
|
22
|
+
export function appendReturnTo(href: string, returnTo: string | null): string {
|
|
23
|
+
if (returnTo === null) return href;
|
|
24
|
+
const hashAt = href.indexOf("#");
|
|
25
|
+
const base = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
26
|
+
const hash = hashAt === -1 ? "" : href.slice(hashAt);
|
|
27
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
28
|
+
return `${base}${separator}${RETURN_TO_QUERY_PARAM}=${encodeURIComponent(returnTo)}${hash}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The safe `next` of this page's URL; null on native, where there is no window.
|
|
32
|
+
export function readNextFromWindow(): string | null {
|
|
33
|
+
return typeof window === "undefined" ? null : readNextFromSearch(window.location.search);
|
|
34
|
+
}
|
|
35
|
+
|
|
34
36
|
// loginUrl/postLogoutUrl come from app code (trusted), but a typo like
|
|
35
37
|
// "javascript:" must fail at startup instead of becoming a navigation target.
|
|
36
38
|
export function assertNavigableUrl(url: string, optionName: string): void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripBasePath, useBasePath } from "@cosmicdrift/kumiko-renderer";
|
|
1
2
|
import type { ReactNode } from "react";
|
|
2
3
|
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.ts";
|
|
3
4
|
import { ConfirmAccountUnlockScreen } from "./confirm-account-unlock-screen.tsx";
|
|
@@ -9,8 +10,9 @@ function hasToken(search: string): boolean {
|
|
|
9
10
|
return (new URLSearchParams(search).get("token") ?? "") !== "";
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
function pickDefaultAuthPage(pathname: string, search: string): ReactNode {
|
|
13
|
-
const
|
|
13
|
+
function pickDefaultAuthPage(pathname: string, search: string, basePath: string): ReactNode {
|
|
14
|
+
const trimmed = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
15
|
+
const path = stripBasePath(trimmed, basePath);
|
|
14
16
|
switch (path) {
|
|
15
17
|
case DEFAULT_FORGOT_PASSWORD_PATH:
|
|
16
18
|
return <ForgotPasswordScreen />;
|
|
@@ -27,5 +29,6 @@ function pickDefaultAuthPage(pathname: string, search: string): ReactNode {
|
|
|
27
29
|
// (outer) still win on the same path. The pages are public: no session needed.
|
|
28
30
|
export function DefaultAuthPagesGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
29
31
|
const { pathname, search } = window.location;
|
|
30
|
-
|
|
32
|
+
const basePath = useBasePath();
|
|
33
|
+
return pickDefaultAuthPage(pathname, search, basePath) ?? <>{children}</>;
|
|
31
34
|
}
|