create-kumiko-app 0.393.0 → 0.395.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/feature-manifest.json +89 -40
- package/package.json +5 -5
package/feature-manifest.json
CHANGED
|
@@ -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": {
|
|
@@ -1786,7 +1793,7 @@
|
|
|
1786
1793
|
},
|
|
1787
1794
|
{
|
|
1788
1795
|
"name": "managed-pages",
|
|
1789
|
-
"description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `
|
|
1796
|
+
"description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one `page` (Markdown or editor-document HTML body, per `bodyFormat`) per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core and document bodies through the template-resolver document sanitizer, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
|
|
1790
1797
|
"toggleableDefault": null,
|
|
1791
1798
|
"adoptsBundledLists": true,
|
|
1792
1799
|
"requires": [
|
|
@@ -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
|
}
|
|
@@ -2775,7 +2824,7 @@
|
|
|
2775
2824
|
},
|
|
2776
2825
|
{
|
|
2777
2826
|
"name": "template-resolver",
|
|
2778
|
-
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nEditing templates is for SystemAdmin only. A TenantAdmin edits only the templates the app unlocks at the mount site, `createTemplateResolverFeature({ tenantEditable: [{ kind: \"mail\", slug: \"status-update\", access: { roles: [\"TenantAdmin\"] } }] })`, and every write handler enforces that on the server (override, draft, publish, archive, reset, copy-locale, import, resources, `set`). Each entry carries the `access` roles allowed to edit it, because a bundled feature cannot know the host's roles. The same list decides delivery: a tenant override is served (mail, PDF, layouts, text blocks) only while its template is listed in tenantEditable; otherwise the system version is served. A locked override stays stored, a SystemAdmin sees it in the overview marked as not delivered, and listing the template makes it effective again. Tenant rows of a collection mounted with `access` are always served. A collection mounted without `access` is SystemAdmin only.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `
|
|
2827
|
+
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nEditing templates is for SystemAdmin only. A TenantAdmin edits only the templates the app unlocks at the mount site, `createTemplateResolverFeature({ tenantEditable: [{ kind: \"mail\", slug: \"status-update\", access: { roles: [\"TenantAdmin\"] } }] })`, and every write handler enforces that on the server (override, draft, publish, archive, reset, copy-locale, import, resources, `set`). Each entry carries the `access` roles allowed to edit it, because a bundled feature cannot know the host's roles. The same list decides delivery: a tenant override is served (mail, PDF, layouts, text blocks) only while its template is listed in tenantEditable; otherwise the system version is served. A locked override stays stored, a SystemAdmin sees it in the overview marked as not delivered, and listing the template makes it effective again. Tenant rows of a collection mounted with `access` are always served. A collection mounted without `access` is SystemAdmin only.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area that stores text, `document` a WYSIWYG document editor that stores sanitized HTML (bold, italic, headings, lists, links, variable chips). Both offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted. `send-test-status` tells an editor beforehand whether that send can work for the caller (`canSend`, else `reason` `no_channel` or `no_address`), never returning the address.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
|
|
2779
2828
|
"toggleableDefault": null,
|
|
2780
2829
|
"requires": [],
|
|
2781
2830
|
"optionalRequires": [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-kumiko-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.395.0",
|
|
4
4
|
"description": "`bun create kumiko-app <name>` — scaffold a new Kumiko app with an interactive feature picker.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cosmicdrift/kumiko-dev-server": "0.
|
|
31
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
32
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
33
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-dev-server": "0.395.0",
|
|
31
|
+
"@cosmicdrift/kumiko-framework": "0.395.0",
|
|
32
|
+
"@cosmicdrift/kumiko-server-runtime": "0.395.0",
|
|
33
|
+
"@cosmicdrift/kumiko-testing": "0.395.0",
|
|
34
34
|
"@inquirer/core": "^10.0.0",
|
|
35
35
|
"@inquirer/prompts": "^7.4.0"
|
|
36
36
|
},
|