@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +21 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
- package/packages/bundled-features/src/billing-foundation/changes.json +34 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
- package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
- package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
- package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
- package/packages/bundled-features/src/channel-discord/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
- package/packages/bundled-features/src/channel-slack/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
- package/packages/bundled-features/src/channel-teams/index.ts +2 -0
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
- package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
- package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
- package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
- package/packages/bundled-features/src/data-retention/feature.ts +2 -3
- package/packages/bundled-features/src/delivery/changes.json +7 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
- package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
- package/packages/bundled-features/src/delivery/index.ts +15 -0
- package/packages/bundled-features/src/delivery/jobs.ts +1 -1
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +8 -3
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
- package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
- package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
- package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +7 -0
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
- package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
- package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
- package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
- package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
- package/samples/apps/use-all-bundled/tsconfig.json +1 -0
- package/samples/recipes/chat-channels/README.md +19 -0
- package/samples/recipes/chat-channels/package.json +9 -0
- package/samples/recipes/chat-channels/src/feature.ts +40 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
- package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
|
@@ -1,6 +1,62 @@
|
|
|
1
|
-
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import type { NotificationRenderer } from "../delivery/index.js";
|
|
3
3
|
|
|
4
|
+
export type MailBranding = {
|
|
5
|
+
/** Shown as text when there is no logo, and as the logo alt text. */
|
|
6
|
+
readonly productName?: string;
|
|
7
|
+
/** Hex color (#rgb, #rrggbb, #rrggbbaa); used for buttons and the header accent. */
|
|
8
|
+
readonly primaryColor?: string;
|
|
9
|
+
/** Absolute http(s) URL; anything else is dropped. */
|
|
10
|
+
readonly logoUrl?: string;
|
|
11
|
+
readonly footerText?: string;
|
|
12
|
+
/** Absolute http(s) URLs only; other entries are dropped. */
|
|
13
|
+
readonly footerLinks?: readonly { readonly label: string; readonly url: string }[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PRIMARY_COLOR = "#2563eb";
|
|
17
|
+
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
18
|
+
|
|
19
|
+
function isHttpUrl(value: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
const { protocol } = new URL(value);
|
|
22
|
+
return protocol === "http:" || protocol === "https:";
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvePrimaryColor(branding: MailBranding | undefined): string {
|
|
29
|
+
const color = branding?.primaryColor;
|
|
30
|
+
return color && HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_PRIMARY_COLOR;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderBrandingHeader(branding: MailBranding | undefined, primaryColor: string): string {
|
|
34
|
+
if (!branding) return "";
|
|
35
|
+
const logoUrl = branding.logoUrl && isHttpUrl(branding.logoUrl) ? branding.logoUrl : undefined;
|
|
36
|
+
if (!logoUrl && !branding.productName) return "";
|
|
37
|
+
const logo = logoUrl
|
|
38
|
+
? `<img src="${escapeHtmlAttr(logoUrl)}" alt="${escapeHtmlAttr(branding.productName ?? "")}" style="display:block;max-height:40px;max-width:200px;border:0" />`
|
|
39
|
+
: `<span style="font-size:18px;font-weight:700;color:${escapeHtmlAttr(primaryColor)}">${escapeHtml(branding.productName ?? "")}</span>`;
|
|
40
|
+
return `<div style="margin:0 0 24px;padding:0 0 16px;border-bottom:3px solid ${escapeHtmlAttr(primaryColor)}">${logo}</div>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderBrandingFooter(branding: MailBranding | undefined): string {
|
|
44
|
+
if (!branding) return "";
|
|
45
|
+
const links = (branding.footerLinks ?? [])
|
|
46
|
+
.filter((link) => isHttpUrl(link.url))
|
|
47
|
+
.map(
|
|
48
|
+
(link) =>
|
|
49
|
+
`<a href="${escapeHtmlAttr(link.url)}" style="color:#999">${escapeHtml(link.label)}</a>`,
|
|
50
|
+
);
|
|
51
|
+
const footerPartsHtml = [
|
|
52
|
+
branding.footerText ? escapeHtml(branding.footerText) : "",
|
|
53
|
+
links.join(" · "),
|
|
54
|
+
].filter((part) => part !== "");
|
|
55
|
+
if (footerPartsHtml.length === 0) return "";
|
|
56
|
+
const footerHtml = footerPartsHtml.join("<br />");
|
|
57
|
+
return `<p style="margin:16px 0 0;color:#999;font-size:12px">${footerHtml}</p>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
4
60
|
type Section =
|
|
5
61
|
| { readonly text: string }
|
|
6
62
|
| { readonly button: { readonly label: string; readonly url: string } };
|
|
@@ -15,51 +71,58 @@ type EmailTemplateData = {
|
|
|
15
71
|
readonly body?: string;
|
|
16
72
|
};
|
|
17
73
|
|
|
18
|
-
function renderSection(section: Section): string {
|
|
74
|
+
function renderSection(section: Section, primaryColor: string): string {
|
|
19
75
|
if ("text" in section) {
|
|
20
76
|
return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
|
|
21
77
|
}
|
|
22
78
|
if ("button" in section) {
|
|
23
|
-
return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background
|
|
79
|
+
return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
|
|
24
80
|
}
|
|
25
81
|
return "";
|
|
26
82
|
}
|
|
27
83
|
|
|
28
84
|
// Simple Renderer: turns structured email template data into HTML with inline CSS.
|
|
29
85
|
// No external dependencies, no template engine — just string concatenation.
|
|
30
|
-
export
|
|
31
|
-
|
|
86
|
+
export function createSimpleRenderer(branding?: MailBranding): NotificationRenderer {
|
|
87
|
+
const primaryColor = resolvePrimaryColor(branding);
|
|
88
|
+
return {
|
|
89
|
+
name: "simple",
|
|
90
|
+
|
|
91
|
+
async render(input) {
|
|
92
|
+
const data = input.variables as EmailTemplateData; // @cast-boundary render-helper
|
|
32
93
|
|
|
33
|
-
|
|
34
|
-
|
|
94
|
+
// Fallback: if no structured fields, use title + body as header + single text section
|
|
95
|
+
const header = data.header ?? data.title;
|
|
96
|
+
const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
|
|
35
97
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
|
|
100
|
+
parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
|
|
101
|
+
parts.push(renderBrandingHeader(branding, primaryColor));
|
|
39
102
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
103
|
+
if (header) {
|
|
104
|
+
parts.push(
|
|
105
|
+
`<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
43
108
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
109
|
+
if (sections) {
|
|
110
|
+
for (const section of sections) {
|
|
111
|
+
parts.push(renderSection(section, primaryColor));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
49
114
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
if (data.footer) {
|
|
116
|
+
parts.push(
|
|
117
|
+
`<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`,
|
|
118
|
+
);
|
|
53
119
|
}
|
|
54
|
-
}
|
|
55
120
|
|
|
56
|
-
|
|
57
|
-
parts.push(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
121
|
+
parts.push(renderBrandingFooter(branding));
|
|
122
|
+
parts.push("</div></body></html>");
|
|
123
|
+
return parts.join("");
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
61
127
|
|
|
62
|
-
|
|
63
|
-
return parts.join("");
|
|
64
|
-
},
|
|
65
|
-
};
|
|
128
|
+
export const simpleRenderer: NotificationRenderer = createSimpleRenderer();
|
|
@@ -183,7 +183,7 @@ export function createSecretsFeature(opts: SecretsFeatureOptions = {}): FeatureD
|
|
|
183
183
|
rotateJob(
|
|
184
184
|
payload,
|
|
185
185
|
ctx,
|
|
186
|
-
ctx.db.unsafeRaw(
|
|
186
|
+
ctx.db.unsafeRaw() as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
187
187
|
),
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -177,7 +177,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
|
|
|
177
177
|
cleanupJob(
|
|
178
178
|
payload,
|
|
179
179
|
ctx,
|
|
180
|
-
ctx.db.unsafeRaw(
|
|
180
|
+
ctx.db.unsafeRaw() as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
181
181
|
),
|
|
182
182
|
});
|
|
183
183
|
|
|
@@ -49,7 +49,7 @@ export const revokeAllForUserWrite = defineWriteHandler({
|
|
|
49
49
|
reason: REVOKE_ALL_SESSIONS_REASON,
|
|
50
50
|
},
|
|
51
51
|
handler: async (event, ctx) => {
|
|
52
|
-
const runner = ctx.db.unsafeRaw(
|
|
52
|
+
const runner = ctx.db.unsafeRaw();
|
|
53
53
|
const updated = await updateMany<{ id: string }>(
|
|
54
54
|
runner,
|
|
55
55
|
userSessionTable,
|
|
@@ -51,10 +51,7 @@ export function createClaimTenantHandoverHandler(opts: ClaimTenantHandoverOption
|
|
|
51
51
|
agent: { expose: false },
|
|
52
52
|
rateLimit: { per: "user", limit: 10, windowSeconds: 60 },
|
|
53
53
|
handler: async (event, ctx) => {
|
|
54
|
-
const db = ctx.db.unsafeRaw(
|
|
55
|
-
"tenant-handover claim: cross-tenant read + ownership write, legitimized by the " +
|
|
56
|
-
"row-bound grant redeemed below",
|
|
57
|
-
);
|
|
54
|
+
const db = ctx.db.unsafeRaw();
|
|
58
55
|
const rootLocation = resolveRootAnchorLocation(ctx.registry, db, event.payload.entityType);
|
|
59
56
|
if (!rootLocation) return notTransferable(event.payload.entityType);
|
|
60
57
|
const { rootTableName, rootIdCol, rootTenantCol, loadAnchor } = rootLocation;
|
|
@@ -126,9 +126,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
|
|
|
126
126
|
}
|
|
127
127
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
128
128
|
await runTenantDestructionSweep({
|
|
129
|
-
db: ctx.db.unsafeRaw(
|
|
130
|
-
RUN_TENANT_DESTRUCTION_REASON,
|
|
131
|
-
) as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
129
|
+
db: ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
132
130
|
registry: ctx.registry,
|
|
133
131
|
now: T.Now.instant(),
|
|
134
132
|
log: (message) => ctx.log?.warn(message),
|
|
@@ -30,11 +30,9 @@ export const cancelDestructionWrite = defineWriteHandler({
|
|
|
30
30
|
},
|
|
31
31
|
handler: async (event, ctx) => {
|
|
32
32
|
const tenantId = event.user.tenantId;
|
|
33
|
-
const row = await fetchOne<TenantLifecycleRow>(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{ id: tenantId },
|
|
37
|
-
);
|
|
33
|
+
const row = await fetchOne<TenantLifecycleRow>(ctx.db.unsafeRaw(), tenantTable, {
|
|
34
|
+
id: tenantId,
|
|
35
|
+
});
|
|
38
36
|
if (!row) {
|
|
39
37
|
return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
|
|
40
38
|
}
|
package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const requestDestructionWrite = defineWriteHandler({
|
|
|
34
34
|
},
|
|
35
35
|
handler: async (event, ctx) => {
|
|
36
36
|
const tenantId = event.user.tenantId;
|
|
37
|
-
const runner = ctx.db.unsafeRaw(
|
|
37
|
+
const runner = ctx.db.unsafeRaw();
|
|
38
38
|
const row = await fetchOne<TenantLifecycleRow>(runner, tenantTable, { id: tenantId });
|
|
39
39
|
if (!row) {
|
|
40
40
|
return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
|
|
@@ -399,7 +399,7 @@ export function createTierEngineFeature<
|
|
|
399
399
|
const rawDb = ctx.systemDb
|
|
400
400
|
? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
|
|
401
401
|
: ctx.db && "unsafeRaw" in ctx.db
|
|
402
|
-
? ctx.db.unsafeRaw(
|
|
402
|
+
? ctx.db.unsafeRaw()
|
|
403
403
|
: undefined;
|
|
404
404
|
// skip: defensive — inTransaction phase always sets db, but AppContext's
|
|
405
405
|
// type makes it optional. Throwing would be overreach (lifecycle
|
|
@@ -25,7 +25,7 @@ export const getTenantTierQuery = defineQueryHandler({
|
|
|
25
25
|
},
|
|
26
26
|
handler: async (query, ctx) => {
|
|
27
27
|
const tenantId = query.payload.tenantId as TenantId; // @cast-boundary engine-bridge
|
|
28
|
-
const tdb = createTenantDb(ctx.db.unsafeRaw(
|
|
28
|
+
const tdb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
|
|
29
29
|
const row = await fetchOne<TierAssignmentRow>(tdb, tierAssignmentTable, { tenantId });
|
|
30
30
|
return row ?? null;
|
|
31
31
|
},
|
|
@@ -73,7 +73,7 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
73
73
|
},
|
|
74
74
|
handler: async (event, ctx) => {
|
|
75
75
|
const tenantId = event.payload.tenantId as TenantId; // @cast-boundary engine-bridge
|
|
76
|
-
const rawDb = ctx.db.unsafeRaw(
|
|
76
|
+
const rawDb = ctx.db.unsafeRaw();
|
|
77
77
|
const tdb = createTenantDb(rawDb, tenantId, "system");
|
|
78
78
|
const systemUser = { ...event.user, tenantId };
|
|
79
79
|
const tier = event.payload.tier;
|
|
@@ -588,9 +588,8 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
588
588
|
// Discriminator wird via handlerName="user-data-rights:run-export-
|
|
589
589
|
// jobs" im Secret-Read-Audit erfasst.
|
|
590
590
|
const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
|
|
591
|
-
const exportDb =
|
|
592
|
-
|
|
593
|
-
) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
591
|
+
const exportDb =
|
|
592
|
+
ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
594
593
|
const exportRegistry = ctx.registry;
|
|
595
594
|
// JobContext carries ctx.meter (raw), not ctx.metrics — that bound
|
|
596
595
|
// handle only exists on HandlerContext (built per write/query call
|
|
@@ -684,9 +683,8 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
684
683
|
}
|
|
685
684
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
686
685
|
const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
|
|
687
|
-
const forgetDb =
|
|
688
|
-
|
|
689
|
-
) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
686
|
+
const forgetDb =
|
|
687
|
+
ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
690
688
|
const forgetRegistry = ctx.registry;
|
|
691
689
|
const tenantModel = await resolveAppTenantModel({
|
|
692
690
|
registry: forgetRegistry,
|
|
@@ -61,7 +61,7 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
61
61
|
return writeFailure(new UnprocessableError("grace_period_expired"));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
64
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), event.user.id, {
|
|
65
65
|
status: USER_STATUS.Active,
|
|
66
66
|
gracePeriodEnd: null,
|
|
67
67
|
// #354/1: closes the replay-after-cancel window — a still-TTL-valid email
|
package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts
CHANGED
|
@@ -83,9 +83,7 @@ export function createConfirmDeletionByTokenHandler(opts: ConfirmDeletionByToken
|
|
|
83
83
|
ctx,
|
|
84
84
|
userId,
|
|
85
85
|
() => resolveGracePeriod(ctx, event.user.tenantId),
|
|
86
|
-
ctx.db.unsafeRaw(
|
|
87
|
-
"appends the user lifecycle event on the SYSTEM_TENANT_ID user stream",
|
|
88
|
-
),
|
|
86
|
+
ctx.db.unsafeRaw(),
|
|
89
87
|
{ pendingDeletionRequestId: requestId },
|
|
90
88
|
);
|
|
91
89
|
if (!res.ok) return false;
|
|
@@ -87,7 +87,7 @@ export const downloadByJobQuery = defineQueryHandler({
|
|
|
87
87
|
// Wert (603/2).
|
|
88
88
|
const auditIp = requestContext.get()?.ip ?? null;
|
|
89
89
|
const auditUa = requestContext.get()?.userAgent ?? null;
|
|
90
|
-
const runner = ctx.db.unsafeRaw(
|
|
90
|
+
const runner = ctx.db.unsafeRaw();
|
|
91
91
|
|
|
92
92
|
// Step 1-2: job-lookup + cross-user-isolation
|
|
93
93
|
const jobRow = await fetchOne<JobRow>(runner, exportJobsTable, { id: jobId });
|
|
@@ -92,7 +92,7 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
92
92
|
handler: async (query, ctx) => {
|
|
93
93
|
const T = getTemporal();
|
|
94
94
|
const now = T.Now.instant();
|
|
95
|
-
const runner = ctx.db.unsafeRaw(
|
|
95
|
+
const runner = ctx.db.unsafeRaw();
|
|
96
96
|
|
|
97
97
|
// Step 1: hash + lookup
|
|
98
98
|
const hash = await hashDownloadToken(query.payload.token);
|
|
@@ -50,7 +50,7 @@ export const exportStatusQuery = defineQueryHandler({
|
|
|
50
50
|
},
|
|
51
51
|
handler: async (query, ctx) => {
|
|
52
52
|
const rows = await selectMany<ExportJobRow>(
|
|
53
|
-
ctx.db.unsafeRaw(
|
|
53
|
+
ctx.db.unsafeRaw(),
|
|
54
54
|
exportJobsTable,
|
|
55
55
|
{ userId: query.user.id },
|
|
56
56
|
{ limit: 1, orderBy: { col: "requestedAt", direction: "desc" } },
|
|
@@ -7,10 +7,6 @@ import { updateUserLifecycle } from "../lib/update-user-lifecycle.js";
|
|
|
7
7
|
|
|
8
8
|
const LIFT_RESTRICTION_ESCAPE_HATCH_REASON =
|
|
9
9
|
"checks the target user's membership in the admin's tenant and appends the user lifecycle status change on the SYSTEM_TENANT_ID user stream, both via DbRunner helpers outside the admin's own tenant scope.";
|
|
10
|
-
const CHECK_TARGET_MEMBERSHIP_REASON =
|
|
11
|
-
"checks the target user's membership in the admin's tenant via the DbRunner helper";
|
|
12
|
-
const APPEND_LIFECYCLE_EVENT_REASON =
|
|
13
|
-
"appends the user lifecycle event on the SYSTEM_TENANT_ID user stream";
|
|
14
10
|
|
|
15
11
|
// POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
|
|
16
12
|
//
|
|
@@ -46,7 +42,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
46
42
|
const targetUserId = event.payload.userId;
|
|
47
43
|
|
|
48
44
|
const outside = await denyIfTargetOutsideAdminTenant(
|
|
49
|
-
ctx.db.unsafeRaw(
|
|
45
|
+
ctx.db.unsafeRaw(),
|
|
50
46
|
event.user,
|
|
51
47
|
targetUserId,
|
|
52
48
|
);
|
|
@@ -73,7 +69,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
73
69
|
);
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
72
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, {
|
|
77
73
|
status: USER_STATUS.Active,
|
|
78
74
|
});
|
|
79
75
|
|
|
@@ -64,7 +64,7 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
64
64
|
type: string;
|
|
65
65
|
payload: Record<string, unknown>;
|
|
66
66
|
created_at: unknown;
|
|
67
|
-
}>(ctx.db.unsafeRaw(
|
|
67
|
+
}>(ctx.db.unsafeRaw(), eventsTable, where, {
|
|
68
68
|
orderBy: { col: "id", direction: "desc" },
|
|
69
69
|
limit: p.limit,
|
|
70
70
|
});
|
package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts
CHANGED
|
@@ -92,7 +92,7 @@ export function createRequestDeletionByEmailHandler(opts: RequestDeletionByEmail
|
|
|
92
92
|
// user-Row landet und in die Token-HMAC-Purpose gefaltet wird. cancel
|
|
93
93
|
// nullt sie → ein nach Cancel nachgespieltes Token verifiziert nicht mehr.
|
|
94
94
|
const requestId = crypto.randomUUID();
|
|
95
|
-
await updateUserLifecycle(ctx.db.unsafeRaw(
|
|
95
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), userRow["id"], {
|
|
96
96
|
pendingDeletionRequestId: requestId,
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -65,7 +65,7 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
65
65
|
ctx,
|
|
66
66
|
event.user.id,
|
|
67
67
|
() => resolveGracePeriod(ctx, event.user.tenantId),
|
|
68
|
-
ctx.db.unsafeRaw(
|
|
68
|
+
ctx.db.unsafeRaw(),
|
|
69
69
|
);
|
|
70
70
|
if (!res.ok) return writeFailure(res.error);
|
|
71
71
|
const { gracePeriodEnd, userEmail, userLocale } = res;
|
|
@@ -77,7 +77,7 @@ export const requestExportWrite = defineWriteHandler({
|
|
|
77
77
|
const userId = event.user.id;
|
|
78
78
|
const T = getTemporal();
|
|
79
79
|
const now = T.Now.instant();
|
|
80
|
-
const exportJobRunner = ctx.db.unsafeRaw(
|
|
80
|
+
const exportJobRunner = ctx.db.unsafeRaw();
|
|
81
81
|
|
|
82
82
|
// Pre-Check: exportJobRunner bypasses the TenantDb-Filter — the
|
|
83
83
|
// TenantDb-Wrapper would hide cross-tenant jobs.
|
|
@@ -70,9 +70,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
const outside = await denyIfTargetOutsideAdminTenant(
|
|
73
|
-
ctx.db.unsafeRaw(
|
|
74
|
-
"checks the target user's membership in the admin's tenant via the DbRunner helper",
|
|
75
|
-
),
|
|
73
|
+
ctx.db.unsafeRaw(),
|
|
76
74
|
event.user,
|
|
77
75
|
targetUserId,
|
|
78
76
|
);
|
|
@@ -107,11 +105,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
107
105
|
);
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
await updateUserLifecycle(
|
|
111
|
-
ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"),
|
|
112
|
-
targetUserId,
|
|
113
|
-
{ status: USER_STATUS.Restricted },
|
|
114
|
-
);
|
|
108
|
+
await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, { status: USER_STATUS.Restricted });
|
|
115
109
|
|
|
116
110
|
// Cross-Feature: alle live sessions revoken — sonst koennte der User
|
|
117
111
|
// mit existierendem JWT bis zur Token-Expiry weiter schreiben.
|
|
@@ -48,7 +48,7 @@ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const T = getTemporal();
|
|
51
|
-
const runner = ctx.db.unsafeRaw(
|
|
51
|
+
const runner = ctx.db.unsafeRaw();
|
|
52
52
|
// Operator-triggered forget must also erase binaries, not just rows —
|
|
53
53
|
// it flips users to Deleted, after which the cron never re-processes
|
|
54
54
|
// them, so a row-only delete here would permanently leak the binaries.
|
|
@@ -29,7 +29,7 @@ export type UpdateUserLifecycleOptions = {
|
|
|
29
29
|
readonly expect?: Readonly<Record<string, string | number | boolean | null>>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// `conn` is ctx.db.unsafeRaw(
|
|
32
|
+
// `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
|
|
33
33
|
// (forget-cleanup sub-tx) — keeps the event append atomic with the write.
|
|
34
34
|
//
|
|
35
35
|
// Returns `{ applied: false }` instead of throwing only when `options.expect`
|
|
@@ -155,7 +155,7 @@ export interface RunForgetCleanupArgs {
|
|
|
155
155
|
|
|
156
156
|
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
157
157
|
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
158
|
-
// escapeHatch when its delete hook calls ctx.db.unsafeRaw(
|
|
158
|
+
// escapeHatch when its delete hook calls ctx.db.unsafeRaw().
|
|
159
159
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
160
160
|
readonly actor?: string;
|
|
161
161
|
}
|
|
@@ -60,7 +60,7 @@ export interface RunUserExportArgs {
|
|
|
60
60
|
readonly now: Instant;
|
|
61
61
|
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
62
62
|
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
63
|
-
// escapeHatch when its export hook calls ctx.db.unsafeRaw(
|
|
63
|
+
// escapeHatch when its export hook calls ctx.db.unsafeRaw().
|
|
64
64
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
65
65
|
readonly actor?: string;
|
|
66
66
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.339.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "workflow-run stream stores a reference to the trigger event instead of its payload",
|
|
6
|
+
"detail": "`workflow.run-started` now holds `triggerEventRef: { eventId, aggregateId, version }` and no `triggerPayload`; the wait, waitForEvent and retry step events no longer embed it either. `workflow_run_pending` gets a `trigger_event_ref` column and the event-subscriber stops writing `trigger_payload`. `resume-run` loads the trigger and the awaited event through `ctx.loadAggregate` and fails the run with `trigger_event_unavailable` or `awaited_event_unavailable` when one is gone.",
|
|
7
|
+
"migration": "Own code that reads `triggerPayload` from `workflow.run-started` must read `triggerEventRef` and load the event with `ctx.loadAggregate(ref.aggregateId)`. Apps generate the migration for the new `workflow_run_pending.trigger_event_ref` column with `kumiko schema generate`. The step events `workflow.step.waiting`, `workflow.step.waiting-for-event` and `workflow.retry.scheduled` no longer carry `triggerPayload`; a custom resume loop that read it must load the trigger event through the `triggerEventRef` on its own run-started event (see `samples/recipes/workflow-engine/src/postgres-resume-loop.ts`). Stored run-started events and pending rows keep their old copy; this release rewrites no events, and runs started before it resume from that copy."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.337.0",
|
|
4
11
|
"type": "fix",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// This subscriber never resumes a run itself (D3, workflow-resume-loop.md):
|
|
21
21
|
// the MultiStreamApplyContext it runs in has no callFeature/runStepList
|
|
22
22
|
// access, only unsafeAppendEvent/loadAggregate. It writes triggerEventType
|
|
23
|
-
// +
|
|
23
|
+
// + triggerEventRef + wakeAt=now() onto the matching pending row(s); the
|
|
24
24
|
// existing resume-due-runs job (Phase 2) picks the row up on its next
|
|
25
25
|
// cron tick — the one resume path stays exactly the one Phase 2 built.
|
|
26
26
|
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
import { evaluateEventMatch } from "@cosmicdrift/kumiko-framework/engine";
|
|
35
35
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
36
36
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
37
|
+
import type { TriggerEventRef } from "./runner.js";
|
|
37
38
|
import { workflowRunPendingTable } from "./tables.js";
|
|
38
39
|
|
|
39
40
|
type CandidateRow = {
|
|
@@ -87,7 +88,11 @@ const wakeupApply: MultiStreamApplyFn = async (event, tx) => {
|
|
|
87
88
|
workflowRunPendingTable,
|
|
88
89
|
{
|
|
89
90
|
triggerEventType: event.type,
|
|
90
|
-
|
|
91
|
+
triggerEventRef: {
|
|
92
|
+
eventId: event.id,
|
|
93
|
+
aggregateId: event.aggregateId,
|
|
94
|
+
version: event.version,
|
|
95
|
+
} satisfies TriggerEventRef,
|
|
91
96
|
wakeAt: getTemporal().Now.instant().toString(),
|
|
92
97
|
},
|
|
93
98
|
{ tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex },
|
|
@@ -85,6 +85,11 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
|
|
|
85
85
|
runId,
|
|
86
86
|
workflow,
|
|
87
87
|
triggerEvent,
|
|
88
|
+
triggerEventRef: {
|
|
89
|
+
eventId: event.id,
|
|
90
|
+
aggregateId: event.aggregateId,
|
|
91
|
+
version: event.version,
|
|
92
|
+
},
|
|
88
93
|
...(idempotencyKey && { idempotencyKey }),
|
|
89
94
|
// @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
|
|
90
95
|
// exposes unsafeAppendEvent/loadAggregate, a subset of
|