@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.
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,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,2 @@
1
+ export type { RealtimeTransport, DeliveryPolicyResolver, DeliveryTarget, DeliveryResult, Channel, EncryptedBlob, PutResult, SettingStore, } from "./types.js";
2
+ //# sourceMappingURL=realtime-transport.d.ts.map
@@ -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"}
@@ -0,0 +1,23 @@
1
+ // CONTRACT: stable — coordinate changes.
2
+ //
3
+ // The RealtimeTransport capability interface — THE seam every other workstream
4
+ // and the Skybber client binds to. The interface body lives in types.ts (single
5
+ // source of truth for the frozen type set); this module re-exports it under its
6
+ // own name so consumers can import the contract from a file whose name states
7
+ // what it is, and so the CONTRACT banner sits with the interface.
8
+ //
9
+ // Binding rules for every transport implementor (poll, noop, appsync-events):
10
+ // - deliver() is BEST-EFFORT and NEVER rolls back a persisted write. It
11
+ // resolves with a DeliveryResult; it must catch its own transport errors
12
+ // and surface them as { delivered: false, reason: "transport_error" }.
13
+ // - The policy fence (the safety floor) runs INSIDE every deliver(), never at
14
+ // the call site. A transport author MUST call the resolver and honor a
15
+ // { deliver: false } by NOT sending. PollTransport, NoopRealtimeTransport,
16
+ // and Skybber's AppSyncEventsTransport all obey this.
17
+ // - payload is OPAQUE bytes the transport never parses (blind relay). Anything
18
+ // sensitive is client-side ciphertext; content-free wakeups use
19
+ // encodeWakeup().
20
+ // - Implementations MUST NOT durably log connection/delivery metadata as
21
+ // ordinary operational data (CLAUDE.md rule 7; retention is runtime config).
22
+ export {};
23
+ //# sourceMappingURL=realtime-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-transport.js","sourceRoot":"","sources":["../../../src/lib/realtime/realtime-transport.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,6EAA6E;AAC7E,2EAA2E;AAC3E,gFAAgF;AAChF,2EAA2E;AAC3E,+EAA+E;AAC/E,0DAA0D;AAC1D,iFAAiF;AACjF,oEAAoE;AACpE,sBAAsB;AACtB,2EAA2E;AAC3E,iFAAiF"}
@@ -0,0 +1,30 @@
1
+ import type { ChangeCursorStore, ChangedSettingMeta, EncryptedBlob, PutResult, SettingStore } from "./types.js";
2
+ export type { SettingStore };
3
+ /**
4
+ * In-memory `SettingStore` with optimistic-concurrency semantics. Keyed by
5
+ * `userId \0 namespace`. The server NEVER parses `ciphertext`.
6
+ *
7
+ * Optimistic concurrency:
8
+ * - First write of a (user, namespace) requires `expectVersion === 0`; a
9
+ * non-zero `expectVersion` against an absent record => `not_found`.
10
+ * - A write whose `expectVersion` does not equal the stored version =>
11
+ * `version_conflict` carrying the current blob for client-side merge.
12
+ * - On success the stored version is set to `expectVersion + 1` and
13
+ * `updatedAt` is server-assigned from the injectable clock.
14
+ */
15
+ export declare class InMemorySettingStore implements SettingStore, ChangeCursorStore {
16
+ private readonly now;
17
+ private readonly store;
18
+ constructor(now?: () => Date);
19
+ private key;
20
+ get(userId: string, namespace: string): Promise<EncryptedBlob | null>;
21
+ put(userId: string, namespace: string, blob: EncryptedBlob, expectVersion: number): Promise<PutResult>;
22
+ /**
23
+ * Track C — offline backfill. Returns metadata for this user's namespaces whose
24
+ * `version` advanced strictly past `sinceVersion`. METADATA ONLY: never carries
25
+ * `ciphertext`. Sorted by ascending `version` so the caller can advance its
26
+ * cursor to the last entry's `version`.
27
+ */
28
+ listChangedSince(userId: string, sinceVersion: number): Promise<ChangedSettingMeta[]>;
29
+ }
30
+ //# sourceMappingURL=setting-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setting-store.d.ts","sourceRoot":"","sources":["../../../src/lib/realtime/setting-store.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,SAAS,EACT,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;;;;;;;;;GAWG;AACH,qBAAa,oBAAqB,YAAW,YAAY,EAAE,iBAAiB;IAQ9D,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAGlB;gBAEyB,GAAG,GAAE,MAAM,IAAuB;IAE/D,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIrE,GAAG,CACP,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,EACnB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,SAAS,CAAC;IAgCrB;;;;;OAKG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAcjC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setting-store.js","sourceRoot":"","sources":["../../../src/lib/realtime/setting-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,200 @@
1
+ import type { NotificationType, AgeTier } from "@prisma/client";
2
+ /**
3
+ * Coarse class of realtime traffic. Lets policy/transport route and rate-shape.
4
+ * v1 ships ONLY the user-scoped kinds (`wakeup`, `setting_sync`, `safety`);
5
+ * `message` and `thread` are taxonomy-reserved but unused (DEFERRED).
6
+ */
7
+ export type ChannelKind = "wakeup" | "setting_sync" | "safety" | "message" | "thread";
8
+ /** The scope a channel addresses. v1 ships only `"user"`. */
9
+ export type ScopeType = "user" | "conversation" | "thread";
10
+ /**
11
+ * A push-delivery channel — a content-free routing address, ALWAYS tenant- and
12
+ * scope-bound and server-verified. The canonical string form is produced ONLY
13
+ * by `channelName()` and parsed ONLY by `parseChannel()` (and the Skybber
14
+ * `@skybber/realtime-channels` parser, which must round-trip them).
15
+ *
16
+ * FROZEN kind→scopeType map (v1 ships ONLY user-scoped kinds):
17
+ * wakeup | setting_sync | safety -> scopeType "user", scopeId = recipient userId
18
+ * message -> scopeType "conversation" (DEFERRED)
19
+ * thread -> scopeType "thread" (DEFERRED)
20
+ */
21
+ export interface Channel {
22
+ kind: ChannelKind;
23
+ /** Server-resolved tenant scope. */
24
+ tenantId: string;
25
+ scopeType: ScopeType;
26
+ /** userId (v1) | conversationId | threadId (deferred). */
27
+ scopeId: string;
28
+ }
29
+ /**
30
+ * Verified identity. Derived ONLY from Cognito claims (custom:userId,
31
+ * custom:activeTenantId) — by the in-core handler AND the Skybber authorizer
32
+ * Lambda — so both call the SAME function. Never an ambient Session, never a
33
+ * client-asserted path.
34
+ */
35
+ export interface VerifiedIdentity {
36
+ userId: string;
37
+ tenantId: string;
38
+ }
39
+ /** Recipient address for `deliver()`. Both fields server-resolved. */
40
+ export interface DeliveryTarget {
41
+ userId: string;
42
+ tenantId: string;
43
+ }
44
+ /**
45
+ * Result of a `deliver()` attempt. `deliver()` is BEST-EFFORT: it NEVER rejects
46
+ * in a way that rolls back a persisted write. It resolves with this result;
47
+ * transports catch their own errors. The policy fence runs INSIDE every
48
+ * transport's `deliver()`.
49
+ */
50
+ export type DeliveryResult = {
51
+ delivered: true;
52
+ } | {
53
+ delivered: false;
54
+ reason: "policy_denied" | "no_transport" | "transport_error";
55
+ };
56
+ /**
57
+ * Input the policy resolver sees. Built by the caller from its own args
58
+ * (`createNotification` has no Session), enriched with recipient ageTier /
59
+ * blocked-sender as needed for the floor.
60
+ */
61
+ export interface DeliveryContext {
62
+ type: NotificationType;
63
+ recipientUserId: string;
64
+ tenantId: string;
65
+ /** blocked-sender floor input. */
66
+ senderUserId?: string;
67
+ /** minor-protection floor input. */
68
+ recipientAgeTier?: AgeTier;
69
+ now: Date;
70
+ quietHours?: QuietHoursConfig | null;
71
+ }
72
+ /** What the resolver decided for ONE delivery attempt. */
73
+ export type DeliveryDecision = {
74
+ deliver: true;
75
+ } | {
76
+ deliver: false;
77
+ reason: "preference" | "quiet_hours" | "blocked_sender" | "floor";
78
+ };
79
+ /**
80
+ * Quiet-hours window. `start`/`end` are opaque to the contract; the resolver
81
+ * that produced the context owns their interpretation. (The core resolver
82
+ * encodes minutes-since-midnight as decimal strings to reproduce the existing
83
+ * `User.quietHoursStart/End` integer behavior byte-identically.)
84
+ */
85
+ export interface QuietHoursConfig {
86
+ enabled: boolean;
87
+ start: string;
88
+ end: string;
89
+ }
90
+ /**
91
+ * The ONLY shape push payloads for wakeup/setting_sync may take. It has NO
92
+ * free-form field — content-free is a property of the TYPE, not a test
93
+ * heuristic. WS4/WS5 are FORBIDDEN from constructing arbitrary Uint8Array for
94
+ * these kinds; they must use `encodeWakeup()`.
95
+ */
96
+ export interface WakeupEnvelope {
97
+ v: 1;
98
+ kind: ChannelKind;
99
+ /** opaque version pointer for setting_sync (NOT ciphertext). */
100
+ changeToken?: string;
101
+ }
102
+ /** Encode a wakeup envelope to its canonical content-free byte form. */
103
+ export declare function encodeWakeup(e: WakeupEnvelope): Uint8Array;
104
+ /** Decode a wakeup envelope. Throws on unknown fields or malformed input. */
105
+ export declare function decodeWakeup(b: Uint8Array): WakeupEnvelope;
106
+ /**
107
+ * Opaque AEAD ciphertext + plaintext sync metadata. The server NEVER reads
108
+ * `ciphertext`. `version`/`updatedAt` are deliberately plaintext (they leak
109
+ * only THAT/WHEN a setting changed, not WHAT).
110
+ */
111
+ export interface EncryptedBlob {
112
+ /** Base64url AEAD ciphertext of the client's JSON document. */
113
+ ciphertext: string;
114
+ /** Monotonic per (userId, namespace). Drives optimistic concurrency. */
115
+ version: number;
116
+ /** ISO-8601; server-assigned on write. */
117
+ updatedAt: string;
118
+ }
119
+ /**
120
+ * Result of `putSetting` — optimistic-concurrency outcome. Never throws on
121
+ * conflict; the caller (and ultimately the client) reconciles against
122
+ * `current`.
123
+ */
124
+ export type PutResult = {
125
+ ok: true;
126
+ stored: EncryptedBlob;
127
+ } | {
128
+ ok: false;
129
+ reason: "version_conflict";
130
+ current: EncryptedBlob;
131
+ } | {
132
+ ok: false;
133
+ reason: "not_found";
134
+ current: null;
135
+ };
136
+ /**
137
+ * Minimal store port the transports use for blob sync. WS5 supplies
138
+ * `PrismaEncryptedSettingsStore`; WS1 ships `InMemorySettingStore` so core
139
+ * runs/tests with zero infra. Holds CIPHERTEXT ONLY.
140
+ *
141
+ * FROZEN (§2.5): do NOT add methods here. Offline-backfill (Track C) is layered
142
+ * as the SEPARATE optional `ChangeCursorStore` capability below so this contract
143
+ * stays byte-stable for every consumer bound to it.
144
+ */
145
+ export interface SettingStore {
146
+ get(userId: string, namespace: string): Promise<EncryptedBlob | null>;
147
+ put(userId: string, namespace: string, blob: EncryptedBlob, expectVersion: number): Promise<PutResult>;
148
+ }
149
+ /**
150
+ * Metadata for ONE namespace whose version advanced past a client's cursor.
151
+ *
152
+ * SERVER-BLIND: deliberately carries NO `ciphertext`. It leaks only THAT/WHEN a
153
+ * namespace changed (already-plaintext sync metadata), never WHAT. A client that
154
+ * sees its namespace here re-pulls the blob over the authenticated GET. The
155
+ * absence of `ciphertext` is a TYPE-level guarantee, asserted by the tests.
156
+ */
157
+ export interface ChangedSettingMeta {
158
+ namespace: string;
159
+ /** Monotonic per (userId, namespace) — the new high-watermark for this row. */
160
+ version: number;
161
+ /** ISO-8601 server-assigned write time. */
162
+ updatedAt: string;
163
+ }
164
+ /**
165
+ * OPTIONAL offline-backfill capability (Track C), layered ALONGSIDE the frozen
166
+ * `SettingStore` rather than baked into it. A store MAY also implement this to
167
+ * answer "which of my namespaces changed since I was last online?".
168
+ *
169
+ * The cursor (`sinceVersion`) is an opaque per-user version high-watermark: the
170
+ * largest `version` the client has already observed across ALL its namespaces.
171
+ * `listChangedSince` returns metadata for every namespace whose stored `version`
172
+ * is strictly greater than that watermark — METADATA ONLY, never `ciphertext`.
173
+ */
174
+ export interface ChangeCursorStore {
175
+ listChangedSince(userId: string, sinceVersion: number): Promise<ChangedSettingMeta[]>;
176
+ }
177
+ /** Narrowing helper: does this store also provide the change-cursor capability? */
178
+ export declare function supportsChangeCursor(store: SettingStore): store is SettingStore & ChangeCursorStore;
179
+ /**
180
+ * The capability seam. Core ships `PollTransport` (default) + `NoopRealtimeTransport`
181
+ * (CI); a consuming app injects a concrete transport (e.g. Skybber's
182
+ * `AppSyncEventsTransport`) via `setRealtimeProvider()` WITHOUT core ever
183
+ * importing an AWS SDK.
184
+ *
185
+ * `subscribeInContext` is intentionally NOT part of the v1 freeze (in-context
186
+ * liveness is messaging-era; adding an optional method later is non-breaking).
187
+ */
188
+ export interface RealtimeTransport {
189
+ readonly kind: "poll" | "appsync-events" | "noop";
190
+ /** Best-effort push. Runs the policy fence (floor) internally, every transport. */
191
+ deliver(target: DeliveryTarget, channel: Channel, payload: Uint8Array): Promise<DeliveryResult>;
192
+ getSetting(userId: string, namespace: string): Promise<EncryptedBlob | null>;
193
+ putSetting(userId: string, namespace: string, blob: EncryptedBlob, expectVersion: number): Promise<PutResult>;
194
+ shutdown?(): Promise<void>;
195
+ }
196
+ /** The policy port. WS1 ships `CalmDeliveryResolver`; WS3 may layer it. */
197
+ export interface DeliveryPolicyResolver {
198
+ decide(ctx: DeliveryContext): DeliveryDecision;
199
+ }
200
+ //# sourceMappingURL=types.d.ts.map