@de-otio/trellis 0.10.11 → 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.
Files changed (85) hide show
  1. package/dist/env.d.ts +64 -0
  2. package/dist/env.d.ts.map +1 -1
  3. package/dist/env.js +66 -0
  4. package/dist/env.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/app.d.ts.map +1 -1
  10. package/dist/lib/app.js +5 -0
  11. package/dist/lib/app.js.map +1 -1
  12. package/dist/lib/encrypted-settings/config.d.ts +13 -0
  13. package/dist/lib/encrypted-settings/config.d.ts.map +1 -0
  14. package/dist/lib/encrypted-settings/config.js +19 -0
  15. package/dist/lib/encrypted-settings/config.js.map +1 -0
  16. package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts +57 -0
  17. package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts.map +1 -0
  18. package/dist/lib/encrypted-settings/encrypted-settings-handler.js +178 -0
  19. package/dist/lib/encrypted-settings/encrypted-settings-handler.js.map +1 -0
  20. package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts +110 -0
  21. package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts.map +1 -0
  22. package/dist/lib/encrypted-settings/encrypted-settings-store.js +103 -0
  23. package/dist/lib/encrypted-settings/encrypted-settings-store.js.map +1 -0
  24. package/dist/lib/encrypted-settings/types.d.ts +26 -0
  25. package/dist/lib/encrypted-settings/types.d.ts.map +1 -0
  26. package/dist/lib/encrypted-settings/types.js +27 -0
  27. package/dist/lib/encrypted-settings/types.js.map +1 -0
  28. package/dist/lib/notification-handler.d.ts +11 -4
  29. package/dist/lib/notification-handler.d.ts.map +1 -1
  30. package/dist/lib/notification-handler.js +161 -29
  31. package/dist/lib/notification-handler.js.map +1 -1
  32. package/dist/lib/realtime/block-store.d.ts +61 -0
  33. package/dist/lib/realtime/block-store.d.ts.map +1 -0
  34. package/dist/lib/realtime/block-store.js +0 -0
  35. package/dist/lib/realtime/block-store.js.map +1 -0
  36. package/dist/lib/realtime/channel.d.ts +34 -0
  37. package/dist/lib/realtime/channel.d.ts.map +1 -0
  38. package/dist/lib/realtime/channel.js +100 -0
  39. package/dist/lib/realtime/channel.js.map +1 -0
  40. package/dist/lib/realtime/delivery-policy.d.ts +51 -0
  41. package/dist/lib/realtime/delivery-policy.d.ts.map +1 -0
  42. package/dist/lib/realtime/delivery-policy.js +98 -0
  43. package/dist/lib/realtime/delivery-policy.js.map +1 -0
  44. package/dist/lib/realtime/index.d.ts +21 -0
  45. package/dist/lib/realtime/index.d.ts.map +1 -0
  46. package/dist/lib/realtime/index.js +39 -0
  47. package/dist/lib/realtime/index.js.map +1 -0
  48. package/dist/lib/realtime/no-op-transport.d.ts +10 -0
  49. package/dist/lib/realtime/no-op-transport.d.ts.map +1 -0
  50. package/dist/lib/realtime/no-op-transport.js +44 -0
  51. package/dist/lib/realtime/no-op-transport.js.map +1 -0
  52. package/dist/lib/realtime/poll-transport.d.ts +11 -0
  53. package/dist/lib/realtime/poll-transport.d.ts.map +1 -0
  54. package/dist/lib/realtime/poll-transport.js +68 -0
  55. package/dist/lib/realtime/poll-transport.js.map +1 -0
  56. package/dist/lib/realtime/push-notifier.d.ts +39 -0
  57. package/dist/lib/realtime/push-notifier.d.ts.map +1 -0
  58. package/dist/lib/realtime/push-notifier.js +76 -0
  59. package/dist/lib/realtime/push-notifier.js.map +1 -0
  60. package/dist/lib/realtime/realtime-transport.d.ts +2 -0
  61. package/dist/lib/realtime/realtime-transport.d.ts.map +1 -0
  62. package/dist/lib/realtime/realtime-transport.js +23 -0
  63. package/dist/lib/realtime/realtime-transport.js.map +1 -0
  64. package/dist/lib/realtime/setting-store.d.ts +30 -0
  65. package/dist/lib/realtime/setting-store.d.ts.map +1 -0
  66. package/dist/lib/realtime/setting-store.js +0 -0
  67. package/dist/lib/realtime/setting-store.js.map +1 -0
  68. package/dist/lib/realtime/types.d.ts +200 -0
  69. package/dist/lib/realtime/types.d.ts.map +1 -0
  70. package/dist/lib/realtime/types.js +61 -0
  71. package/dist/lib/realtime/types.js.map +1 -0
  72. package/dist/lib/routes/index.d.ts.map +1 -1
  73. package/dist/lib/routes/index.js +3 -0
  74. package/dist/lib/routes/index.js.map +1 -1
  75. package/dist/lib/routes/settings.d.ts +17 -0
  76. package/dist/lib/routes/settings.d.ts.map +1 -0
  77. package/dist/lib/routes/settings.js +187 -0
  78. package/dist/lib/routes/settings.js.map +1 -0
  79. package/dist/lib/tenant-scope.d.ts.map +1 -1
  80. package/dist/lib/tenant-scope.js +2 -0
  81. package/dist/lib/tenant-scope.js.map +1 -1
  82. package/package.json +22 -22
  83. package/prisma/migrations/20260620051144_add_encrypted_user_settings/migration.sql +24 -0
  84. package/prisma/migrations/20260620120000_add_blocked_users/migration.sql +29 -0
  85. package/prisma/schema.prisma +40 -0
