@cosmicdrift/kumiko-bundled-features 0.339.0 → 0.341.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/dist/auth-email-password/bootstrap.d.ts +74 -0
- package/dist/auth-email-password/bootstrap.js +165 -0
- package/dist/auth-email-password/constants.d.ts +2 -0
- package/dist/auth-email-password/constants.js +5 -0
- package/dist/auth-email-password/errors.d.ts +1 -0
- package/dist/auth-email-password/errors.js +6 -0
- package/dist/auth-email-password/feature.js +2 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +9 -1
- package/dist/auth-email-password/handlers/invite-accept.write.js +8 -0
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +11 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +134 -87
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +5 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +1 -0
- package/dist/auth-email-password/invite-token-store.d.ts +3 -0
- package/dist/auth-email-password/invite-token-store.js +5 -0
- package/dist/auth-email-password/magic-link-mail.js +1 -0
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +8 -3
- package/dist/auth-mfa/feature.d.ts +2 -0
- package/dist/auth-mfa/feature.js +2 -1
- package/dist/auth-mfa/mfa-status-checker.d.ts +2 -0
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -1
- package/dist/billing-foundation/consumer-protection/termination-pages.js +59 -32
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -1
- package/dist/billing-foundation/projection.js +23 -40
- package/dist/channel-discord/discord-channel.d.ts +4 -0
- package/dist/channel-discord/discord-channel.js +4 -0
- package/dist/channel-discord/feature.js +7 -2
- package/dist/channel-discord/index.d.ts +1 -1
- package/dist/channel-discord/index.js +1 -1
- package/dist/channel-email/email-channel.js +1 -0
- package/dist/channel-slack/feature.js +4 -2
- package/dist/channel-slack/index.d.ts +1 -1
- package/dist/channel-slack/index.js +1 -1
- package/dist/channel-slack/slack-channel.d.ts +4 -0
- package/dist/channel-slack/slack-channel.js +4 -0
- package/dist/channel-teams/feature.js +4 -2
- package/dist/channel-teams/index.d.ts +1 -1
- package/dist/channel-teams/index.js +1 -1
- package/dist/channel-teams/teams-channel.d.ts +4 -0
- package/dist/channel-teams/teams-channel.js +4 -0
- package/dist/channel-telegram/feature.js +4 -2
- package/dist/channel-telegram/index.d.ts +1 -1
- package/dist/channel-telegram/index.js +1 -1
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +28 -12
- package/dist/delivery/chat-webhook-channel.d.ts +5 -1
- package/dist/delivery/chat-webhook-channel.js +10 -5
- package/dist/delivery/chat-webhook-sender.d.ts +6 -2
- package/dist/delivery/chat-webhook-sender.js +13 -8
- package/dist/delivery/delivery-service.d.ts +4 -3
- package/dist/delivery/delivery-service.js +53 -33
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +2 -2
- package/dist/delivery/jobs.js +3 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/inbound-mail-foundation/projection.js +37 -45
- package/dist/legal-pages/feature.d.ts +3 -15
- package/dist/legal-pages/feature.js +20 -50
- package/dist/legal-pages/lib/boot-check.d.ts +10 -0
- package/dist/legal-pages/lib/boot-check.js +33 -0
- package/dist/legal-pages/lib/validate-routes.d.ts +2 -0
- package/dist/legal-pages/lib/validate-routes.js +17 -0
- package/dist/page-render/index.d.ts +2 -1
- package/dist/page-render/index.js +1 -0
- package/dist/page-render/layout.d.ts +7 -0
- package/dist/page-render/layout.js +5 -0
- package/dist/page-render/trailing-slash.d.ts +2 -0
- package/dist/page-render/trailing-slash.js +15 -0
- package/dist/renderer-simple/feature.js +1 -0
- package/dist/renderer-simple/index.d.ts +1 -1
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +12 -5
- package/dist/renderer-simple/simple-renderer.js +66 -12
- package/dist/secrets/announce-secret-change.d.ts +8 -0
- package/dist/secrets/announce-secret-change.js +18 -0
- package/dist/secrets/feature.js +1 -1
- package/dist/secrets/handlers/delete.write.js +2 -0
- package/dist/secrets/handlers/set.write.js +6 -1
- package/dist/secrets/i18n.d.ts +4 -0
- package/dist/secrets/i18n.js +4 -0
- package/dist/secrets/secrets-context.d.ts +2 -0
- package/dist/secrets/secrets-context.js +3 -0
- package/dist/secrets/write-gate.d.ts +5 -1
- package/dist/secrets/write-gate.js +24 -1
- package/dist/tenant/constants.d.ts +1 -0
- package/dist/tenant/constants.js +1 -0
- package/dist/tenant/handlers/add-member.write.js +14 -3
- package/dist/tenant/handlers/invitations.query.d.ts +8 -0
- package/dist/tenant/handlers/invitations.query.js +15 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +2 -0
- package/dist/tenant/invitation-table.js +10 -1
- package/dist/tenant-lifecycle/lifecycle-gate.d.ts +1 -0
- package/dist/tenant-lifecycle/lifecycle-gate.js +2 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/feature.js +83 -14
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +3 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +17 -10
- package/dist/user/handlers/update-roles.js +7 -18
- package/dist/user/invited-global-roles.d.ts +14 -0
- package/dist/user/invited-global-roles.js +35 -0
- package/dist/user-data-rights/feature.js +3 -0
- package/dist/waitlist/constants.d.ts +33 -0
- package/dist/waitlist/constants.js +35 -0
- package/dist/waitlist/entity.d.ts +433 -0
- package/dist/waitlist/entity.js +64 -0
- package/dist/waitlist/feature.d.ts +3 -0
- package/dist/waitlist/feature.js +38 -0
- package/dist/waitlist/handlers/invite.write.d.ts +9 -0
- package/dist/waitlist/handlers/invite.write.js +119 -0
- package/dist/waitlist/handlers/list.query.d.ts +1 -0
- package/dist/waitlist/handlers/list.query.js +10 -0
- package/dist/waitlist/handlers/reject.write.d.ts +7 -0
- package/dist/waitlist/handlers/reject.write.js +80 -0
- package/dist/waitlist/handlers/submit.write.d.ts +13 -0
- package/dist/waitlist/handlers/submit.write.js +112 -0
- package/dist/waitlist/i18n.d.ts +6 -0
- package/dist/waitlist/i18n.js +31 -0
- package/dist/waitlist/index.d.ts +5 -0
- package/dist/waitlist/index.js +4 -0
- package/dist/waitlist/lib.d.ts +10 -0
- package/dist/waitlist/lib.js +24 -0
- package/dist/waitlist/mail.d.ts +15 -0
- package/dist/waitlist/mail.js +73 -0
- package/dist/waitlist/options.d.ts +29 -0
- package/dist/waitlist/options.js +8 -0
- package/dist/waitlist/payloads.d.ts +14 -0
- package/dist/waitlist/payloads.js +47 -0
- package/dist/waitlist/screens.d.ts +2 -0
- package/dist/waitlist/screens.js +42 -0
- package/dist/waitlist/user-data-hooks.d.ts +3 -0
- package/dist/waitlist/user-data-hooks.js +71 -0
- package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
- package/dist/workflow-runner/db/queries/due-runs.js +5 -4
- package/dist/workflow-runner/feature.js +4 -6
- package/package.json +17 -9
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +13 -0
- package/src/channel-telegram/changes.json +9 -0
- package/src/delivery/changes.json +7 -0
- package/src/files/changes.json +13 -1
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/secrets/changes.json +12 -0
- package/src/tenant/changes.json +6 -0
- package/src/user/changes.json +13 -6
- package/src/waitlist/changes.json +17 -0
- package/src/workflow-runner/changes.json +7 -0
- package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
- package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
|
@@ -25,7 +25,7 @@ export function collectChannels(registry) {
|
|
|
25
25
|
// nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
|
|
26
26
|
// `renderer-foundation`-Bundle via `collectRendererPlugins`.
|
|
27
27
|
export function createDeliveryService(options) {
|
|
28
|
-
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, escapeHatchAuditSink, log, } = options;
|
|
28
|
+
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, escapeHatchAuditSink, log, } = options;
|
|
29
29
|
const idemRedis = idempotencyRedis ?? rateLimit?.redis;
|
|
30
30
|
// Rate limit check: atomic INCR + TTL + over-limit rollback via server-side
|
|
31
31
|
// Lua. Runs single-threaded in Redis, so two parallel clients can't both
|
|
@@ -119,11 +119,21 @@ export function createDeliveryService(options) {
|
|
|
119
119
|
...bridgeStub(),
|
|
120
120
|
}));
|
|
121
121
|
}
|
|
122
|
+
function recordDelivery(run, entry, deliveryAttemptId) {
|
|
123
|
+
run.deliveries.push({
|
|
124
|
+
channel: entry.channel,
|
|
125
|
+
recipientId: entry.recipientId,
|
|
126
|
+
status: entry.status,
|
|
127
|
+
error: entry.error,
|
|
128
|
+
deliveryAttemptId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
122
131
|
// Single-shot terminal log (inline channels, skips, idempotency dups). Async
|
|
123
132
|
// attempts instead append a queued event up front and a terminal event from
|
|
124
133
|
// the send job — see deliverViaChannel + jobs.ts.
|
|
125
|
-
async function logDelivery(entry) {
|
|
126
|
-
await logAttempt(db, registry, entry);
|
|
134
|
+
async function logDelivery(run, entry) {
|
|
135
|
+
const attemptId = await logAttempt(db, registry, entry);
|
|
136
|
+
recordDelivery(run, entry, attemptId);
|
|
127
137
|
}
|
|
128
138
|
// Deliver one resolved (channel, address) pair. Inline channels (inApp) and
|
|
129
139
|
// the no-job-runner fallback render + send synchronously and log the terminal
|
|
@@ -131,13 +141,13 @@ export function createDeliveryService(options) {
|
|
|
131
141
|
// hand off to the delivery.render (or delivery.send) job, which appends the
|
|
132
142
|
// terminal event on the same attempt stream.
|
|
133
143
|
async function deliverViaChannel(args) {
|
|
134
|
-
const { channel, address, message, channelCtx, tenantId, recipientId, notificationType, priority, } = args;
|
|
135
|
-
if (channel.mode === "queued" &&
|
|
144
|
+
const { run, channel, address, message, channelCtx, tenantId, recipientId, notificationType, priority, } = args;
|
|
145
|
+
if (channel.mode === "queued" && run.jobDispatcher) {
|
|
136
146
|
// Async hand-off: record the attempt as queued, then dispatch the
|
|
137
147
|
// render/send job — the terminal sent/failed event is appended by the
|
|
138
148
|
// job, not here.
|
|
139
149
|
const deliveryAttemptId = generateId();
|
|
140
|
-
|
|
150
|
+
const queuedEntry = {
|
|
141
151
|
tenantId,
|
|
142
152
|
notificationType,
|
|
143
153
|
channel: channel.name,
|
|
@@ -146,8 +156,9 @@ export function createDeliveryService(options) {
|
|
|
146
156
|
status: "queued",
|
|
147
157
|
error: null,
|
|
148
158
|
priority,
|
|
149
|
-
}
|
|
150
|
-
await
|
|
159
|
+
};
|
|
160
|
+
await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
|
|
161
|
+
await run.jobDispatcher.dispatch(channel.render ? DeliveryJobs.render : DeliveryJobs.send, {
|
|
151
162
|
channelName: channel.name,
|
|
152
163
|
address,
|
|
153
164
|
tenantId,
|
|
@@ -157,12 +168,13 @@ export function createDeliveryService(options) {
|
|
|
157
168
|
priority,
|
|
158
169
|
message,
|
|
159
170
|
}, { priority: deliveryPriorityRank[priority] });
|
|
171
|
+
recordDelivery(run, queuedEntry, deliveryAttemptId);
|
|
160
172
|
}
|
|
161
173
|
else {
|
|
162
174
|
// Inline (inApp) or no-job-runner fallback: render + send synchronously.
|
|
163
175
|
const rendered = channel.render ? await channel.render(message, channelCtx) : undefined;
|
|
164
176
|
const result = await channel.send(address, message, channelCtx, rendered);
|
|
165
|
-
await logDelivery({
|
|
177
|
+
await logDelivery(run, {
|
|
166
178
|
tenantId,
|
|
167
179
|
notificationType,
|
|
168
180
|
channel: channel.name,
|
|
@@ -174,7 +186,7 @@ export function createDeliveryService(options) {
|
|
|
174
186
|
});
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
|
-
function buildMessage(notificationType, data, channelName) {
|
|
189
|
+
function buildMessage(notificationType, data, channelName, locale) {
|
|
178
190
|
// Look up per-channel template from notification definition
|
|
179
191
|
const notifDef = registry.getAllNotifications().get(notificationType);
|
|
180
192
|
const templateFn = notifDef?.templates?.[channelName];
|
|
@@ -186,6 +198,7 @@ export function createDeliveryService(options) {
|
|
|
186
198
|
title: channelData["title"] ?? data["title"] ?? notificationType,
|
|
187
199
|
body: channelData["body"],
|
|
188
200
|
data: channelData,
|
|
201
|
+
locale,
|
|
189
202
|
};
|
|
190
203
|
}
|
|
191
204
|
// @cast-boundary engine-payload — generic notification.data shape
|
|
@@ -194,6 +207,7 @@ export function createDeliveryService(options) {
|
|
|
194
207
|
title: data?.["title"] ?? notificationType,
|
|
195
208
|
body: data?.["body"],
|
|
196
209
|
data,
|
|
210
|
+
locale,
|
|
197
211
|
};
|
|
198
212
|
}
|
|
199
213
|
// Shared by deliverToUser (resolved account address) and deliverDirect (a
|
|
@@ -226,18 +240,18 @@ export function createDeliveryService(options) {
|
|
|
226
240
|
// and conservative — DB ordering no longer decides the outcome).
|
|
227
241
|
return !prefs.some((p) => p.enabled === false);
|
|
228
242
|
}
|
|
229
|
-
async function deliverToUser(userId, notificationType, data, tenantId, priority) {
|
|
230
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId);
|
|
243
|
+
async function deliverToUser(userId, notificationType, data, tenantId, priority, run) {
|
|
244
|
+
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
231
245
|
for (const channel of channels) {
|
|
232
246
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
233
247
|
if (!channel.resolve)
|
|
234
248
|
continue;
|
|
235
|
-
const message = buildMessage(notificationType, data, channel.name);
|
|
249
|
+
const message = buildMessage(notificationType, data, channel.name, run.locale);
|
|
236
250
|
// Kill switch: tenant admin disabled this channel entirely
|
|
237
251
|
if (isChannelKilled) {
|
|
238
252
|
const killed = await isChannelKilled(tenantId, channel.name);
|
|
239
253
|
if (killed) {
|
|
240
|
-
await logDelivery({
|
|
254
|
+
await logDelivery(run, {
|
|
241
255
|
tenantId,
|
|
242
256
|
notificationType,
|
|
243
257
|
channel: channel.name,
|
|
@@ -254,7 +268,7 @@ export function createDeliveryService(options) {
|
|
|
254
268
|
if (priority !== "critical") {
|
|
255
269
|
const enabled = await isChannelEnabled(userId, tenantId, notificationType, channel.name);
|
|
256
270
|
if (!enabled) {
|
|
257
|
-
await logDelivery({
|
|
271
|
+
await logDelivery(run, {
|
|
258
272
|
tenantId,
|
|
259
273
|
notificationType,
|
|
260
274
|
channel: channel.name,
|
|
@@ -271,7 +285,7 @@ export function createDeliveryService(options) {
|
|
|
271
285
|
if (rateLimit) {
|
|
272
286
|
const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
|
|
273
287
|
if (!allowed) {
|
|
274
|
-
await logDelivery({
|
|
288
|
+
await logDelivery(run, {
|
|
275
289
|
tenantId,
|
|
276
290
|
notificationType,
|
|
277
291
|
channel: channel.name,
|
|
@@ -287,7 +301,7 @@ export function createDeliveryService(options) {
|
|
|
287
301
|
try {
|
|
288
302
|
const address = await channel.resolve(userId, channelCtx);
|
|
289
303
|
if (!address) {
|
|
290
|
-
await logDelivery({
|
|
304
|
+
await logDelivery(run, {
|
|
291
305
|
tenantId,
|
|
292
306
|
notificationType,
|
|
293
307
|
channel: channel.name,
|
|
@@ -301,7 +315,7 @@ export function createDeliveryService(options) {
|
|
|
301
315
|
}
|
|
302
316
|
if (priority !== "critical" &&
|
|
303
317
|
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
304
|
-
await logDelivery({
|
|
318
|
+
await logDelivery(run, {
|
|
305
319
|
tenantId,
|
|
306
320
|
notificationType,
|
|
307
321
|
channel: channel.name,
|
|
@@ -314,6 +328,7 @@ export function createDeliveryService(options) {
|
|
|
314
328
|
continue;
|
|
315
329
|
}
|
|
316
330
|
await deliverViaChannel({
|
|
331
|
+
run,
|
|
317
332
|
channel,
|
|
318
333
|
address,
|
|
319
334
|
message,
|
|
@@ -325,7 +340,7 @@ export function createDeliveryService(options) {
|
|
|
325
340
|
});
|
|
326
341
|
}
|
|
327
342
|
catch (err) {
|
|
328
|
-
await logDelivery({
|
|
343
|
+
await logDelivery(run, {
|
|
329
344
|
tenantId,
|
|
330
345
|
notificationType,
|
|
331
346
|
channel: channel.name,
|
|
@@ -338,20 +353,20 @@ export function createDeliveryService(options) {
|
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
355
|
}
|
|
341
|
-
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId) {
|
|
342
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId);
|
|
356
|
+
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId, run) {
|
|
357
|
+
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
343
358
|
// Direct routing skips preferences (no user account) but NOT rate limit
|
|
344
359
|
// — direct sends can still be abused (webhook replays, test harnesses).
|
|
345
360
|
for (const channel of channels) {
|
|
346
361
|
const address = route[channel.name];
|
|
347
|
-
const message = buildMessage(notificationType, data, channel.name);
|
|
362
|
+
const message = buildMessage(notificationType, data, channel.name, run.locale);
|
|
348
363
|
if (!address)
|
|
349
364
|
continue;
|
|
350
365
|
// Address opt-out (critical priority skips it, same rule as user
|
|
351
366
|
// preferences).
|
|
352
367
|
if (priority !== "critical" &&
|
|
353
368
|
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
354
|
-
await logDelivery({
|
|
369
|
+
await logDelivery(run, {
|
|
355
370
|
tenantId,
|
|
356
371
|
notificationType,
|
|
357
372
|
channel: channel.name,
|
|
@@ -367,7 +382,7 @@ export function createDeliveryService(options) {
|
|
|
367
382
|
if (rateLimit) {
|
|
368
383
|
const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
|
|
369
384
|
if (!allowed) {
|
|
370
|
-
await logDelivery({
|
|
385
|
+
await logDelivery(run, {
|
|
371
386
|
tenantId,
|
|
372
387
|
notificationType,
|
|
373
388
|
channel: channel.name,
|
|
@@ -382,6 +397,7 @@ export function createDeliveryService(options) {
|
|
|
382
397
|
}
|
|
383
398
|
try {
|
|
384
399
|
await deliverViaChannel({
|
|
400
|
+
run,
|
|
385
401
|
channel,
|
|
386
402
|
address,
|
|
387
403
|
message,
|
|
@@ -393,7 +409,7 @@ export function createDeliveryService(options) {
|
|
|
393
409
|
});
|
|
394
410
|
}
|
|
395
411
|
catch (err) {
|
|
396
|
-
await logDelivery({
|
|
412
|
+
await logDelivery(run, {
|
|
397
413
|
tenantId,
|
|
398
414
|
notificationType,
|
|
399
415
|
channel: channel.name,
|
|
@@ -407,13 +423,18 @@ export function createDeliveryService(options) {
|
|
|
407
423
|
}
|
|
408
424
|
}
|
|
409
425
|
return {
|
|
410
|
-
async notify(notificationType, options, _user, tenantId) {
|
|
426
|
+
async notify(notificationType, options, _user, tenantId, jobDispatcher) {
|
|
411
427
|
const { to, route, data, idempotencyKey } = options;
|
|
412
428
|
const priority = options.priority ?? "normal";
|
|
429
|
+
const run = {
|
|
430
|
+
deliveries: [],
|
|
431
|
+
jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
|
|
432
|
+
locale: options.locale,
|
|
433
|
+
};
|
|
413
434
|
if (idempotencyKey) {
|
|
414
435
|
const first = await claimIdempotency(tenantId, idempotencyKey);
|
|
415
436
|
if (!first) {
|
|
416
|
-
await logDelivery({
|
|
437
|
+
await logDelivery(run, {
|
|
417
438
|
tenantId,
|
|
418
439
|
notificationType,
|
|
419
440
|
channel: "*",
|
|
@@ -423,14 +444,12 @@ export function createDeliveryService(options) {
|
|
|
423
444
|
error: "duplicate_idempotency_key",
|
|
424
445
|
priority,
|
|
425
446
|
});
|
|
426
|
-
|
|
427
|
-
return;
|
|
447
|
+
return { deliveries: run.deliveries };
|
|
428
448
|
}
|
|
429
449
|
}
|
|
430
450
|
if (route) {
|
|
431
|
-
await deliverDirect(route, notificationType, data, tenantId, priority, options.recipientId ?? null);
|
|
432
|
-
|
|
433
|
-
return;
|
|
451
|
+
await deliverDirect(route, notificationType, data, tenantId, priority, options.recipientId ?? null, run);
|
|
452
|
+
return { deliveries: run.deliveries };
|
|
434
453
|
}
|
|
435
454
|
if (to !== undefined) {
|
|
436
455
|
let userIds;
|
|
@@ -444,9 +463,10 @@ export function createDeliveryService(options) {
|
|
|
444
463
|
userIds = to;
|
|
445
464
|
}
|
|
446
465
|
for (const userId of userIds) {
|
|
447
|
-
await deliverToUser(userId, notificationType, data, tenantId, priority);
|
|
466
|
+
await deliverToUser(userId, notificationType, data, tenantId, priority, run);
|
|
448
467
|
}
|
|
449
468
|
}
|
|
469
|
+
return { deliveries: run.deliveries };
|
|
450
470
|
},
|
|
451
471
|
};
|
|
452
472
|
}
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
-
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
-
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
2
|
+
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
+
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookTarget, chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
4
|
export type { DeliveryStatusValue } from "./constants.js";
|
|
5
5
|
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
6
6
|
export { collectChannels, createDeliveryService, type DeliveryServiceOptions, type KillSwitchResolver, type RateLimitConfig, } from "./delivery-service.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
-
export { chatMessageText, createChatWebhookChannel, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
-
export { chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
2
|
+
export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
+
export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
4
|
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
5
5
|
export { collectChannels, createDeliveryService, } from "./delivery-service.js";
|
|
6
6
|
export { createDeliveryFeature } from "./feature.js";
|
package/dist/delivery/jobs.js
CHANGED
|
@@ -15,6 +15,7 @@ const channelMessageSchema = z.object({
|
|
|
15
15
|
title: z.string(),
|
|
16
16
|
body: z.string().optional(),
|
|
17
17
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
18
|
+
locale: z.string().optional(),
|
|
18
19
|
});
|
|
19
20
|
const renderJobPayloadSchema = z.object({
|
|
20
21
|
channelName: z.string(),
|
|
@@ -56,6 +57,7 @@ function toMessage(p) {
|
|
|
56
57
|
title: p.message.title,
|
|
57
58
|
body: p.message.body,
|
|
58
59
|
data: p.message.data,
|
|
60
|
+
locale: p.message.locale,
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
function entryFor(p, status, error, address) {
|
|
@@ -81,7 +83,7 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
81
83
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
82
84
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
83
85
|
const channel = resolveChannel(registry, p.channelName);
|
|
84
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId);
|
|
86
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
85
87
|
try {
|
|
86
88
|
if (!channel.render) {
|
|
87
89
|
throw new Error(`delivery.render: channel "${p.channelName}" has no render step`);
|
package/dist/delivery/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { NotifyOptions, NotifyPriority, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import type { NotifyDeliveryStatus, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
5
|
import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
6
6
|
export type ChannelContext = {
|
|
@@ -15,6 +15,7 @@ export type ChannelMessage = {
|
|
|
15
15
|
readonly title: string;
|
|
16
16
|
readonly body: string | undefined;
|
|
17
17
|
readonly data: Readonly<Record<string, unknown>> | undefined;
|
|
18
|
+
readonly locale?: string | undefined;
|
|
18
19
|
};
|
|
19
20
|
export type ChannelResult = {
|
|
20
21
|
readonly status: "sent" | "failed" | "skipped";
|
|
@@ -37,6 +38,7 @@ export type DeliveryChannel = {
|
|
|
37
38
|
export type RendererInput = {
|
|
38
39
|
readonly template: string;
|
|
39
40
|
readonly variables: Readonly<Record<string, unknown>>;
|
|
41
|
+
readonly locale?: string | undefined;
|
|
40
42
|
};
|
|
41
43
|
export type NotificationRenderer = {
|
|
42
44
|
readonly name: string;
|
|
@@ -48,12 +50,12 @@ export type DeliveryLogEntry = {
|
|
|
48
50
|
readonly channel: string;
|
|
49
51
|
readonly recipientId: string | null;
|
|
50
52
|
readonly recipientAddress: string | null;
|
|
51
|
-
readonly status:
|
|
53
|
+
readonly status: NotifyDeliveryStatus;
|
|
52
54
|
readonly error: string | null;
|
|
53
55
|
readonly priority: NotifyPriority;
|
|
54
56
|
};
|
|
55
57
|
export type DeliveryService = {
|
|
56
|
-
notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId): Promise<
|
|
58
|
+
notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher): Promise<NotifyResult>;
|
|
57
59
|
};
|
|
58
60
|
export type DeliveryChannelPlugin = Omit<DeliveryChannel, "name">;
|
|
59
61
|
export declare function isDeliveryChannelPlugin(o: unknown): o is DeliveryChannelPlugin;
|
|
@@ -16,51 +16,45 @@
|
|
|
16
16
|
// kommen bereits als Ciphertext an — encrypted VOR dem append im
|
|
17
17
|
// write-handler. Die applies kopieren nur durch; decrypted wird erst
|
|
18
18
|
// in den list-queries.
|
|
19
|
+
import { insertOnConflictDoNothing, requireEntityTableMeta, upsertOnConflict, } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
20
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
20
21
|
import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
-
import { insertIgnoreProjectionRow, upsertProjectionRow, } from "./db/queries/inbound-projections.js";
|
|
22
22
|
import { inboundMessageEntity, mailAccountEntity, mailThreadEntity } from "./entities.js";
|
|
23
23
|
// Drizzle-table-instances aus den entity-shapes — geteilt zwischen
|
|
24
24
|
// projection-applies und list-queries (ein column-namespace).
|
|
25
25
|
export const mailAccountsProjectionTable = buildEntityTable("mail-account", mailAccountEntity);
|
|
26
26
|
export const inboundMessagesProjectionTable = buildEntityTable("inbound-message", inboundMessageEntity);
|
|
27
27
|
export const mailThreadsProjectionTable = buildEntityTable("mail-thread", mailThreadEntity);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Unbranded metas: the apply functions below ARE the executor of these
|
|
29
|
+
// projections, so they may write through the typed helpers.
|
|
30
|
+
const mailAccountsProjectionMeta = requireEntityTableMeta(mailAccountsProjectionTable, "mail-account");
|
|
31
|
+
const inboundMessagesProjectionMeta = requireEntityTableMeta(inboundMessagesProjectionTable, "inbound-message");
|
|
32
|
+
const mailThreadsProjectionMeta = requireEntityTableMeta(mailThreadsProjectionTable, "mail-thread");
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// mail-account — connected/updated/disconnected teilen den payload-shape,
|
|
33
35
|
// der event-type taggt was passiert ist. Alle drei UPSERT-full für
|
|
34
36
|
// defensive consistency (rebuild-aus-dem-Nichts, out-of-order).
|
|
35
37
|
// =============================================================================
|
|
36
|
-
const ACCOUNT_SET_CLAUSES = [
|
|
37
|
-
`"provider" = EXCLUDED."provider"`,
|
|
38
|
-
`"auth_method" = EXCLUDED."auth_method"`,
|
|
39
|
-
`"owner_user_id" = EXCLUDED."owner_user_id"`,
|
|
40
|
-
`"display_name" = EXCLUDED."display_name"`,
|
|
41
|
-
`"address" = EXCLUDED."address"`,
|
|
42
|
-
`"status" = EXCLUDED."status"`,
|
|
43
|
-
`"watch_state" = EXCLUDED."watch_state"`,
|
|
44
|
-
// connected_at bewusst NICHT im SET: der Erst-Connect-Zeitpunkt
|
|
45
|
-
// bleibt stehen, updated/disconnected überschreiben ihn nicht.
|
|
46
|
-
];
|
|
47
38
|
const applyMailAccountUpsert = defineApply(async (event, tx) => {
|
|
48
39
|
const p = event.payload;
|
|
49
|
-
const
|
|
50
|
-
id: event.aggregateId,
|
|
51
|
-
tenant_id: event.tenantId,
|
|
40
|
+
const mutable = {
|
|
52
41
|
provider: p.provider,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
authMethod: p.authMethod,
|
|
43
|
+
ownerUserId: p.ownerUserId,
|
|
44
|
+
displayName: p.displayName,
|
|
56
45
|
address: p.address,
|
|
57
46
|
status: p.status,
|
|
58
|
-
|
|
59
|
-
// Insert-Pfad = Erst-Connect (bzw. Rebuild: createdAt des ersten
|
|
60
|
-
// events des Streams — fachlich derselbe Zeitpunkt).
|
|
61
|
-
connected_at: event.createdAt.toString(),
|
|
47
|
+
watchState: p.watchState,
|
|
62
48
|
};
|
|
63
|
-
await
|
|
49
|
+
await upsertOnConflict(tx, mailAccountsProjectionMeta, {
|
|
50
|
+
id: event.aggregateId,
|
|
51
|
+
tenantId: event.tenantId,
|
|
52
|
+
...mutable,
|
|
53
|
+
// Only the insert path is the first connect (on rebuild: createdAt of the
|
|
54
|
+
// stream's first event, the same moment). Left out of `update` so the
|
|
55
|
+
// first-connect time survives updated/disconnected events.
|
|
56
|
+
connectedAt: event.createdAt,
|
|
57
|
+
}, { conflictKeys: ["id"], update: mutable });
|
|
64
58
|
});
|
|
65
59
|
/** mail-account-connected → UPSERT full. */
|
|
66
60
|
export const applyMailAccountConnected = applyMailAccountUpsert;
|
|
@@ -77,20 +71,20 @@ export const applyMailAccountDisconnected = applyMailAccountUpsert;
|
|
|
77
71
|
/** inbound-message-received → INSERT ... ON CONFLICT DO NOTHING. */
|
|
78
72
|
export const applyInboundMessageReceived = defineApply(async (event, tx) => {
|
|
79
73
|
const p = event.payload;
|
|
80
|
-
await
|
|
74
|
+
await insertOnConflictDoNothing(tx, inboundMessagesProjectionMeta, {
|
|
81
75
|
id: event.aggregateId,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
tenantId: event.tenantId,
|
|
77
|
+
accountId: p.accountId,
|
|
78
|
+
ownerUserId: p.ownerUserId,
|
|
79
|
+
messageIdHeader: p.messageIdHeader,
|
|
80
|
+
threadKey: p.threadKey,
|
|
87
81
|
from: p.from,
|
|
88
82
|
to: p.to,
|
|
89
83
|
cc: p.cc,
|
|
90
84
|
subject: p.subject,
|
|
91
85
|
snippet: p.snippet,
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
receivedAt: p.receivedAtIso,
|
|
87
|
+
bodyRef: p.bodyRef,
|
|
94
88
|
scope: p.scope,
|
|
95
89
|
});
|
|
96
90
|
});
|
|
@@ -103,17 +97,15 @@ export const applyInboundMessageReceived = defineApply(async (event, tx) => {
|
|
|
103
97
|
/** mail-thread-updated → UPSERT full mit dem Payload-Snapshot. */
|
|
104
98
|
export const applyMailThreadUpdated = defineApply(async (event, tx) => {
|
|
105
99
|
const p = event.payload;
|
|
106
|
-
const
|
|
107
|
-
id: event.aggregateId,
|
|
108
|
-
tenant_id: event.tenantId,
|
|
109
|
-
thread_key: p.threadKey,
|
|
100
|
+
const mutable = {
|
|
110
101
|
subject: p.subject,
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
lastMessageAt: p.lastMessageAtIso,
|
|
103
|
+
messageCount: p.messageCount,
|
|
113
104
|
};
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
await upsertOnConflict(tx, mailThreadsProjectionMeta, {
|
|
106
|
+
id: event.aggregateId,
|
|
107
|
+
tenantId: event.tenantId,
|
|
108
|
+
threadKey: p.threadKey,
|
|
109
|
+
...mutable,
|
|
110
|
+
}, { conflictKeys: ["id"], update: mutable });
|
|
119
111
|
});
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { type TemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
2
1
|
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { type PublicPageWrapLayout } from "../page-render/index.js";
|
|
3
3
|
import { type LegalPageRoute, type LegalRequiredBlock } from "./constants.js";
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
readonly bodyHtml: string;
|
|
7
|
-
readonly lang: string;
|
|
8
|
-
readonly slug?: string;
|
|
9
|
-
}) => string;
|
|
4
|
+
export { type LegalPagesBootCheckCtx, runLegalPagesBootCheck } from "./lib/boot-check.js";
|
|
5
|
+
export type LegalPagesWrapLayout = PublicPageWrapLayout;
|
|
10
6
|
export type LegalPagesOptions = {
|
|
11
7
|
/** Custom Layout-Wrapper für die /legal/*-Routes. Default: minimaler
|
|
12
8
|
* HTML-Skeleton aus markdown.ts (`wrapInLayout`). Apps die ihr eigenes
|
|
@@ -27,11 +23,3 @@ export type LegalPagesOptions = {
|
|
|
27
23
|
readonly requiredBlocks?: readonly LegalRequiredBlock[];
|
|
28
24
|
};
|
|
29
25
|
export declare function createLegalPagesFeature(opts?: LegalPagesOptions): FeatureDefinition;
|
|
30
|
-
export type LegalPagesBootCheckCtx = {
|
|
31
|
-
readonly templateResolver?: TemplateResolverApi;
|
|
32
|
-
readonly log?: {
|
|
33
|
-
readonly info?: (msg: string) => void;
|
|
34
|
-
readonly warn?: (msg: string) => void;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
export declare function runLegalPagesBootCheck(ctx: LegalPagesBootCheckCtx, requiredBlocks?: readonly LegalRequiredBlock[]): Promise<void>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { requireTemplateResolver, TEXT_BLOCK_KIND, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
2
1
|
import { computeRevisionEtag, etagMatches } from "@cosmicdrift/kumiko-framework/api";
|
|
3
2
|
import { defineFeature, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import { cachedSecurePageResponse } from "../page-render/index.js";
|
|
3
|
+
import { cachedSecurePageResponse, redirectToCanonicalPath, slashVariantOf, } from "../page-render/index.js";
|
|
5
4
|
import { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES, } from "./constants.js";
|
|
5
|
+
import { runLegalPagesBootCheck } from "./lib/boot-check.js";
|
|
6
|
+
import { validateRoutes } from "./lib/validate-routes.js";
|
|
6
7
|
import { renderMarkdownToHtml, wrapInLayout } from "./markdown.js";
|
|
8
|
+
export { runLegalPagesBootCheck } from "./lib/boot-check.js";
|
|
7
9
|
// QN-Konstante als dokumentierter Public-Contract des template-resolver-
|
|
8
10
|
// Features. Ein magic-string statt eines Code-Imports ist hier explizit
|
|
9
11
|
// gewollt: Cross-Feature-Calls gehen nur über stable Public-API
|
|
@@ -14,23 +16,6 @@ import { renderMarkdownToHtml, wrapInLayout } from "./markdown.js";
|
|
|
14
16
|
const TEXT_BLOCK_BY_SLUG_QN = "template-resolver:query:by-slug";
|
|
15
17
|
// 60s-shared-cache saves the origin-revalidate roundtrip; legal-content edits are live within 60s.
|
|
16
18
|
const PUBLIC_PAGE_CACHE = { kind: "revalidate", maxAgeSeconds: 60 };
|
|
17
|
-
// Validated once at feature-build time (not per-request) so a config typo
|
|
18
|
-
// fails app startup instead of surfacing as a confusing 404 later.
|
|
19
|
-
function validateRoutes(routes) {
|
|
20
|
-
const seenPaths = new Set();
|
|
21
|
-
for (const route of routes) {
|
|
22
|
-
if (!route.path || !route.slug || !route.lang) {
|
|
23
|
-
throw new Error(`legal-pages: route is missing a required field (path/slug/lang): ${JSON.stringify(route)}`);
|
|
24
|
-
}
|
|
25
|
-
if (!route.path.startsWith("/")) {
|
|
26
|
-
throw new Error(`legal-pages: route path must start with "/", got "${route.path}"`);
|
|
27
|
-
}
|
|
28
|
-
if (seenPaths.has(route.path)) {
|
|
29
|
-
throw new Error(`legal-pages: duplicate route path "${route.path}"`);
|
|
30
|
-
}
|
|
31
|
-
seenPaths.add(route.path);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
19
|
export function createLegalPagesFeature(opts = {}) {
|
|
35
20
|
const wrapLayout = opts.wrapLayout ?? wrapInLayout;
|
|
36
21
|
const routes = opts.routes ?? LEGAL_ROUTES;
|
|
@@ -56,6 +41,8 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
56
41
|
method: "GET",
|
|
57
42
|
path: route.path,
|
|
58
43
|
anonymous: true,
|
|
44
|
+
// Public pages backed by a DB query: bound the per-IP read rate.
|
|
45
|
+
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
|
|
59
46
|
handler: async (c, { systemQuery }) => {
|
|
60
47
|
// Architektur: 1 App = X Tenants = 1 Impressum. Egal welche
|
|
61
48
|
// Subdomain der Visitor besucht (apex, admin.*, tenant-x.*) —
|
|
@@ -113,6 +100,20 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
113
100
|
},
|
|
114
101
|
});
|
|
115
102
|
}
|
|
103
|
+
// Hono matches strictly, so "/legal/x/" would 404 for sites that link with
|
|
104
|
+
// a trailing slash. GET also serves HEAD.
|
|
105
|
+
const configuredPaths = new Set(routes.map((route) => route.path));
|
|
106
|
+
for (const route of routes) {
|
|
107
|
+
const variant = slashVariantOf(route.path);
|
|
108
|
+
if (variant === null || configuredPaths.has(variant))
|
|
109
|
+
continue;
|
|
110
|
+
r.httpRoute({
|
|
111
|
+
method: "GET",
|
|
112
|
+
path: variant,
|
|
113
|
+
anonymous: true,
|
|
114
|
+
handler: (c) => redirectToCanonicalPath(c.req.url, route.path),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
116
117
|
// Boot-Check via ctx.templateResolver (extraContext-Pattern, symmetrisch
|
|
117
118
|
// zu requireConfigResolver in config). App-Bootstrap muss templateResolver
|
|
118
119
|
// wired haben — der Helper gibt einen klaren Wiring-Hinweis wenn nicht.
|
|
@@ -132,34 +133,3 @@ export function createLegalPagesFeature(opts = {}) {
|
|
|
132
133
|
return {};
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
|
-
// Exportiert für direkte Tests. Wirft InternalError wenn ctx.templateResolver
|
|
136
|
-
// nicht gewired ist (Hinweis auf fehlenden extraContext). Wirft Error
|
|
137
|
-
// in NODE_ENV=production wenn Pflicht-Blocks fehlen, sonst log.warn.
|
|
138
|
-
// Logged log.info wenn alles vorhanden ist (kein silent-skip).
|
|
139
|
-
export async function runLegalPagesBootCheck(ctx, requiredBlocks = LEGAL_REQUIRED_BLOCKS) {
|
|
140
|
-
const templateResolver = requireTemplateResolver(ctx, "legal-pages-boot-check");
|
|
141
|
-
const missing = [];
|
|
142
|
-
for (const required of requiredBlocks) {
|
|
143
|
-
const block = await templateResolver.findExact({
|
|
144
|
-
tenantId: SYSTEM_TENANT_ID,
|
|
145
|
-
slug: required.slug,
|
|
146
|
-
kind: TEXT_BLOCK_KIND,
|
|
147
|
-
locale: required.lang,
|
|
148
|
-
});
|
|
149
|
-
if (!block?.content) {
|
|
150
|
-
missing.push({ slug: required.slug, lang: required.lang });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (missing.length === 0) {
|
|
154
|
-
ctx.log?.info?.("legal-pages boot-check: alle Pflicht-Blocks vorhanden");
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
const message = `legal-pages: missing ${missing.length} required text-block(s) in SYSTEM_TENANT: ` +
|
|
158
|
-
missing.map((m) => `${m.slug}/${m.lang}`).join(", ") +
|
|
159
|
-
". Seed via template-resolver:write:set or the seedTextBlock helper.";
|
|
160
|
-
if (process.env["NODE_ENV"] === "production") {
|
|
161
|
-
throw new Error(`Boot-Validation failed: ${message}`);
|
|
162
|
-
}
|
|
163
|
-
ctx.log?.warn?.(message);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
2
|
+
import { type LegalRequiredBlock } from "../constants.js";
|
|
3
|
+
export type LegalPagesBootCheckCtx = {
|
|
4
|
+
readonly templateResolver?: TemplateResolverApi;
|
|
5
|
+
readonly log?: {
|
|
6
|
+
readonly info?: (msg: string) => void;
|
|
7
|
+
readonly warn?: (msg: string) => void;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare function runLegalPagesBootCheck(ctx: LegalPagesBootCheckCtx, requiredBlocks?: readonly LegalRequiredBlock[]): Promise<void>;
|