@de-otio/trellis 0.10.10 → 0.11.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 +64 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +66 -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/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/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/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/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/tenant-scope.d.ts.map +1 -1
- package/dist/lib/tenant-scope.js +2 -0
- package/dist/lib/tenant-scope.js.map +1 -1
- package/package.json +22 -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/schema.prisma +40 -0
|
@@ -8,12 +8,113 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createPrisma } from "../db.js";
|
|
10
10
|
import { getLogger } from "./logger.js";
|
|
11
|
-
|
|
11
|
+
import { CalmDeliveryResolver } from "./realtime/index.js";
|
|
12
|
+
import { PushNotifier } from "./realtime/push-notifier.js";
|
|
13
|
+
import { PrismaBlockStore, } from "./realtime/block-store.js";
|
|
14
|
+
/**
|
|
15
|
+
* Notification types that always bypass preferences and quiet hours.
|
|
16
|
+
* Kept here for the preference-gate short-circuit (preference-off => no row),
|
|
17
|
+
* which is a DIFFERENT outcome from quiet-hours suppression. The deliver-time
|
|
18
|
+
* decision (deliveredAt) is owned by the shared `DeliveryPolicyResolver` (WS1
|
|
19
|
+
* `CalmDeliveryResolver`), which also carries the ALWAYS_DELIVER bypass.
|
|
20
|
+
*/
|
|
12
21
|
const ALWAYS_DELIVER_TYPES = [
|
|
13
22
|
"SAFETY_ALERT",
|
|
14
23
|
"PARENTAL_LINK",
|
|
15
24
|
];
|
|
25
|
+
/**
|
|
26
|
+
* Extract a candidate sender id from a notification's `data` payload, for the
|
|
27
|
+
* blocked-sender floor input (`DeliveryContext.senderUserId`). Notification
|
|
28
|
+
* producers conventionally stamp the actor under one of these keys; we read it
|
|
29
|
+
* defensively (the payload is untyped JSON) and return `undefined` when absent
|
|
30
|
+
* or non-string. This NEVER trusts the value as a security boundary — it is
|
|
31
|
+
* only an input to the floor's blocked-sender check.
|
|
32
|
+
*/
|
|
33
|
+
function extractSenderUserId(data) {
|
|
34
|
+
if (typeof data !== "object" || data === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const obj = data;
|
|
37
|
+
for (const key of ["senderUserId", "senderId", "actorId", "fromUserId"]) {
|
|
38
|
+
const v = obj[key];
|
|
39
|
+
if (typeof v === "string" && v.length > 0)
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the realtime `DeliveryContext` from the recipient row.
|
|
46
|
+
*
|
|
47
|
+
* The resolver's quiet-hours window uses `QuietHoursConfig.start/end` as
|
|
48
|
+
* decimal strings of minutes-since-midnight — the exact units the existing
|
|
49
|
+
* `User.quietHoursStart/End` integers use — and reads the current minute from
|
|
50
|
+
* `ctx.now`, so the decision is identical to the legacy inline check while
|
|
51
|
+
* remaining deterministic.
|
|
52
|
+
*
|
|
53
|
+
* Track D enriches the context with `recipientAgeTier` (minor-protection floor)
|
|
54
|
+
* and `senderUserId` (blocked-sender floor) so the non-configurable floor in
|
|
55
|
+
* `CalmDeliveryResolver` has the inputs it needs. The resolver is pure/sync, so
|
|
56
|
+
* the async block-set lookup happens in the caller: `blockedSenderUserId` is the
|
|
57
|
+
* sender id ONLY when the recipient has blocked that sender (else undefined),
|
|
58
|
+
* matching the resolver's "presence of senderUserId == in the block set"
|
|
59
|
+
* contract. For ALWAYS_DELIVER types the recipient row is not fetched (the
|
|
60
|
+
* bypass short-circuits before the user lookup), so `user` is null and the
|
|
61
|
+
* enrichment fields are omitted — those types bypass the floor regardless.
|
|
62
|
+
*/
|
|
63
|
+
function buildDeliveryContext(type, recipientUserId, tenantId, now, user, blockedSenderUserId) {
|
|
64
|
+
let quietHours = null;
|
|
65
|
+
if (user?.quietHoursEnabled &&
|
|
66
|
+
user.quietHoursStart != null &&
|
|
67
|
+
user.quietHoursEnd != null) {
|
|
68
|
+
quietHours = {
|
|
69
|
+
enabled: true,
|
|
70
|
+
start: String(user.quietHoursStart),
|
|
71
|
+
end: String(user.quietHoursEnd),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const ctx = {
|
|
75
|
+
type,
|
|
76
|
+
recipientUserId,
|
|
77
|
+
tenantId,
|
|
78
|
+
now,
|
|
79
|
+
quietHours,
|
|
80
|
+
};
|
|
81
|
+
if (user)
|
|
82
|
+
ctx.recipientAgeTier = user.ageTier;
|
|
83
|
+
if (blockedSenderUserId !== undefined)
|
|
84
|
+
ctx.senderUserId = blockedSenderUserId;
|
|
85
|
+
return ctx;
|
|
86
|
+
}
|
|
16
87
|
export class NotificationHandler {
|
|
88
|
+
// The delivery floor is migrated into this shared resolver (WS1). The SAME
|
|
89
|
+
// decision drives both the persistence `deliveredAt` choice and the
|
|
90
|
+
// (default-off) push hand-off, so polling and pushing can never diverge.
|
|
91
|
+
// Injectable so tests can exercise floor outcomes (blocked_sender, etc.)
|
|
92
|
+
// without reaching into WS1-owned floor logic; defaults to the WS1 floor.
|
|
93
|
+
// Injected resolver (tests) or null => build the default from env so the
|
|
94
|
+
// re-engagement denylist (runtime config) reaches the floor. The SAME
|
|
95
|
+
// decision drives both the persistence `deliveredAt`/drop choice and the
|
|
96
|
+
// (default-off) push hand-off, so polling and pushing can never diverge.
|
|
97
|
+
injectedResolver;
|
|
98
|
+
// Block-set port for the blocked-sender floor. Injectable so tests exercise
|
|
99
|
+
// the drop without a live DB; defaults to a Prisma-backed store bound to the
|
|
100
|
+
// per-call client. `null` = resolve from env's db (the production path).
|
|
101
|
+
blockStore;
|
|
102
|
+
constructor(deliveryResolver = null, blockStore = null) {
|
|
103
|
+
this.injectedResolver = deliveryResolver;
|
|
104
|
+
this.blockStore = blockStore;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The resolver for this call: the injected one (tests) or a default built
|
|
108
|
+
* from env's runtime re-engagement denylist (threshold-secrecy: the denylist
|
|
109
|
+
* is env-driven config, never a compiled-in constant).
|
|
110
|
+
*/
|
|
111
|
+
resolverFor(env) {
|
|
112
|
+
if (this.injectedResolver)
|
|
113
|
+
return this.injectedResolver;
|
|
114
|
+
return new CalmDeliveryResolver({
|
|
115
|
+
reengagementTypes: env.REALTIME_REENGAGEMENT_TYPES,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
17
118
|
/**
|
|
18
119
|
* Create a notification for a user.
|
|
19
120
|
* Checks preferences (unless SAFETY_ALERT or PARENTAL_LINK) and quiet hours.
|
|
@@ -40,22 +141,58 @@ export class NotificationHandler {
|
|
|
40
141
|
}
|
|
41
142
|
}
|
|
42
143
|
}
|
|
43
|
-
// 3.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
144
|
+
// 3. Resolve the delivery decision (quiet hours + ALWAYS_DELIVER bypass)
|
|
145
|
+
// via the shared policy resolver. Capture `now` once so the context
|
|
146
|
+
// clock and the persisted deliveredAt agree.
|
|
147
|
+
const now = new Date();
|
|
148
|
+
const user = bypassPreferences
|
|
149
|
+
? null
|
|
150
|
+
: await db.user.findUnique({
|
|
47
151
|
where: { id: userId },
|
|
48
152
|
select: {
|
|
49
153
|
quietHoursEnabled: true,
|
|
50
154
|
quietHoursStart: true,
|
|
51
155
|
quietHoursEnd: true,
|
|
156
|
+
ageTier: true,
|
|
52
157
|
},
|
|
53
158
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
159
|
+
// Enrich the floor inputs (Track D): blocked-sender comes from the
|
|
160
|
+
// notification payload's actor stamp, resolved against the block set;
|
|
161
|
+
// minor-protection comes from the recipient's ageTier loaded above.
|
|
162
|
+
// ALWAYS_DELIVER types bypass the floor, so we skip the block lookup for
|
|
163
|
+
// them (no row was fetched and the resolver short-circuits anyway).
|
|
164
|
+
const candidateSenderUserId = extractSenderUserId(data);
|
|
165
|
+
let blockedSenderUserId;
|
|
166
|
+
if (!bypassPreferences && candidateSenderUserId !== undefined) {
|
|
167
|
+
const blockStore = this.blockStore ?? new PrismaBlockStore(db);
|
|
168
|
+
// The recipient (userId) is the blocker; the notification's sender is
|
|
169
|
+
// the blocked candidate. Presence in the set => pass the id to the
|
|
170
|
+
// resolver as the deny signal.
|
|
171
|
+
const blocked = await blockStore.isBlocked(tenantId, userId, candidateSenderUserId);
|
|
172
|
+
if (blocked)
|
|
173
|
+
blockedSenderUserId = candidateSenderUserId;
|
|
174
|
+
}
|
|
175
|
+
const deliveryContext = buildDeliveryContext(type, userId, tenantId, now, user, blockedSenderUserId);
|
|
176
|
+
const decision = this.resolverFor(env).decide(deliveryContext);
|
|
177
|
+
// 4. Floor DROP vs DEFERRAL. A floor drop (blocked_sender / minor-floor)
|
|
178
|
+
// is a hard suppression — no row at all (same observable as a
|
|
179
|
+
// preference-off skip: `{ id: "" }`), so the dropped notification can
|
|
180
|
+
// never be read back via polling. A quiet-hours deferral keeps the row
|
|
181
|
+
// with deliveredAt=null (delivered on a later poll). This single
|
|
182
|
+
// decision also gates the push hand-off below, so polling and pushing
|
|
183
|
+
// can never diverge.
|
|
184
|
+
if (!decision.deliver &&
|
|
185
|
+
(decision.reason === "blocked_sender" || decision.reason === "floor")) {
|
|
186
|
+
logger.info("Notification dropped by delivery floor", {
|
|
187
|
+
userId,
|
|
188
|
+
type,
|
|
189
|
+
reason: decision.reason,
|
|
190
|
+
});
|
|
191
|
+
return { id: "" };
|
|
58
192
|
}
|
|
193
|
+
// deliveredAt: now when the resolver says deliver, null when deferred
|
|
194
|
+
// (quiet hours). ALWAYS_DELIVER types resolve to deliver => now.
|
|
195
|
+
const deliveredAt = decision.deliver ? now : null;
|
|
59
196
|
// 5. Create notification
|
|
60
197
|
const notification = await db.notification.create({
|
|
61
198
|
data: {
|
|
@@ -68,6 +205,21 @@ export class NotificationHandler {
|
|
|
68
205
|
tenantId,
|
|
69
206
|
},
|
|
70
207
|
});
|
|
208
|
+
// 6. Content-free push hand-off (default OFF — gated by
|
|
209
|
+
// features.realtimePush; WS4 owns this). Best-effort and non-fatal: a
|
|
210
|
+
// transport failure never blocks or rolls back persistence (polling
|
|
211
|
+
// still delivers on the next poll). We gate here on the SAME decision
|
|
212
|
+
// that set deliveredAt, so polling and pushing can never diverge; the
|
|
213
|
+
// policy fence ALSO re-runs inside the transport's deliver().
|
|
214
|
+
// ALWAYS_DELIVER types route to the floor "safety" channel; everything
|
|
215
|
+
// else to "wakeup". The payload is content-free by construction — it is
|
|
216
|
+
// built only via encodeWakeup() inside PushNotifier, so no title/body/
|
|
217
|
+
// data can ever reach the wire.
|
|
218
|
+
if (env.features?.realtimePush && decision.deliver) {
|
|
219
|
+
const kind = bypassPreferences ? "safety" : "wakeup";
|
|
220
|
+
const pushNotifier = new PushNotifier(env.realtimeTransport, logger);
|
|
221
|
+
await pushNotifier.notify({ target: { userId, tenantId }, kind });
|
|
222
|
+
}
|
|
71
223
|
return { id: notification.id };
|
|
72
224
|
}
|
|
73
225
|
catch (error) {
|
|
@@ -203,26 +355,6 @@ export class NotificationHandler {
|
|
|
203
355
|
return true;
|
|
204
356
|
}
|
|
205
357
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Check if current time falls within user's quiet hours.
|
|
208
|
-
*/
|
|
209
|
-
isInQuietHours(user) {
|
|
210
|
-
if (!user.quietHoursEnabled ||
|
|
211
|
-
user.quietHoursStart == null ||
|
|
212
|
-
user.quietHoursEnd == null) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const now = new Date();
|
|
216
|
-
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
|
|
217
|
-
const start = user.quietHoursStart;
|
|
218
|
-
const end = user.quietHoursEnd;
|
|
219
|
-
// Handle overnight quiet hours (e.g., 22:00 to 07:00)
|
|
220
|
-
if (start > end) {
|
|
221
|
-
return minutesSinceMidnight >= start || minutesSinceMidnight < end;
|
|
222
|
-
}
|
|
223
|
-
// Same-day quiet hours (e.g., 13:00 to 15:00)
|
|
224
|
-
return minutesSinceMidnight >= start && minutesSinceMidnight < end;
|
|
225
|
-
}
|
|
226
358
|
}
|
|
227
359
|
export class NotificationNotFoundError extends Error {
|
|
228
360
|
constructor(notificationId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-handler.js","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,SAAS,EAAU,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"notification-handler.js","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal block-list port the delivery floor consults. Mirrors the
|
|
3
|
+
* `SettingStore` port style: one small interface, an in-memory default for
|
|
4
|
+
* zero-infra tests, and a structural Prisma-backed production implementation.
|
|
5
|
+
*
|
|
6
|
+
* `isBlocked(tenantId, blockerId, blockedId)` answers "within `tenantId`, has
|
|
7
|
+
* `blockerId` blocked `blockedId`?" — for the floor, the recipient is the
|
|
8
|
+
* blocker and the notification's sender is the blocked candidate.
|
|
9
|
+
*/
|
|
10
|
+
export interface BlockStore {
|
|
11
|
+
isBlocked(tenantId: string, blockerId: string, blockedId: string): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* In-memory `BlockStore` for zero-infra tests and the core default. Holds a set
|
|
15
|
+
* of `tenantId \0 blockerId \0 blockedId` keys. Mirrors `InMemorySettingStore`.
|
|
16
|
+
*/
|
|
17
|
+
export declare class InMemoryBlockStore implements BlockStore {
|
|
18
|
+
private readonly blocks;
|
|
19
|
+
private key;
|
|
20
|
+
/** Test/seed helper: record a directed block edge. */
|
|
21
|
+
block(tenantId: string, blockerId: string, blockedId: string): void;
|
|
22
|
+
isBlocked(tenantId: string, blockerId: string, blockedId: string): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
/** The row shape (id only) this store reads back from Postgres. */
|
|
25
|
+
interface BlockedUserRow {
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The minimal Prisma surface this store needs. Declared structurally (like
|
|
30
|
+
* `EncryptedUserSettingDelegate`) so the store is unit-testable against a mock
|
|
31
|
+
* without importing the generated client, and is decoupled from Prisma version
|
|
32
|
+
* drift.
|
|
33
|
+
*/
|
|
34
|
+
export interface BlockedUserDelegate {
|
|
35
|
+
findUnique(args: {
|
|
36
|
+
where: {
|
|
37
|
+
tenantId_blockerId_blockedId: {
|
|
38
|
+
tenantId: string;
|
|
39
|
+
blockerId: string;
|
|
40
|
+
blockedId: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
select: {
|
|
44
|
+
id: true;
|
|
45
|
+
};
|
|
46
|
+
}): Promise<BlockedUserRow | null>;
|
|
47
|
+
}
|
|
48
|
+
export interface PrismaWithBlockedUser {
|
|
49
|
+
blockedUser: BlockedUserDelegate;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Production `BlockStore` backed by the `blocked_users` table. A single indexed
|
|
53
|
+
* unique lookup on `[tenantId, blockerId, blockedId]` — `null` row ⇒ not blocked.
|
|
54
|
+
*/
|
|
55
|
+
export declare class PrismaBlockStore implements BlockStore {
|
|
56
|
+
private readonly db;
|
|
57
|
+
constructor(db: PrismaWithBlockedUser);
|
|
58
|
+
isBlocked(tenantId: string, blockerId: string, blockedId: string): Promise<boolean>;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=block-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-store.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/block-store.ts"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,UAAU;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAE5C,OAAO,CAAC,GAAG;IAIX,sDAAsD;IACtD,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAI7D,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;CAGpB;AAED,mEAAmE;AACnE,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,IAAI,EAAE;QACf,KAAK,EAAE;YACL,4BAA4B,EAAE;gBAC5B,QAAQ,EAAE,MAAM,CAAC;gBACjB,SAAS,EAAE,MAAM,CAAC;gBAClB,SAAS,EAAE,MAAM,CAAC;aACnB,CAAC;SACH,CAAC;QACF,MAAM,EAAE;YAAE,EAAE,EAAE,IAAI,CAAA;SAAE,CAAC;KACtB,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,UAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEhD,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;CASpB"}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-store.js","sourceRoot":"","sources":["../../../src/lib/realtime/block-store.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,kEAAkE;AAClE,EAAE;AACF,gFAAgF;AAChF,yBAAyB;AAmBzB;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACZ,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,GAAG,CAAC,QAAgB,EAAE,SAAiB,EAAE,SAAiB;QAChE,OAAO,GAAG,QAAQ,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;IACjD,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,QAAgB,EAAE,SAAiB,EAAE,SAAiB;QAC1D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,SAAS,CACb,QAAgB,EAChB,SAAiB,EACjB,SAAiB;QAEjB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;IACnE,CAAC;CACF;AA8BD;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IACE;IAA7B,YAA6B,EAAyB;QAAzB,OAAE,GAAF,EAAE,CAAuB;IAAG,CAAC;IAE1D,KAAK,CAAC,SAAS,CACb,QAAgB,EAChB,SAAiB,EACjB,SAAiB;QAEjB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC;YAC/C,KAAK,EAAE;gBACL,4BAA4B,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;aACjE;YACD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;SACrB,CAAC,CAAC;QACH,OAAO,GAAG,KAAK,IAAI,CAAC;IACtB,CAAC;CACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Channel, ChannelKind, VerifiedIdentity } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Produce the canonical channel path. This is the ONLY way a channel string is
|
|
4
|
+
* minted server-side; the Skybber `@skybber/realtime-channels` parser must
|
|
5
|
+
* round-trip this output.
|
|
6
|
+
*/
|
|
7
|
+
export declare function channelName(c: Channel): string;
|
|
8
|
+
/**
|
|
9
|
+
* Parse a canonical channel path. Returns `null` on ANY malformed input — a
|
|
10
|
+
* leading-slash-less path, wrong arity, unknown kind/scopeType, or an empty
|
|
11
|
+
* segment.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseChannel(path: string): Channel | null;
|
|
14
|
+
/**
|
|
15
|
+
* Build a user-scoped channel (v1: the only scope). `kind` is constrained to
|
|
16
|
+
* the v1 user-scoped kinds; `message`/`thread` are deferred and excluded.
|
|
17
|
+
*/
|
|
18
|
+
export declare function channelFor(kind: Exclude<ChannelKind, "message" | "thread">, scope: {
|
|
19
|
+
tenantId: string;
|
|
20
|
+
userId: string;
|
|
21
|
+
}): Channel;
|
|
22
|
+
/**
|
|
23
|
+
* THE security boundary. The channel is a client *assertion* this checks
|
|
24
|
+
* against a server-verified identity.
|
|
25
|
+
*
|
|
26
|
+
* ALLOW iff `c.tenantId === id.tenantId` AND
|
|
27
|
+
* scopeType "user" -> `c.scopeId === id.userId`
|
|
28
|
+
* scopeType conversation/thread -> membership(id.userId, c.scopeId) [v1: false]
|
|
29
|
+
*
|
|
30
|
+
* End-user PUBLISH is ALWAYS denied elsewhere (publish is server-only via IAM);
|
|
31
|
+
* this function governs SUBSCRIBE only.
|
|
32
|
+
*/
|
|
33
|
+
export declare function authorizeSubscription(id: VerifiedIdentity, c: Channel): boolean;
|
|
34
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/channel.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EAEX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAsBpB;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAW9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAezD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,SAAS,GAAG,QAAQ,CAAC,EAChD,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAOT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,gBAAgB,EACpB,CAAC,EAAE,OAAO,GACT,OAAO,CAOT"}
|
|
@@ -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":"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"}
|
|
@@ -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"}
|