@cosmicdrift/kumiko-bundled-features 0.342.0 → 0.343.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/audit/constants.d.ts +5 -1
- package/dist/audit/constants.js +5 -1
- package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
- package/dist/audit/escape-hatch-audit-sink.js +1 -0
- package/dist/audit/run-escape-hatch-retention.d.ts +3 -2
- package/dist/audit/run-escape-hatch-retention.js +71 -7
- package/dist/auth-email-password/email-templates.d.ts +4 -0
- package/dist/auth-email-password/email-templates.js +69 -16
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
- package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +2 -0
- package/dist/channel-discord/feature.js +1 -0
- package/dist/channel-email/feature.js +1 -0
- package/dist/channel-in-app/feature.js +1 -0
- package/dist/channel-push/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- package/dist/channel-teams/teams-channel.d.ts +2 -1
- package/dist/channel-teams/teams-channel.js +12 -0
- package/dist/channel-telegram/feature.js +1 -0
- package/dist/delivery/chat-webhook-channel.d.ts +2 -1
- package/dist/delivery/chat-webhook-channel.js +5 -2
- package/dist/delivery/chat-webhook-sender.d.ts +8 -1
- package/dist/delivery/chat-webhook-sender.js +45 -4
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/delivery-service.d.ts +2 -1
- package/dist/delivery/delivery-service.js +55 -4
- package/dist/delivery/events.d.ts +19 -1
- package/dist/delivery/events.js +11 -1
- package/dist/delivery/feature.js +29 -5
- package/dist/delivery/handlers/log.query.d.ts +4 -2
- package/dist/delivery/handlers/log.query.js +46 -18
- package/dist/delivery/i18n.js +36 -0
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/jobs.js +24 -10
- package/dist/delivery/notify-factory.d.ts +5 -0
- package/dist/delivery/notify-factory.js +6 -0
- package/dist/delivery/public-names.d.ts +4 -0
- package/dist/delivery/public-names.js +4 -0
- package/dist/delivery/tables.js +3 -0
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/testing.js +2 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/delivery/web/client-plugin.js +9 -1
- package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-channel-cell.js +10 -0
- package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-error-cell.js +16 -0
- package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
- package/dist/delivery/web/delivery-status-cell.js +11 -1
- package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-time-cell.js +6 -0
- package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-type-cell.js +17 -0
- package/dist/secrets/constants.d.ts +6 -0
- package/dist/secrets/constants.js +8 -0
- package/dist/secrets/feature.d.ts +3 -0
- package/dist/secrets/feature.js +3 -0
- package/dist/secrets/i18n.js +4 -3
- package/dist/secrets/index.d.ts +3 -2
- package/dist/secrets/index.js +3 -2
- package/dist/secrets/write-gate.d.ts +1 -0
- package/dist/secrets/write-gate.js +14 -6
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/redact.d.ts +4 -0
- package/dist/shared/redact.js +21 -0
- package/dist/step-dispatcher/feature.js +2 -1
- package/dist/step-dispatcher/mail-runner.d.ts +0 -1
- package/dist/step-dispatcher/mail-runner.js +0 -5
- package/dist/tenant/web/member-roles-cell.js +1 -1
- package/dist/tenant/web/member-status-cell.js +1 -1
- package/dist/tenant-lifecycle/feature.js +8 -2
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
- package/dist/user/index.d.ts +1 -0
- package/dist/user/index.js +1 -0
- package/dist/user/resolve-display-names.d.ts +2 -0
- package/dist/user/resolve-display-names.js +24 -0
- package/package.json +9 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/channel-teams/changes.json +8 -0
- package/src/delivery/changes.json +32 -0
- package/src/jobs/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
- /package/dist/{tenant → shared}/web/translate-or-raw.js +0 -0
|
@@ -3,7 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
|
|
|
3
3
|
// Extension-point name for delivery-channel plugins (channel-email,
|
|
4
4
|
// channel-in-app, channel-push, ...).
|
|
5
5
|
export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
|
|
6
|
-
export { DELIVERY_ATTEMPT_EVENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
6
|
+
export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
7
7
|
// notify() priority → BullMQ job priority. Lower number = processed first; all
|
|
8
8
|
// > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
|
|
9
9
|
// FIFO" bucket. critical jobs jump ahead of normal/low in the worker queue.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import type
|
|
4
|
+
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
5
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
6
|
import type { Redis } from "ioredis";
|
|
7
7
|
import { type DeliveryChannel, type DeliveryService } from "./types.js";
|
|
8
|
+
export declare function redactedMessageOf(err: unknown): string;
|
|
8
9
|
export type RateLimitConfig = {
|
|
9
10
|
readonly redis: Redis;
|
|
10
11
|
readonly maxPerHour: number;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
3
4
|
import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
4
5
|
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
|
|
5
6
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
7
|
+
import { redactErrorText } from "../shared/index.js";
|
|
6
8
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
7
9
|
import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
|
|
8
10
|
import { buildChannelContext } from "./channel-context.js";
|
|
@@ -10,6 +12,9 @@ import { DELIVERY_CHANNEL_EXTENSION, DeliveryJobs, deliveryPriorityRank } from "
|
|
|
10
12
|
import { isAddressOptedOut } from "./db/queries/address-opt-outs.js";
|
|
11
13
|
import { selectNotificationPreferences } from "./db/queries/preferences.js";
|
|
12
14
|
import { isDeliveryChannelPlugin, } from "./types.js";
|
|
15
|
+
export function redactedMessageOf(err) {
|
|
16
|
+
return redactErrorText(err instanceof Error ? err.message : String(err));
|
|
17
|
+
}
|
|
13
18
|
// Build channel list from registry extension usages
|
|
14
19
|
export function collectChannels(registry) {
|
|
15
20
|
const usages = registry.getExtensionUsages(DELIVERY_CHANNEL_EXTENSION);
|
|
@@ -27,6 +32,7 @@ export function collectChannels(registry) {
|
|
|
27
32
|
export function createDeliveryService(options) {
|
|
28
33
|
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, escapeHatchAuditSink, log, } = options;
|
|
29
34
|
const idemRedis = idempotencyRedis ?? rateLimit?.redis;
|
|
35
|
+
const logError = createFallbackLogger("delivery", log);
|
|
30
36
|
// Rate limit check: atomic INCR + TTL + over-limit rollback via server-side
|
|
31
37
|
// Lua. Runs single-threaded in Redis, so two parallel clients can't both
|
|
32
38
|
// observe `count <= max` and slip past. The prior non-atomic JS version
|
|
@@ -125,6 +131,7 @@ export function createDeliveryService(options) {
|
|
|
125
131
|
recipientId: entry.recipientId,
|
|
126
132
|
status: entry.status,
|
|
127
133
|
error: entry.error,
|
|
134
|
+
...(entry.confirmed === false && { confirmed: entry.confirmed }),
|
|
128
135
|
deliveryAttemptId,
|
|
129
136
|
});
|
|
130
137
|
}
|
|
@@ -135,6 +142,23 @@ export function createDeliveryService(options) {
|
|
|
135
142
|
const attemptId = await logAttempt(db, registry, entry);
|
|
136
143
|
recordDelivery(run, entry, attemptId);
|
|
137
144
|
}
|
|
145
|
+
// The stored/returned error is only the code; the redacted message goes to the log.
|
|
146
|
+
async function logInlineFailure(run, args, code, err) {
|
|
147
|
+
logError.error(`${args.channel.name} ${code}: ${redactedMessageOf(err)}`, {
|
|
148
|
+
notificationType: args.notificationType,
|
|
149
|
+
channel: args.channel.name,
|
|
150
|
+
});
|
|
151
|
+
await logDelivery(run, {
|
|
152
|
+
tenantId: args.tenantId,
|
|
153
|
+
notificationType: args.notificationType,
|
|
154
|
+
channel: args.channel.name,
|
|
155
|
+
recipientId: args.recipientId,
|
|
156
|
+
recipientAddress: args.address,
|
|
157
|
+
status: "failed",
|
|
158
|
+
error: code,
|
|
159
|
+
priority: args.priority,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
138
162
|
// Deliver one resolved (channel, address) pair. Inline channels (inApp) and
|
|
139
163
|
// the no-job-runner fallback render + send synchronously and log the terminal
|
|
140
164
|
// status. Queued channels (email/push) record a `queued` attempt up front and
|
|
@@ -172,8 +196,26 @@ export function createDeliveryService(options) {
|
|
|
172
196
|
}
|
|
173
197
|
else {
|
|
174
198
|
// Inline (inApp) or no-job-runner fallback: render + send synchronously.
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
let rendered;
|
|
200
|
+
if (channel.render) {
|
|
201
|
+
try {
|
|
202
|
+
rendered = await channel.render(message, channelCtx);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
await logInlineFailure(run, args, "render_failed", err);
|
|
206
|
+
// skip: logInlineFailure recorded and logged the failure
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await channel.send(address, message, channelCtx, rendered);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
await logInlineFailure(run, args, "send_failed", err);
|
|
216
|
+
// skip: logInlineFailure recorded and logged the failure
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
177
219
|
await logDelivery(run, {
|
|
178
220
|
tenantId,
|
|
179
221
|
notificationType,
|
|
@@ -183,6 +225,7 @@ export function createDeliveryService(options) {
|
|
|
183
225
|
status: result.status,
|
|
184
226
|
error: result.error ?? null,
|
|
185
227
|
priority,
|
|
228
|
+
...(result.confirmed === false && { confirmed: result.confirmed }),
|
|
186
229
|
});
|
|
187
230
|
}
|
|
188
231
|
}
|
|
@@ -340,6 +383,10 @@ export function createDeliveryService(options) {
|
|
|
340
383
|
});
|
|
341
384
|
}
|
|
342
385
|
catch (err) {
|
|
386
|
+
logError.error(`${channel.name} channel_error: ${redactedMessageOf(err)}`, {
|
|
387
|
+
notificationType,
|
|
388
|
+
channel: channel.name,
|
|
389
|
+
});
|
|
343
390
|
await logDelivery(run, {
|
|
344
391
|
tenantId,
|
|
345
392
|
notificationType,
|
|
@@ -347,7 +394,7 @@ export function createDeliveryService(options) {
|
|
|
347
394
|
recipientId: userId,
|
|
348
395
|
recipientAddress: null,
|
|
349
396
|
status: "failed",
|
|
350
|
-
error:
|
|
397
|
+
error: "channel_error",
|
|
351
398
|
priority,
|
|
352
399
|
});
|
|
353
400
|
}
|
|
@@ -409,6 +456,10 @@ export function createDeliveryService(options) {
|
|
|
409
456
|
});
|
|
410
457
|
}
|
|
411
458
|
catch (err) {
|
|
459
|
+
logError.error(`${channel.name} channel_error: ${redactedMessageOf(err)}`, {
|
|
460
|
+
notificationType,
|
|
461
|
+
channel: channel.name,
|
|
462
|
+
});
|
|
412
463
|
await logDelivery(run, {
|
|
413
464
|
tenantId,
|
|
414
465
|
notificationType,
|
|
@@ -416,7 +467,7 @@ export function createDeliveryService(options) {
|
|
|
416
467
|
recipientId,
|
|
417
468
|
recipientAddress: address,
|
|
418
469
|
status: "failed",
|
|
419
|
-
error:
|
|
470
|
+
error: "channel_error",
|
|
420
471
|
priority,
|
|
421
472
|
});
|
|
422
473
|
}
|
|
@@ -10,11 +10,29 @@ export declare const deliveryAttemptSchema: z.ZodObject<{
|
|
|
10
10
|
sent: "sent";
|
|
11
11
|
skipped: "skipped";
|
|
12
12
|
}>;
|
|
13
|
-
error: z.ZodNullable<z.
|
|
13
|
+
error: z.ZodNullable<z.ZodUnion<readonly [z.ZodEnum<{
|
|
14
|
+
channel_disabled: "channel_disabled";
|
|
15
|
+
channel_error: "channel_error";
|
|
16
|
+
duplicate_idempotency_key: "duplicate_idempotency_key";
|
|
17
|
+
host_not_allowed: "host_not_allowed";
|
|
18
|
+
invalid_address: "invalid_address";
|
|
19
|
+
missing_credentials: "missing_credentials";
|
|
20
|
+
network_error: "network_error";
|
|
21
|
+
no_address: "no_address";
|
|
22
|
+
preference_disabled: "preference_disabled";
|
|
23
|
+
rate_limited: "rate_limited";
|
|
24
|
+
redirect_blocked: "redirect_blocked";
|
|
25
|
+
render_failed: "render_failed";
|
|
26
|
+
send_failed: "send_failed";
|
|
27
|
+
timeout: "timeout";
|
|
28
|
+
unexpected_response: "unexpected_response";
|
|
29
|
+
unsubscribed: "unsubscribed";
|
|
30
|
+
}>, z.ZodString]>>;
|
|
14
31
|
priority: z.ZodEnum<{
|
|
15
32
|
critical: "critical";
|
|
16
33
|
low: "low";
|
|
17
34
|
normal: "normal";
|
|
18
35
|
}>;
|
|
36
|
+
confirmed: z.ZodOptional<z.ZodLiteral<false>>;
|
|
19
37
|
}, z.core.$strip>;
|
|
20
38
|
export type DeliveryAttemptPayload = z.infer<typeof deliveryAttemptSchema>;
|
package/dist/delivery/events.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// delivery-feature.ts (registers it via r.defineEvent) and
|
|
3
3
|
// delivery-service.ts (validates payloads before the low-level append()
|
|
4
4
|
// — out-of-dispatcher writes otherwise skip schema enforcement).
|
|
5
|
+
import { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
6
|
import * as z from "zod";
|
|
6
7
|
import { DeliveryStatus } from "./constants.js";
|
|
7
8
|
export const deliveryAttemptSchema = z.object({
|
|
@@ -15,6 +16,15 @@ export const deliveryAttemptSchema = z.object({
|
|
|
15
16
|
DeliveryStatus.failed,
|
|
16
17
|
DeliveryStatus.skipped,
|
|
17
18
|
]),
|
|
18
|
-
|
|
19
|
+
// Closed code set; z.custom would not serialize to JSON-Schema. Events written
|
|
20
|
+
// before the code vocabulary may hold free text, they are not re-validated on replay.
|
|
21
|
+
error: z
|
|
22
|
+
.union([
|
|
23
|
+
z.enum([...DELIVERY_FAILURE_CODES, ...DELIVERY_SKIP_REASONS]),
|
|
24
|
+
z.string().regex(HTTP_ERROR_CODE_PATTERN),
|
|
25
|
+
])
|
|
26
|
+
.nullable(),
|
|
19
27
|
priority: z.enum(["critical", "normal", "low"]),
|
|
28
|
+
// Provider accepted the request but does not confirm delivery (e.g. Teams 202).
|
|
29
|
+
confirmed: z.literal(false).optional(),
|
|
20
30
|
});
|
package/dist/delivery/feature.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { upsertByPk } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_LOG_SCREEN_ID, DELIVERY_STATUS_CELL_COMPONENT, DeliveryJobNames, DeliveryQueries, } from "./constants.js";
|
|
3
|
+
import { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_LOG_SCREEN_ID, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DeliveryJobNames, DeliveryQueries, } from "./constants.js";
|
|
4
4
|
import { deliveryAttemptSchema } from "./events.js";
|
|
5
5
|
import { logQuery } from "./handlers/log.query.js";
|
|
6
6
|
import { preferencesQuery } from "./handlers/preferences.query.js";
|
|
@@ -81,7 +81,13 @@ export function createDeliveryFeature(options) {
|
|
|
81
81
|
status: p.status,
|
|
82
82
|
error: p.error,
|
|
83
83
|
priority: p.priority,
|
|
84
|
-
|
|
84
|
+
confirmed: p.confirmed ?? null,
|
|
85
|
+
}, {
|
|
86
|
+
status: p.status,
|
|
87
|
+
error: p.error,
|
|
88
|
+
recipientAddress: p.recipientAddress,
|
|
89
|
+
confirmed: p.confirmed ?? null,
|
|
90
|
+
});
|
|
85
91
|
},
|
|
86
92
|
},
|
|
87
93
|
});
|
|
@@ -116,20 +122,38 @@ export function createDeliveryFeature(options) {
|
|
|
116
122
|
type: "projectionList",
|
|
117
123
|
query: DeliveryQueries.log,
|
|
118
124
|
columns: [
|
|
125
|
+
{
|
|
126
|
+
field: "createdAt",
|
|
127
|
+
label: i18nKey("delivery.log.col.createdAt"),
|
|
128
|
+
renderer: { react: { __component: DELIVERY_TIME_CELL_COMPONENT } },
|
|
129
|
+
},
|
|
119
130
|
{
|
|
120
131
|
field: "tenantId",
|
|
121
132
|
label: i18nKey("delivery.log.col.tenantId"),
|
|
122
133
|
refEntity: "tenant:tenant",
|
|
123
134
|
refLabelField: "name",
|
|
124
135
|
},
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
{
|
|
137
|
+
field: "type",
|
|
138
|
+
label: i18nKey("delivery.log.col.type"),
|
|
139
|
+
renderer: { react: { __component: DELIVERY_TYPE_CELL_COMPONENT } },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
field: "channel",
|
|
143
|
+
label: i18nKey("delivery.log.col.channel"),
|
|
144
|
+
renderer: { react: { __component: DELIVERY_CHANNEL_CELL_COMPONENT } },
|
|
145
|
+
},
|
|
146
|
+
{ field: "recipientLabel", label: i18nKey("delivery.log.col.recipient") },
|
|
128
147
|
{
|
|
129
148
|
field: "status",
|
|
130
149
|
label: i18nKey("delivery.log.col.status"),
|
|
131
150
|
renderer: { react: { __component: DELIVERY_STATUS_CELL_COMPONENT } },
|
|
132
151
|
},
|
|
152
|
+
{
|
|
153
|
+
field: "error",
|
|
154
|
+
label: i18nKey("delivery.log.col.error"),
|
|
155
|
+
renderer: { react: { __component: DELIVERY_ERROR_CELL_COMPONENT } },
|
|
156
|
+
},
|
|
133
157
|
],
|
|
134
158
|
// Cursor-based paging: deliveryAttemptsTable's id is an event-stream
|
|
135
159
|
// aggregate id (not a sequential PK), so the "pages" default (offset +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { type DeliveryErrorCode, type NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { Temporal } from "temporal-polyfill";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import type { DeliveryStatusValue } from "../constants.js";
|
|
@@ -17,8 +17,10 @@ export declare const logQuery: import("@cosmicdrift/kumiko-framework/engine").Pa
|
|
|
17
17
|
type: string;
|
|
18
18
|
channel: string;
|
|
19
19
|
recipient: string | null;
|
|
20
|
+
recipientLabel: string | null;
|
|
20
21
|
status: DeliveryStatusValue;
|
|
21
|
-
error:
|
|
22
|
+
error: DeliveryErrorCode | null;
|
|
22
23
|
priority: NotifyPriority;
|
|
24
|
+
confirmed: boolean | null;
|
|
23
25
|
createdAt: Temporal.Instant;
|
|
24
26
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { decodeKeysetCursor, encodeKeysetCursor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { access, definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { decodeKeysetCursor, encodeKeysetCursor, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { access, definePagedQueryHandler, isDeliveryErrorCode, isUuid, MAX_LIST_LIMIT, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import { Temporal } from "temporal-polyfill";
|
|
6
6
|
import * as z from "zod";
|
|
7
7
|
import { decryptStoredPii } from "../../shared/index.js";
|
|
8
|
+
import { resolveUserDisplayNames, USER_FEATURE } from "../../user/index.js";
|
|
8
9
|
import { deliveryAttemptsTable } from "../tables.js";
|
|
10
|
+
function toClientError(stored) {
|
|
11
|
+
if (stored === null)
|
|
12
|
+
return null;
|
|
13
|
+
return isDeliveryErrorCode(stored) ? stored : "channel_error";
|
|
14
|
+
}
|
|
9
15
|
// Sort whitelist keyed by the DISPLAY field the client actually sends: the
|
|
10
16
|
// declarative projectionList renderer marks every listed column uniformly
|
|
11
17
|
// sortable and a header click sends `sort=<screen.columns[].field>`, not the
|
|
12
18
|
// DB column name (see projection-list-shim.ts synthesizeProjectionEntity).
|
|
13
|
-
// "recipient" is deliberately absent — it's PII decrypted
|
|
14
|
-
// application code, so there is no column to ORDER BY in SQL for it.
|
|
19
|
+
// "recipient" is deliberately absent — it's PII (name or decrypted address)
|
|
20
|
+
// resolved in application code, so there is no column to ORDER BY in SQL for it.
|
|
21
|
+
// A click on such a header sends an unlisted sort value and falls back to createdAt.
|
|
15
22
|
const DELIVERY_LOG_SORT_COLUMNS = {
|
|
16
23
|
createdAt: "createdAt",
|
|
17
24
|
tenantId: "tenantId",
|
|
@@ -38,6 +45,39 @@ function decodeSortCursor(column, cursor) {
|
|
|
38
45
|
const sortValue = column === "createdAt" ? Temporal.Instant.from(raw) : raw;
|
|
39
46
|
return { sortValue, lastId: decoded.sortValue === undefined ? undefined : decoded.id };
|
|
40
47
|
}
|
|
48
|
+
// One batched name lookup for the whole page. Only when the user feature is
|
|
49
|
+
// mounted: delivery does not require it, and a direct-address send has no user.
|
|
50
|
+
async function loadDisplayNames(db, registry, rows) {
|
|
51
|
+
const recipientIds = [
|
|
52
|
+
...new Set(rows
|
|
53
|
+
.map((row) => row.recipientId)
|
|
54
|
+
.filter((id) => id !== null && id !== SYSTEM_USER_ID && isUuid(id))),
|
|
55
|
+
];
|
|
56
|
+
if (recipientIds.length === 0 || registry.getFeature(USER_FEATURE) === undefined)
|
|
57
|
+
return new Map();
|
|
58
|
+
return resolveUserDisplayNames(db, recipientIds);
|
|
59
|
+
}
|
|
60
|
+
// recipientLabel: display name, else the decrypted address, else the id.
|
|
61
|
+
async function toClientRow(row, displayNames) {
|
|
62
|
+
const address = row.recipientAddress !== null
|
|
63
|
+
? await decryptStoredPii(row.recipientAddress, "recipientAddress", "delivery-log")
|
|
64
|
+
: null;
|
|
65
|
+
const name = row.recipientId !== null ? displayNames.get(row.recipientId) : undefined;
|
|
66
|
+
return {
|
|
67
|
+
id: row.id,
|
|
68
|
+
tenantId: row.tenantId,
|
|
69
|
+
recipientId: row.recipientId,
|
|
70
|
+
type: row.notificationType,
|
|
71
|
+
channel: row.channel,
|
|
72
|
+
recipient: address,
|
|
73
|
+
recipientLabel: name ?? address ?? row.recipientId,
|
|
74
|
+
status: row.status,
|
|
75
|
+
error: toClientError(row.error),
|
|
76
|
+
priority: row.priority,
|
|
77
|
+
confirmed: row.confirmed,
|
|
78
|
+
createdAt: row.createdAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
41
81
|
export const logQuery = definePagedQueryHandler({
|
|
42
82
|
name: "log",
|
|
43
83
|
schema: z.object({
|
|
@@ -102,21 +142,9 @@ export const logQuery = definePagedQueryHandler({
|
|
|
102
142
|
// here, not in the client: a projectionList screen has no entity to
|
|
103
143
|
// derive a field-mapping from, so the row shape the query returns is the
|
|
104
144
|
// shape the declarative columns read directly.
|
|
145
|
+
const displayNames = await loadDisplayNames(db, ctx.registry, rows);
|
|
105
146
|
return {
|
|
106
|
-
rows: await Promise.all(rows.map(
|
|
107
|
-
id: row.id,
|
|
108
|
-
tenantId: row.tenantId,
|
|
109
|
-
recipientId: row.recipientId,
|
|
110
|
-
type: row.notificationType,
|
|
111
|
-
channel: row.channel,
|
|
112
|
-
recipient: row.recipientAddress !== null
|
|
113
|
-
? await decryptStoredPii(row.recipientAddress, "recipientAddress", "delivery-log")
|
|
114
|
-
: row.recipientAddress,
|
|
115
|
-
status: row.status,
|
|
116
|
-
error: row.error,
|
|
117
|
-
priority: row.priority,
|
|
118
|
-
createdAt: row.createdAt,
|
|
119
|
-
}))),
|
|
147
|
+
rows: await Promise.all(rows.map((row) => toClientRow(row, displayNames))),
|
|
120
148
|
nextCursor,
|
|
121
149
|
};
|
|
122
150
|
},
|
package/dist/delivery/i18n.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
|
+
import { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { DeliveryStatus } from "./public-names.js";
|
|
3
|
+
// Typed over the code vocabularies, so a new code without a label fails to compile.
|
|
4
|
+
const ERROR_LABELS = {
|
|
5
|
+
timeout: "Timed out",
|
|
6
|
+
network_error: "Network error",
|
|
7
|
+
redirect_blocked: "Redirect blocked",
|
|
8
|
+
host_not_allowed: "Host not allowed",
|
|
9
|
+
missing_credentials: "Missing credentials",
|
|
10
|
+
invalid_address: "Invalid address",
|
|
11
|
+
unexpected_response: "Unexpected provider response",
|
|
12
|
+
render_failed: "Rendering failed",
|
|
13
|
+
send_failed: "Sending failed",
|
|
14
|
+
channel_error: "Channel error",
|
|
15
|
+
channel_disabled: "Channel disabled",
|
|
16
|
+
preference_disabled: "Disabled in preferences",
|
|
17
|
+
rate_limited: "Rate limited",
|
|
18
|
+
no_address: "No address",
|
|
19
|
+
unsubscribed: "Unsubscribed",
|
|
20
|
+
duplicate_idempotency_key: "Duplicate request",
|
|
21
|
+
};
|
|
22
|
+
const STATUS_LABELS = {
|
|
23
|
+
queued: "Queued",
|
|
24
|
+
sent: "Sent",
|
|
25
|
+
failed: "Failed",
|
|
26
|
+
skipped: "Skipped",
|
|
27
|
+
};
|
|
28
|
+
function labelKeys(prefix, codes, labels) {
|
|
29
|
+
return Object.fromEntries(codes.map((code) => [`${prefix}.${code}`, { en: labels[code] ?? code }]));
|
|
30
|
+
}
|
|
1
31
|
export const DELIVERY_I18N = {
|
|
2
32
|
"screen:delivery-log.title": { en: "Delivery log" },
|
|
3
33
|
"delivery:nav.deliveryLog": { en: "Delivery" },
|
|
34
|
+
"delivery.log.col.createdAt": { en: "Time" },
|
|
4
35
|
"delivery.log.col.tenantId": { en: "Tenant" },
|
|
5
36
|
"delivery.log.col.type": { en: "Type" },
|
|
6
37
|
"delivery.log.col.channel": { en: "Channel" },
|
|
7
38
|
"delivery.log.col.recipient": { en: "Recipient" },
|
|
8
39
|
"delivery.log.col.status": { en: "Status" },
|
|
40
|
+
"delivery.log.col.error": { en: "Error" },
|
|
41
|
+
...labelKeys("delivery.status", Object.values(DeliveryStatus), STATUS_LABELS),
|
|
42
|
+
...labelKeys("delivery.error", [...DELIVERY_FAILURE_CODES, ...DELIVERY_SKIP_REASONS], ERROR_LABELS),
|
|
43
|
+
"delivery.status.sentUnconfirmed": { en: "Sent (unconfirmed)" },
|
|
44
|
+
"delivery.error.http": { en: "HTTP {status}" },
|
|
9
45
|
};
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
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";
|
|
3
|
+
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookResponse, 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";
|
|
7
7
|
export { createDeliveryFeature, type DeliveryFeatureOptions } from "./feature.js";
|
|
8
|
+
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
8
9
|
export { deliveryAttemptsTable, notificationAddressOptOutEntity, notificationAddressOptOutsTable, notificationPreferenceEntity, notificationPreferencesTable, } from "./tables.js";
|
|
9
10
|
export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing.js";
|
|
10
11
|
export { type ChannelContext, type ChannelMessage, type ChannelResult, type DeliveryChannel, type DeliveryChannelMode, type DeliveryChannelPlugin, type DeliveryLogEntry, type DeliveryService, isDeliveryChannelPlugin, type NotificationRenderer, type RenderedMessage, type RendererInput, } from "./types.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget,
|
|
|
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";
|
|
7
|
+
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
7
8
|
export { deliveryAttemptsTable, notificationAddressOptOutEntity, notificationAddressOptOutsTable, notificationPreferenceEntity, notificationPreferencesTable, } from "./tables.js";
|
|
8
9
|
export { createDeliveryTestContext } from "./testing.js";
|
|
9
10
|
export { isDeliveryChannelPlugin, } from "./types.js";
|
package/dist/delivery/jobs.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as z from "zod";
|
|
|
9
9
|
import { appendAttemptEvent } from "./attempt-log.js";
|
|
10
10
|
import { buildChannelContext } from "./channel-context.js";
|
|
11
11
|
import { DeliveryJobs, deliveryPriorityRank } from "./constants.js";
|
|
12
|
-
import { collectChannels } from "./delivery-service.js";
|
|
12
|
+
import { collectChannels, redactedMessageOf } from "./delivery-service.js";
|
|
13
13
|
const channelMessageSchema = z.object({
|
|
14
14
|
notificationType: z.string(),
|
|
15
15
|
title: z.string(),
|
|
@@ -60,7 +60,7 @@ function toMessage(p) {
|
|
|
60
60
|
locale: p.message.locale,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
function entryFor(p, status, error, address) {
|
|
63
|
+
function entryFor(p, status, error, address, confirmed) {
|
|
64
64
|
return {
|
|
65
65
|
tenantId: p.tenantId, // @cast-boundary engine-payload — job payload string is the stream tenant
|
|
66
66
|
notificationType: p.notificationType,
|
|
@@ -70,10 +70,20 @@ function entryFor(p, status, error, address) {
|
|
|
70
70
|
status,
|
|
71
71
|
error,
|
|
72
72
|
priority: p.priority,
|
|
73
|
+
...(confirmed === false && { confirmed }),
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
// The job runner stores err.message in the run row, and BullMQ keeps failedReason and
|
|
77
|
+
// the stack trace in Redis in plain text, so the rethrown error carries only the code.
|
|
78
|
+
// The redacted message goes to the log.
|
|
79
|
+
async function failAttempt(ctx, db, registry, p, code, err) {
|
|
80
|
+
const message = redactedMessageOf(err);
|
|
81
|
+
ctx.log.error(`delivery.${p.channelName} ${code}: ${message}`, {
|
|
82
|
+
notificationType: p.notificationType,
|
|
83
|
+
channel: p.channelName,
|
|
84
|
+
});
|
|
85
|
+
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, "failed", code, p.address));
|
|
86
|
+
throw new Error(code);
|
|
77
87
|
}
|
|
78
88
|
// Render the message and hand off to delivery.send. On failure: record the
|
|
79
89
|
// terminal failed event and rethrow so BullMQ retries this step (re-render);
|
|
@@ -84,17 +94,22 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
84
94
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
85
95
|
const channel = resolveChannel(registry, p.channelName);
|
|
86
96
|
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
97
|
+
let rendered;
|
|
87
98
|
try {
|
|
88
99
|
if (!channel.render) {
|
|
89
100
|
throw new Error(`delivery.render: channel "${p.channelName}" has no render step`);
|
|
90
101
|
}
|
|
91
|
-
|
|
102
|
+
rendered = await channel.render(toMessage(p), channelCtx);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return failAttempt(ctx, db, registry, p, "render_failed", err);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
92
108
|
const jobRunner = ctx["jobRunner"]; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
|
|
93
109
|
await jobRunner.dispatch(DeliveryJobs.send, { ...p, rendered }, { priority: deliveryPriorityRank[p.priority] });
|
|
94
110
|
}
|
|
95
111
|
catch (err) {
|
|
96
|
-
|
|
97
|
-
throw err;
|
|
112
|
+
return failAttempt(ctx, db, registry, p, "channel_error", err);
|
|
98
113
|
}
|
|
99
114
|
};
|
|
100
115
|
// Deliver via the channel using the rendered payload (if any). On failure:
|
|
@@ -108,10 +123,9 @@ export const deliverySendJob = async (payload, ctx) => {
|
|
|
108
123
|
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
109
124
|
try {
|
|
110
125
|
const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
|
|
111
|
-
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, result.status, result.error ?? null, result.address ?? p.address));
|
|
126
|
+
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, result.status, result.error ?? null, result.address ?? p.address, result.confirmed));
|
|
112
127
|
}
|
|
113
128
|
catch (err) {
|
|
114
|
-
|
|
115
|
-
throw err;
|
|
129
|
+
return failAttempt(ctx, db, registry, p, "send_failed", err);
|
|
116
130
|
}
|
|
117
131
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { NotifyFactory } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { DeliveryService } from "./types.js";
|
|
3
|
+
export declare function createDeliveryNotifyFactory(deliveryService: Pick<DeliveryService, "notify">, options?: {
|
|
4
|
+
readonly deliverQueuedInline?: boolean;
|
|
5
|
+
}): NotifyFactory;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The same binding runProdApp wires into the app context: the calling context's
|
|
2
|
+
// job dispatcher is handed to notify() per call, so queued channels go through
|
|
3
|
+
// the delivery jobs. deliverQueuedInline drops the dispatcher and sends inline.
|
|
4
|
+
export function createDeliveryNotifyFactory(deliveryService, options = {}) {
|
|
5
|
+
return (user, tenantId, jobDispatcher) => (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : jobDispatcher);
|
|
6
|
+
}
|
|
@@ -14,6 +14,10 @@ export declare const DeliveryQueries: {
|
|
|
14
14
|
};
|
|
15
15
|
export declare const DELIVERY_LOG_SCREEN_ID: "delivery-log";
|
|
16
16
|
export declare const DELIVERY_STATUS_CELL_COMPONENT: "DeliveryStatusCell";
|
|
17
|
+
export declare const DELIVERY_TYPE_CELL_COMPONENT: "DeliveryTypeCell";
|
|
18
|
+
export declare const DELIVERY_CHANNEL_CELL_COMPONENT: "DeliveryChannelCell";
|
|
19
|
+
export declare const DELIVERY_TIME_CELL_COMPONENT: "DeliveryTimeCell";
|
|
20
|
+
export declare const DELIVERY_ERROR_CELL_COMPONENT: "DeliveryErrorCell";
|
|
17
21
|
export declare const DeliveryErrors: {
|
|
18
22
|
readonly noRecipient: "delivery_no_recipient";
|
|
19
23
|
readonly channelFailed: "delivery_channel_failed";
|
|
@@ -26,6 +26,10 @@ export const DELIVERY_LOG_SCREEN_ID = "delivery-log";
|
|
|
26
26
|
// column — see `screen.columns[].renderer.react.__component` in feature.ts
|
|
27
27
|
// and `deliveryClient()`'s `columnRenderers` map.
|
|
28
28
|
export const DELIVERY_STATUS_CELL_COMPONENT = "DeliveryStatusCell";
|
|
29
|
+
export const DELIVERY_TYPE_CELL_COMPONENT = "DeliveryTypeCell";
|
|
30
|
+
export const DELIVERY_CHANNEL_CELL_COMPONENT = "DeliveryChannelCell";
|
|
31
|
+
export const DELIVERY_TIME_CELL_COMPONENT = "DeliveryTimeCell";
|
|
32
|
+
export const DELIVERY_ERROR_CELL_COMPONENT = "DeliveryErrorCell";
|
|
29
33
|
export const DeliveryErrors = {
|
|
30
34
|
noRecipient: "delivery_no_recipient",
|
|
31
35
|
channelFailed: "delivery_channel_failed",
|
package/dist/delivery/tables.js
CHANGED
|
@@ -23,6 +23,8 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
|
|
|
23
23
|
recipientAddress: text("recipient_address"),
|
|
24
24
|
status: text("status").notNull().$type(),
|
|
25
25
|
error: text("error"),
|
|
26
|
+
// false = provider accepted but did not confirm; null = confirmed or not applicable.
|
|
27
|
+
confirmed: boolean("confirmed"),
|
|
26
28
|
// Default covers rows that predate the column; new rows always carry the
|
|
27
29
|
// notify() priority from the event payload.
|
|
28
30
|
priority: text("priority").notNull().default("normal").$type(),
|
|
@@ -48,6 +50,7 @@ export const deliveryAttemptsTableMeta = defineUnmanagedTable({
|
|
|
48
50
|
{ name: "recipient_address", pgType: "text", notNull: false },
|
|
49
51
|
{ name: "status", pgType: "text", notNull: true },
|
|
50
52
|
{ name: "error", pgType: "text", notNull: false },
|
|
53
|
+
{ name: "confirmed", pgType: "boolean", notNull: false },
|
|
51
54
|
{ name: "priority", pgType: "text", notNull: true, defaultSql: "'normal'" },
|
|
52
55
|
{ name: "created_at", pgType: "timestamptz", notNull: true, defaultSql: "now()" },
|
|
53
56
|
],
|
|
@@ -3,6 +3,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
import type { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
5
|
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
7
|
import type { Redis } from "ioredis";
|
|
7
8
|
import type { KillSwitchResolver, RateLimitConfig } from "./delivery-service.js";
|
|
8
9
|
import type { DeliveryService } from "./types.js";
|
|
@@ -13,6 +14,7 @@ export type CreateDeliveryTestContextOptions = {
|
|
|
13
14
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
14
15
|
readonly log?: Logger;
|
|
15
16
|
readonly jobRunner?: JobRunner;
|
|
17
|
+
readonly secrets?: SecretsContext;
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
|
package/dist/delivery/testing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectChannels, createDeliveryService } from "./delivery-service.js";
|
|
2
|
+
import { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
2
3
|
/**
|
|
3
4
|
* Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
|
|
4
5
|
* Abstracts the boilerplate that every Delivery-using test needs.
|
|
@@ -21,6 +22,6 @@ export function createDeliveryTestContext(deps, options = {}) {
|
|
|
21
22
|
});
|
|
22
23
|
return {
|
|
23
24
|
deliveryService, // exposed so tests can inspect/call directly if needed
|
|
24
|
-
_notifyFactory: (
|
|
25
|
+
_notifyFactory: createDeliveryNotifyFactory(deliveryService),
|
|
25
26
|
};
|
|
26
27
|
}
|