@de-otio/trellis 0.10.11 → 0.12.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/env.d.ts +232 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +221 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lambda/media-completion-worker.d.ts +175 -0
- package/dist/lambda/media-completion-worker.d.ts.map +1 -0
- package/dist/lambda/media-completion-worker.js +373 -0
- package/dist/lambda/media-completion-worker.js.map +1 -0
- package/dist/lambda/media-processing-worker.d.ts +172 -1
- package/dist/lambda/media-processing-worker.d.ts.map +1 -1
- package/dist/lambda/media-processing-worker.js +343 -49
- package/dist/lambda/media-processing-worker.js.map +1 -1
- package/dist/lib/app.d.ts.map +1 -1
- package/dist/lib/app.js +5 -0
- package/dist/lib/app.js.map +1 -1
- package/dist/lib/encrypted-settings/config.d.ts +13 -0
- package/dist/lib/encrypted-settings/config.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/config.js +19 -0
- package/dist/lib/encrypted-settings/config.js.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts +57 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.js +178 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.js.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts +110 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.js +103 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.js.map +1 -0
- package/dist/lib/encrypted-settings/types.d.ts +26 -0
- package/dist/lib/encrypted-settings/types.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/types.js +27 -0
- package/dist/lib/encrypted-settings/types.js.map +1 -0
- package/dist/lib/exif-stripper.d.ts +37 -22
- package/dist/lib/exif-stripper.d.ts.map +1 -1
- package/dist/lib/exif-stripper.js +101 -41
- package/dist/lib/exif-stripper.js.map +1 -1
- package/dist/lib/media/cas-keys.d.ts +63 -0
- package/dist/lib/media/cas-keys.d.ts.map +1 -0
- package/dist/lib/media/cas-keys.js +102 -0
- package/dist/lib/media/cas-keys.js.map +1 -0
- package/dist/lib/media/classify-worker-error.d.ts +48 -0
- package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
- package/dist/lib/media/classify-worker-error.js +319 -0
- package/dist/lib/media/classify-worker-error.js.map +1 -0
- package/dist/lib/media/dedupe-key.d.ts +29 -0
- package/dist/lib/media/dedupe-key.d.ts.map +1 -0
- package/dist/lib/media/dedupe-key.js +49 -0
- package/dist/lib/media/dedupe-key.js.map +1 -0
- package/dist/lib/media/duration-cap.d.ts +30 -0
- package/dist/lib/media/duration-cap.d.ts.map +1 -0
- package/dist/lib/media/duration-cap.js +37 -0
- package/dist/lib/media/duration-cap.js.map +1 -0
- package/dist/lib/media/ffmpeg-args.d.ts +83 -0
- package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
- package/dist/lib/media/ffmpeg-args.js +119 -0
- package/dist/lib/media/ffmpeg-args.js.map +1 -0
- package/dist/lib/media/media-ports.d.ts +126 -0
- package/dist/lib/media/media-ports.d.ts.map +1 -0
- package/dist/lib/media/media-ports.js +129 -0
- package/dist/lib/media/media-ports.js.map +1 -0
- package/dist/lib/media/media-upsert.d.ts +55 -0
- package/dist/lib/media/media-upsert.d.ts.map +1 -0
- package/dist/lib/media/media-upsert.js +38 -0
- package/dist/lib/media/media-upsert.js.map +1 -0
- package/dist/lib/media/moderation-provider.d.ts +111 -0
- package/dist/lib/media/moderation-provider.d.ts.map +1 -0
- package/dist/lib/media/moderation-provider.js +130 -0
- package/dist/lib/media/moderation-provider.js.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.js +37 -0
- package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
- package/dist/lib/media/moderation-status.d.ts +98 -0
- package/dist/lib/media/moderation-status.d.ts.map +1 -0
- package/dist/lib/media/moderation-status.js +122 -0
- package/dist/lib/media/moderation-status.js.map +1 -0
- package/dist/lib/media/processing-types.d.ts +45 -0
- package/dist/lib/media/processing-types.d.ts.map +1 -0
- package/dist/lib/media/processing-types.js +9 -0
- package/dist/lib/media/processing-types.js.map +1 -0
- package/dist/lib/media/promote-decision.d.ts +64 -0
- package/dist/lib/media/promote-decision.d.ts.map +1 -0
- package/dist/lib/media/promote-decision.js +76 -0
- package/dist/lib/media/promote-decision.js.map +1 -0
- package/dist/lib/media/quota-check.d.ts +22 -0
- package/dist/lib/media/quota-check.d.ts.map +1 -0
- package/dist/lib/media/quota-check.js +42 -0
- package/dist/lib/media/quota-check.js.map +1 -0
- package/dist/lib/media/quota-types.d.ts +15 -0
- package/dist/lib/media/quota-types.d.ts.map +1 -0
- package/dist/lib/media/quota-types.js +9 -0
- package/dist/lib/media/quota-types.js.map +1 -0
- package/dist/lib/media/route-upload.d.ts +58 -0
- package/dist/lib/media/route-upload.d.ts.map +1 -0
- package/dist/lib/media/route-upload.js +80 -0
- package/dist/lib/media/route-upload.js.map +1 -0
- package/dist/lib/media/serve-gate.d.ts +51 -0
- package/dist/lib/media/serve-gate.d.ts.map +1 -0
- package/dist/lib/media/serve-gate.js +68 -0
- package/dist/lib/media/serve-gate.js.map +1 -0
- package/dist/lib/media/tenant-resolution.d.ts +42 -0
- package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
- package/dist/lib/media/tenant-resolution.js +45 -0
- package/dist/lib/media/tenant-resolution.js.map +1 -0
- package/dist/lib/media/text-moderation.d.ts +28 -0
- package/dist/lib/media/text-moderation.d.ts.map +1 -0
- package/dist/lib/media/text-moderation.js +62 -0
- package/dist/lib/media/text-moderation.js.map +1 -0
- package/dist/lib/media/track-verdict.d.ts +45 -0
- package/dist/lib/media/track-verdict.d.ts.map +1 -0
- package/dist/lib/media/track-verdict.js +52 -0
- package/dist/lib/media/track-verdict.js.map +1 -0
- package/dist/lib/media/transcript-moderation.d.ts +47 -0
- package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
- package/dist/lib/media/transcript-moderation.js +70 -0
- package/dist/lib/media/transcript-moderation.js.map +1 -0
- package/dist/lib/media-handler.d.ts.map +1 -1
- package/dist/lib/media-handler.js +15 -9
- package/dist/lib/media-handler.js.map +1 -1
- package/dist/lib/notification-handler.d.ts +11 -4
- package/dist/lib/notification-handler.d.ts.map +1 -1
- package/dist/lib/notification-handler.js +161 -29
- package/dist/lib/notification-handler.js.map +1 -1
- package/dist/lib/post-handler.d.ts.map +1 -1
- package/dist/lib/post-handler.js +4 -1
- package/dist/lib/post-handler.js.map +1 -1
- package/dist/lib/realtime/block-store.d.ts +61 -0
- package/dist/lib/realtime/block-store.d.ts.map +1 -0
- package/dist/lib/realtime/block-store.js +0 -0
- package/dist/lib/realtime/block-store.js.map +1 -0
- package/dist/lib/realtime/channel.d.ts +34 -0
- package/dist/lib/realtime/channel.d.ts.map +1 -0
- package/dist/lib/realtime/channel.js +100 -0
- package/dist/lib/realtime/channel.js.map +1 -0
- package/dist/lib/realtime/delivery-policy.d.ts +51 -0
- package/dist/lib/realtime/delivery-policy.d.ts.map +1 -0
- package/dist/lib/realtime/delivery-policy.js +98 -0
- package/dist/lib/realtime/delivery-policy.js.map +1 -0
- package/dist/lib/realtime/index.d.ts +21 -0
- package/dist/lib/realtime/index.d.ts.map +1 -0
- package/dist/lib/realtime/index.js +39 -0
- package/dist/lib/realtime/index.js.map +1 -0
- package/dist/lib/realtime/no-op-transport.d.ts +10 -0
- package/dist/lib/realtime/no-op-transport.d.ts.map +1 -0
- package/dist/lib/realtime/no-op-transport.js +44 -0
- package/dist/lib/realtime/no-op-transport.js.map +1 -0
- package/dist/lib/realtime/poll-transport.d.ts +11 -0
- package/dist/lib/realtime/poll-transport.d.ts.map +1 -0
- package/dist/lib/realtime/poll-transport.js +68 -0
- package/dist/lib/realtime/poll-transport.js.map +1 -0
- package/dist/lib/realtime/push-notifier.d.ts +39 -0
- package/dist/lib/realtime/push-notifier.d.ts.map +1 -0
- package/dist/lib/realtime/push-notifier.js +76 -0
- package/dist/lib/realtime/push-notifier.js.map +1 -0
- package/dist/lib/realtime/realtime-transport.d.ts +2 -0
- package/dist/lib/realtime/realtime-transport.d.ts.map +1 -0
- package/dist/lib/realtime/realtime-transport.js +23 -0
- package/dist/lib/realtime/realtime-transport.js.map +1 -0
- package/dist/lib/realtime/setting-store.d.ts +30 -0
- package/dist/lib/realtime/setting-store.d.ts.map +1 -0
- package/dist/lib/realtime/setting-store.js +0 -0
- package/dist/lib/realtime/setting-store.js.map +1 -0
- package/dist/lib/realtime/types.d.ts +200 -0
- package/dist/lib/realtime/types.d.ts.map +1 -0
- package/dist/lib/realtime/types.js +61 -0
- package/dist/lib/realtime/types.js.map +1 -0
- package/dist/lib/routes/index.d.ts.map +1 -1
- package/dist/lib/routes/index.js +3 -0
- package/dist/lib/routes/index.js.map +1 -1
- package/dist/lib/routes/media.d.ts +21 -0
- package/dist/lib/routes/media.d.ts.map +1 -1
- package/dist/lib/routes/media.js +584 -483
- package/dist/lib/routes/media.js.map +1 -1
- package/dist/lib/routes/settings.d.ts +17 -0
- package/dist/lib/routes/settings.d.ts.map +1 -0
- package/dist/lib/routes/settings.js +187 -0
- package/dist/lib/routes/settings.js.map +1 -0
- package/dist/lib/services/image-normalizer.d.ts +64 -6
- package/dist/lib/services/image-normalizer.d.ts.map +1 -1
- package/dist/lib/services/image-normalizer.js +88 -6
- package/dist/lib/services/image-normalizer.js.map +1 -1
- package/dist/lib/services/media-upload-service.d.ts +2 -2
- package/dist/lib/services/media-upload-service.d.ts.map +1 -1
- package/dist/lib/services/media-upload-service.js +22 -21
- package/dist/lib/services/media-upload-service.js.map +1 -1
- package/dist/lib/tenant-scope.d.ts.map +1 -1
- package/dist/lib/tenant-scope.js +18 -1
- package/dist/lib/tenant-scope.js.map +1 -1
- package/package.json +23 -22
- package/prisma/migrations/20260620051144_add_encrypted_user_settings/migration.sql +24 -0
- package/prisma/migrations/20260620120000_add_blocked_users/migration.sql +29 -0
- package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
- package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
- package/prisma/schema.prisma +133 -15
- package/src/lambda/media-completion-worker.ts +567 -0
- package/src/lambda/media-processing-worker.ts +508 -59
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes. See types.ts banner.
|
|
2
|
+
//
|
|
3
|
+
// Channel naming + the subscription authorization boundary. Per §2.1/§2.2 of
|
|
4
|
+
// the frozen contract, the canonical path is produced ONLY by `channelName()`
|
|
5
|
+
// and parsed ONLY by `parseChannel()`; `authorizeSubscription()` is THE
|
|
6
|
+
// security boundary — identity comes from a VerifiedIdentity (Cognito claims),
|
|
7
|
+
// never an ambient Session and never a client-asserted path.
|
|
8
|
+
/** Canonical path grammar: /{kind}/{tenantId}/{scopeType}/{scopeId}. */
|
|
9
|
+
const VALID_KINDS = new Set([
|
|
10
|
+
"wakeup",
|
|
11
|
+
"setting_sync",
|
|
12
|
+
"safety",
|
|
13
|
+
"message",
|
|
14
|
+
"thread",
|
|
15
|
+
]);
|
|
16
|
+
const VALID_SCOPE_TYPES = new Set([
|
|
17
|
+
"user",
|
|
18
|
+
"conversation",
|
|
19
|
+
"thread",
|
|
20
|
+
]);
|
|
21
|
+
/** A path segment may not be empty and may not contain a path separator. */
|
|
22
|
+
function isValidSegment(s) {
|
|
23
|
+
return s.length > 0 && !s.includes("/");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Produce the canonical channel path. This is the ONLY way a channel string is
|
|
27
|
+
* minted server-side; the Skybber `@skybber/realtime-channels` parser must
|
|
28
|
+
* round-trip this output.
|
|
29
|
+
*/
|
|
30
|
+
export function channelName(c) {
|
|
31
|
+
if (!VALID_KINDS.has(c.kind)) {
|
|
32
|
+
throw new Error(`channelName: invalid kind "${String(c.kind)}"`);
|
|
33
|
+
}
|
|
34
|
+
if (!VALID_SCOPE_TYPES.has(c.scopeType)) {
|
|
35
|
+
throw new Error(`channelName: invalid scopeType "${String(c.scopeType)}"`);
|
|
36
|
+
}
|
|
37
|
+
if (!isValidSegment(c.tenantId) || !isValidSegment(c.scopeId)) {
|
|
38
|
+
throw new Error("channelName: tenantId/scopeId must be non-empty, no slash");
|
|
39
|
+
}
|
|
40
|
+
return `/${c.kind}/${c.tenantId}/${c.scopeType}/${c.scopeId}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse a canonical channel path. Returns `null` on ANY malformed input — a
|
|
44
|
+
* leading-slash-less path, wrong arity, unknown kind/scopeType, or an empty
|
|
45
|
+
* segment.
|
|
46
|
+
*/
|
|
47
|
+
export function parseChannel(path) {
|
|
48
|
+
if (typeof path !== "string" || !path.startsWith("/"))
|
|
49
|
+
return null;
|
|
50
|
+
// Drop the leading "" produced by the leading slash; require exactly 4 parts.
|
|
51
|
+
const parts = path.split("/");
|
|
52
|
+
if (parts.length !== 5 || parts[0] !== "")
|
|
53
|
+
return null;
|
|
54
|
+
const [, kind, tenantId, scopeType, scopeId] = parts;
|
|
55
|
+
if (!VALID_KINDS.has(kind))
|
|
56
|
+
return null;
|
|
57
|
+
if (!VALID_SCOPE_TYPES.has(scopeType))
|
|
58
|
+
return null;
|
|
59
|
+
if (!isValidSegment(tenantId) || !isValidSegment(scopeId))
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
kind: kind,
|
|
63
|
+
tenantId,
|
|
64
|
+
scopeType: scopeType,
|
|
65
|
+
scopeId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a user-scoped channel (v1: the only scope). `kind` is constrained to
|
|
70
|
+
* the v1 user-scoped kinds; `message`/`thread` are deferred and excluded.
|
|
71
|
+
*/
|
|
72
|
+
export function channelFor(kind, scope) {
|
|
73
|
+
return {
|
|
74
|
+
kind,
|
|
75
|
+
tenantId: scope.tenantId,
|
|
76
|
+
scopeType: "user",
|
|
77
|
+
scopeId: scope.userId,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* THE security boundary. The channel is a client *assertion* this checks
|
|
82
|
+
* against a server-verified identity.
|
|
83
|
+
*
|
|
84
|
+
* ALLOW iff `c.tenantId === id.tenantId` AND
|
|
85
|
+
* scopeType "user" -> `c.scopeId === id.userId`
|
|
86
|
+
* scopeType conversation/thread -> membership(id.userId, c.scopeId) [v1: false]
|
|
87
|
+
*
|
|
88
|
+
* End-user PUBLISH is ALWAYS denied elsewhere (publish is server-only via IAM);
|
|
89
|
+
* this function governs SUBSCRIBE only.
|
|
90
|
+
*/
|
|
91
|
+
export function authorizeSubscription(id, c) {
|
|
92
|
+
if (c.tenantId !== id.tenantId)
|
|
93
|
+
return false;
|
|
94
|
+
if (c.scopeType === "user") {
|
|
95
|
+
return c.scopeId === id.userId;
|
|
96
|
+
}
|
|
97
|
+
// conversation / thread membership — DEFERRED in v1, deny.
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../../../src/lib/realtime/channel.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,wEAAwE;AACxE,+EAA+E;AAC/E,6DAA6D;AAS7D,wEAAwE;AACxE,MAAM,WAAW,GAA6B,IAAI,GAAG,CAAc;IACjE,QAAQ;IACR,cAAc;IACd,QAAQ;IACR,SAAS;IACT,QAAQ;CACT,CAAC,CAAC;AAEH,MAAM,iBAAiB,GAA2B,IAAI,GAAG,CAAY;IACnE,MAAM;IACN,cAAc;IACd,QAAQ;CACT,CAAC,CAAC;AAEH,4EAA4E;AAC5E,SAAS,cAAc,CAAC,CAAS;IAC/B,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,8EAA8E;IAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACvD,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC;IACrD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAmB,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,SAAsB,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACvE,OAAO;QACL,IAAI,EAAE,IAAmB;QACzB,QAAQ;QACR,SAAS,EAAE,SAAsB;QACjC,OAAO;KACR,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAgD,EAChD,KAA2C;IAE3C,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,MAAM;QACjB,OAAO,EAAE,KAAK,CAAC,MAAM;KACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,EAAoB,EACpB,CAAU;IAEV,IAAI,CAAC,CAAC,QAAQ,KAAK,EAAE,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7C,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM,EAAE,CAAC;QAC3B,OAAO,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,MAAM,CAAC;IACjC,CAAC;IACD,2DAA2D;IAC3D,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DeliveryContext, DeliveryDecision, DeliveryPolicyResolver } from "./types.js";
|
|
2
|
+
import type { NotificationType } from "@prisma/client";
|
|
3
|
+
/**
|
|
4
|
+
* Notification types that ALWAYS deliver — they bypass user preference and
|
|
5
|
+
* quiet hours. Migrated verbatim from `notification-handler.ts`'s
|
|
6
|
+
* `ALWAYS_DELIVER_TYPES`. This is the critical-always floor.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ALWAYS_DELIVER_TYPES: ReadonlySet<NotificationType>;
|
|
9
|
+
/**
|
|
10
|
+
* Track D runtime config for the floor. The minor-protection rule needs the set
|
|
11
|
+
* of "manipulative re-engagement" NotificationTypes to deny to non-adults. Per
|
|
12
|
+
* the threshold-secrecy invariant (CLAUDE.md rule 8) this list is RUNTIME CONFIG
|
|
13
|
+
* — passed in here, never a compiled-in constant sprinkled at a call site.
|
|
14
|
+
* v1 ships it EMPTY (no such type exists yet); a deployment populates it via env.
|
|
15
|
+
*/
|
|
16
|
+
export interface DeliveryFloorConfig {
|
|
17
|
+
/** NotificationTypes that re-engage and are denied to non-adult recipients. */
|
|
18
|
+
reengagementTypes?: ReadonlySet<NotificationType>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The WS1 + Track D default resolver. Pure and synchronous over `DeliveryContext`
|
|
22
|
+
* (the caller does any async lookups and passes resolved signals in):
|
|
23
|
+
*
|
|
24
|
+
* 1. ALWAYS_DELIVER_TYPES (SAFETY_ALERT, PARENTAL_LINK) bypass everything ->
|
|
25
|
+
* `{ deliver: true }`. Safety must NOT be over-blocked: this wins over the
|
|
26
|
+
* blocked-sender and minor-protection floor below.
|
|
27
|
+
* 2. Non-configurable FLOOR (checked only when the caller supplies the input):
|
|
28
|
+
* - blocked sender: the caller resolves block-set membership async (via
|
|
29
|
+
* BlockStore) and populates `ctx.senderUserId` ONLY when the sender is
|
|
30
|
+
* blocked — so presence of `senderUserId` == "in the recipient's block
|
|
31
|
+
* set". The decision is a hard drop no preference can override.
|
|
32
|
+
* - minor-protection: a non-adult recipient (`recipientAgeTier` CHILD/TEEN)
|
|
33
|
+
* targeted by a configured re-engagement type is dropped.
|
|
34
|
+
* 3. Else honor preference (caller pre-resolves `deliver:false`/`preference`)
|
|
35
|
+
* and quiet hours.
|
|
36
|
+
*
|
|
37
|
+
* Note on preference: in the existing handler the type-preference check happens
|
|
38
|
+
* BEFORE creating the row (preference-off => no row at all), which is a
|
|
39
|
+
* different outcome from quiet-hours (row created with deliveredAt=null). The
|
|
40
|
+
* caller maps the decision reasons accordingly. To keep the resolver pure and
|
|
41
|
+
* total it accepts the preference outcome via `ctx` indirectly: the caller only
|
|
42
|
+
* invokes the resolver's quiet-hours path once preference has passed. For a
|
|
43
|
+
* single source of truth the resolver still exposes the full decision so a
|
|
44
|
+
* push-only caller (WS4) can gate solely on it.
|
|
45
|
+
*/
|
|
46
|
+
export declare class CalmDeliveryResolver implements DeliveryPolicyResolver {
|
|
47
|
+
private readonly reengagementTypes;
|
|
48
|
+
constructor(config?: DeliveryFloorConfig);
|
|
49
|
+
decide(ctx: DeliveryContext): DeliveryDecision;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=delivery-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-policy.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/delivery-policy.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,gBAAgB,CAE3B,CAAC;AA8BrC;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,oBAAqB,YAAW,sBAAsB;IACjE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAgC;gBAEtD,MAAM,GAAE,mBAAwB;IAK5C,MAAM,CAAC,GAAG,EAAE,eAAe,GAAG,gBAAgB;CAgC/C"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes. See types.ts banner.
|
|
2
|
+
//
|
|
3
|
+
// The DeliveryPolicyResolver port and the CalmDeliveryResolver default. Per §3
|
|
4
|
+
// of the frozen contract, WS1 (not WS4) owns migrating the existing hardcoded
|
|
5
|
+
// notification floor into CalmDeliveryResolver, under a golden test that pins
|
|
6
|
+
// byte-identical behavior. The same resolver decision drives BOTH the
|
|
7
|
+
// persistence `deliveredAt` choice AND the (default-off) push hand-off.
|
|
8
|
+
/**
|
|
9
|
+
* Notification types that ALWAYS deliver — they bypass user preference and
|
|
10
|
+
* quiet hours. Migrated verbatim from `notification-handler.ts`'s
|
|
11
|
+
* `ALWAYS_DELIVER_TYPES`. This is the critical-always floor.
|
|
12
|
+
*/
|
|
13
|
+
export const ALWAYS_DELIVER_TYPES = new Set(["SAFETY_ALERT", "PARENTAL_LINK"]);
|
|
14
|
+
/**
|
|
15
|
+
* Reproduce the existing `notification-handler.ts` quiet-hours check exactly.
|
|
16
|
+
*
|
|
17
|
+
* The core encodes `User.quietHoursStart/End` (minutes-since-midnight integers)
|
|
18
|
+
* as decimal strings in `QuietHoursConfig.start/end`, and derives the current
|
|
19
|
+
* minute from `ctx.now` (rather than an ambient `new Date()`), so the decision
|
|
20
|
+
* is deterministic. The wrap-around / same-day arithmetic mirrors
|
|
21
|
+
* `NotificationHandler.isInQuietHours`.
|
|
22
|
+
*/
|
|
23
|
+
function isInQuietHours(quietHours, now) {
|
|
24
|
+
if (!quietHours || !quietHours.enabled)
|
|
25
|
+
return false;
|
|
26
|
+
const start = Number.parseInt(quietHours.start, 10);
|
|
27
|
+
const end = Number.parseInt(quietHours.end, 10);
|
|
28
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
29
|
+
return false;
|
|
30
|
+
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
|
|
31
|
+
// Overnight window (e.g. 22:00 -> 07:00).
|
|
32
|
+
if (start > end) {
|
|
33
|
+
return minutesSinceMidnight >= start || minutesSinceMidnight < end;
|
|
34
|
+
}
|
|
35
|
+
// Same-day window (e.g. 13:00 -> 15:00).
|
|
36
|
+
return minutesSinceMidnight >= start && minutesSinceMidnight < end;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The WS1 + Track D default resolver. Pure and synchronous over `DeliveryContext`
|
|
40
|
+
* (the caller does any async lookups and passes resolved signals in):
|
|
41
|
+
*
|
|
42
|
+
* 1. ALWAYS_DELIVER_TYPES (SAFETY_ALERT, PARENTAL_LINK) bypass everything ->
|
|
43
|
+
* `{ deliver: true }`. Safety must NOT be over-blocked: this wins over the
|
|
44
|
+
* blocked-sender and minor-protection floor below.
|
|
45
|
+
* 2. Non-configurable FLOOR (checked only when the caller supplies the input):
|
|
46
|
+
* - blocked sender: the caller resolves block-set membership async (via
|
|
47
|
+
* BlockStore) and populates `ctx.senderUserId` ONLY when the sender is
|
|
48
|
+
* blocked — so presence of `senderUserId` == "in the recipient's block
|
|
49
|
+
* set". The decision is a hard drop no preference can override.
|
|
50
|
+
* - minor-protection: a non-adult recipient (`recipientAgeTier` CHILD/TEEN)
|
|
51
|
+
* targeted by a configured re-engagement type is dropped.
|
|
52
|
+
* 3. Else honor preference (caller pre-resolves `deliver:false`/`preference`)
|
|
53
|
+
* and quiet hours.
|
|
54
|
+
*
|
|
55
|
+
* Note on preference: in the existing handler the type-preference check happens
|
|
56
|
+
* BEFORE creating the row (preference-off => no row at all), which is a
|
|
57
|
+
* different outcome from quiet-hours (row created with deliveredAt=null). The
|
|
58
|
+
* caller maps the decision reasons accordingly. To keep the resolver pure and
|
|
59
|
+
* total it accepts the preference outcome via `ctx` indirectly: the caller only
|
|
60
|
+
* invokes the resolver's quiet-hours path once preference has passed. For a
|
|
61
|
+
* single source of truth the resolver still exposes the full decision so a
|
|
62
|
+
* push-only caller (WS4) can gate solely on it.
|
|
63
|
+
*/
|
|
64
|
+
export class CalmDeliveryResolver {
|
|
65
|
+
reengagementTypes;
|
|
66
|
+
constructor(config = {}) {
|
|
67
|
+
this.reengagementTypes =
|
|
68
|
+
config.reengagementTypes ?? new Set();
|
|
69
|
+
}
|
|
70
|
+
decide(ctx) {
|
|
71
|
+
// 1. Critical-always bypass — wins over the floor so safety is never
|
|
72
|
+
// over-blocked (a blocked sender or a minor still gets SAFETY_ALERT /
|
|
73
|
+
// PARENTAL_LINK).
|
|
74
|
+
if (ALWAYS_DELIVER_TYPES.has(ctx.type)) {
|
|
75
|
+
return { deliver: true };
|
|
76
|
+
}
|
|
77
|
+
// 2. Non-configurable floor.
|
|
78
|
+
// 2a. Blocked sender. The caller has already resolved block-set
|
|
79
|
+
// membership and only sets `senderUserId` when the sender IS blocked,
|
|
80
|
+
// so its presence is the deny signal. Hard drop, no override.
|
|
81
|
+
if (ctx.senderUserId !== undefined) {
|
|
82
|
+
return { deliver: false, reason: "blocked_sender" };
|
|
83
|
+
}
|
|
84
|
+
// 2b. Minor protection. A non-adult recipient targeted by a configured
|
|
85
|
+
// manipulative re-engagement type is dropped (FLOOR, not preference).
|
|
86
|
+
if (ctx.recipientAgeTier !== undefined &&
|
|
87
|
+
ctx.recipientAgeTier !== "ADULT" &&
|
|
88
|
+
this.reengagementTypes.has(ctx.type)) {
|
|
89
|
+
return { deliver: false, reason: "floor" };
|
|
90
|
+
}
|
|
91
|
+
// 3. Quiet hours (preference is handled by the caller; see class doc).
|
|
92
|
+
if (isInQuietHours(ctx.quietHours, ctx.now)) {
|
|
93
|
+
return { deliver: false, reason: "quiet_hours" };
|
|
94
|
+
}
|
|
95
|
+
return { deliver: true };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=delivery-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-policy.js","sourceRoot":"","sources":["../../../src/lib/realtime/delivery-policy.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,8EAA8E;AAC9E,sEAAsE;AACtE,wEAAwE;AASxE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAkC,IAAI,GAAG,CAExE,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC,CAAC;AAErC;;;;;;;;GAQG;AACH,SAAS,cAAc,CACrB,UAAyC,EACzC,GAAS;IAET,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAEnE,MAAM,oBAAoB,GAAG,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;IAEpE,0CAA0C;IAC1C,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QAChB,OAAO,oBAAoB,IAAI,KAAK,IAAI,oBAAoB,GAAG,GAAG,CAAC;IACrE,CAAC;IACD,yCAAyC;IACzC,OAAO,oBAAoB,IAAI,KAAK,IAAI,oBAAoB,GAAG,GAAG,CAAC;AACrE,CAAC;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,oBAAoB;IACd,iBAAiB,CAAgC;IAElE,YAAY,SAA8B,EAAE;QAC1C,IAAI,CAAC,iBAAiB;YACpB,MAAM,CAAC,iBAAiB,IAAI,IAAI,GAAG,EAAoB,CAAC;IAC5D,CAAC;IAED,MAAM,CAAC,GAAoB;QACzB,qEAAqE;QACrE,yEAAyE;QACzE,qBAAqB;QACrB,IAAI,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAED,6BAA6B;QAC7B,mEAAmE;QACnE,6EAA6E;QAC7E,qEAAqE;QACrE,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACtD,CAAC;QACD,0EAA0E;QAC1E,6EAA6E;QAC7E,IACE,GAAG,CAAC,gBAAgB,KAAK,SAAS;YAClC,GAAG,CAAC,gBAAgB,KAAK,OAAO;YAChC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EACpC,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAC7C,CAAC;QAED,uEAAuE;QACvE,IAAI,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACnD,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type { Channel, ChannelKind, ScopeType, VerifiedIdentity, DeliveryTarget, DeliveryResult, DeliveryContext, DeliveryDecision, QuietHoursConfig, WakeupEnvelope, EncryptedBlob, PutResult, SettingStore, ChangedSettingMeta, ChangeCursorStore, RealtimeTransport, DeliveryPolicyResolver, } from "./types.js";
|
|
2
|
+
export { encodeWakeup, decodeWakeup, supportsChangeCursor } from "./types.js";
|
|
3
|
+
export { channelName, parseChannel, channelFor, authorizeSubscription, } from "./channel.js";
|
|
4
|
+
export { CalmDeliveryResolver, ALWAYS_DELIVER_TYPES, } from "./delivery-policy.js";
|
|
5
|
+
export { InMemorySettingStore } from "./setting-store.js";
|
|
6
|
+
export { PollTransport } from "./poll-transport.js";
|
|
7
|
+
export { NoopRealtimeTransport } from "./no-op-transport.js";
|
|
8
|
+
import type { RealtimeTransport } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Consuming app (Skybber) calls this at startup with its concrete transport
|
|
11
|
+
* (e.g. AppSyncEventsTransport). MUST run before buildEnv-consumers serve.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setRealtimeProvider(transport: RealtimeTransport): void;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the injected transport if a provider was registered, else the
|
|
16
|
+
* supplied fallback. `buildEnv` calls this with the default Poll/Noop transport.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveRealtimeTransport(fallback: RealtimeTransport): RealtimeTransport;
|
|
19
|
+
/** Test-only: clear the injected provider so tests don't leak across cases. */
|
|
20
|
+
export declare function __resetRealtimeProviderForTests(): void;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/index.ts"],"names":[],"mappings":"AAQA,YAAY,EAEV,OAAO,EACP,WAAW,EACX,SAAS,EAET,gBAAgB,EAEhB,cAAc,EACd,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,aAAa,EACb,SAAS,EACT,YAAY,EAEZ,kBAAkB,EAClB,iBAAiB,EAEjB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE9E,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAWpD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI,CAEtE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,iBAAiB,GAC1B,iBAAiB,CAEnB;AAED,+EAA+E;AAC/E,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes. See types.ts banner.
|
|
2
|
+
//
|
|
3
|
+
// Public barrel for the RealtimeTransport seam — the ONLY import path consumers
|
|
4
|
+
// use (published as `@de-otio/trellis/realtime`). Re-exports the frozen types,
|
|
5
|
+
// the channel helpers, the ports + defaults, the two in-core transports, and
|
|
6
|
+
// the provider-injection hook a consuming app (Skybber) uses to plug in its
|
|
7
|
+
// AppSyncEventsTransport WITHOUT core importing any AWS SDK.
|
|
8
|
+
export { encodeWakeup, decodeWakeup, supportsChangeCursor } from "./types.js";
|
|
9
|
+
export { channelName, parseChannel, channelFor, authorizeSubscription, } from "./channel.js";
|
|
10
|
+
export { CalmDeliveryResolver, ALWAYS_DELIVER_TYPES, } from "./delivery-policy.js";
|
|
11
|
+
export { InMemorySettingStore } from "./setting-store.js";
|
|
12
|
+
export { PollTransport } from "./poll-transport.js";
|
|
13
|
+
export { NoopRealtimeTransport } from "./no-op-transport.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Provider-injection hook (mirrors registerExtension). A consuming app calls
|
|
16
|
+
// setRealtimeProvider() at startup, BEFORE buildEnv-consumers serve, with its
|
|
17
|
+
// concrete transport. resolveRealtimeTransport() returns the injected transport
|
|
18
|
+
// if present, else the supplied fallback (the core Poll/Noop default).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
let injected;
|
|
21
|
+
/**
|
|
22
|
+
* Consuming app (Skybber) calls this at startup with its concrete transport
|
|
23
|
+
* (e.g. AppSyncEventsTransport). MUST run before buildEnv-consumers serve.
|
|
24
|
+
*/
|
|
25
|
+
export function setRealtimeProvider(transport) {
|
|
26
|
+
injected = transport;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the injected transport if a provider was registered, else the
|
|
30
|
+
* supplied fallback. `buildEnv` calls this with the default Poll/Noop transport.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveRealtimeTransport(fallback) {
|
|
33
|
+
return injected ?? fallback;
|
|
34
|
+
}
|
|
35
|
+
/** Test-only: clear the injected provider so tests don't leak across cases. */
|
|
36
|
+
export function __resetRealtimeProviderForTests() {
|
|
37
|
+
injected = undefined;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/realtime/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,4EAA4E;AAC5E,6DAA6D;AA6B7D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE9E,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAI7D,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,gFAAgF;AAChF,uEAAuE;AACvE,8EAA8E;AAE9E,IAAI,QAAuC,CAAC;AAE5C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAA4B;IAC9D,QAAQ,GAAG,SAAS,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAA2B;IAE3B,OAAO,QAAQ,IAAI,QAAQ,CAAC;AAC9B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,+BAA+B;IAC7C,QAAQ,GAAG,SAAS,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Channel, DeliveryPolicyResolver, DeliveryResult, DeliveryTarget, EncryptedBlob, PutResult, RealtimeTransport } from "./types.js";
|
|
2
|
+
export declare class NoopRealtimeTransport implements RealtimeTransport {
|
|
3
|
+
private readonly policy;
|
|
4
|
+
readonly kind: "noop";
|
|
5
|
+
constructor(policy: DeliveryPolicyResolver);
|
|
6
|
+
deliver(target: DeliveryTarget, channel: Channel, _payload: Uint8Array): Promise<DeliveryResult>;
|
|
7
|
+
getSetting(_userId: string, _namespace: string): Promise<EncryptedBlob | null>;
|
|
8
|
+
putSetting(_userId: string, _namespace: string, _blob: EncryptedBlob, _expectVersion: number): Promise<PutResult>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=no-op-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-op-transport.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/no-op-transport.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,OAAO,EAEP,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAepB,qBAAa,qBAAsB,YAAW,iBAAiB;IAGjD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;gBAEH,MAAM,EAAE,sBAAsB;IAErD,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,UAAU,GACnB,OAAO,CAAC,cAAc,CAAC;IAepB,UAAU,CACd,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAI1B,UAAU,CACd,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,aAAa,EACpB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,SAAS,CAAC;CAGtB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// CONTRACT-adjacent — the CI transport. See types.ts banner.
|
|
2
|
+
//
|
|
3
|
+
// NoopRealtimeTransport: the same shape as PollTransport but with NO store and
|
|
4
|
+
// NO wire. It still runs the policy fence inside deliver() (so the fence-runs-
|
|
5
|
+
// on-every-transport invariant holds), then drops. getSetting/putSetting are
|
|
6
|
+
// inert (no store). Used in tests/CI where neither a store nor a socket exists.
|
|
7
|
+
function fenceContextFor(target, channel) {
|
|
8
|
+
const type = channel.kind === "safety" ? "SAFETY_ALERT" : "SYSTEM";
|
|
9
|
+
return {
|
|
10
|
+
type,
|
|
11
|
+
recipientUserId: target.userId,
|
|
12
|
+
tenantId: target.tenantId,
|
|
13
|
+
now: new Date(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export class NoopRealtimeTransport {
|
|
17
|
+
policy;
|
|
18
|
+
kind = "noop";
|
|
19
|
+
constructor(policy) {
|
|
20
|
+
this.policy = policy;
|
|
21
|
+
}
|
|
22
|
+
async deliver(target, channel, _payload) {
|
|
23
|
+
// Best-effort: a transport-internal fault (e.g. a throwing resolver) is
|
|
24
|
+
// caught and surfaced as transport_error, never a reject that could roll
|
|
25
|
+
// back a persisted write (frozen contract §2.3).
|
|
26
|
+
try {
|
|
27
|
+
const decision = this.policy.decide(fenceContextFor(target, channel));
|
|
28
|
+
if (!decision.deliver) {
|
|
29
|
+
return { delivered: false, reason: "policy_denied" };
|
|
30
|
+
}
|
|
31
|
+
return { delivered: false, reason: "no_transport" };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { delivered: false, reason: "transport_error" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async getSetting(_userId, _namespace) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async putSetting(_userId, _namespace, _blob, _expectVersion) {
|
|
41
|
+
return { ok: false, reason: "not_found", current: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=no-op-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-op-transport.js","sourceRoot":"","sources":["../../../src/lib/realtime/no-op-transport.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,gFAAgF;AAahF,SAAS,eAAe,CACtB,MAAsB,EACtB,OAAgB;IAEhB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC;IACnE,OAAO;QACL,IAAI;QACJ,eAAe,EAAE,MAAM,CAAC,MAAM;QAC9B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,IAAI,IAAI,EAAE;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,qBAAqB;IAGH;IAFpB,IAAI,GAAG,MAAe,CAAC;IAEhC,YAA6B,MAA8B;QAA9B,WAAM,GAAN,MAAM,CAAwB;IAAG,CAAC;IAE/D,KAAK,CAAC,OAAO,CACX,MAAsB,EACtB,OAAgB,EAChB,QAAoB;QAEpB,wEAAwE;QACxE,yEAAyE;QACzE,iDAAiD;QACjD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YACtE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACtB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;YACvD,CAAC;YACD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACzD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CACd,OAAe,EACf,UAAkB;QAElB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,UAAU,CACd,OAAe,EACf,UAAkB,EAClB,KAAoB,EACpB,cAAsB;QAEtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3D,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Channel, DeliveryPolicyResolver, DeliveryResult, DeliveryTarget, EncryptedBlob, PutResult, RealtimeTransport, SettingStore } from "./types.js";
|
|
2
|
+
export declare class PollTransport implements RealtimeTransport {
|
|
3
|
+
private readonly store;
|
|
4
|
+
private readonly policy;
|
|
5
|
+
readonly kind: "poll";
|
|
6
|
+
constructor(store: SettingStore, policy: DeliveryPolicyResolver);
|
|
7
|
+
deliver(target: DeliveryTarget, channel: Channel, _payload: Uint8Array): Promise<DeliveryResult>;
|
|
8
|
+
getSetting(userId: string, namespace: string): Promise<EncryptedBlob | null>;
|
|
9
|
+
putSetting(userId: string, namespace: string, blob: EncryptedBlob, expectVersion: number): Promise<PutResult>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=poll-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-transport.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/poll-transport.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,OAAO,EAEP,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,YAAY,EACb,MAAM,YAAY,CAAC;AA0BpB,qBAAa,aAAc,YAAW,iBAAiB;IAInD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;gBAGb,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,sBAAsB;IAG3C,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,UAAU,GACnB,OAAO,CAAC,cAAc,CAAC;IAmB1B,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAI5E,UAAU,CACR,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,EACnB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,SAAS,CAAC;CAGtB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// CONTRACT-adjacent — the default transport. See types.ts banner.
|
|
2
|
+
//
|
|
3
|
+
// PollTransport makes core runnable and testable with NO realtime infra. It is
|
|
4
|
+
// a correctness default, explicitly NOT the scale answer.
|
|
5
|
+
//
|
|
6
|
+
// deliver() -> runs the policy fence, then NO-OPs the wire send and returns
|
|
7
|
+
// { delivered: false, reason: "no_transport" } (clients poll;
|
|
8
|
+
// the side-effecting persistence — the Notification row / the
|
|
9
|
+
// setting version bump — is what the next poll observes). The
|
|
10
|
+
// fence still runs so swapping in AppSync changes the pipe, not
|
|
11
|
+
// a single call site.
|
|
12
|
+
// getSetting / putSetting -> delegate to the injected SettingStore (the real
|
|
13
|
+
// REST-backed sync path).
|
|
14
|
+
/**
|
|
15
|
+
* Build the `DeliveryContext` the policy fence needs from a `deliver()` call.
|
|
16
|
+
* PollTransport has no notification metadata at the wire boundary (the floor
|
|
17
|
+
* decision was already made by the caller for persistence), so it constructs a
|
|
18
|
+
* minimal, fence-running context. The interface keeps payloads opaque, so the
|
|
19
|
+
* transport derives only routing-level inputs.
|
|
20
|
+
*/
|
|
21
|
+
function fenceContextFor(target, channel) {
|
|
22
|
+
// Map the channel kind back onto a NotificationType-shaped floor input: the
|
|
23
|
+
// "safety" kind is critical-always, everything else is best-effort. We do not
|
|
24
|
+
// have the original NotificationType here, so we synthesize the floor-relevant
|
|
25
|
+
// signal: a "safety" channel must never be suppressed.
|
|
26
|
+
const type = channel.kind === "safety" ? "SAFETY_ALERT" : "SYSTEM";
|
|
27
|
+
return {
|
|
28
|
+
type,
|
|
29
|
+
recipientUserId: target.userId,
|
|
30
|
+
tenantId: target.tenantId,
|
|
31
|
+
now: new Date(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export class PollTransport {
|
|
35
|
+
store;
|
|
36
|
+
policy;
|
|
37
|
+
kind = "poll";
|
|
38
|
+
constructor(store, policy) {
|
|
39
|
+
this.store = store;
|
|
40
|
+
this.policy = policy;
|
|
41
|
+
}
|
|
42
|
+
async deliver(target, channel, _payload) {
|
|
43
|
+
// deliver() is BEST-EFFORT: it MUST NOT reject in a way that could roll back
|
|
44
|
+
// a persisted write upstream. Any internal fault (e.g. a throwing resolver)
|
|
45
|
+
// is caught and surfaced as transport_error (frozen contract §2.3 + the
|
|
46
|
+
// realtime-transport.ts binding rules).
|
|
47
|
+
try {
|
|
48
|
+
// The policy fence runs on EVERY transport, even the no-op one.
|
|
49
|
+
const decision = this.policy.decide(fenceContextFor(target, channel));
|
|
50
|
+
if (!decision.deliver) {
|
|
51
|
+
return { delivered: false, reason: "policy_denied" };
|
|
52
|
+
}
|
|
53
|
+
// Poll model: there is no socket. The client learns of the change on its
|
|
54
|
+
// next poll, so the wire send is a deliberate no-op.
|
|
55
|
+
return { delivered: false, reason: "no_transport" };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { delivered: false, reason: "transport_error" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getSetting(userId, namespace) {
|
|
62
|
+
return this.store.get(userId, namespace);
|
|
63
|
+
}
|
|
64
|
+
putSetting(userId, namespace, blob, expectVersion) {
|
|
65
|
+
return this.store.put(userId, namespace, blob, expectVersion);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=poll-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-transport.js","sourceRoot":"","sources":["../../../src/lib/realtime/poll-transport.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,+EAA+E;AAC/E,0DAA0D;AAC1D,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,8EAA8E;AAC9E,8EAA8E;AAC9E,gFAAgF;AAChF,sCAAsC;AACtC,+EAA+E;AAC/E,0CAA0C;AAc1C;;;;;;GAMG;AACH,SAAS,eAAe,CACtB,MAAsB,EACtB,OAAgB;IAEhB,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,uDAAuD;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC;IACnE,OAAO;QACL,IAAI;QACJ,eAAe,EAAE,MAAM,CAAC,MAAM;QAC9B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,IAAI,IAAI,EAAE;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,aAAa;IAIL;IACA;IAJV,IAAI,GAAG,MAAe,CAAC;IAEhC,YACmB,KAAmB,EACnB,MAA8B;QAD9B,UAAK,GAAL,KAAK,CAAc;QACnB,WAAM,GAAN,MAAM,CAAwB;IAC9C,CAAC;IAEJ,KAAK,CAAC,OAAO,CACX,MAAsB,EACtB,OAAgB,EAChB,QAAoB;QAEpB,6EAA6E;QAC7E,4EAA4E;QAC5E,wEAAwE;QACxE,wCAAwC;QACxC,IAAI,CAAC;YACH,gEAAgE;YAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YACtE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACtB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;YACvD,CAAC;YACD,yEAAyE;YACzE,qDAAqD;YACrD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACzD,CAAC;IACH,CAAC;IAED,UAAU,CAAC,MAAc,EAAE,SAAiB;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC3C,CAAC;IAED,UAAU,CACR,MAAc,EACd,SAAiB,EACjB,IAAmB,EACnB,aAAqB;QAErB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;IAChE,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Logger } from "../logger.js";
|
|
2
|
+
import type { ChannelKind, RealtimeTransport } from "./types.js";
|
|
3
|
+
/** The kinds WS4 routes a content-free notification wakeup onto. */
|
|
4
|
+
export type WakeupKind = Extract<ChannelKind, "wakeup" | "safety">;
|
|
5
|
+
export interface PushNotifierInput {
|
|
6
|
+
/** Server-resolved recipient. */
|
|
7
|
+
target: {
|
|
8
|
+
userId: string;
|
|
9
|
+
tenantId: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Channel kind: ALWAYS_DELIVER notifications route to "safety" (the floor
|
|
13
|
+
* channel), everything else to "wakeup". Constrained to the two content-free
|
|
14
|
+
* kinds WS4 owns — there is no overload that accepts "message"/"thread".
|
|
15
|
+
*/
|
|
16
|
+
kind: WakeupKind;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the content-free wakeup payload for a notification. The envelope is the
|
|
20
|
+
* frozen WS1 `WakeupEnvelope` and carries NO notification content — only the
|
|
21
|
+
* envelope version and the channel kind. There is deliberately no `changeToken`
|
|
22
|
+
* for notification wakeups (that field is the setting_sync version pointer); a
|
|
23
|
+
* notification wakeup says only "something changed on this surface; refetch".
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildNotificationWakeup(kind: WakeupKind): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Relay a content-free wakeup over the realtime transport, best-effort.
|
|
28
|
+
*
|
|
29
|
+
* Resolves to `true` if the transport reported a delivery, `false` otherwise
|
|
30
|
+
* (policy-denied, no transport, transport error, or a thrown transport). NEVER
|
|
31
|
+
* throws — the caller's persisted write is durable regardless.
|
|
32
|
+
*/
|
|
33
|
+
export declare class PushNotifier {
|
|
34
|
+
private readonly transport;
|
|
35
|
+
private readonly logger;
|
|
36
|
+
constructor(transport: RealtimeTransport, logger: Logger);
|
|
37
|
+
notify(input: PushNotifierInput): Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=push-notifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-notifier.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/push-notifier.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAkB,MAAM,YAAY,CAAC;AAEjF,oEAAoE;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C;;;;OAIG;IACH,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAGpE;AAED;;;;;;GAMG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,MAAM;IAG3B,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;CA8BzD"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// WS4 — content-free push consumer.
|
|
2
|
+
//
|
|
3
|
+
// PushNotifier is the ONE place that turns a "deliver" decision into a wire
|
|
4
|
+
// wakeup. It exists so `notification-handler.ts` stays slim and so the
|
|
5
|
+
// content-free guarantee is structural, not a per-call-site discipline:
|
|
6
|
+
//
|
|
7
|
+
// 1. The payload is built ONLY via `encodeWakeup()` (the frozen WS1 envelope).
|
|
8
|
+
// WS4 is FORBIDDEN by the contract from constructing arbitrary Uint8Array
|
|
9
|
+
// for wakeup/setting_sync/safety kinds — there is no code path here that
|
|
10
|
+
// can put a title/body/data on the wire (types.ts §2.4).
|
|
11
|
+
// 2. The channel is built ONLY via `channelFor()` — tenant- and user-scoped,
|
|
12
|
+
// server-resolved, never client-asserted.
|
|
13
|
+
// 3. `transport.deliver()` is BEST-EFFORT: a transport throw is caught and
|
|
14
|
+
// logged, NEVER rethrown, so it can never roll back the already-persisted
|
|
15
|
+
// Notification row. Polling remains the floor.
|
|
16
|
+
//
|
|
17
|
+
// The policy fence (CalmDeliveryResolver floor) runs in TWO places by design:
|
|
18
|
+
// the caller gates on its decision (so a deferred/blocked/preference-off
|
|
19
|
+
// notification never reaches here), AND the transport re-runs the fence inside
|
|
20
|
+
// `deliver()`. PushNotifier itself does not re-decide — it relays the decision
|
|
21
|
+
// the caller already made onto the correct channel kind.
|
|
22
|
+
import { channelFor } from "./channel.js";
|
|
23
|
+
import { encodeWakeup } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Build the content-free wakeup payload for a notification. The envelope is the
|
|
26
|
+
* frozen WS1 `WakeupEnvelope` and carries NO notification content — only the
|
|
27
|
+
* envelope version and the channel kind. There is deliberately no `changeToken`
|
|
28
|
+
* for notification wakeups (that field is the setting_sync version pointer); a
|
|
29
|
+
* notification wakeup says only "something changed on this surface; refetch".
|
|
30
|
+
*/
|
|
31
|
+
export function buildNotificationWakeup(kind) {
|
|
32
|
+
const envelope = { v: 1, kind };
|
|
33
|
+
return encodeWakeup(envelope);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Relay a content-free wakeup over the realtime transport, best-effort.
|
|
37
|
+
*
|
|
38
|
+
* Resolves to `true` if the transport reported a delivery, `false` otherwise
|
|
39
|
+
* (policy-denied, no transport, transport error, or a thrown transport). NEVER
|
|
40
|
+
* throws — the caller's persisted write is durable regardless.
|
|
41
|
+
*/
|
|
42
|
+
export class PushNotifier {
|
|
43
|
+
transport;
|
|
44
|
+
logger;
|
|
45
|
+
constructor(transport, logger) {
|
|
46
|
+
this.transport = transport;
|
|
47
|
+
this.logger = logger;
|
|
48
|
+
}
|
|
49
|
+
async notify(input) {
|
|
50
|
+
const { target, kind } = input;
|
|
51
|
+
const channel = channelFor(kind, {
|
|
52
|
+
tenantId: target.tenantId,
|
|
53
|
+
userId: target.userId,
|
|
54
|
+
});
|
|
55
|
+
const payload = buildNotificationWakeup(kind);
|
|
56
|
+
try {
|
|
57
|
+
const result = await this.transport.deliver({ userId: target.userId, tenantId: target.tenantId }, channel, payload);
|
|
58
|
+
if (!result.delivered) {
|
|
59
|
+
// Not an error — poll/no-transport/policy are normal outcomes. Logged
|
|
60
|
+
// at debug so the absence of a push is observable without noise.
|
|
61
|
+
this.logger.debug("realtime wakeup not delivered", {
|
|
62
|
+
reason: result.reason,
|
|
63
|
+
kind,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return result.delivered;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// BEST-EFFORT: a transport hiccup must never surface to the caller. The
|
|
70
|
+
// notification row is already persisted; the next poll delivers it.
|
|
71
|
+
this.logger.warn("realtime wakeup deliver threw (non-fatal)", err);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=push-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-notifier.js","sourceRoot":"","sources":["../../../src/lib/realtime/push-notifier.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,EAAE;AACF,iFAAiF;AACjF,+EAA+E;AAC/E,8EAA8E;AAC9E,8DAA8D;AAC9D,+EAA+E;AAC/E,+CAA+C;AAC/C,6EAA6E;AAC7E,+EAA+E;AAC/E,oDAAoD;AACpD,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAC/E,+EAA+E;AAC/E,yDAAyD;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAiB1C;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAgB;IACtD,MAAM,QAAQ,GAAmB,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IAChD,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IAEJ;IACA;IAFnB,YACmB,SAA4B,EAC5B,MAAc;QADd,cAAS,GAAT,SAAS,CAAmB;QAC5B,WAAM,GAAN,MAAM,CAAQ;IAC9B,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAwB;QACnC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE;YAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;QAE9C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CACzC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EACpD,OAAO,EACP,OAAO,CACR,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBACtB,sEAAsE;gBACtE,iEAAiE;gBACjE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE;oBACjD,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,IAAI;iBACL,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC,SAAS,CAAC;QAC1B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,oEAAoE;YACpE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2CAA2C,EAAE,GAAG,CAAC,CAAC;YACnE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-transport.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/realtime-transport.ts"],"names":[],"mappings":"AAsBA,YAAY,EACV,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,OAAO,EACP,aAAa,EACb,SAAS,EACT,YAAY,GACb,MAAM,YAAY,CAAC"}
|