@cosmicdrift/kumiko-samples 0.343.0 → 0.345.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 +1 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +31 -12
- package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
- package/packages/bundled-features/src/auth-email-password/constants.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +9 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +33 -10
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +32 -15
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +60 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -1
- package/packages/bundled-features/src/auth-email-password/i18n.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +20 -7
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -1
- package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +35 -7
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +36 -22
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +12 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-locale.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +34 -1
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +14 -5
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +61 -36
- package/packages/bundled-features/src/billing-foundation/index.ts +11 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +25 -7
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +36 -3
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -1
- package/packages/bundled-features/src/billing-foundation/web/cancel-contract-dialog.tsx +1 -2
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +16 -6
- package/packages/bundled-features/src/billing-foundation/web/index.ts +12 -0
- package/packages/bundled-features/src/legal-pages/changes.json +6 -0
- package/packages/bundled-features/src/legal-pages/constants.ts +2 -0
- package/packages/bundled-features/src/legal-pages/feature.ts +8 -3
- package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +10 -0
- package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +2 -1
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +10 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +10 -11
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +2 -1
- package/samples/recipes/i18n/README.md +1 -1
- package/samples/recipes/rate-limiting/README.md +1 -1
- package/samples/recipes/webhook-step/README.md +8 -1
|
@@ -13,11 +13,23 @@ export {
|
|
|
13
13
|
type SubscriptionStatus,
|
|
14
14
|
SubscriptionStatuses,
|
|
15
15
|
} from "../constants.js";
|
|
16
|
+
export {
|
|
17
|
+
CONSENT_LOCALES,
|
|
18
|
+
type ConsentLocale,
|
|
19
|
+
resolveConsentLocale,
|
|
20
|
+
} from "../consumer-protection/consent-locale.js";
|
|
16
21
|
export type {
|
|
17
22
|
BillingPlanBenefit,
|
|
18
23
|
BillingPlanCatalog,
|
|
19
24
|
BillingPlansResult,
|
|
20
25
|
BillingPlanView,
|
|
26
|
+
LegalLinkSet,
|
|
21
27
|
} from "../types.js";
|
|
22
28
|
export { BillingPlansPanel } from "./billing-plans-panel.js";
|
|
29
|
+
export {
|
|
30
|
+
CheckoutConsentDialog,
|
|
31
|
+
type CheckoutConsentDialogProps,
|
|
32
|
+
type CheckoutConsentPayload,
|
|
33
|
+
CONSENT_TEXT_OUTDATED_CODE,
|
|
34
|
+
} from "./checkout-consent-dialog.js";
|
|
23
35
|
export { type BillingFoundationClientOptions, billingFoundationClient } from "./client-plugin.js";
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.344.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Optional title heading on legal pages",
|
|
6
|
+
"detail": "`LegalPageRoute` has an optional `titleHeading` flag. When `true`, the page body starts with an `<h1>` holding the escaped page title. The default (`false`) leaves the body unchanged."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.335.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -10,6 +10,8 @@ export type LegalPageRoute = {
|
|
|
10
10
|
readonly slug: string;
|
|
11
11
|
readonly lang: string;
|
|
12
12
|
readonly titleFallback: string;
|
|
13
|
+
/** Prepend the page title as `<h1>` to the rendered body. Default false. */
|
|
14
|
+
readonly titleHeading?: boolean;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
// Shape of one boot-check-required block — the type behind
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type FeatureDefinition,
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
7
8
|
import {
|
|
8
9
|
cachedSecurePageResponse,
|
|
9
10
|
type PublicPageWrapLayout,
|
|
@@ -87,7 +88,7 @@ export type LegalPagesOptions = {
|
|
|
87
88
|
|
|
88
89
|
export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDefinition {
|
|
89
90
|
const wrapLayout = opts.wrapLayout ?? wrapInLayout;
|
|
90
|
-
const routes = opts.routes ?? LEGAL_ROUTES;
|
|
91
|
+
const routes: readonly LegalPageRoute[] = opts.routes ?? LEGAL_ROUTES;
|
|
91
92
|
const requiredBlocks = opts.requiredBlocks ?? LEGAL_REQUIRED_BLOCKS;
|
|
92
93
|
validateRoutes(routes);
|
|
93
94
|
if (opts.routes !== undefined && opts.routes.length > 0 && opts.requiredBlocks === undefined) {
|
|
@@ -172,9 +173,13 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
|
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
const title = data.title || route.titleFallback;
|
|
177
|
+
const renderedBody = renderMarkdownToHtml(data.content);
|
|
175
178
|
const html = wrapLayout({
|
|
176
|
-
title
|
|
177
|
-
bodyHtml:
|
|
179
|
+
title,
|
|
180
|
+
bodyHtml: route.titleHeading
|
|
181
|
+
? `<h1>${escapeHtml(title)}</h1>${renderedBody}`
|
|
182
|
+
: renderedBody,
|
|
178
183
|
lang: route.lang,
|
|
179
184
|
slug: route.slug,
|
|
180
185
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.344.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Heading and Markdown sections in the simple mail renderer",
|
|
6
|
+
"detail": "`createSimpleRenderer` accepts two more section kinds: `{ heading: string }` (rendered as an escaped `<h2>`) and `{ markdown: string }` (rendered with the hardened Markdown renderer; raw HTML is escaped and unsafe link targets are replaced by `#`). Existing section kinds render as before."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.341.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import type { NotificationRenderer } from "../delivery/index.js";
|
|
3
|
+
import { renderSafeMarkdown } from "../page-render/index.js";
|
|
3
4
|
|
|
4
5
|
export type MailBranding = {
|
|
5
6
|
/** Shown as text when there is no logo, and as the logo alt text. */
|
|
@@ -141,6 +142,8 @@ function renderBrandingFooter(
|
|
|
141
142
|
|
|
142
143
|
type Section =
|
|
143
144
|
| { readonly text: string }
|
|
145
|
+
| { readonly heading: string }
|
|
146
|
+
| { readonly markdown: string }
|
|
144
147
|
| { readonly button: { readonly label: string; readonly url: string } };
|
|
145
148
|
|
|
146
149
|
type EmailTemplateData = {
|
|
@@ -157,6 +160,13 @@ function renderSection(section: Section, primaryColor: string): string {
|
|
|
157
160
|
if ("text" in section) {
|
|
158
161
|
return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
|
|
159
162
|
}
|
|
163
|
+
if ("heading" in section) {
|
|
164
|
+
return `<h2 style="margin:24px 0 12px;color:#111;font-size:16px;font-weight:600">${escapeHtml(section.heading)}</h2>`;
|
|
165
|
+
}
|
|
166
|
+
if ("markdown" in section) {
|
|
167
|
+
const markdownHtml = renderSafeMarkdown(section.markdown);
|
|
168
|
+
return `<div style="color:#333;font-size:14px;line-height:1.5">${markdownHtml}</div>`;
|
|
169
|
+
}
|
|
160
170
|
if ("button" in section) {
|
|
161
171
|
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>`;
|
|
162
172
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.345.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Webhook sends carry an Idempotency-Key header that is stable across redeliveries",
|
|
6
|
+
"detail": "Webhook sends carry an Idempotency-Key header\nEvery `r.step.webhook.send` request now has `Idempotency-Key: <dispatch stream id>`. The value stays the same when the same dispatch request is delivered again, so receivers can deduplicate. An explicit `Idempotency-Key` in `headers` (any casing) takes precedence."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.335.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -193,7 +193,7 @@ function buildDispatchSpec(
|
|
|
193
193
|
export function createStepDispatcherFeature(): FeatureDefinition {
|
|
194
194
|
return defineFeature("step-dispatcher", (r) => {
|
|
195
195
|
r.describe(
|
|
196
|
-
"Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`.",
|
|
196
|
+
"Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`. Every `webhook.send` request carries `Idempotency-Key: <dispatch stream id>`, stable across redeliveries of the same dispatch request; an explicit `Idempotency-Key` in `headers` takes precedence.",
|
|
197
197
|
);
|
|
198
198
|
r.secretNamespace("webhook-auth", WEBHOOK_AUTH_SECRET_NAMESPACE_OPTIONS);
|
|
199
199
|
r.uiHints({
|
|
@@ -270,6 +270,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
|
|
|
270
270
|
tenantId: event.tenantId,
|
|
271
271
|
userId: event.metadata.userId || SYSTEM_USER_ID,
|
|
272
272
|
secrets: ctx.secrets,
|
|
273
|
+
idempotencyKey: event.aggregateId,
|
|
273
274
|
})
|
|
274
275
|
: await performMailDispatch(dispatchSpec.spec);
|
|
275
276
|
if (result.ok) {
|
|
@@ -114,6 +114,7 @@ export type WebhookDispatchDeps = {
|
|
|
114
114
|
readonly tenantId: TenantId;
|
|
115
115
|
readonly userId: string;
|
|
116
116
|
readonly secrets: SecretsContext | undefined;
|
|
117
|
+
readonly idempotencyKey: string;
|
|
117
118
|
};
|
|
118
119
|
|
|
119
120
|
// Never includes the secret name or value — spec.auth.secret is a
|
|
@@ -121,11 +122,20 @@ export type WebhookDispatchDeps = {
|
|
|
121
122
|
// dispatch-failed event, so it stays generic.
|
|
122
123
|
const WEBHOOK_AUTH_SECRET_UNAVAILABLE_ERROR = "webhook auth secret is not available";
|
|
123
124
|
|
|
125
|
+
export const WEBHOOK_IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
126
|
+
|
|
127
|
+
function hasIdempotencyKeyHeader(headers: Readonly<Record<string, string>>): boolean {
|
|
128
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === WEBHOOK_IDEMPOTENCY_KEY_HEADER);
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
async function buildWebhookHeaders(
|
|
125
132
|
spec: WebhookSpec,
|
|
126
133
|
deps: WebhookDispatchDeps,
|
|
127
134
|
): Promise<{ ok: true; headers: Record<string, string> } | { ok: false; error: string }> {
|
|
128
135
|
const headers: Record<string, string> = { "content-type": "application/json", ...spec.headers };
|
|
136
|
+
if (!hasIdempotencyKeyHeader(headers)) {
|
|
137
|
+
headers[WEBHOOK_IDEMPOTENCY_KEY_HEADER] = deps.idempotencyKey;
|
|
138
|
+
}
|
|
129
139
|
if (!spec.auth) return { ok: true, headers };
|
|
130
140
|
if (!deps.secrets) return { ok: false, error: WEBHOOK_AUTH_SECRET_UNAVAILABLE_ERROR };
|
|
131
141
|
// A throwing get (corrupt envelope, rotated key, KMS error) must become a
|
|
@@ -69,8 +69,8 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
69
69
|
baseUrl,
|
|
70
70
|
catalog: {
|
|
71
71
|
plans: ["starter", "pro"],
|
|
72
|
-
tierLabelKey: (tier) => `gallery
|
|
73
|
-
benefits: (tier) => [{ labelKey: `gallery
|
|
72
|
+
tierLabelKey: (tier) => `gallery-screens:plan.${tier}.label`,
|
|
73
|
+
benefits: (tier) => [{ labelKey: `gallery-screens:plan.${tier}.benefit` }],
|
|
74
74
|
resolveCurrentTier,
|
|
75
75
|
viewRoles: ["TenantAdmin", "SystemAdmin"],
|
|
76
76
|
successPath: GALLERY_PLAN_PATH,
|
|
@@ -83,9 +83,8 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
83
83
|
operatorEmail: GALLERY_OPERATOR_EMAIL,
|
|
84
84
|
terminationScope: "platform",
|
|
85
85
|
legalLinks: {
|
|
86
|
-
terms: "/legal/terms",
|
|
87
|
-
withdrawal: "/legal/withdrawal",
|
|
88
|
-
privacy: "/legal/privacy",
|
|
86
|
+
de: { terms: "/legal/terms", withdrawal: "/legal/withdrawal", privacy: "/legal/privacy" },
|
|
87
|
+
en: { terms: "/legal/terms", withdrawal: "/legal/withdrawal", privacy: "/legal/privacy" },
|
|
89
88
|
},
|
|
90
89
|
},
|
|
91
90
|
});
|
|
@@ -140,12 +139,12 @@ export const galleryScreensFeature: FeatureDefinition = defineFeature("gallery-s
|
|
|
140
139
|
r.translations({
|
|
141
140
|
keys: {
|
|
142
141
|
"gallery:nav.billing": { de: "Abrechnung", en: "Billing" },
|
|
143
|
-
"gallery
|
|
144
|
-
"gallery
|
|
145
|
-
"gallery
|
|
146
|
-
"gallery
|
|
147
|
-
"gallery
|
|
148
|
-
"gallery
|
|
142
|
+
"gallery-screens:plan.free.label": { de: "Free", en: "Free" },
|
|
143
|
+
"gallery-screens:plan.starter.label": { de: "Starter", en: "Starter" },
|
|
144
|
+
"gallery-screens:plan.pro.label": { de: "Pro", en: "Pro" },
|
|
145
|
+
"gallery-screens:plan.starter.benefit": { de: "Für kleine Teams", en: "For small teams" },
|
|
146
|
+
"gallery-screens:plan.pro.benefit": { de: "Für wachsende Teams", en: "For growing teams" },
|
|
147
|
+
"gallery-screens:plan.free.benefit": { de: "Zum Ausprobieren", en: "To try things out" },
|
|
149
148
|
},
|
|
150
149
|
});
|
|
151
150
|
return {};
|
|
@@ -42,8 +42,8 @@ const features = [
|
|
|
42
42
|
];
|
|
43
43
|
|
|
44
44
|
const TERMS_TEXT: Readonly<Record<string, string>> = {
|
|
45
|
-
de: "
|
|
46
|
-
en: "
|
|
45
|
+
de: "Diese Bedingungen gelten für alle Verträge über die Plattform.",
|
|
46
|
+
en: "These terms apply to every contract concluded through the platform.",
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const seedTerms: SeedFn = async (stack) => {
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"auth-email-password:write:reset-password",
|
|
128
128
|
"auth-email-password:write:signup-confirm",
|
|
129
129
|
"auth-email-password:write:signup-request",
|
|
130
|
+
"auth-email-password:write:switch-tenant-mfa-gate",
|
|
130
131
|
"auth-email-password:write:system-invite-create",
|
|
131
132
|
"auth-email-password:write:verify-email"
|
|
132
133
|
],
|
|
@@ -2301,7 +2302,7 @@
|
|
|
2301
2302
|
},
|
|
2302
2303
|
{
|
|
2303
2304
|
"name": "step-dispatcher",
|
|
2304
|
-
"description": "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`.",
|
|
2305
|
+
"description": "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`. Every `webhook.send` request carries `Idempotency-Key: <dispatch stream id>`, stable across redeliveries of the same dispatch request; an explicit `Idempotency-Key` in `headers` takes precedence.",
|
|
2305
2306
|
"toggleableDefault": null,
|
|
2306
2307
|
"requires": [
|
|
2307
2308
|
"secrets"
|
|
@@ -13,7 +13,7 @@ A translation value can be a CLDR plural-forms object instead of a plain
|
|
|
13
13
|
string. `other` is required; the rest (`zero`/`one`/`two`/`few`/`many`) only
|
|
14
14
|
exist where the locale's grammar needs them:
|
|
15
15
|
|
|
16
|
-
```ts
|
|
16
|
+
```ts illustration
|
|
17
17
|
"greeting.unread_count": {
|
|
18
18
|
de: { one: "{count} ungelesene Nachricht", other: "{count} ungelesene Nachrichten" },
|
|
19
19
|
en: { one: "{count} unread message", other: "{count} unread messages" },
|
|
@@ -12,7 +12,7 @@ End-to-end rate limiting: L1 global-IP + L2 auth + L3 handler opt-in.
|
|
|
12
12
|
A public write handler that mails an address typed by the caller is also limited per
|
|
13
13
|
address, so rotating IPs does not help against mail flooding. Declare it next to `rateLimit`:
|
|
14
14
|
|
|
15
|
-
```ts
|
|
15
|
+
```ts illustration
|
|
16
16
|
r.writeHandler({
|
|
17
17
|
// ...
|
|
18
18
|
access: { roles: ["anonymous"] },
|
|
@@ -16,7 +16,7 @@ never a process-wide env var. The `secrets` feature must be mounted
|
|
|
16
16
|
(`createSecretsFeature()` + a `MasterKeyProvider`) alongside
|
|
17
17
|
`step-dispatcher`. Set the secret per tenant before dispatching:
|
|
18
18
|
|
|
19
|
-
```ts
|
|
19
|
+
```ts illustration
|
|
20
20
|
await stack.http.writeOk(
|
|
21
21
|
"secrets:write:set",
|
|
22
22
|
{ key: "step-dispatcher:webhook-auth.incident-hook", value: "<token>" },
|
|
@@ -33,6 +33,13 @@ Never combine `auth.secret` with a caller-controlled `url`: the caller would
|
|
|
33
33
|
receive the secret as the `Authorization` header. `incident:open-authenticated`
|
|
34
34
|
therefore posts to a fixed URL.
|
|
35
35
|
|
|
36
|
+
## Idempotency-Key
|
|
37
|
+
|
|
38
|
+
Every webhook request carries `Idempotency-Key: <dispatch stream id>`. The
|
|
39
|
+
value is the same when the same dispatch request is delivered again, so a
|
|
40
|
+
receiver can deduplicate redelivered calls. An `Idempotency-Key` set explicitly in
|
|
41
|
+
`headers` (any casing) wins.
|
|
42
|
+
|
|
36
43
|
## Source
|
|
37
44
|
|
|
38
45
|
Feature entry point: `src/feature.ts`.
|