@cosmicdrift/kumiko-samples 0.338.0 → 0.339.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.
Files changed (138) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +21 -1
  3. package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
  4. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
  5. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
  6. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
  7. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
  8. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
  10. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
  11. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
  12. package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
  13. package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
  14. package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
  15. package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
  16. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
  17. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
  18. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
  19. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
  20. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
  21. package/packages/bundled-features/src/billing-foundation/changes.json +34 -0
  22. package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
  24. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
  27. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
  28. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
  29. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
  33. package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
  34. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
  35. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
  36. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  37. package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
  38. package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
  39. package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
  40. package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
  41. package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
  42. package/packages/bundled-features/src/channel-discord/index.ts +2 -0
  43. package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
  44. package/packages/bundled-features/src/channel-slack/index.ts +2 -0
  45. package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
  46. package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
  47. package/packages/bundled-features/src/channel-teams/index.ts +2 -0
  48. package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
  49. package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
  50. package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
  51. package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
  52. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
  53. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  54. package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
  55. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  56. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
  57. package/packages/bundled-features/src/data-retention/feature.ts +2 -3
  58. package/packages/bundled-features/src/delivery/changes.json +7 -0
  59. package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
  60. package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
  61. package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
  62. package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
  63. package/packages/bundled-features/src/delivery/index.ts +15 -0
  64. package/packages/bundled-features/src/delivery/jobs.ts +1 -1
  65. package/packages/bundled-features/src/delivery/testing.ts +2 -0
  66. package/packages/bundled-features/src/delivery/types.ts +8 -3
  67. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
  68. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  69. package/packages/bundled-features/src/form-draft/feature.ts +1 -1
  70. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
  71. package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
  72. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
  74. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  75. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
  76. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
  78. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
  79. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
  80. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
  81. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
  82. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
  83. package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
  84. package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
  85. package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
  86. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
  87. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  88. package/packages/bundled-features/src/sessions/feature.ts +1 -1
  89. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  90. package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
  91. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
  92. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
  93. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
  94. package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
  95. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
  96. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
  97. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
  98. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  99. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
  100. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
  101. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  102. package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
  103. package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
  104. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  105. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
  106. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  107. package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
  108. package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
  109. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
  110. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
  111. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  112. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
  113. package/packages/bundled-features/src/workflow-runner/changes.json +7 -0
  114. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
  115. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
  116. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
  117. package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
  118. package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
  119. package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
  120. package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
  121. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
  122. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
  123. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
  124. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
  125. package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
  126. package/samples/apps/use-all-bundled/package.json +1 -0
  127. package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
  128. package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
  129. package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
  130. package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
  131. package/samples/apps/use-all-bundled/tsconfig.json +1 -0
  132. package/samples/recipes/chat-channels/README.md +19 -0
  133. package/samples/recipes/chat-channels/package.json +9 -0
  134. package/samples/recipes/chat-channels/src/feature.ts +40 -0
  135. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  136. package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
  137. package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
  138. package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