@@ -0,0 +1,178 @@
1
+ // WS5 — EncryptedSettingsHandler: server-blind state-sync boundary.
2
+ //
3
+ // Owns request-boundary validation (namespace allowlist, size cap, Zod body),
4
+ // optimistic-concurrency mapping (CAS -> 200 / 409-with-current), the idle-client
5
+ // 304 fast path, and the publish-on-change content-free wakeup.
6
+ //
7
+ // SERVER-BLIND INVARIANT: the handler treats `ciphertext` as an opaque string.
8
+ // It never JSON-parses it, never logs it, and never puts it on the realtime wire
9
+ // (the wakeup carries only `version` as the changeToken). The blob travels ONLY
10
+ // over the authenticated REST GET. Asserted by the unit tests.
11
+ import { z } from "zod";
12
+ import { channelFor } from "../realtime/channel.js";
13
+ import { encodeWakeup } from "../realtime/types.js";
14
+ import { getLogger } from "../logger.js";
15
+ import { supportsChangeCursor } from "../realtime/types.js";
16
+ import { BlobTooLargeError, UnknownNamespaceError } from "./types.js";
17
+ /** PUT body: opaque ciphertext + the version the client believes it is editing. */
18
+ const putBodySchema = z.object({
19
+ ciphertext: z.string(),
20
+ // expectVersion: 0 for a first write, else the version last seen.
21
+ expectVersion: z.number().int().nonnegative(),
22
+ });
23
+ const JSON_HEADERS = { "content-type": "application/json" };
24
+ function json(body, status, extraHeaders) {
25
+ return new Response(JSON.stringify(body), {
26
+ status,
27
+ headers: { ...JSON_HEADERS, ...extraHeaders },
28
+ });
29
+ }
30
+ /**
31
+ * The change-token / ETag for a blob is just its `version` rendered as a string.
32
+ * It leaks only THAT/WHEN a setting changed (version is already plaintext sync
33
+ * metadata per the frozen contract), never WHAT.
34
+ */
35
+ function changeTokenFor(version) {
36
+ return String(version);
37
+ }
38
+ export class EncryptedSettingsHandler {
39
+ store;
40
+ config;
41
+ transport;
42
+ constructor(store, config, transport) {
43
+ this.store = store;
44
+ this.config = config;
45
+ this.transport = transport;
46
+ }
47
+ assertNamespaceAllowed(namespace) {
48
+ if (!this.config.allowedNamespaces.has(namespace)) {
49
+ throw new UnknownNamespaceError(namespace);
50
+ }
51
+ }
52
+ /**
53
+ * GET /api/settings/:namespace. Session-scoped to `userId`.
54
+ * - unknown namespace -> 404
55
+ * - matching If-None-Match (the version) -> 304, empty body (idle fast path)
56
+ * - no blob yet -> 404
57
+ * - else -> 200 { ciphertext, version, updatedAt } + ETag: <version>
58
+ */
59
+ async handleGet(userId, namespace, ifNoneMatch) {
60
+ this.assertNamespaceAllowed(namespace);
61
+ const blob = await this.store.get(userId, namespace);
62
+ if (!blob) {
63
+ return json({ error: "NOT_FOUND", message: "No setting blob" }, 404);
64
+ }
65
+ const token = changeTokenFor(blob.version);
66
+ if (ifNoneMatch !== null && ifNoneMatch === token) {
67
+ // Idle-client fast path: nothing changed, return an empty 304.
68
+ return new Response(null, { status: 304, headers: { ETag: token } });
69
+ }
70
+ return json({
71
+ ciphertext: blob.ciphertext,
72
+ version: blob.version,
73
+ updatedAt: blob.updatedAt,
74
+ }, 200, { ETag: token });
75
+ }
76
+ /**
77
+ * GET /api/settings/changes?since=<cursor>. Track C — offline backfill.
78
+ * Session-scoped to `userId`. Returns METADATA ONLY for namespaces whose
79
+ * version advanced past the opaque `sinceVersion` cursor — NEVER `ciphertext`.
80
+ * The client re-pulls each changed namespace's blob over the per-namespace GET.
81
+ *
82
+ * - store lacks the change-cursor capability -> 501 (not configured)
83
+ * - else -> 200 { changes: [{ namespace, version, updatedAt }, ...], cursor }
84
+ *
85
+ * `cursor` echoes back the new high-watermark (max version seen, or the input
86
+ * `sinceVersion` when nothing changed) so the client can persist it verbatim.
87
+ */
88
+ async handleChanges(userId, sinceVersion) {
89
+ if (!supportsChangeCursor(this.store)) {
90
+ // The configured store does not implement offline backfill.
91
+ return json({
92
+ error: "NOT_IMPLEMENTED",
93
+ message: "Change cursor is not available on this store",
94
+ }, 501);
95
+ }
96
+ const changes = await this.store.listChangedSince(userId, sinceVersion);
97
+ // New high-watermark: the largest version observed, else the unchanged cursor.
98
+ const cursor = changes.reduce((max, c) => (c.version > max ? c.version : max), sinceVersion);
99
+ // METADATA ONLY — `changes` entries are ChangedSettingMeta, which is
100
+ // structurally incapable of carrying ciphertext (asserted by the tests).
101
+ return json({ changes, cursor }, 200);
102
+ }
103
+ /**
104
+ * PUT /api/settings/:namespace. Session-scoped to `userId`; `tenantId` is the
105
+ * server-resolved active tenant (used ONLY to scope the wakeup channel — the
106
+ * blob itself has no tenant).
107
+ * - unknown namespace -> 404
108
+ * - invalid body -> 400
109
+ * - ciphertext over the size cap -> 413, store NEVER called
110
+ * - CAS conflict -> 409 with the server's current blob (client merges)
111
+ * - CAS not_found (non-zero expectVersion, no row) -> 409 with null current
112
+ * - success -> 200 { version, changeToken } + ETag; best-effort wakeup published
113
+ */
114
+ async handlePut(userId, tenantId, namespace, body) {
115
+ this.assertNamespaceAllowed(namespace);
116
+ const parsed = putBodySchema.safeParse(body);
117
+ if (!parsed.success) {
118
+ return json({ error: "VALIDATION_ERROR", message: "Invalid request body" }, 400);
119
+ }
120
+ const { ciphertext, expectVersion } = parsed.data;
121
+ // Size cap is enforced BEFORE the store is touched.
122
+ const bytes = Buffer.byteLength(ciphertext, "utf8");
123
+ if (bytes > this.config.maxSettingBytes) {
124
+ throw new BlobTooLargeError(bytes, this.config.maxSettingBytes);
125
+ }
126
+ const result = await this.store.put(userId, namespace,
127
+ // updatedAt is server-assigned by the store; pass a placeholder.
128
+ { ciphertext, version: expectVersion, updatedAt: "" }, expectVersion);
129
+ if (!result.ok) {
130
+ if (result.reason === "version_conflict") {
131
+ return json({
132
+ error: "VERSION_CONFLICT",
133
+ message: "Stale version; reconcile against current",
134
+ current: {
135
+ ciphertext: result.current.ciphertext,
136
+ version: result.current.version,
137
+ updatedAt: result.current.updatedAt,
138
+ },
139
+ }, 409, { ETag: changeTokenFor(result.current.version) });
140
+ }
141
+ // not_found: non-zero expectVersion against an absent row.
142
+ return json({
143
+ error: "NOT_FOUND",
144
+ message: "No setting blob to update at that version",
145
+ current: null,
146
+ }, 409);
147
+ }
148
+ const version = result.stored.version;
149
+ const changeToken = changeTokenFor(version);
150
+ // Publish-on-change: ONE content-free setting_sync wakeup, best-effort. A
151
+ // delivery failure must NOT fail the PUT — REST is the source of truth and
152
+ // the client's next poll converges.
153
+ await this.publishWakeup(userId, tenantId, changeToken);
154
+ return json({ version, changeToken }, 200, { ETag: changeToken });
155
+ }
156
+ /**
157
+ * Best-effort content-free wakeup. Uses encodeWakeup() (the ONLY sanctioned
158
+ * payload builder for setting_sync) with changeToken=version — NEVER the
159
+ * ciphertext. Swallows transport errors; logs at warn.
160
+ */
161
+ async publishWakeup(userId, tenantId, changeToken) {
162
+ if (!this.transport)
163
+ return;
164
+ try {
165
+ const channel = channelFor("setting_sync", { tenantId, userId });
166
+ const payload = encodeWakeup({
167
+ v: 1,
168
+ kind: "setting_sync",
169
+ changeToken,
170
+ });
171
+ await this.transport.deliver({ userId, tenantId }, channel, payload);
172
+ }
173
+ catch (err) {
174
+ getLogger().warn("setting_sync wakeup publish failed (best-effort)", err);
175
+ }
176
+ }
177
+ }
178
+ //# sourceMappingURL=encrypted-settings-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-settings-handler.js","sourceRoot":"","sources":["../../../src/lib/encrypted-settings/encrypted-settings-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import type { ChangeCursorStore, ChangedSettingMeta, EncryptedBlob, PutResult, SettingStore } from "./types.js";
2
+ /** The row shape this store reads back from Postgres. */
3
+ interface EncryptedUserSettingRow {
4
+ ciphertext: string;
5
+ version: number;
6
+ updatedAt: Date;
7
+ }
8
+ /**
9
+ * The change-cursor projection. Note the DELIBERATE absence of `ciphertext`:
10
+ * `listChangedSince` selects metadata ONLY, so the blob body never leaves the DB.
11
+ */
12
+ interface ChangedSettingRow {
13
+ namespace: string;
14
+ version: number;
15
+ updatedAt: Date;
16
+ }
17
+ /**
18
+ * The minimal Prisma surface this store needs. Declared structurally so the
19
+ * store is unit-testable against a mock without importing the generated client
20
+ * (which also keeps it decoupled from Prisma version drift).
21
+ */
22
+ export interface EncryptedUserSettingDelegate {
23
+ findUnique(args: {
24
+ where: {
25
+ userId_namespace: {
26
+ userId: string;
27
+ namespace: string;
28
+ };
29
+ };
30
+ select: {
31
+ ciphertext: true;
32
+ version: true;
33
+ updatedAt: true;
34
+ };
35
+ }): Promise<EncryptedUserSettingRow | null>;
36
+ create(args: {
37
+ data: {
38
+ userId: string;
39
+ namespace: string;
40
+ ciphertext: string;
41
+ version: number;
42
+ };
43
+ select: {
44
+ ciphertext: true;
45
+ version: true;
46
+ updatedAt: true;
47
+ };
48
+ }): Promise<EncryptedUserSettingRow>;
49
+ updateMany(args: {
50
+ where: {
51
+ userId: string;
52
+ namespace: string;
53
+ version: number;
54
+ };
55
+ data: {
56
+ ciphertext: string;
57
+ version: {
58
+ increment: number;
59
+ };
60
+ };
61
+ }): Promise<{
62
+ count: number;
63
+ }>;
64
+ findMany(args: {
65
+ where: {
66
+ userId: string;
67
+ version: {
68
+ gt: number;
69
+ };
70
+ };
71
+ select: {
72
+ namespace: true;
73
+ version: true;
74
+ updatedAt: true;
75
+ };
76
+ orderBy: {
77
+ version: "asc";
78
+ };
79
+ }): Promise<ChangedSettingRow[]>;
80
+ }
81
+ export interface PrismaWithEncryptedUserSetting {
82
+ encryptedUserSetting: EncryptedUserSettingDelegate;
83
+ }
84
+ export declare class PrismaEncryptedSettingsStore implements SettingStore, ChangeCursorStore {
85
+ private readonly db;
86
+ constructor(db: PrismaWithEncryptedUserSetting);
87
+ get(userId: string, namespace: string): Promise<EncryptedBlob | null>;
88
+ /**
89
+ * Optimistic concurrency, matching InMemorySettingStore semantics exactly:
90
+ * - `expectVersion === 0`: a fresh create. A pre-existing row (lost the race)
91
+ * surfaces as a P2002 unique violation -> version_conflict with current.
92
+ * - `expectVersion > 0`: conditional `updateMany WHERE version = expectVersion`.
93
+ * `count === 0` means the stored version moved (or the row is gone) ->
94
+ * re-read and return version_conflict / not_found.
95
+ * On success the new version is `expectVersion + 1`; the DB assigns updatedAt.
96
+ */
97
+ put(userId: string, namespace: string, blob: EncryptedBlob, expectVersion: number): Promise<PutResult>;
98
+ /**
99
+ * Track C — offline backfill cursor. Returns metadata for this user's
100
+ * namespaces whose `version` is strictly greater than `sinceVersion`, ordered
101
+ * by ascending version. Backed by the `@@index([userId])` on the table.
102
+ *
103
+ * SERVER-BLIND: the `select` projects `namespace`/`version`/`updatedAt` ONLY.
104
+ * `ciphertext` is NEVER selected, so the opaque blob body never leaves the DB
105
+ * on this path. The cursor is an opaque per-user version high-watermark.
106
+ */
107
+ listChangedSince(userId: string, sinceVersion: number): Promise<ChangedSettingMeta[]>;
108
+ }
109
+ export {};
110
+ //# sourceMappingURL=encrypted-settings-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-settings-store.d.ts","sourceRoot":"","sources":["../../../src/lib/encrypted-settings/encrypted-settings-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ // WS5 — PrismaEncryptedSettingsStore: the production SettingStore.
2
+ //
3
+ // Implements the FROZEN WS1 `SettingStore` port (apps/api/src/lib/realtime/
4
+ // types.ts) backed by the `EncryptedUserSetting` Postgres table. Postgres (not
5
+ // DynamoKv) is chosen for two reasons recorded in the frozen contract §M5:
6
+ // 1. Cascade-on-user-delete — the blob is dropped via the existing User
7
+ // deletion path; no second deletion surface to audit
8
+ // (surveillance-threat-model data-minimization).
9
+ // 2. Prisma consistency — optimistic concurrency (CAS) is a single conditional
10
+ // `updateMany({ where: { ..., version } })`, atomic at the DB.
11
+ //
12
+ // SERVER-BLIND: this store reads/writes `ciphertext` as an opaque string. It
13
+ // NEVER parses, decodes, or logs the ciphertext. Only plaintext sync metadata
14
+ // (`version`, `updatedAt`) is interpreted.
15
+ function toBlob(row) {
16
+ return {
17
+ ciphertext: row.ciphertext,
18
+ version: row.version,
19
+ updatedAt: row.updatedAt.toISOString(),
20
+ };
21
+ }
22
+ export class PrismaEncryptedSettingsStore {
23
+ db;
24
+ constructor(db) {
25
+ this.db = db;
26
+ }
27
+ async get(userId, namespace) {
28
+ const row = await this.db.encryptedUserSetting.findUnique({
29
+ where: { userId_namespace: { userId, namespace } },
30
+ select: { ciphertext: true, version: true, updatedAt: true },
31
+ });
32
+ return row ? toBlob(row) : null;
33
+ }
34
+ /**
35
+ * Optimistic concurrency, matching InMemorySettingStore semantics exactly:
36
+ * - `expectVersion === 0`: a fresh create. A pre-existing row (lost the race)
37
+ * surfaces as a P2002 unique violation -> version_conflict with current.
38
+ * - `expectVersion > 0`: conditional `updateMany WHERE version = expectVersion`.
39
+ * `count === 0` means the stored version moved (or the row is gone) ->
40
+ * re-read and return version_conflict / not_found.
41
+ * On success the new version is `expectVersion + 1`; the DB assigns updatedAt.
42
+ */
43
+ async put(userId, namespace, blob, expectVersion) {
44
+ if (expectVersion === 0) {
45
+ try {
46
+ const row = await this.db.encryptedUserSetting.create({
47
+ data: { userId, namespace, ciphertext: blob.ciphertext, version: 1 },
48
+ select: { ciphertext: true, version: true, updatedAt: true },
49
+ });
50
+ return { ok: true, stored: toBlob(row) };
51
+ }
52
+ catch (err) {
53
+ if (err?.code === "P2002") {
54
+ // A row already exists: caller's expectVersion=0 is stale.
55
+ const current = await this.get(userId, namespace);
56
+ if (current)
57
+ return { ok: false, reason: "version_conflict", current };
58
+ // Vanishingly rare: row deleted between create-fail and re-read.
59
+ return { ok: false, reason: "not_found", current: null };
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+ const result = await this.db.encryptedUserSetting.updateMany({
65
+ where: { userId, namespace, version: expectVersion },
66
+ data: { ciphertext: blob.ciphertext, version: { increment: 1 } },
67
+ });
68
+ if (result.count === 0) {
69
+ // CAS failed: either the version moved (conflict) or no row (not_found).
70
+ const current = await this.get(userId, namespace);
71
+ if (current)
72
+ return { ok: false, reason: "version_conflict", current };
73
+ return { ok: false, reason: "not_found", current: null };
74
+ }
75
+ const stored = await this.get(userId, namespace);
76
+ // The row exists (we just updated it); narrow for the type system.
77
+ if (!stored)
78
+ return { ok: false, reason: "not_found", current: null };
79
+ return { ok: true, stored };
80
+ }
81
+ /**
82
+ * Track C — offline backfill cursor. Returns metadata for this user's
83
+ * namespaces whose `version` is strictly greater than `sinceVersion`, ordered
84
+ * by ascending version. Backed by the `@@index([userId])` on the table.
85
+ *
86
+ * SERVER-BLIND: the `select` projects `namespace`/`version`/`updatedAt` ONLY.
87
+ * `ciphertext` is NEVER selected, so the opaque blob body never leaves the DB
88
+ * on this path. The cursor is an opaque per-user version high-watermark.
89
+ */
90
+ async listChangedSince(userId, sinceVersion) {
91
+ const rows = await this.db.encryptedUserSetting.findMany({
92
+ where: { userId, version: { gt: sinceVersion } },
93
+ select: { namespace: true, version: true, updatedAt: true },
94
+ orderBy: { version: "asc" },
95
+ });
96
+ return rows.map((row) => ({
97
+ namespace: row.namespace,
98
+ version: row.version,
99
+ updatedAt: row.updatedAt.toISOString(),
100
+ }));
101
+ }
102
+ }
103
+ //# sourceMappingURL=encrypted-settings-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-settings-store.js","sourceRoot":"","sources":["../../../src/lib/encrypted-settings/encrypted-settings-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { EncryptedBlob, PutResult, SettingStore, ChangedSettingMeta, ChangeCursorStore } from "../realtime/types.js";
2
+ export type { EncryptedBlob, PutResult, SettingStore, ChangedSettingMeta, ChangeCursorStore, };
3
+ /**
4
+ * Resolved runtime configuration for the settings-sync surface. Sourced ONLY
5
+ * from env (REALTIME_SETTING_NAMESPACES + REALTIME_SETTING_MAX_BYTES, read in
6
+ * env.ts which is the single writer). No compiled-in thresholds at call sites
7
+ * (CLAUDE.md threshold-secrecy rule).
8
+ */
9
+ export interface SettingsConfig {
10
+ /** Allowlisted namespaces. A namespace outside this set is a 404. */
11
+ allowedNamespaces: ReadonlySet<string>;
12
+ /** Max bytes for a single ciphertext blob; exceeded => 413. */
13
+ maxSettingBytes: number;
14
+ }
15
+ /** Raised when a request names a namespace not in the allowlist. */
16
+ export declare class UnknownNamespaceError extends Error {
17
+ readonly namespace: string;
18
+ constructor(namespace: string);
19
+ }
20
+ /** Raised when a ciphertext blob exceeds the configured size cap. */
21
+ export declare class BlobTooLargeError extends Error {
22
+ readonly bytes: number;
23
+ readonly maxBytes: number;
24
+ constructor(bytes: number, maxBytes: number);
25
+ }
26
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/encrypted-settings/types.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,GAClB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,+DAA+D;IAC/D,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,oEAAoE;AACpE,qBAAa,qBAAsB,SAAQ,KAAK;aAClB,SAAS,EAAE,MAAM;gBAAjB,SAAS,EAAE,MAAM;CAI9C;AAED,qEAAqE;AACrE,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,KAAK,EAAE,MAAM;aACb,QAAQ,EAAE,MAAM;gBADhB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM;CAKnC"}
@@ -0,0 +1,27 @@
1
+ // WS5 — encrypted-settings state-sync types.
2
+ //
3
+ // The blob/store contract is OWNED by the frozen WS1 realtime contract
4
+ // (apps/api/src/lib/realtime/types.ts). We re-export the frozen shapes here so
5
+ // the encrypted-settings module has a single import surface and can NEVER drift
6
+ // from WS1. Do not redefine EncryptedBlob/PutResult/SettingStore — import them.
7
+ /** Raised when a request names a namespace not in the allowlist. */
8
+ export class UnknownNamespaceError extends Error {
9
+ namespace;
10
+ constructor(namespace) {
11
+ super(`Unknown setting namespace: ${namespace}`);
12
+ this.namespace = namespace;
13
+ this.name = "UnknownNamespaceError";
14
+ }
15
+ }
16
+ /** Raised when a ciphertext blob exceeds the configured size cap. */
17
+ export class BlobTooLargeError extends Error {
18
+ bytes;
19
+ maxBytes;
20
+ constructor(bytes, maxBytes) {
21
+ super(`Encrypted setting blob too large: ${bytes} > ${maxBytes} bytes`);
22
+ this.bytes = bytes;
23
+ this.maxBytes = maxBytes;
24
+ this.name = "BlobTooLargeError";
25
+ }
26
+ }
27
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/encrypted-settings/types.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,gFAAgF;AAChF,gFAAgF;AA+BhF,oEAAoE;AACpE,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAClB;IAA5B,YAA4B,SAAiB;QAC3C,KAAK,CAAC,8BAA8B,SAAS,EAAE,CAAC,CAAC;QADvB,cAAS,GAAT,SAAS,CAAQ;QAE3C,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,qEAAqE;AACrE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAExB;IACA;IAFlB,YACkB,KAAa,EACb,QAAgB;QAEhC,KAAK,CAAC,qCAAqC,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;QAHxD,UAAK,GAAL,KAAK,CAAQ;QACb,aAAQ,GAAR,QAAQ,CAAQ;QAGhC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF"}
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import type { Env } from "../env.js";
10
10
  import type { NotificationType } from "@prisma/client";
11
+ import { type BlockStore } from "./realtime/block-store.js";
12
+ import type { DeliveryPolicyResolver } from "./realtime/index.js";
11
13
  export interface NotificationListResponse {
12
14
  notifications: Array<{
13
15
  id: string;
@@ -22,6 +24,15 @@ export interface NotificationListResponse {
22
24
  hasMore: boolean;
23
25
  }
24
26
  export declare class NotificationHandler {
27
+ private readonly injectedResolver;
28
+ private readonly blockStore;
29
+ constructor(deliveryResolver?: DeliveryPolicyResolver | null, blockStore?: BlockStore | null);
30
+ /**
31
+ * The resolver for this call: the injected one (tests) or a default built
32
+ * from env's runtime re-engagement denylist (threshold-secrecy: the denylist
33
+ * is env-driven config, never a compiled-in constant).
34
+ */
35
+ private resolverFor;
25
36
  /**
26
37
  * Create a notification for a user.
27
38
  * Checks preferences (unless SAFETY_ALERT or PARENTAL_LINK) and quiet hours.
@@ -54,10 +65,6 @@ export declare class NotificationHandler {
54
65
  * Check whether a notification type is enabled in preferences.
55
66
  */
56
67
  private isTypeEnabled;
57
- /**
58
- * Check if current time falls within user's quiet hours.
59
- */
60
- private isInQuietHours;
61
68
  }
62
69
  export declare class NotificationNotFoundError extends Error {
63
70
  constructor(notificationId: string);
@@ -1 +1 @@
1
- {"version":3,"file":"notification-handler.d.ts","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,KAAK,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,gBAAgB,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,GAAG,CAAC;QACV,IAAI,EAAE,OAAO,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAQD,qBAAa,mBAAmB;IAC9B;;;OAGG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAmE1B;;OAEG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,CAAC;IAuCpC;;OAEG;IACG,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAqBhB;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5E;;;;OAIG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBlD;;OAEG;IACH,OAAO,CAAC,aAAa;IAgCrB;;OAEG;IACH,OAAO,CAAC,cAAc;CA0BvB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,cAAc,EAAE,MAAM;CAInC"}
1
+ {"version":3,"file":"notification-handler.d.ts","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGrC,OAAO,KAAK,EAAE,gBAAgB,EAAW,MAAM,gBAAgB,CAAC;AAKhE,OAAO,EAEL,KAAK,UAAU,EAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,KAAK,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,gBAAgB,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,GAAG,CAAC;QACV,IAAI,EAAE,OAAO,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AA+FD,qBAAa,mBAAmB;IAU9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgC;IAIjE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;gBAG7C,gBAAgB,GAAE,sBAAsB,GAAG,IAAW,EACtD,UAAU,GAAE,UAAU,GAAG,IAAW;IAMtC;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAOnB;;;OAGG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAoI1B;;OAEG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,CAAC;IAuCpC;;OAEG;IACG,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAqBhB;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5E;;;;OAIG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBlD;;OAEG;IACH,OAAO,CAAC,aAAa;CAmCtB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,cAAc,EAAE,MAAM;CAInC"}