@cosmicdrift/kumiko-samples 0.393.0 → 0.394.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 +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +30 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -49
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +87 -38
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +8 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"name": "billing-foundation",
|
|
319
|
-
"description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-
|
|
319
|
+
"description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-paddle` unless you are writing a new payment provider.",
|
|
320
320
|
"toggleableDefault": null,
|
|
321
321
|
"requires": [
|
|
322
322
|
"tenant-lifecycle",
|
|
@@ -1421,9 +1421,15 @@
|
|
|
1421
1421
|
"configKeys": [],
|
|
1422
1422
|
"secrets": [],
|
|
1423
1423
|
"writeHandlers": [
|
|
1424
|
+
"inbound-mail-foundation:write:acknowledge-quarantine",
|
|
1425
|
+
"inbound-mail-foundation:write:attach-message-content",
|
|
1424
1426
|
"inbound-mail-foundation:write:connect-account",
|
|
1425
1427
|
"inbound-mail-foundation:write:disconnect-account",
|
|
1426
1428
|
"inbound-mail-foundation:write:ingest-message",
|
|
1429
|
+
"inbound-mail-foundation:write:mark-credential-stored",
|
|
1430
|
+
"inbound-mail-foundation:write:reactivate-account",
|
|
1431
|
+
"inbound-mail-foundation:write:retry-quarantined-message",
|
|
1432
|
+
"inbound-mail-foundation:write:set-trusted-authserv-id",
|
|
1427
1433
|
"inbound-mail-foundation:write:update-account"
|
|
1428
1434
|
],
|
|
1429
1435
|
"uiHints": {
|
|
@@ -1434,7 +1440,7 @@
|
|
|
1434
1440
|
},
|
|
1435
1441
|
{
|
|
1436
1442
|
"name": "inbound-provider-imap",
|
|
1437
|
-
"description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a mailbox through the \"Connect mailbox\" form on the mail accounts list (`connect-mailbox`
|
|
1443
|
+
"description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a shared mailbox through the \"Connect mailbox\" form on the mail accounts list and members a personal one through the same form on \"My mail accounts\" (`connect-mailbox` and `connect-my-mailbox` check the IMAP login and the SMTP login of the outgoing server first, then creates the account and its credential secret in one transaction; `reconnect-mailbox` does the same check for an existing mailbox with a sign-in error and overwrites its credential secret); programmatic setups store the per-account connection JSON (host/port/secure/user/password, plus smtpHost/smtpPort for sending) in the account credential secret slot before the first sync. Implements the provider `send`: `sendMailFromAccount` sends over the SMTP server of the mailbox itself (implicit TLS, same login) with the mailbox address as From and stores a copy in its sent folder.",
|
|
1438
1444
|
"toggleableDefault": null,
|
|
1439
1445
|
"requires": [
|
|
1440
1446
|
"inbound-mail-foundation",
|
|
@@ -1454,6 +1460,7 @@
|
|
|
1454
1460
|
"secrets": [],
|
|
1455
1461
|
"writeHandlers": [
|
|
1456
1462
|
"inbound-provider-imap:write:connect-mailbox",
|
|
1463
|
+
"inbound-provider-imap:write:connect-my-mailbox",
|
|
1457
1464
|
"inbound-provider-imap:write:reconnect-mailbox"
|
|
1458
1465
|
],
|
|
1459
1466
|
"uiHints": {
|
|
@@ -2505,8 +2512,34 @@
|
|
|
2505
2512
|
}
|
|
2506
2513
|
},
|
|
2507
2514
|
{
|
|
2508
|
-
"name": "subscription-
|
|
2509
|
-
"description": "
|
|
2515
|
+
"name": "subscription-mollie",
|
|
2516
|
+
"description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
|
|
2517
|
+
"toggleableDefault": null,
|
|
2518
|
+
"requires": [
|
|
2519
|
+
"billing-foundation"
|
|
2520
|
+
],
|
|
2521
|
+
"optionalRequires": [],
|
|
2522
|
+
"configReads": [],
|
|
2523
|
+
"exposesApis": [],
|
|
2524
|
+
"usesApis": [],
|
|
2525
|
+
"extensionsUsed": [
|
|
2526
|
+
{
|
|
2527
|
+
"extensionName": "subscriptionProvider",
|
|
2528
|
+
"entityName": "mollie"
|
|
2529
|
+
}
|
|
2530
|
+
],
|
|
2531
|
+
"configKeys": [],
|
|
2532
|
+
"secrets": [],
|
|
2533
|
+
"writeHandlers": [],
|
|
2534
|
+
"uiHints": {
|
|
2535
|
+
"displayLabel": "Billing · Mollie",
|
|
2536
|
+
"category": "billing",
|
|
2537
|
+
"recommended": false
|
|
2538
|
+
}
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
"name": "subscription-paddle",
|
|
2542
|
+
"description": "Paddle Billing provider plugin for `billing-foundation`. Paddle acts as merchant of record: it sells to the customer and issues invoice and VAT, so the consumer-protection texts for this provider come from `consumerProtection.providerTexts`. Reads its API key, webhook secret and tenant binding key from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant), the public Paddle.js `client-token`, a `billingLive` master switch and an `environment` key (sandbox | live). `environment` is its own key because Paddle serves sandbox and live from different API hosts and a live key must never hit the sandbox host or the other way round; `billingLive` only decides whether checkouts are offered at all. All of them are runtime config, so keys rotate and prod goes live without a redeploy, and the `mask` on each derives the sysadmin settings screen and nav. Mount via `createSubscriptionPaddleFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret`/`tenantBindingKey`/`clientToken` options are env to config bridge fallbacks (`PADDLE_API_KEY`, `PADDLE_WEBHOOK_SECRET`, `PADDLE_TENANT_BINDING_KEY`, `PADDLE_CLIENT_TOKEN`). The plugin always mounts; `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true, and billing counts as enabled only with API key and binding key present. Webhooks arrive at `/webhooks/subscription/paddle` when mounted through the server-runtime `billing.paddle` boot option, or at `/api/subscription/webhook/paddle` when `createSubscriptionWebhookRoute` is mounted by hand and are verified with the `Paddle-Signature` header (HMAC-SHA256 over `ts:rawBody` keyed with the raw secret, any `h1` may match, 5 minute tolerance). Paddle.js can open checkouts with arbitrary `customData` from the browser, so `custom_data.tenantId` is only trusted together with `tenantBinding`, an HMAC of the tenant id under the binding key; events without a valid binding, for prices outside `priceToTier` or without a tenant are dropped. Rotating the binding key makes the webhooks of existing subscriptions fail that check until they are re-bound. Paddle has no hosted checkout page, so the feature serves the Default Payment Link itself at `/billing/paddle-checkout` (the Default Payment Link in the Paddle dashboard must point to `<APP_BASE_URL>/billing/paddle-checkout`; it is the only source, because checkouts are created without a `checkout.url`). Create subscription prices in the Paddle catalog with quantity minimum and maximum 1; for one-off packs set the quantity limits on purpose, because the payment event carries `quantity` and the app has to multiply its credits by it: an anonymous page with its own CSP that loads Paddle.js from cdn.paddle.com, the one external script this framework allows, and opens the overlay for the transaction. Implements checkout, customer portal, cancel and resume, `retrievePrices` (batched, 10-minute TTL cache), `retrieveSubscription` for the sync job, and an in-app plan switch: `previewPlanSwitch` shows the proration before the customer confirms, `applyPlanSwitch` changes the plan without a hosted page and waits for the `subscription.updated` webhook to move the tier.",
|
|
2510
2543
|
"toggleableDefault": null,
|
|
2511
2544
|
"requires": [
|
|
2512
2545
|
"billing-foundation",
|
|
@@ -2520,13 +2553,13 @@
|
|
|
2520
2553
|
"extensionsUsed": [
|
|
2521
2554
|
{
|
|
2522
2555
|
"extensionName": "subscriptionProvider",
|
|
2523
|
-
"entityName": "
|
|
2556
|
+
"entityName": "paddle"
|
|
2524
2557
|
}
|
|
2525
2558
|
],
|
|
2526
2559
|
"configKeys": [
|
|
2527
2560
|
{
|
|
2528
2561
|
"key": "api-key",
|
|
2529
|
-
"qualifiedName": "subscription-
|
|
2562
|
+
"qualifiedName": "subscription-paddle:config:api-key",
|
|
2530
2563
|
"type": "text",
|
|
2531
2564
|
"scope": "system",
|
|
2532
2565
|
"default": null,
|
|
@@ -2547,7 +2580,7 @@
|
|
|
2547
2580
|
},
|
|
2548
2581
|
{
|
|
2549
2582
|
"key": "billing-live",
|
|
2550
|
-
"qualifiedName": "subscription-
|
|
2583
|
+
"qualifiedName": "subscription-paddle:config:billing-live",
|
|
2551
2584
|
"type": "boolean",
|
|
2552
2585
|
"scope": "system",
|
|
2553
2586
|
"default": false,
|
|
@@ -2567,17 +2600,38 @@
|
|
|
2567
2600
|
"SystemAdmin"
|
|
2568
2601
|
]
|
|
2569
2602
|
},
|
|
2603
|
+
{
|
|
2604
|
+
"key": "client-token",
|
|
2605
|
+
"qualifiedName": "subscription-paddle:config:client-token",
|
|
2606
|
+
"type": "text",
|
|
2607
|
+
"scope": "system",
|
|
2608
|
+
"default": null,
|
|
2609
|
+
"encrypted": false,
|
|
2610
|
+
"computed": false,
|
|
2611
|
+
"validated": false,
|
|
2612
|
+
"options": null,
|
|
2613
|
+
"bounds": null,
|
|
2614
|
+
"pattern": null,
|
|
2615
|
+
"writeRoles": [
|
|
2616
|
+
"SystemAdmin"
|
|
2617
|
+
],
|
|
2618
|
+
"readRoles": [
|
|
2619
|
+
"TenantAdmin",
|
|
2620
|
+
"Admin",
|
|
2621
|
+
"SystemAdmin"
|
|
2622
|
+
]
|
|
2623
|
+
},
|
|
2570
2624
|
{
|
|
2571
2625
|
"key": "environment",
|
|
2572
|
-
"qualifiedName": "subscription-
|
|
2626
|
+
"qualifiedName": "subscription-paddle:config:environment",
|
|
2573
2627
|
"type": "select",
|
|
2574
2628
|
"scope": "system",
|
|
2575
|
-
"default": "
|
|
2629
|
+
"default": "sandbox",
|
|
2576
2630
|
"encrypted": false,
|
|
2577
2631
|
"computed": false,
|
|
2578
2632
|
"validated": false,
|
|
2579
2633
|
"options": [
|
|
2580
|
-
"
|
|
2634
|
+
"sandbox",
|
|
2581
2635
|
"live"
|
|
2582
2636
|
],
|
|
2583
2637
|
"bounds": null,
|
|
@@ -2592,8 +2646,8 @@
|
|
|
2592
2646
|
]
|
|
2593
2647
|
},
|
|
2594
2648
|
{
|
|
2595
|
-
"key": "
|
|
2596
|
-
"qualifiedName": "subscription-
|
|
2649
|
+
"key": "tenant-binding-key",
|
|
2650
|
+
"qualifiedName": "subscription-paddle:config:tenant-binding-key",
|
|
2597
2651
|
"type": "text",
|
|
2598
2652
|
"scope": "system",
|
|
2599
2653
|
"default": null,
|
|
@@ -2611,38 +2665,33 @@
|
|
|
2611
2665
|
"Admin",
|
|
2612
2666
|
"SystemAdmin"
|
|
2613
2667
|
]
|
|
2614
|
-
}
|
|
2615
|
-
],
|
|
2616
|
-
"secrets": [],
|
|
2617
|
-
"writeHandlers": [],
|
|
2618
|
-
"uiHints": {
|
|
2619
|
-
"displayLabel": "Billing · Dodo Payments",
|
|
2620
|
-
"category": "billing",
|
|
2621
|
-
"recommended": false
|
|
2622
|
-
}
|
|
2623
|
-
},
|
|
2624
|
-
{
|
|
2625
|
-
"name": "subscription-mollie",
|
|
2626
|
-
"description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
|
|
2627
|
-
"toggleableDefault": null,
|
|
2628
|
-
"requires": [
|
|
2629
|
-
"billing-foundation"
|
|
2630
|
-
],
|
|
2631
|
-
"optionalRequires": [],
|
|
2632
|
-
"configReads": [],
|
|
2633
|
-
"exposesApis": [],
|
|
2634
|
-
"usesApis": [],
|
|
2635
|
-
"extensionsUsed": [
|
|
2668
|
+
},
|
|
2636
2669
|
{
|
|
2637
|
-
"
|
|
2638
|
-
"
|
|
2670
|
+
"key": "webhook-secret",
|
|
2671
|
+
"qualifiedName": "subscription-paddle:config:webhook-secret",
|
|
2672
|
+
"type": "text",
|
|
2673
|
+
"scope": "system",
|
|
2674
|
+
"default": null,
|
|
2675
|
+
"encrypted": true,
|
|
2676
|
+
"computed": false,
|
|
2677
|
+
"validated": false,
|
|
2678
|
+
"options": null,
|
|
2679
|
+
"bounds": null,
|
|
2680
|
+
"pattern": null,
|
|
2681
|
+
"writeRoles": [
|
|
2682
|
+
"SystemAdmin"
|
|
2683
|
+
],
|
|
2684
|
+
"readRoles": [
|
|
2685
|
+
"TenantAdmin",
|
|
2686
|
+
"Admin",
|
|
2687
|
+
"SystemAdmin"
|
|
2688
|
+
]
|
|
2639
2689
|
}
|
|
2640
2690
|
],
|
|
2641
|
-
"configKeys": [],
|
|
2642
2691
|
"secrets": [],
|
|
2643
2692
|
"writeHandlers": [],
|
|
2644
2693
|
"uiHints": {
|
|
2645
|
-
"displayLabel": "Billing ·
|
|
2694
|
+
"displayLabel": "Billing · Paddle",
|
|
2646
2695
|
"category": "billing",
|
|
2647
2696
|
"recommended": false
|
|
2648
2697
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dev": "PORT=4186 bun --env-file=../../../.env run src/app/server.ts",
|
|
11
11
|
"screenshots": "bun x playwright test e2e/screenshots.spec.ts --config=playwright.config.ts",
|
|
12
12
|
"billing-gallery": "bun x playwright test --config=playwright.billing-gallery.config.ts",
|
|
13
|
+
"docs-guides": "bun x playwright test e2e/docs-guides.screenshots.spec.ts --config=playwright.config.ts && bun x playwright test --config=playwright.billing-docs.config.ts",
|
|
13
14
|
"e2e": "bun x playwright test e2e/mfa-login.spec.ts --config=playwright.config.ts",
|
|
14
15
|
"gen:manifest": "bun run scripts/gen-feature-manifest.ts",
|
|
15
16
|
"gen:sample-index": "bun run scripts/gen-sample-index.ts"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @runtime test
|
|
2
|
+
// English docs screenshots of the billing plans screen. Boots the gallery server with the docs
|
|
3
|
+
// variant switched on (net prices with a note, gross recurring price in the plan switch); the
|
|
4
|
+
// German review gallery has its own config. Gated by SCREENSHOT_DIR, see the `billing-docs` script.
|
|
5
|
+
import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
|
|
6
|
+
import { E2E_PORTS } from "../../e2e/e2e-ports";
|
|
7
|
+
import { GALLERY_DOCS_VARIANT_ENV } from "./e2e-billing-gallery/gallery-constants";
|
|
8
|
+
|
|
9
|
+
export default defineAppE2eConfig({
|
|
10
|
+
port: E2E_PORTS["framework/use-all-bundled-billing-gallery"],
|
|
11
|
+
env: { ...PLAYWRIGHT_DEMO_ENV, [GALLERY_DOCS_VARIANT_ENV]: "1" },
|
|
12
|
+
locale: "en",
|
|
13
|
+
testDir: "./e2e-billing-docs",
|
|
14
|
+
serverEntry: "e2e-billing-gallery/server.ts",
|
|
15
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CapSpec } from "@cosmicdrift/kumiko-bundled-features/cap-overview";
|
|
2
2
|
import { noteEntryTable } from "@cosmicdrift/kumiko-bundled-features/notes-history";
|
|
3
3
|
import { tagEntity } from "@cosmicdrift/kumiko-bundled-features/tags";
|
|
4
|
+
import { templateResourcesTable } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
4
5
|
import { tenantMembershipsTable } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
5
6
|
|
|
6
7
|
import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
@@ -11,10 +12,11 @@ const tagTable = buildEntityTable("tag", tagEntity);
|
|
|
11
12
|
const NOTE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 10, pro: 100 };
|
|
12
13
|
const TAG_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 50 };
|
|
13
14
|
const SEAT_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 20 };
|
|
15
|
+
const TEMPLATE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 3, pro: 30 };
|
|
14
16
|
const DEFAULT_LIMIT = 10;
|
|
15
17
|
|
|
16
|
-
//
|
|
17
|
-
// (notes-history, tags, tenant-membership) — demonstrates the caller-supplied
|
|
18
|
+
// Five example caps built from entities this sample already mounts
|
|
19
|
+
// (notes-history, tags, tenant-membership, template-resolver) — demonstrates the caller-supplied
|
|
18
20
|
// CapSpec contract cap-overview reads against, the same shape a real app
|
|
19
21
|
// would provide for its own usage tables.
|
|
20
22
|
export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
|
|
@@ -68,4 +70,16 @@ export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
|
|
|
68
70
|
},
|
|
69
71
|
icon: "gauge",
|
|
70
72
|
},
|
|
73
|
+
{
|
|
74
|
+
id: "templates",
|
|
75
|
+
label: "cap-overview.caps.templates",
|
|
76
|
+
limit: (tier) => TEMPLATE_LIMIT_BY_TIER[tier] ?? DEFAULT_LIMIT,
|
|
77
|
+
usage: async (db, tenantId) => {
|
|
78
|
+
const rows = await selectMany(db, templateResourcesTable, { tenantId: [tenantId] });
|
|
79
|
+
return rows.length;
|
|
80
|
+
},
|
|
81
|
+
// Counter only: the limit stays enforced server-side but is not shown to the tenant.
|
|
82
|
+
showLimit: false,
|
|
83
|
+
icon: "file",
|
|
84
|
+
},
|
|
71
85
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
|
|
3
|
-
// Labels for the
|
|
3
|
+
// Labels for the five example caps (notes, tags, seats, apiCalls, templates) wired up in
|
|
4
4
|
// cap-overview-caps.ts. Cap ids are the app's choice, so their labels are
|
|
5
5
|
// too — cap-overview itself carries no vocabulary for app-owned caps, same
|
|
6
6
|
// reasoning as collection-labels-feature.ts for content collections.
|
|
@@ -12,6 +12,7 @@ export const capOverviewLabelsFeature = defineFeature("cap-overview-labels", (r)
|
|
|
12
12
|
"cap-overview.caps.tags": { de: "Tags", en: "Tags" },
|
|
13
13
|
"cap-overview.caps.seats": { de: "Plätze", en: "Seats" },
|
|
14
14
|
"cap-overview.caps.apiCalls": { de: "API-Aufrufe", en: "API calls" },
|
|
15
|
+
"cap-overview.caps.templates": { de: "Vorlagen", en: "Templates" },
|
|
15
16
|
},
|
|
16
17
|
});
|
|
17
18
|
return {};
|
|
@@ -26,6 +26,7 @@ import { customFieldsClient } from "@cosmicdrift/kumiko-bundled-features/custom-
|
|
|
26
26
|
import { deliveryClient } from "@cosmicdrift/kumiko-bundled-features/delivery/web";
|
|
27
27
|
import { featureTogglesClient } from "@cosmicdrift/kumiko-bundled-features/feature-toggles/web";
|
|
28
28
|
import { foldersClient } from "@cosmicdrift/kumiko-bundled-features/folders/web";
|
|
29
|
+
import { inboundMailFoundationClient } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation/web";
|
|
29
30
|
import { jobsClient } from "@cosmicdrift/kumiko-bundled-features/jobs/web";
|
|
30
31
|
import { sessionsClient } from "@cosmicdrift/kumiko-bundled-features/sessions/web";
|
|
31
32
|
import { tagsClient } from "@cosmicdrift/kumiko-bundled-features/tags/web";
|
|
@@ -115,6 +116,7 @@ createKumikoApp({
|
|
|
115
116
|
userDataRightsClient(),
|
|
116
117
|
customFieldsClient(),
|
|
117
118
|
foldersClient(),
|
|
119
|
+
inboundMailFoundationClient(),
|
|
118
120
|
complianceProfilesClient(),
|
|
119
121
|
featureTogglesClient(),
|
|
120
122
|
// tag-list management screen (TagManager) + TagSection/TagFilter extension
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// (#1258): an open screen not reachable via nav is no longer an eligible
|
|
6
6
|
// fallback candidate.
|
|
7
7
|
|
|
8
|
+
import { MAIL_ACCOUNT_LIST_SCREEN_QN } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
8
9
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { ADMIN_NAV_QN, ADMIN_NAV_WORKSPACES } from "./admin-nav-feature";
|
|
9
11
|
|
|
10
12
|
export const appScreensFeature: FeatureDefinition = defineFeature("app-screens", (r) => {
|
|
11
|
-
r.describe("App-side nav placement for the user-profile self-service page.");
|
|
12
|
-
r.requires({ features: ["user-profile"] });
|
|
13
|
+
r.describe("App-side nav placement for the user-profile self-service page and the mailbox list.");
|
|
14
|
+
r.requires({ features: ["user-profile", "inbound-mail-foundation"] });
|
|
13
15
|
r.nav({
|
|
14
16
|
id: "profile",
|
|
15
17
|
label: "screen:profile.title",
|
|
@@ -29,8 +31,18 @@ export const appScreensFeature: FeatureDefinition = defineFeature("app-screens",
|
|
|
29
31
|
access: { roles: ["SystemAdmin"] },
|
|
30
32
|
workspaces: ["admin-shell:workspace:platform"],
|
|
31
33
|
});
|
|
34
|
+
r.nav({
|
|
35
|
+
id: "mail-accounts",
|
|
36
|
+
label: "nav:mail-accounts.title",
|
|
37
|
+
icon: "mail",
|
|
38
|
+
screen: MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
39
|
+
parent: ADMIN_NAV_QN,
|
|
40
|
+
workspaces: ADMIN_NAV_WORKSPACES,
|
|
41
|
+
order: 20,
|
|
42
|
+
});
|
|
32
43
|
r.translations({
|
|
33
44
|
keys: {
|
|
45
|
+
"nav:mail-accounts.title": { de: "Postfächer", en: "Mailboxes" },
|
|
34
46
|
"screen:profile.title": { de: "Profil", en: "Profile" },
|
|
35
47
|
"nav:content.title": { de: "Inhalte", en: "Content" },
|
|
36
48
|
},
|
|
@@ -468,10 +468,13 @@ export const seedScreenshotData: SeedFn = async (stack) => {
|
|
|
468
468
|
// carry whatever the handler derives instead of a hand-built projection.
|
|
469
469
|
// SYSTEM_TENANT, not the dev tenant: the screenshot admin reads the system
|
|
470
470
|
// tenant — the same reason the text-block tree above seeds there.
|
|
471
|
+
// The real admin, not TestUsers.systemAdmin's id: the seed signs in with a live session row,
|
|
472
|
+
// and the session list shows that row's user. The sessionChecker re-derives roles from the
|
|
473
|
+
// admin's global SystemAdmin role, which the collection's access accepts.
|
|
471
474
|
const snippetAuthor: SessionUser = {
|
|
472
475
|
...TestUsers.systemAdmin,
|
|
473
476
|
tenantId: SYSTEM_TENANT_ID,
|
|
474
|
-
|
|
477
|
+
id: devAdminId,
|
|
475
478
|
};
|
|
476
479
|
for (const snippet of [
|
|
477
480
|
{
|
|
@@ -75,8 +75,8 @@ import { createRendererPdfFeature } from "@cosmicdrift/kumiko-bundled-features/r
|
|
|
75
75
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
76
76
|
import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
|
|
77
77
|
import { createStepDispatcherFeature } from "@cosmicdrift/kumiko-bundled-features/step-dispatcher";
|
|
78
|
-
import { createSubscriptionDodoFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
79
78
|
import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie";
|
|
79
|
+
import { createSubscriptionPaddleFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
|
|
80
80
|
import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
81
81
|
import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags";
|
|
82
82
|
import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
@@ -212,10 +212,12 @@ export const APP_FEATURES = [
|
|
|
212
212
|
webhookSecret: "whsec_smoke",
|
|
213
213
|
priceToTier: { price_smoke: "free" },
|
|
214
214
|
}),
|
|
215
|
-
|
|
216
|
-
apiKey: "
|
|
217
|
-
webhookSecret: "
|
|
218
|
-
|
|
215
|
+
createSubscriptionPaddleFeature({
|
|
216
|
+
apiKey: "paddle_smoke",
|
|
217
|
+
webhookSecret: "pdl_ntfset_smoke",
|
|
218
|
+
tenantBindingKey: "paddle_smoke_binding_key",
|
|
219
|
+
clientToken: "test_smoke_client_token",
|
|
220
|
+
priceToTier: { pri_smoke: "free" },
|
|
219
221
|
}),
|
|
220
222
|
createSubscriptionMollieFeature({
|
|
221
223
|
apiKey: "test_smoke_key",
|
|
@@ -26,6 +26,13 @@ Three building blocks:
|
|
|
26
26
|
external system). With `table`, the apply materializes a persistent
|
|
27
27
|
read model.
|
|
28
28
|
|
|
29
|
+
- **`r.job(name, { trigger: { on: event.name, catchUp: true } }, run)`**
|
|
30
|
+
runs a job per stored event. With `catchUp` the job owns its own
|
|
31
|
+
consumer cursor (`job:<jobQn>`) that starts at the beginning of the
|
|
32
|
+
event store, so a job added to a running app also processes the
|
|
33
|
+
events from before its deploy. Each event enqueues at most one run.
|
|
34
|
+
Without `catchUp` a job only sees events appended after it went live.
|
|
35
|
+
|
|
29
36
|
## Flow
|
|
30
37
|
|
|
31
38
|
```
|
|
@@ -61,3 +68,6 @@ The integration test pins three guarantees:
|
|
|
61
68
|
```bash
|
|
62
69
|
bun kumiko test integration samples/cross-feature-events
|
|
63
70
|
```
|
|
71
|
+
4. **Catch-up job** — `catch-up-job.integration.test.ts` places orders
|
|
72
|
+
before the first dispatcher pass and checks that the `catchUp` job
|
|
73
|
+
invoices each of them exactly once.
|
|
@@ -60,6 +60,7 @@ export type CapturedEvent = {
|
|
|
60
60
|
readonly tenantId: string;
|
|
61
61
|
};
|
|
62
62
|
export const capturedEvents: CapturedEvent[] = [];
|
|
63
|
+
export const invoicedOrderIds: string[] = [];
|
|
63
64
|
|
|
64
65
|
// --- Feature ---
|
|
65
66
|
|
|
@@ -103,6 +104,19 @@ export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
|
|
|
103
104
|
access: { roles: ["Admin", "Customer"] },
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
// A job can react to the same event. `catchUp: true` gives the job its own
|
|
108
|
+
// consumer cursor that starts at the beginning of the event store, so a job
|
|
109
|
+
// added to a running app also processes the orders placed before it existed.
|
|
110
|
+
// Without catchUp the job only sees events appended after its deploy.
|
|
111
|
+
r.job(
|
|
112
|
+
"invoice-order",
|
|
113
|
+
{ trigger: { on: orderPlaced.name, catchUp: true }, runIn: "worker" },
|
|
114
|
+
async (payload) => {
|
|
115
|
+
const order = orderPlaced.schema.parse(payload);
|
|
116
|
+
invoicedOrderIds.push(order.id);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
106
120
|
// Async consumer — Marten-style. Fires after commit, via the event-
|
|
107
121
|
// dispatcher. No `table` means this MSP is pure side-effect (no state
|
|
108
122
|
// persisted). Delivery is at-least-once and ordered per consumer.
|
|
@@ -20,6 +20,14 @@ query and the fields to read, the query does the counting.
|
|
|
20
20
|
`list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
|
|
21
21
|
over the whole WHERE clause, so no rows are loaded just to be counted.
|
|
22
22
|
- **Labels are translation keys**: declared with `r.translations`.
|
|
23
|
+
- **Record-bound dashboard**: `ticket-record-dashboard` binds to a record with `record`
|
|
24
|
+
and embeds the `ticket-card` projectionDetail as a screen panel; the record id
|
|
25
|
+
becomes its entityId.
|
|
26
|
+
- **Gated toolbar button**: `visibleWhen` on a toolbar action asks a flat
|
|
27
|
+
capability query; the button is hidden while it loads or fails.
|
|
28
|
+
- **Fallback by reason**: `fallback: { forbidden, notFound }` renders a screen on
|
|
29
|
+
403/404 of the gate query; other reasons get the default notice or an error
|
|
30
|
+
banner with retry.
|
|
23
31
|
|
|
24
32
|
## When to reach for it
|
|
25
33
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Dashboard Stats Sample
|
|
2
2
|
// Shows: a dashboard screen with a stat-group and a single stat, backed by one
|
|
3
|
-
// query handler that counts in the database.
|
|
3
|
+
// query handler that counts in the database. A second, record-bound dashboard embeds a
|
|
4
|
+
// projectionDetail as a panel, hides a toolbar button behind a capability query, and picks its
|
|
5
|
+
// fallback screen by the reason the gate closed.
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
createEntity,
|
|
@@ -22,6 +24,7 @@ export const ticketEntity = createEntity({
|
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
|
|
27
|
+
export const TICKET_CAPABILITIES_QUERY = "tickets:query:ticket:capabilities";
|
|
25
28
|
|
|
26
29
|
const readerRoles = { roles: ["Admin", "User"] } as const;
|
|
27
30
|
|
|
@@ -57,8 +60,51 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
57
60
|
access: readerRoles,
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
// Gate for the screens below: a flat answer with one boolean the screens test with `eq`.
|
|
64
|
+
r.queryHandler({
|
|
65
|
+
name: "ticket:capabilities",
|
|
66
|
+
schema: z.object({}),
|
|
67
|
+
outputSchema: z.object({ canClose: z.boolean() }),
|
|
68
|
+
handler: async (query) => ({ canClose: query.user.roles.includes("Admin") }),
|
|
69
|
+
access: readerRoles,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
r.queryHandler({
|
|
73
|
+
name: "ticket:card",
|
|
74
|
+
schema: z.object({ id: z.string() }),
|
|
75
|
+
handler: async (query, ctx) => {
|
|
76
|
+
const page = await ticketExecutor.list(
|
|
77
|
+
{ filter: { field: "id", op: "eq", value: query.payload.id }, limit: 1 },
|
|
78
|
+
query.user,
|
|
79
|
+
ctx.db,
|
|
80
|
+
);
|
|
81
|
+
const ticket = page.rows[0];
|
|
82
|
+
return { id: query.payload.id, title: ticket?.title ?? "", status: ticket?.status ?? "" };
|
|
83
|
+
},
|
|
84
|
+
access: readerRoles,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
r.queryHandler({
|
|
88
|
+
name: "ticket:rows",
|
|
89
|
+
schema: z.object({}),
|
|
90
|
+
handler: async (query, ctx) => {
|
|
91
|
+
const page = await ticketExecutor.list({ limit: 50 }, query.user, ctx.db);
|
|
92
|
+
return { rows: page.rows, nextCursor: null };
|
|
93
|
+
},
|
|
94
|
+
access: readerRoles,
|
|
95
|
+
});
|
|
96
|
+
|
|
60
97
|
r.translations({
|
|
61
98
|
keys: {
|
|
99
|
+
"screen:ticket-list.title": { en: "Tickets" },
|
|
100
|
+
"screen:ticket-card.title": { en: "Ticket" },
|
|
101
|
+
"screen:ticket-record-dashboard.title": { en: "Ticket overview" },
|
|
102
|
+
"entity:ticket:field:title": { en: "Title" },
|
|
103
|
+
"entity:ticket:field:status": { en: "Status" },
|
|
104
|
+
"entity:__projection-detail__:field:title": { en: "Title" },
|
|
105
|
+
"entity:__projection-detail__:field:status": { en: "Status" },
|
|
106
|
+
"tickets.toolbar.close": { en: "Close all done" },
|
|
107
|
+
"tickets.dashboard.card": { en: "Ticket" },
|
|
62
108
|
"tickets.dashboard.group": { en: "Tickets by status" },
|
|
63
109
|
"tickets.dashboard.open": { en: "Open" },
|
|
64
110
|
"tickets.dashboard.closed": { en: "Closed" },
|
|
@@ -101,4 +147,48 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
101
147
|
],
|
|
102
148
|
access: readerRoles,
|
|
103
149
|
});
|
|
150
|
+
|
|
151
|
+
// Three additions: a projectionDetail embedded in a record-bound dashboard, a gated toolbar
|
|
152
|
+
// button, and a fallback chosen by reason.
|
|
153
|
+
const gate = { query: TICKET_CAPABILITIES_QUERY, field: "canClose", eq: true } as const;
|
|
154
|
+
|
|
155
|
+
// The list doubles as the fallback target: it has no visibleWhen of its own.
|
|
156
|
+
r.screen({
|
|
157
|
+
id: "ticket-list",
|
|
158
|
+
type: "projectionList",
|
|
159
|
+
query: "tickets:query:ticket:rows",
|
|
160
|
+
columns: ["title", "status"],
|
|
161
|
+
access: readerRoles,
|
|
162
|
+
toolbarActions: [
|
|
163
|
+
{
|
|
164
|
+
kind: "writeHandler",
|
|
165
|
+
id: "close-done",
|
|
166
|
+
label: "tickets.toolbar.close",
|
|
167
|
+
handler: "tickets:write:ticket:update",
|
|
168
|
+
// Live and fail-closed: hidden while the query loads and when it fails.
|
|
169
|
+
visibleWhen: gate,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
r.screen({
|
|
175
|
+
id: "ticket-card",
|
|
176
|
+
type: "projectionDetail",
|
|
177
|
+
query: "tickets:query:ticket:card",
|
|
178
|
+
layout: { sections: [{ title: "tickets.dashboard.card", fields: ["title", "status"] }] },
|
|
179
|
+
access: readerRoles,
|
|
180
|
+
visibleWhen: gate,
|
|
181
|
+
// 403 and 404 on the gate query get their own screen; an unmet condition keeps the
|
|
182
|
+
// default notice and any other failure shows an error banner with retry.
|
|
183
|
+
fallback: { forbidden: "ticket-list", notFound: "ticket-list" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// `record` reads the id from the URL; the embedded projectionDetail receives it as its entityId.
|
|
187
|
+
r.screen({
|
|
188
|
+
id: "ticket-record-dashboard",
|
|
189
|
+
type: "dashboard",
|
|
190
|
+
record: { param: "ticketId", payloadKey: "id" },
|
|
191
|
+
panels: [{ kind: "screen", id: "card", screen: "ticket-card" }],
|
|
192
|
+
access: readerRoles,
|
|
193
|
+
});
|
|
104
194
|
});
|
|
@@ -10,7 +10,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
10
10
|
`kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
|
|
11
11
|
records which feature declared each variable.
|
|
12
12
|
- **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
|
|
13
|
-
`stripeEnvSchema({ prices })`, `
|
|
13
|
+
`stripeEnvSchema({ prices })`, `paddleEnvSchema({ prices })` and
|
|
14
14
|
`createS3EnvSchema({ prefix })` for a
|
|
15
15
|
second bucket. These belong to no mounted feature, so the app adds them.
|
|
16
16
|
- **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
|
|
@@ -27,7 +27,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
27
27
|
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
28
28
|
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
29
29
|
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
30
|
-
| `
|
|
30
|
+
| `paddleEnvSchema({ prices })` | `subscription-paddle` | app (`extend`); the feature registers the bridge keys |
|
|
31
31
|
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
32
32
|
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
33
33
|
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
demoAdminEnvSchema,
|
|
16
16
|
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
17
17
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
18
|
-
import {
|
|
18
|
+
import { paddleEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
|
|
19
19
|
import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
20
20
|
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
21
21
|
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
@@ -37,7 +37,7 @@ const appEnvSchema = z.object({
|
|
|
37
37
|
...buildInfoEnvSchema.shape,
|
|
38
38
|
...demoAdminEnvSchema.shape,
|
|
39
39
|
...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
40
|
-
...
|
|
40
|
+
...paddleEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
41
41
|
...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
|
|
42
42
|
APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
|
|
43
43
|
});
|