@@ -0,0 +1,82 @@
1
+ // @runtime test
2
+ //
3
+ // Gallery boot: the stock dev app with consumer protection switched on. Only
4
+ // this server mounts the billing catalog, the mock provider, the in-memory
5
+ // mail transport, the webhook/termination routes and a host-based tenant
6
+ // resolver; the shared dev app and its specs stay untouched.
7
+
8
+ import {
9
+ createContractTerminationRoutes,
10
+ createSubscriptionWebhookRoute,
11
+ } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
12
+ import { createDeliveryTestContext } from "@cosmicdrift/kumiko-bundled-features/delivery";
13
+ import { createTemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
14
+ import { seedTextBlock } from "@cosmicdrift/kumiko-bundled-features/template-resolver/seeding";
15
+ import { runDevApp, type SeedFn } from "@cosmicdrift/kumiko-dev-server";
16
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
17
+ import { createE2eSeedRoutes } from "@cosmicdrift/kumiko-testing/e2e/seed-route";
18
+ import { devAppOptions } from "../src/app/dev-app-options";
19
+ import { GALLERY_TENANT_HOST, GALLERY_TERMS_SLUG } from "./gallery-constants";
20
+ import {
21
+ createGalleryBillingFeature,
22
+ createGalleryEmailFeature,
23
+ galleryMailTransport,
24
+ galleryProviderFeature,
25
+ galleryScreensFeature,
26
+ } from "./gallery-features";
27
+
28
+ const port = Number.parseInt(process.env["PORT"] ?? "4196", 10);
29
+
30
+ const BILLING_FEATURE = "billing-foundation";
31
+ const EMAIL_FEATURE = "channel-email";
32
+
33
+ const features = [
34
+ ...devAppOptions.features.map((feature) => {
35
+ if (feature.name === BILLING_FEATURE)
36
+ return createGalleryBillingFeature(`http://localhost:${port}`);
37
+ if (feature.name === EMAIL_FEATURE) return createGalleryEmailFeature();
38
+ return feature;
39
+ }),
40
+ galleryProviderFeature,
41
+ galleryScreensFeature,
42
+ ];
43
+
44
+ const TERMS_TEXT: Readonly<Record<string, string>> = {
45
+ de: "## Allgemeine Geschäftsbedingungen\n\nDiese Bedingungen gelten für alle Verträge über die Plattform.",
46
+ en: "## Terms and conditions\n\nThese terms apply to every contract concluded through the platform.",
47
+ };
48
+
49
+ const seedTerms: SeedFn = async (stack) => {
50
+ for (const [locale, content] of Object.entries(TERMS_TEXT)) {
51
+ await seedTextBlock(stack.db, {
52
+ tenantId: SYSTEM_TENANT_ID,
53
+ slug: GALLERY_TERMS_SLUG,
54
+ locale,
55
+ title: "Terms",
56
+ content,
57
+ });
58
+ }
59
+ };
60
+
61
+ await runDevApp({
62
+ ...devAppOptions,
63
+ features,
64
+ seeds: [...(devAppOptions.seeds ?? []), seedTerms],
65
+ // The stock dev app pins every host to the SYSTEM tenant, which would hide
66
+ // platform mode: here only the named host resolves a tenant.
67
+ anonymousAccess: {
68
+ tenantResolver: (c) =>
69
+ c.req.header("host")?.startsWith(`${GALLERY_TENANT_HOST}:`) ? SYSTEM_TENANT_ID : null,
70
+ resolverTrust: "authoritative",
71
+ tenantExists: async (id) => id === SYSTEM_TENANT_ID,
72
+ },
73
+ extraContext: (deps) => ({
74
+ ...createDeliveryTestContext(deps),
75
+ templateResolver: createTemplateResolverApi(deps.db),
76
+ }),
77
+ extraRoutes: [
78
+ ...createE2eSeedRoutes({ mailOutbox: galleryMailTransport }),
79
+ createSubscriptionWebhookRoute(),
80
+ ...createContractTerminationRoutes(),
81
+ ],
82
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
3
- "featureCount": 76,
3
+ "featureCount": 80,
4
4
  "features": [
5
5
  {
6
6
  "name": "admin-shell",
@@ -352,6 +352,33 @@
352
352
  "secrets": [],
353
353
  "writeHandlers": []
354
354
  },
355
+ {
356
+ "name": "channel-discord",
357
+ "description": "Posts delivery notifications to a Discord webhook, registered as the `discord` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { discord: \"<connection name>\" } })`. The webhook URL is a tenant secret under `channel-discord:webhooks.<connection name>`; only the connection name is logged. Mentions are disabled and content is cut to 2000 characters. Requires `delivery` and `secrets`. Host allowlist (discord.com, discordapp.com, path /api/webhooks/), https-only and the request timeout are app options of `createChannelDiscordFeature(opts)`, never tenant config.",
358
+ "toggleableDefault": null,
359
+ "requires": [
360
+ "delivery",
361
+ "secrets"
362
+ ],
363
+ "optionalRequires": [],
364
+ "configReads": [],
365
+ "exposesApis": [],
366
+ "usesApis": [],
367
+ "extensionsUsed": [
368
+ {
369
+ "extensionName": "deliveryChannel",
370
+ "entityName": "discord"
371
+ }
372
+ ],
373
+ "configKeys": [],
374
+ "secrets": [],
375
+ "writeHandlers": [],
376
+ "uiHints": {
377
+ "displayLabel": "Discord Channel",
378
+ "category": "notifications",
379
+ "recommended": false
380
+ }
381
+ },
355
382
  {
356
383
  "name": "channel-email",
357
384
  "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport`, a `renderer: NotificationRenderer` (e.g. backed by `renderer-simple`), and a `resolveEmail` function that maps a user ID to their email address.",
@@ -433,6 +460,94 @@
433
460
  "recommended": false
434
461
  }
435
462
  },
463
+ {
464
+ "name": "channel-slack",
465
+ "description": "Posts delivery notifications to a Slack incoming webhook, registered as the `slack` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { slack: \"<connection name>\" } })`. The webhook URL is a tenant secret under `channel-slack:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (hooks.slack.com), https-only and the request timeout are app options of `createChannelSlackFeature(opts)`, never tenant config.",
466
+ "toggleableDefault": null,
467
+ "requires": [
468
+ "delivery",
469
+ "secrets"
470
+ ],
471
+ "optionalRequires": [],
472
+ "configReads": [],
473
+ "exposesApis": [],
474
+ "usesApis": [],
475
+ "extensionsUsed": [
476
+ {
477
+ "extensionName": "deliveryChannel",
478
+ "entityName": "slack"
479
+ }
480
+ ],
481
+ "configKeys": [],
482
+ "secrets": [],
483
+ "writeHandlers": [],
484
+ "uiHints": {
485
+ "displayLabel": "Slack Channel",
486
+ "category": "notifications",
487
+ "recommended": false
488
+ }
489
+ },
490
+ {
491
+ "name": "channel-teams",
492
+ "description": "Posts delivery notifications to a Microsoft Teams webhook (Workflows or connector) as an Adaptive Card, registered as the `teams` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { teams: \"<connection name>\" } })`. The webhook URL is a tenant secret under `channel-teams:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (.webhook.office.com, .logic.azure.com, .powerplatform.com), https-only and the request timeout are app options of `createChannelTeamsFeature(opts)`, never tenant config.",
493
+ "toggleableDefault": null,
494
+ "requires": [
495
+ "delivery",
496
+ "secrets"
497
+ ],
498
+ "optionalRequires": [],
499
+ "configReads": [],
500
+ "exposesApis": [],
501
+ "usesApis": [],
502
+ "extensionsUsed": [
503
+ {
504
+ "extensionName": "deliveryChannel",
505
+ "entityName": "teams"
506
+ }
507
+ ],
508
+ "configKeys": [],
509
+ "secrets": [],
510
+ "writeHandlers": [],
511
+ "uiHints": {
512
+ "displayLabel": "Microsoft Teams Channel",
513
+ "category": "notifications",
514
+ "recommended": false
515
+ }
516
+ },
517
+ {
518
+ "name": "channel-telegram",
519
+ "description": "Sends delivery notifications through the Telegram Bot API (`sendMessage`, plain text), registered as the `telegram` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { telegram: \"<chat id or @channel>\" } })`. The bot token is the tenant secret `channel-telegram:secret:bot-token`; the token never reaches logs or delivery attempts. Requires `delivery` and `secrets`. The API base URL and request timeout are app options of `createChannelTelegramFeature(opts)`, never tenant config.",
520
+ "toggleableDefault": null,
521
+ "requires": [
522
+ "delivery",
523
+ "secrets"
524
+ ],
525
+ "optionalRequires": [],
526
+ "configReads": [],
527
+ "exposesApis": [],
528
+ "usesApis": [],
529
+ "extensionsUsed": [
530
+ {
531
+ "extensionName": "deliveryChannel",
532
+ "entityName": "telegram"
533
+ }
534
+ ],
535
+ "configKeys": [],
536
+ "secrets": [
537
+ {
538
+ "qualifiedName": "channel-telegram:secret:bot-token",
539
+ "scope": "tenant",
540
+ "label": "Telegram bot token",
541
+ "hint": "Token from @BotFather. Used for every Telegram chat the tenant addresses via route: { telegram: \"<chat id>\" }."
542
+ }
543
+ ],
544
+ "writeHandlers": [],
545
+ "uiHints": {
546
+ "displayLabel": "Telegram Channel",
547
+ "category": "notifications",
548
+ "recommended": false
549
+ }
550
+ },
436
551
  {
437
552
  "name": "collection-labels",
438
553
  "description": "Nav labels for the content collections mounted by this sample app.",
@@ -2959,5 +3074,35 @@
2959
3074
  "recommended": false
2960
3075
  }
2961
3076
  }
3077
+ ],
3078
+ "systemEvents": [
3079
+ {
3080
+ "name": "kumiko:system:workflow.retry.scheduled",
3081
+ "version": 1
3082
+ },
3083
+ {
3084
+ "name": "kumiko:system:workflow.run-completed",
3085
+ "version": 1
3086
+ },
3087
+ {
3088
+ "name": "kumiko:system:workflow.run-failed",
3089
+ "version": 1
3090
+ },
3091
+ {
3092
+ "name": "kumiko:system:workflow.run-started",
3093
+ "version": 1
3094
+ },
3095
+ {
3096
+ "name": "kumiko:system:workflow.step.resumed",
3097
+ "version": 1
3098
+ },
3099
+ {
3100
+ "name": "kumiko:system:workflow.step.waiting",
3101
+ "version": 1
3102
+ },
3103
+ {
3104
+ "name": "kumiko:system:workflow.step.waiting-for-event",
3105
+ "version": 1
3106
+ }
2962
3107
  ]
2963
3108
  }
@@ -9,6 +9,7 @@
9
9
  "boot:bundled": "KUMIKO_DRY_RUN_ENV=boot bun dist-server/server.js",
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
+ "billing-gallery": "bun x playwright test --config=playwright.billing-gallery.config.ts",
12
13
  "e2e": "bun x playwright test e2e/mfa-login.spec.ts --config=playwright.config.ts",
13
14
  "gen:manifest": "bun run scripts/gen-feature-manifest.ts",
14
15
  "gen:sample-index": "bun run scripts/gen-sample-index.ts"
@@ -0,0 +1,14 @@
1
+ // @runtime test
2
+ // Review gallery for the consent/termination flows. Not part of the CI e2e
3
+ // list (.github/workflows/ci.yml) and gated by SCREENSHOT_DIR like every other
4
+ // screenshot spec: run it by hand, see the `billing-gallery` script.
5
+ import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
6
+ import { E2E_PORTS } from "../../e2e/e2e-ports";
7
+
8
+ export default defineAppE2eConfig({
9
+ port: E2E_PORTS["framework/use-all-bundled-billing-gallery"],
10
+ env: PLAYWRIGHT_DEMO_ENV,
11
+ locale: "de",
12
+ testDir: "./e2e-billing-gallery",
13
+ serverEntry: "e2e-billing-gallery/server.ts",
14
+ });
@@ -445,6 +445,26 @@ const FEATURE_IMPORT_REGISTRY: Record<string, FeatureImport> = {
445
445
  path: "@cosmicdrift/kumiko-bundled-features/channel-push",
446
446
  factory: "createChannelPushFeature",
447
447
  },
448
+ "channel-slack": {
449
+ kind: "factory",
450
+ path: "@cosmicdrift/kumiko-bundled-features/channel-slack",
451
+ factory: "createChannelSlackFeature",
452
+ },
453
+ "channel-discord": {
454
+ kind: "factory",
455
+ path: "@cosmicdrift/kumiko-bundled-features/channel-discord",
456
+ factory: "createChannelDiscordFeature",
457
+ },
458
+ "channel-teams": {
459
+ kind: "factory",
460
+ path: "@cosmicdrift/kumiko-bundled-features/channel-teams",
461
+ factory: "createChannelTeamsFeature",
462
+ },
463
+ "channel-telegram": {
464
+ kind: "factory",
465
+ path: "@cosmicdrift/kumiko-bundled-features/channel-telegram",
466
+ factory: "createChannelTelegramFeature",
467
+ },
448
468
  readiness: {
449
469
  kind: "named",
450
470
  path: "@cosmicdrift/kumiko-bundled-features/readiness",
@@ -36,6 +36,7 @@ export type {
36
36
  ManifestExtension,
37
37
  ManifestFeature,
38
38
  ManifestSecret,
39
+ ManifestSystemEvent,
39
40
  } from "@cosmicdrift/kumiko-framework/engine";
40
41
  export { serializeManifest };
41
42
 
@@ -22,6 +22,7 @@ import { authMfaUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/aut
22
22
  import { billingFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
23
23
  import { capCounterFeature } from "@cosmicdrift/kumiko-bundled-features/cap-counter";
24
24
  import { createCapOverviewFeature } from "@cosmicdrift/kumiko-bundled-features/cap-overview";
25
+ import { createChannelDiscordFeature } from "@cosmicdrift/kumiko-bundled-features/channel-discord";
25
26
  import {
26
27
  createChannelEmailFeature,
27
28
  type EmailTransport,
@@ -31,6 +32,9 @@ import {
31
32
  createChannelPushFeature,
32
33
  type PushTransport,
33
34
  } from "@cosmicdrift/kumiko-bundled-features/channel-push";
35
+ import { createChannelSlackFeature } from "@cosmicdrift/kumiko-bundled-features/channel-slack";
36
+ import { createChannelTeamsFeature } from "@cosmicdrift/kumiko-bundled-features/channel-teams";
37
+ import { createChannelTelegramFeature } from "@cosmicdrift/kumiko-bundled-features/channel-telegram";
34
38
  import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
35
39
  import { complianceProfilesOpsFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles-ops";
36
40
  import { customFieldsFeature } from "@cosmicdrift/kumiko-bundled-features/custom-fields";
@@ -171,6 +175,12 @@ export const APP_FEATURES = [
171
175
  transport: stubPushTransport,
172
176
  resolveToken: async () => "smoke-push-token",
173
177
  }),
178
+ // Chat channels are route-only and read their webhook URL / bot token from the
179
+ // tenant's secrets at send time, so the smoke app needs no credentials here.
180
+ createChannelSlackFeature(),
181
+ createChannelDiscordFeature(),
182
+ createChannelTeamsFeature(),
183
+ createChannelTelegramFeature(),
174
184
 
175
185
  // mail (foundation before transport)
176
186
  mailFoundationFeature,
@@ -26,6 +26,7 @@
26
26
  "scripts",
27
27
  "kumiko.config.ts",
28
28
  "e2e",
29
+ "e2e-billing-gallery",
29
30
  "../../e2e"
30
31
  ]
31
32
  }
@@ -0,0 +1,19 @@
1
+ # Sample: Chat Channels
2
+
3
+ Shows how a feature posts to a tenant's Slack channel without ever handling the webhook URL.
4
+
5
+ ## What you learn here
6
+
7
+ **Route-only targets:** Chat targets belong to the tenant, not to a user. A handler addresses one with `ctx.notify(type, { route: { slack: "ops" } })`; `"ops"` is a connection name. The chat channels declare no `resolve`, so a normal `to: userId` notification skips them without a log row.
8
+
9
+ **Secrets for the URL:** The tenant stores the webhook URL as a secret under `channel-slack:webhooks.<connection name>` (namespace declared by the channel, written through `secrets:write:set`). The `delivery.send` job reads it with an audit entry (`channel-slack:send`), posts the message and records only the connection name and an outcome code (`http_500`, `timeout`, `redirect_blocked`, `host_not_allowed`, `missing_credentials`, `invalid_address`).
10
+
11
+ **App-level hardening:** Each channel only talks to its provider's hosts, over https, without following redirects. `allowedHosts`, `requireHttps` and `timeoutMs` are options of `createChannelSlackFeature(opts)`. Tenants cannot change them; the test uses them to point at a local stub.
12
+
13
+ The same shape works for `channel-discord`, `channel-teams` (connection name + webhook secret) and `channel-telegram` (one `botToken` secret, the address is the chat id).
14
+
15
+ ## Run
16
+
17
+ ```bash
18
+ bun test --config=bunfig.integration.toml --timeout=15000 samples/recipes/chat-channels
19
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-sample-chat-channels",
3
+ "description": "Chat channels. Delivery notifications to Slack, minus the browser tab.",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@cosmicdrift/kumiko-bundled-features": "workspace:*",
7
+ "@cosmicdrift/kumiko-framework": "workspace:*"
8
+ }
9
+ }
@@ -0,0 +1,40 @@
1
+ // Chat Channels Sample
2
+ //
3
+ // A handler tells the tenant's ops chat about something. Chat targets belong to
4
+ // the tenant, not to a user, so the feature addresses one by connection name via
5
+ // `route` — it never sees the webhook URL. The tenant stores that URL as a secret
6
+ // under `channel-slack:webhooks.<connection name>`.
7
+
8
+ import {
9
+ defineFeature,
10
+ defineWriteHandler,
11
+ type NotifyFn,
12
+ qn,
13
+ } from "@cosmicdrift/kumiko-framework/engine";
14
+ import * as z from "zod";
15
+
16
+ export const OPS_ANNOUNCEMENT_TYPE = qn("ops", "notify", "announcement");
17
+
18
+ export const opsFeature = defineFeature("ops", (r) => {
19
+ r.requires("delivery");
20
+
21
+ r.writeHandler(
22
+ defineWriteHandler({
23
+ name: "announce",
24
+ schema: z.object({
25
+ connection: z.string(),
26
+ title: z.string().min(1),
27
+ body: z.string().optional(),
28
+ }),
29
+ access: { roles: ["Admin"] },
30
+ handler: async (event, ctx) => {
31
+ const notify = ctx.notify as NotifyFn;
32
+ await notify(OPS_ANNOUNCEMENT_TYPE, {
33
+ route: { slack: event.payload.connection },
34
+ data: { title: event.payload.title, body: event.payload.body },
35
+ });
36
+ return { isSuccess: true, data: { announced: true } };
37
+ },
38
+ }),
39
+ );
40
+ });
@@ -169,7 +169,7 @@ export const shareByTokenQuery = defineQueryHandler({
169
169
  },
170
170
  handler: async (query, ctx) => {
171
171
  const hash = await hashToken(query.payload.token);
172
- const row = await fetchOne<ShareLinkRow>(ctx.db.unsafeRaw(SHARE_BY_TOKEN_REASON), table, {
172
+ const row = await fetchOne<ShareLinkRow>(ctx.db.unsafeRaw(), table, {
173
173
  tokenHash: hash,
174
174
  });
175
175
 
@@ -60,6 +60,11 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
60
60
  runId,
61
61
  workflow,
62
62
  triggerEvent,
63
+ triggerEventRef: {
64
+ eventId: event.id,
65
+ aggregateId: event.aggregateId,
66
+ version: event.version,
67
+ },
63
68
  ...(idempotencyKey && { idempotencyKey }),
64
69
  handlerCtx: ctx as never,
65
70
  });
@@ -4,19 +4,23 @@
4
4
  // 1. createSuspendedRunFetcher(db, workflowRegistry) — Postgres-backed
5
5
  // 2. createInMemorySuspendedRunFetcher(runs) — for unit tests
6
6
  //
7
- // The fetcher extracts the trigger-event snapshot + Q7 fingerprint directly
8
- // from the suspension event payload (the wait/waitForEvent/retry steps
9
- // stamp them there), avoiding a per-run lookup against run.started.
7
+ // The fetcher reads the Q7 fingerprint from the suspension event payload and
8
+ // re-loads the trigger event from the event store through the
9
+ // triggerEventRef on the run's own run.started event (no payload copy lives
10
+ // on the run stream).
10
11
 
11
12
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
12
- import type { WorkflowDefinition } from "@cosmicdrift/kumiko-framework/engine";
13
+ import type { TenantId, WorkflowDefinition } from "@cosmicdrift/kumiko-framework/engine";
13
14
  import {
14
15
  WORKFLOW_RETRY_SCHEDULED_TYPE,
16
+ WORKFLOW_RUN_STARTED_TYPE,
15
17
  WORKFLOW_WAITING_FOR_EVENT_TYPE,
16
18
  WORKFLOW_WAITING_TYPE,
17
19
  } from "@cosmicdrift/kumiko-framework/engine";
20
+ import { loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
18
21
  import { selectExpiredSuspensionEvents } from "./db/queries/suspended-runs";
19
22
  import type { SuspendableRun } from "./resume-loop";
23
+ import type { TriggerEventRef } from "./workflow-runner";
20
24
 
21
25
  export type WorkflowRegistry = ReadonlyMap<string, WorkflowDefinition>;
22
26
 
@@ -26,6 +30,42 @@ const SUSPEND_EVENT_TYPES = [
26
30
  WORKFLOW_RETRY_SCHEDULED_TYPE,
27
31
  ] as const;
28
32
 
33
+ function isTriggerEventRef(value: unknown): value is TriggerEventRef {
34
+ return (
35
+ typeof value === "object" &&
36
+ value !== null &&
37
+ "eventId" in value &&
38
+ typeof value.eventId === "string" &&
39
+ "aggregateId" in value &&
40
+ typeof value.aggregateId === "string" &&
41
+ "version" in value &&
42
+ typeof value.version === "number"
43
+ );
44
+ }
45
+
46
+ // undefined when the run has no usable reference or the trigger event can no
47
+ // longer be loaded (archived stream): the run is skipped, never resumed on
48
+ // made-up data.
49
+ async function loadTriggerEvent(
50
+ db: DbRunner,
51
+ tenantId: TenantId,
52
+ runId: string,
53
+ ): Promise<{ aggregateId: string; type: string; payload: unknown } | undefined> {
54
+ const runEvents = await loadAggregate(db, runId, tenantId);
55
+ const started = runEvents.find((e) => e.type === WORKFLOW_RUN_STARTED_TYPE);
56
+ if (!started) return undefined;
57
+ const ref = started.payload["triggerEventRef"];
58
+ if (ref === undefined) {
59
+ // Cron run: the synthetic trigger event was never stored.
60
+ return { aggregateId: runId, type: String(started.payload["triggerEventType"]), payload: {} };
61
+ }
62
+ if (!isTriggerEventRef(ref)) return undefined;
63
+ const source = (await loadAggregate(db, ref.aggregateId, tenantId)).find(
64
+ (e) => e.id === ref.eventId,
65
+ );
66
+ return source && { aggregateId: source.aggregateId, type: source.type, payload: source.payload };
67
+ }
68
+
29
69
  /**
30
70
  * Postgres-backed fetcher. Queries kumiko_events for suspension events with
31
71
  * expired wakeAt/timeoutAt timestamps. The resume-loop applies the Q7
@@ -64,11 +104,12 @@ export function createSuspendedRunFetcher(
64
104
  const wakeAt = (payload["wakeAt"] ?? payload["timeoutAt"]) as string | undefined;
65
105
  if (!wakeAt) continue;
66
106
 
67
- const triggerEvent = {
68
- aggregateId: (payload["triggerAggregateId"] as string | undefined) ?? aggregateId,
69
- type: (payload["triggerEventType"] as string | undefined) ?? "kumiko:system:resume",
70
- payload: payload["triggerPayload"] ?? {},
71
- };
107
+ const triggerEvent = await loadTriggerEvent(
108
+ db,
109
+ (row["tenantId"] ?? row["tenant_id"]) as TenantId, // @cast-boundary raw-sql-row
110
+ aggregateId,
111
+ );
112
+ if (!triggerEvent) continue;
72
113
 
73
114
  results.push({
74
115
  runId: aggregateId,
@@ -26,10 +26,19 @@ import {
26
26
  WORKFLOW_RUN_STARTED_TYPE,
27
27
  } from "@cosmicdrift/kumiko-framework/engine";
28
28
 
29
+ // Reference into the event store; the run stream never copies a foreign
30
+ // payload, so erasing the source event also covers the workflow.
31
+ export type TriggerEventRef = {
32
+ readonly eventId: string;
33
+ readonly aggregateId: string;
34
+ readonly version: number;
35
+ };
36
+
29
37
  export type WorkflowRunStartedPayload = {
30
38
  readonly workflowName: string;
31
39
  readonly triggerEventType: string;
32
- readonly triggerPayload: unknown;
40
+ // Absent for cron runs: their synthetic trigger event has no stored source.
41
+ readonly triggerEventRef?: TriggerEventRef;
33
42
  readonly definitionFingerprint: string;
34
43
  readonly idempotencyKey?: string;
35
44
  };
@@ -47,6 +56,7 @@ export async function startAndRunWorkflow(args: {
47
56
  readonly runId: string;
48
57
  readonly workflow: WorkflowDefinition;
49
58
  readonly triggerEvent: WriteEvent;
59
+ readonly triggerEventRef?: TriggerEventRef;
50
60
  readonly idempotencyKey?: string;
51
61
  readonly handlerCtx: HandlerContext;
52
62
  }): Promise<{ readonly outcome: "completed" | "suspended" }> {
@@ -55,7 +65,7 @@ export async function startAndRunWorkflow(args: {
55
65
  const startedPayload: WorkflowRunStartedPayload = {
56
66
  workflowName: args.workflow.name,
57
67
  triggerEventType: args.triggerEvent.type,
58
- triggerPayload: args.triggerEvent.payload,
68
+ ...(args.triggerEventRef && { triggerEventRef: args.triggerEventRef }),
59
69
  definitionFingerprint: fingerprint,
60
70
  ...(args.idempotencyKey && { idempotencyKey: args.idempotencyKey }),
61
71
  };