@smthrs/sync 0.0.0-stage → 1.0.0-rc.3

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 (266) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -2
  4. package/dist/cjs/BranchCommands.d.ts +228 -0
  5. package/dist/cjs/BranchCommands.d.ts.map +1 -0
  6. package/dist/cjs/BranchCommands.js +316 -0
  7. package/dist/cjs/BranchCommands.js.map +7 -0
  8. package/dist/cjs/BranchIds.d.ts +65 -0
  9. package/dist/cjs/BranchIds.d.ts.map +1 -0
  10. package/dist/cjs/BranchIds.js +53 -0
  11. package/dist/cjs/BranchIds.js.map +7 -0
  12. package/dist/cjs/BranchPresence.d.ts +195 -0
  13. package/dist/cjs/BranchPresence.d.ts.map +1 -0
  14. package/dist/cjs/BranchPresence.js +276 -0
  15. package/dist/cjs/BranchPresence.js.map +7 -0
  16. package/dist/cjs/BranchProjection.d.ts +179 -0
  17. package/dist/cjs/BranchProjection.d.ts.map +1 -0
  18. package/dist/cjs/BranchProjection.js +197 -0
  19. package/dist/cjs/BranchProjection.js.map +7 -0
  20. package/dist/cjs/BranchProtocol.d.ts +415 -0
  21. package/dist/cjs/BranchProtocol.d.ts.map +1 -0
  22. package/dist/cjs/BranchProtocol.js +172 -0
  23. package/dist/cjs/BranchProtocol.js.map +7 -0
  24. package/dist/cjs/BranchRpcs.d.ts +177 -0
  25. package/dist/cjs/BranchRpcs.d.ts.map +1 -0
  26. package/dist/cjs/BranchRpcs.js +85 -0
  27. package/dist/cjs/BranchRpcs.js.map +7 -0
  28. package/dist/cjs/BranchServer.d.ts +55 -0
  29. package/dist/cjs/BranchServer.d.ts.map +1 -0
  30. package/dist/cjs/BranchServer.js +147 -0
  31. package/dist/cjs/BranchServer.js.map +7 -0
  32. package/dist/cjs/BranchShare.d.ts +159 -0
  33. package/dist/cjs/BranchShare.d.ts.map +1 -0
  34. package/dist/cjs/BranchShare.js +147 -0
  35. package/dist/cjs/BranchShare.js.map +7 -0
  36. package/dist/cjs/RunCatalog.d.ts +207 -0
  37. package/dist/cjs/RunCatalog.d.ts.map +1 -0
  38. package/dist/cjs/RunCatalog.js +122 -0
  39. package/dist/cjs/RunCatalog.js.map +7 -0
  40. package/dist/cjs/SyncAuth.d.ts +70 -0
  41. package/dist/cjs/SyncAuth.d.ts.map +1 -0
  42. package/dist/cjs/SyncAuth.js +109 -0
  43. package/dist/cjs/SyncAuth.js.map +7 -0
  44. package/dist/cjs/SyncClient.d.ts +274 -0
  45. package/dist/cjs/SyncClient.d.ts.map +1 -0
  46. package/dist/cjs/SyncClient.js +474 -0
  47. package/dist/cjs/SyncClient.js.map +7 -0
  48. package/dist/cjs/SyncError.d.ts +107 -0
  49. package/dist/cjs/SyncError.d.ts.map +1 -0
  50. package/dist/cjs/SyncError.js +110 -0
  51. package/dist/cjs/SyncError.js.map +7 -0
  52. package/dist/cjs/SyncPrincipal.d.ts +100 -0
  53. package/dist/cjs/SyncPrincipal.d.ts.map +1 -0
  54. package/dist/cjs/SyncPrincipal.js +48 -0
  55. package/dist/cjs/SyncPrincipal.js.map +7 -0
  56. package/dist/cjs/SyncProtocol.d.ts +592 -0
  57. package/dist/cjs/SyncProtocol.d.ts.map +1 -0
  58. package/dist/cjs/SyncProtocol.js +165 -0
  59. package/dist/cjs/SyncProtocol.js.map +7 -0
  60. package/dist/cjs/SyncRpcs.d.ts +92 -0
  61. package/dist/cjs/SyncRpcs.d.ts.map +1 -0
  62. package/dist/cjs/SyncRpcs.js +35 -0
  63. package/dist/cjs/SyncRpcs.js.map +7 -0
  64. package/dist/cjs/SyncServer.d.ts +184 -0
  65. package/dist/cjs/SyncServer.d.ts.map +1 -0
  66. package/dist/cjs/SyncServer.js +513 -0
  67. package/dist/cjs/SyncServer.js.map +7 -0
  68. package/dist/cjs/WorkspaceShare.d.ts +174 -0
  69. package/dist/cjs/WorkspaceShare.d.ts.map +1 -0
  70. package/dist/cjs/WorkspaceShare.js +149 -0
  71. package/dist/cjs/WorkspaceShare.js.map +7 -0
  72. package/dist/cjs/index.d.ts +91 -0
  73. package/dist/cjs/index.d.ts.map +1 -0
  74. package/dist/cjs/index.js +67 -0
  75. package/dist/cjs/index.js.map +7 -0
  76. package/dist/cjs/internal/Admission.d.ts +43 -0
  77. package/dist/cjs/internal/Admission.d.ts.map +1 -0
  78. package/dist/cjs/internal/Admission.js +90 -0
  79. package/dist/cjs/internal/Admission.js.map +7 -0
  80. package/dist/cjs/internal/CauseText.d.ts +55 -0
  81. package/dist/cjs/internal/CauseText.d.ts.map +1 -0
  82. package/dist/cjs/internal/CauseText.js +38 -0
  83. package/dist/cjs/internal/CauseText.js.map +7 -0
  84. package/dist/cjs/internal/JournalErrorCode.d.ts +23 -0
  85. package/dist/cjs/internal/JournalErrorCode.d.ts.map +1 -0
  86. package/dist/cjs/internal/JournalErrorCode.js +34 -0
  87. package/dist/cjs/internal/JournalErrorCode.js.map +7 -0
  88. package/dist/cjs/internal/PolicyOptions.d.ts +57 -0
  89. package/dist/cjs/internal/PolicyOptions.d.ts.map +1 -0
  90. package/dist/cjs/internal/PolicyOptions.js +62 -0
  91. package/dist/cjs/internal/PolicyOptions.js.map +7 -0
  92. package/dist/cjs/internal/ProtocolVersion.d.ts +17 -0
  93. package/dist/cjs/internal/ProtocolVersion.d.ts.map +1 -0
  94. package/dist/cjs/internal/ProtocolVersion.js +44 -0
  95. package/dist/cjs/internal/ProtocolVersion.js.map +7 -0
  96. package/dist/cjs/internal/ShareSigner.d.ts +173 -0
  97. package/dist/cjs/internal/ShareSigner.d.ts.map +1 -0
  98. package/dist/cjs/internal/ShareSigner.js +150 -0
  99. package/dist/cjs/internal/ShareSigner.js.map +7 -0
  100. package/dist/cjs/internal/SnapshotBoundary.d.ts +17 -0
  101. package/dist/cjs/internal/SnapshotBoundary.d.ts.map +1 -0
  102. package/dist/cjs/internal/SnapshotBoundary.js +97 -0
  103. package/dist/cjs/internal/SnapshotBoundary.js.map +7 -0
  104. package/dist/cjs/package.json +1 -0
  105. package/dist/cjs/test/TestSocket.d.ts +55 -0
  106. package/dist/cjs/test/TestSocket.d.ts.map +1 -0
  107. package/dist/cjs/test/TestSocket.js +145 -0
  108. package/dist/cjs/test/TestSocket.js.map +7 -0
  109. package/dist/cjs/test/TestSync.d.ts +54 -0
  110. package/dist/cjs/test/TestSync.d.ts.map +1 -0
  111. package/dist/cjs/test/TestSync.js +96 -0
  112. package/dist/cjs/test/TestSync.js.map +7 -0
  113. package/dist/esm/BranchCommands.d.ts +228 -0
  114. package/dist/esm/BranchCommands.d.ts.map +1 -0
  115. package/dist/esm/BranchCommands.js +496 -0
  116. package/dist/esm/BranchCommands.js.map +1 -0
  117. package/dist/esm/BranchIds.d.ts +65 -0
  118. package/dist/esm/BranchIds.d.ts.map +1 -0
  119. package/dist/esm/BranchIds.js +61 -0
  120. package/dist/esm/BranchIds.js.map +1 -0
  121. package/dist/esm/BranchPresence.d.ts +195 -0
  122. package/dist/esm/BranchPresence.d.ts.map +1 -0
  123. package/dist/esm/BranchPresence.js +361 -0
  124. package/dist/esm/BranchPresence.js.map +1 -0
  125. package/dist/esm/BranchProjection.d.ts +179 -0
  126. package/dist/esm/BranchProjection.d.ts.map +1 -0
  127. package/dist/esm/BranchProjection.js +269 -0
  128. package/dist/esm/BranchProjection.js.map +1 -0
  129. package/dist/esm/BranchProtocol.d.ts +415 -0
  130. package/dist/esm/BranchProtocol.d.ts.map +1 -0
  131. package/dist/esm/BranchProtocol.js +323 -0
  132. package/dist/esm/BranchProtocol.js.map +1 -0
  133. package/dist/esm/BranchRpcs.d.ts +177 -0
  134. package/dist/esm/BranchRpcs.d.ts.map +1 -0
  135. package/dist/esm/BranchRpcs.js +103 -0
  136. package/dist/esm/BranchRpcs.js.map +1 -0
  137. package/dist/esm/BranchServer.d.ts +55 -0
  138. package/dist/esm/BranchServer.d.ts.map +1 -0
  139. package/dist/esm/BranchServer.js +168 -0
  140. package/dist/esm/BranchServer.js.map +1 -0
  141. package/dist/esm/BranchShare.d.ts +159 -0
  142. package/dist/esm/BranchShare.d.ts.map +1 -0
  143. package/dist/esm/BranchShare.js +213 -0
  144. package/dist/esm/BranchShare.js.map +1 -0
  145. package/dist/esm/RunCatalog.d.ts +207 -0
  146. package/dist/esm/RunCatalog.d.ts.map +1 -0
  147. package/dist/esm/RunCatalog.js +196 -0
  148. package/dist/esm/RunCatalog.js.map +1 -0
  149. package/dist/esm/SyncAuth.d.ts +70 -0
  150. package/dist/esm/SyncAuth.d.ts.map +1 -0
  151. package/dist/esm/SyncAuth.js +119 -0
  152. package/dist/esm/SyncAuth.js.map +1 -0
  153. package/dist/esm/SyncClient.d.ts +274 -0
  154. package/dist/esm/SyncClient.d.ts.map +1 -0
  155. package/dist/esm/SyncClient.js +586 -0
  156. package/dist/esm/SyncClient.js.map +1 -0
  157. package/dist/esm/SyncError.d.ts +107 -0
  158. package/dist/esm/SyncError.d.ts.map +1 -0
  159. package/dist/esm/SyncError.js +133 -0
  160. package/dist/esm/SyncError.js.map +1 -0
  161. package/dist/esm/SyncPrincipal.d.ts +100 -0
  162. package/dist/esm/SyncPrincipal.d.ts.map +1 -0
  163. package/dist/esm/SyncPrincipal.js +58 -0
  164. package/dist/esm/SyncPrincipal.js.map +1 -0
  165. package/dist/esm/SyncProtocol.d.ts +592 -0
  166. package/dist/esm/SyncProtocol.d.ts.map +1 -0
  167. package/dist/esm/SyncProtocol.js +434 -0
  168. package/dist/esm/SyncProtocol.js.map +1 -0
  169. package/dist/esm/SyncRpcs.d.ts +92 -0
  170. package/dist/esm/SyncRpcs.d.ts.map +1 -0
  171. package/dist/esm/SyncRpcs.js +35 -0
  172. package/dist/esm/SyncRpcs.js.map +1 -0
  173. package/dist/esm/SyncServer.d.ts +184 -0
  174. package/dist/esm/SyncServer.d.ts.map +1 -0
  175. package/dist/esm/SyncServer.js +897 -0
  176. package/dist/esm/SyncServer.js.map +1 -0
  177. package/dist/esm/WorkspaceShare.d.ts +174 -0
  178. package/dist/esm/WorkspaceShare.d.ts.map +1 -0
  179. package/dist/esm/WorkspaceShare.js +208 -0
  180. package/dist/esm/WorkspaceShare.js.map +1 -0
  181. package/dist/esm/index.d.ts +91 -0
  182. package/dist/esm/index.d.ts.map +1 -0
  183. package/dist/esm/index.js +91 -0
  184. package/dist/esm/index.js.map +1 -0
  185. package/dist/esm/internal/Admission.d.ts +43 -0
  186. package/dist/esm/internal/Admission.d.ts.map +1 -0
  187. package/dist/esm/internal/Admission.js +80 -0
  188. package/dist/esm/internal/Admission.js.map +1 -0
  189. package/dist/esm/internal/CauseText.d.ts +55 -0
  190. package/dist/esm/internal/CauseText.d.ts.map +1 -0
  191. package/dist/esm/internal/CauseText.js +67 -0
  192. package/dist/esm/internal/CauseText.js.map +1 -0
  193. package/dist/esm/internal/JournalErrorCode.d.ts +23 -0
  194. package/dist/esm/internal/JournalErrorCode.d.ts.map +1 -0
  195. package/dist/esm/internal/JournalErrorCode.js +44 -0
  196. package/dist/esm/internal/JournalErrorCode.js.map +1 -0
  197. package/dist/esm/internal/PolicyOptions.d.ts +57 -0
  198. package/dist/esm/internal/PolicyOptions.d.ts.map +1 -0
  199. package/dist/esm/internal/PolicyOptions.js +70 -0
  200. package/dist/esm/internal/PolicyOptions.js.map +1 -0
  201. package/dist/esm/internal/ProtocolVersion.d.ts +17 -0
  202. package/dist/esm/internal/ProtocolVersion.d.ts.map +1 -0
  203. package/dist/esm/internal/ProtocolVersion.js +24 -0
  204. package/dist/esm/internal/ProtocolVersion.js.map +1 -0
  205. package/dist/esm/internal/ShareSigner.d.ts +173 -0
  206. package/dist/esm/internal/ShareSigner.d.ts.map +1 -0
  207. package/dist/esm/internal/ShareSigner.js +219 -0
  208. package/dist/esm/internal/ShareSigner.js.map +1 -0
  209. package/dist/esm/internal/SnapshotBoundary.d.ts +17 -0
  210. package/dist/esm/internal/SnapshotBoundary.d.ts.map +1 -0
  211. package/dist/esm/internal/SnapshotBoundary.js +68 -0
  212. package/dist/esm/internal/SnapshotBoundary.js.map +1 -0
  213. package/dist/esm/test/TestSocket.d.ts +55 -0
  214. package/dist/esm/test/TestSocket.d.ts.map +1 -0
  215. package/dist/esm/test/TestSocket.js +128 -0
  216. package/dist/esm/test/TestSocket.js.map +1 -0
  217. package/dist/esm/test/TestSync.d.ts +54 -0
  218. package/dist/esm/test/TestSync.d.ts.map +1 -0
  219. package/dist/esm/test/TestSync.js +76 -0
  220. package/dist/esm/test/TestSync.js.map +1 -0
  221. package/docs/README.md +156 -0
  222. package/docs/api.md +328 -0
  223. package/docs/concepts/authorization.md +117 -0
  224. package/docs/concepts/branches.md +195 -0
  225. package/docs/concepts/compaction.md +144 -0
  226. package/docs/concepts/replay-then-follow.md +129 -0
  227. package/docs/concepts/scopes-and-cursors.md +135 -0
  228. package/docs/guides/authorize-a-connection.md +174 -0
  229. package/docs/guides/follow-a-run.md +174 -0
  230. package/docs/guides/handle-a-compacted-run.md +158 -0
  231. package/docs/guides/list-workspace-runs.md +120 -0
  232. package/docs/guides/run-a-long-soak.md +80 -0
  233. package/docs/guides/serve-the-read-path.md +146 -0
  234. package/docs/guides/test-a-follower.md +273 -0
  235. package/docs/installation.md +92 -0
  236. package/docs/protocol.md +183 -0
  237. package/docs/quickstart.md +159 -0
  238. package/docs/troubleshooting.md +234 -0
  239. package/package.json +304 -3
  240. package/src/BranchCommands.ts +684 -0
  241. package/src/BranchIds.ts +76 -0
  242. package/src/BranchPresence.ts +512 -0
  243. package/src/BranchProjection.ts +308 -0
  244. package/src/BranchProtocol.ts +411 -0
  245. package/src/BranchRpcs.ts +163 -0
  246. package/src/BranchServer.ts +202 -0
  247. package/src/BranchShare.ts +297 -0
  248. package/src/RunCatalog.ts +308 -0
  249. package/src/SyncAuth.ts +146 -0
  250. package/src/SyncClient.ts +902 -0
  251. package/src/SyncError.ts +147 -0
  252. package/src/SyncPrincipal.ts +115 -0
  253. package/src/SyncProtocol.ts +594 -0
  254. package/src/SyncRpcs.ts +40 -0
  255. package/src/SyncServer.ts +1184 -0
  256. package/src/WorkspaceShare.ts +292 -0
  257. package/src/index.ts +107 -0
  258. package/src/internal/Admission.ts +119 -0
  259. package/src/internal/CauseText.ts +74 -0
  260. package/src/internal/JournalErrorCode.ts +46 -0
  261. package/src/internal/PolicyOptions.ts +97 -0
  262. package/src/internal/ProtocolVersion.ts +28 -0
  263. package/src/internal/ShareSigner.ts +300 -0
  264. package/src/internal/SnapshotBoundary.ts +91 -0
  265. package/src/test/TestSocket.ts +192 -0
  266. package/src/test/TestSync.ts +113 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Share capabilities: the authorization boundary of a shared branch.
3
+ *
4
+ * A share link carries a capability, not a session. The capability names
5
+ * exactly one branch, one access level, and one expiry, and it is signed, so
6
+ * the holder cannot widen it. Every branch operation authorizes through
7
+ * `verify`, which is therefore the single place cross-branch access and
8
+ * expired links are refused.
9
+ *
10
+ * The signature is HMAC-SHA-256 over a length-prefixed encoding of the claims,
11
+ * led by a scheme label. Length prefixes matter: without them a branch id
12
+ * ending in the separator could be re-cut into a different, still-validly-
13
+ * signed claim set. The label matters for the same reason across schemes: a
14
+ * branch capability can never verify as a workspace capability even if one
15
+ * secret is misconfigured into both authorities. Web Crypto is used directly
16
+ * so the same module runs in the browser and on node.
17
+ *
18
+ * A `kid` inside the claims names the key that signed them, so a branch secret
19
+ * rotates the way the workspace secret already did: the new key signs, the
20
+ * retired key stays in the verification keyring, and the share links already
21
+ * out keep working until they expire.
22
+ *
23
+ * @since 0.1.0
24
+ */
25
+ import * as Config from "effect/Config";
26
+ import * as Context from "effect/Context";
27
+ import * as Effect from "effect/Effect";
28
+ import * as Layer from "effect/Layer";
29
+ import * as Schema from "effect/Schema";
30
+ import { Access, BranchId, ShareCapability, ShareClaims } from "./BranchProtocol.ts";
31
+ import * as shareSigner from "./internal/ShareSigner.ts";
32
+ import type { SyncError } from "./SyncError.ts";
33
+ /**
34
+ * The branch and access one authorization request needs.
35
+ *
36
+ * @category models
37
+ * @since 0.1.0
38
+ */
39
+ export interface AuthorizeRequest {
40
+ readonly branchId: BranchId;
41
+ readonly access: Access;
42
+ }
43
+ /**
44
+ * What a freshly minted capability grants.
45
+ *
46
+ * @category models
47
+ * @since 0.1.0
48
+ */
49
+ export declare const MintRequest: Schema.Struct<{
50
+ readonly branchId: Schema.brand<Schema.NonEmptyString, "@smthrs/sync/BranchProtocol/BranchId">;
51
+ readonly capabilityId: Schema.NonEmptyString;
52
+ readonly access: Schema.Literals<readonly ["read", "write"]>;
53
+ readonly ttlMs: Schema.Int;
54
+ /** Absolute expiry ceiling for a capability delegated from a parent. */
55
+ readonly maxExpiresAtMs: Schema.optionalKey<Schema.Int>;
56
+ }>;
57
+ /**
58
+ * What a freshly minted capability grants.
59
+ *
60
+ * @category models
61
+ * @since 0.1.0
62
+ */
63
+ export type MintRequest = typeof MintRequest.Type;
64
+ /**
65
+ * Share capability operations.
66
+ *
67
+ * `mint` fails with a `SyncError` when the Web Crypto signing operation
68
+ * rejects or the absolute expiry ceiling has elapsed; `verify` fails with a
69
+ * `SyncError` when signing rejects or the capability is refused.
70
+ *
71
+ * @category models
72
+ * @since 0.1.0
73
+ */
74
+ export interface Service {
75
+ readonly mint: (request: MintRequest) => Effect.Effect<ShareCapability, SyncError>;
76
+ readonly verify: (capability: ShareCapability, request: AuthorizeRequest) => Effect.Effect<ShareClaims, SyncError>;
77
+ }
78
+ declare const BranchShare_base: Context.ServiceClass<BranchShare, "@smthrs/sync/BranchShare", Service>;
79
+ /**
80
+ * The branch share-capability authority.
81
+ *
82
+ * @category services
83
+ * @since 0.1.0
84
+ */
85
+ export declare class BranchShare extends BranchShare_base {
86
+ }
87
+ /**
88
+ * Constructs a share authority that mints nothing and trusts nothing.
89
+ *
90
+ * @category constructors
91
+ * @since 0.1.0
92
+ */
93
+ export declare const makeNoop: (overrides?: Partial<Service>) => Service;
94
+ /**
95
+ * Provides a share authority that refuses every capability.
96
+ *
97
+ * @category layers
98
+ * @since 0.1.0
99
+ */
100
+ export declare const layerNoop: Layer.Layer<BranchShare>;
101
+ /**
102
+ * One named signing key. The secret is `Redacted` so a keyring never renders
103
+ * it into logs or inspection output.
104
+ *
105
+ * @category models
106
+ * @since 1.0.0-rc.0
107
+ */
108
+ export type Key = shareSigner.Key;
109
+ /**
110
+ * A keyring: the key that signs new capabilities plus every key still
111
+ * accepted for verification. Rotation adds a new active key and keeps the
112
+ * retired one in `keys` until its outstanding links expire, so rotating the
113
+ * branch secret no longer breaks every share link that is already out.
114
+ *
115
+ * @category models
116
+ * @since 1.0.0-rc.0
117
+ */
118
+ export type Keyring = shareSigner.Keyring;
119
+ /**
120
+ * Constructs the HMAC-SHA-256 share authority over a keyring.
121
+ *
122
+ * Every key is imported up front, so a misconfigured keyring — an unknown
123
+ * active kid, a duplicate kid, or a key Web Crypto refuses — fails at
124
+ * construction rather than at the first request. Secrets enter as `Redacted`
125
+ * values: an authority never holds a plain string that a log, a span, or an
126
+ * inspection of the keyring could render.
127
+ *
128
+ * @category constructors
129
+ * @since 0.1.0
130
+ */
131
+ export declare const makeHmac: (keyring: Keyring) => Effect.Effect<Service, SyncError>;
132
+ /**
133
+ * Provides the HMAC-SHA-256 share authority over a keyring.
134
+ *
135
+ * @category layers
136
+ * @since 0.1.0
137
+ */
138
+ export declare const layerHmac: (keyring: Keyring) => Layer.Layer<BranchShare, SyncError>;
139
+ /**
140
+ * Provides the branch share authority from configuration: the secret from
141
+ * `SMITHERS_SYNC_BRANCH_SECRET` (read as `Redacted`, never logged) and the key
142
+ * name from `SMITHERS_SYNC_BRANCH_KEY_ID`, defaulting to `primary`. Rotation
143
+ * beyond one configured key uses {@link layerHmac} with an explicit keyring.
144
+ *
145
+ * The names are the branch authority's own, not the workspace's: the scheme
146
+ * label makes one secret in both authorities survivable, not advisable, and a
147
+ * deployment that rotates one credential should not silently invalidate the
148
+ * other's outstanding capabilities.
149
+ *
150
+ * There is deliberately no default secret: a deployment that configures
151
+ * neither name fails to construct the authority, and every branch operation
152
+ * stays closed.
153
+ *
154
+ * @category layers
155
+ * @since 1.0.0-rc.0
156
+ */
157
+ export declare const layerConfig: Layer.Layer<BranchShare, SyncError | Config.ConfigError>;
158
+ export {};
159
+ //# sourceMappingURL=BranchShare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BranchShare.d.ts","sourceRoot":"","sources":["../../src/BranchShare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEpF,OAAO,KAAK,WAAW,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;IAKtB,wEAAwE;;EAExE,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAA;IAClF,QAAQ,CAAC,MAAM,EAAE,CACf,UAAU,EAAE,eAAe,EAC3B,OAAO,EAAE,gBAAgB,KACtB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;CAC3C;;AAED;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,gBAAmE;CAAG;AAIvG;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,OAQxD,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAA0C,CAAA;AA0CzF;;;;;;GAMG;AACH,MAAM,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAA;AAEzC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAkEzE,CAAA;AAEH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,OAAO,KAAG,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,CACjC,CAAA;AAE9C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,WAAW,CAOhF,CAAA"}
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var BranchShare_exports = {};
30
+ __export(BranchShare_exports, {
31
+ BranchShare: () => BranchShare,
32
+ MintRequest: () => MintRequest,
33
+ layerConfig: () => layerConfig,
34
+ layerHmac: () => layerHmac,
35
+ layerNoop: () => layerNoop,
36
+ makeHmac: () => makeHmac,
37
+ makeNoop: () => makeNoop
38
+ });
39
+ module.exports = __toCommonJS(BranchShare_exports);
40
+ var Clock = __toESM(require("effect/Clock"), 1);
41
+ var Config = __toESM(require("effect/Config"), 1);
42
+ var Context = __toESM(require("effect/Context"), 1);
43
+ var Effect = __toESM(require("effect/Effect"), 1);
44
+ var Layer = __toESM(require("effect/Layer"), 1);
45
+ var Schema = __toESM(require("effect/Schema"), 1);
46
+ var import_BranchProtocol = require("./BranchProtocol.js");
47
+ var Admission = __toESM(require("./internal/Admission.js"), 1);
48
+ var shareSigner = __toESM(require("./internal/ShareSigner.js"), 1);
49
+ const MintRequest = Schema.Struct({
50
+ branchId: import_BranchProtocol.BranchId,
51
+ capabilityId: Schema.NonEmptyString,
52
+ access: import_BranchProtocol.Access,
53
+ ttlMs: Schema.Int.check(Schema.isGreaterThan(0)),
54
+ /** Absolute expiry ceiling for a capability delegated from a parent. */
55
+ maxExpiresAtMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
56
+ });
57
+ class BranchShare extends Context.Service()("@smthrs/sync/BranchShare") {
58
+ }
59
+ const denied = shareSigner.unauthorized;
60
+ const makeNoop = (overrides = {}) => BranchShare.of({
61
+ // Both operations FAIL. `mint` used to die, which contradicted its own
62
+ // declared `Effect<ShareCapability, SyncError>` and left a consumer
63
+ // wired to this authority unable to tell "sharing is off" from a bug.
64
+ mint: () => Effect.fail(denied("Branch sharing is unavailable")),
65
+ verify: () => Effect.fail(denied("Branch sharing is unavailable")),
66
+ ...overrides
67
+ });
68
+ const layerNoop = Layer.succeed(BranchShare, makeNoop());
69
+ const schemeLabel = "@smthrs/sync/BranchShare/v2";
70
+ const canonical = (claims) => shareSigner.lengthPrefixed([
71
+ schemeLabel,
72
+ claims.kid,
73
+ claims.branchId,
74
+ claims.capabilityId,
75
+ claims.access,
76
+ String(claims.issuedAtMs),
77
+ String(claims.expiresAtMs)
78
+ ]);
79
+ const snapshot = (claims) => new import_BranchProtocol.ShareClaims({
80
+ kid: claims.kid,
81
+ branchId: claims.branchId,
82
+ capabilityId: claims.capabilityId,
83
+ access: claims.access,
84
+ issuedAtMs: claims.issuedAtMs,
85
+ expiresAtMs: claims.expiresAtMs
86
+ });
87
+ const makeHmac = (keyring) => Effect.map(
88
+ shareSigner.importKeyring(keyring, "branch"),
89
+ (ring) => {
90
+ const mint = Effect.fn("BranchShare.mint")(function* (request) {
91
+ const admitted = yield* Admission.decode(MintRequest, request, "invalid_request");
92
+ yield* Effect.annotateCurrentSpan({ branchId: admitted.branchId, access: admitted.access });
93
+ const issuedAtMs = yield* Clock.currentTimeMillis;
94
+ const maxExpiresAtMs = admitted.maxExpiresAtMs ?? Infinity;
95
+ if (issuedAtMs >= maxExpiresAtMs) {
96
+ return yield* Effect.fail(denied("The share capability has expired"));
97
+ }
98
+ const claims = new import_BranchProtocol.ShareClaims({
99
+ kid: ring.activeKid,
100
+ branchId: admitted.branchId,
101
+ capabilityId: admitted.capabilityId,
102
+ access: admitted.access,
103
+ issuedAtMs,
104
+ expiresAtMs: Math.min(issuedAtMs + admitted.ttlMs, maxExpiresAtMs)
105
+ });
106
+ return new import_BranchProtocol.ShareCapability({
107
+ claims,
108
+ signature: yield* shareSigner.signHmac(ring.active, canonical(claims))
109
+ });
110
+ });
111
+ const verify = Effect.fn("BranchShare.verify")(function* (capability, request) {
112
+ yield* Effect.annotateCurrentSpan({ branchId: request.branchId, access: request.access });
113
+ const claims = snapshot(capability.claims);
114
+ const signature = capability.signature;
115
+ const branchId = request.branchId;
116
+ const key = ring.verification.get(claims.kid);
117
+ if (key === void 0) {
118
+ return yield* Effect.fail(denied("The share capability names an unknown signing key"));
119
+ }
120
+ yield* shareSigner.verifyClaims({
121
+ key,
122
+ canonical: canonical(claims),
123
+ signature,
124
+ expiresAtMs: claims.expiresAtMs,
125
+ granted: claims.access,
126
+ requested: request.access,
127
+ subject: "The share capability",
128
+ // The one check this authority adds, run after the signature so a
129
+ // forged claim set is refused as a bad signature and never reported
130
+ // back as the branch it names.
131
+ scope: claims.branchId === branchId ? Effect.void : Effect.fail(denied(`The share capability is scoped to branch ${claims.branchId}`))
132
+ });
133
+ return claims;
134
+ });
135
+ return BranchShare.of({ mint, verify });
136
+ }
137
+ );
138
+ const layerHmac = (keyring) => Layer.effect(BranchShare, makeHmac(keyring));
139
+ const layerConfig = Layer.effect(
140
+ BranchShare,
141
+ Effect.gen(function* () {
142
+ const secret = yield* Config.Redacted("SMITHERS_SYNC_BRANCH_SECRET");
143
+ const kid = yield* Config.String("SMITHERS_SYNC_BRANCH_KEY_ID").pipe(Config.withDefault("primary"));
144
+ return yield* makeHmac({ activeKid: kid, keys: [{ kid, secret }] });
145
+ })
146
+ );
147
+ //# sourceMappingURL=BranchShare.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/BranchShare.ts"],
4
+ "sourcesContent": ["/**\n * Share capabilities: the authorization boundary of a shared branch.\n *\n * A share link carries a capability, not a session. The capability names\n * exactly one branch, one access level, and one expiry, and it is signed, so\n * the holder cannot widen it. Every branch operation authorizes through\n * `verify`, which is therefore the single place cross-branch access and\n * expired links are refused.\n *\n * The signature is HMAC-SHA-256 over a length-prefixed encoding of the claims,\n * led by a scheme label. Length prefixes matter: without them a branch id\n * ending in the separator could be re-cut into a different, still-validly-\n * signed claim set. The label matters for the same reason across schemes: a\n * branch capability can never verify as a workspace capability even if one\n * secret is misconfigured into both authorities. Web Crypto is used directly\n * so the same module runs in the browser and on node.\n *\n * A `kid` inside the claims names the key that signed them, so a branch secret\n * rotates the way the workspace secret already did: the new key signs, the\n * retired key stays in the verification keyring, and the share links already\n * out keep working until they expire.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport * as Config from \"effect/Config\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport { Access, BranchId, ShareCapability, ShareClaims } from \"./BranchProtocol.ts\"\nimport * as Admission from \"./internal/Admission.ts\"\nimport * as shareSigner from \"./internal/ShareSigner.ts\"\nimport type { SyncError } from \"./SyncError.ts\"\n\n/**\n * The branch and access one authorization request needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface AuthorizeRequest {\n readonly branchId: BranchId\n readonly access: Access\n}\n\n/**\n * What a freshly minted capability grants.\n *\n * @category models\n * @since 0.1.0\n */\nexport const MintRequest = Schema.Struct({\n branchId: BranchId,\n capabilityId: Schema.NonEmptyString,\n access: Access,\n ttlMs: Schema.Int.check(Schema.isGreaterThan(0)),\n /** Absolute expiry ceiling for a capability delegated from a parent. */\n maxExpiresAtMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))\n})\n\n/**\n * What a freshly minted capability grants.\n *\n * @category models\n * @since 0.1.0\n */\nexport type MintRequest = typeof MintRequest.Type\n\n/**\n * Share capability operations.\n *\n * `mint` fails with a `SyncError` when the Web Crypto signing operation\n * rejects or the absolute expiry ceiling has elapsed; `verify` fails with a\n * `SyncError` when signing rejects or the capability is refused.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n readonly mint: (request: MintRequest) => Effect.Effect<ShareCapability, SyncError>\n readonly verify: (\n capability: ShareCapability,\n request: AuthorizeRequest\n ) => Effect.Effect<ShareClaims, SyncError>\n}\n\n/**\n * The branch share-capability authority.\n *\n * @category services\n * @since 0.1.0\n */\nexport class BranchShare extends Context.Service<BranchShare, Service>()(\"@smthrs/sync/BranchShare\") {}\n\nconst denied = shareSigner.unauthorized\n\n/**\n * Constructs a share authority that mints nothing and trusts nothing.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service =>\n BranchShare.of({\n // Both operations FAIL. `mint` used to die, which contradicted its own\n // declared `Effect<ShareCapability, SyncError>` and left a consumer\n // wired to this authority unable to tell \"sharing is off\" from a bug.\n mint: () => Effect.fail(denied(\"Branch sharing is unavailable\")),\n verify: () => Effect.fail(denied(\"Branch sharing is unavailable\")),\n ...overrides\n })\n\n/**\n * Provides a share authority that refuses every capability.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop: Layer.Layer<BranchShare> = Layer.succeed(BranchShare, makeNoop())\n\n/**\n * Domain separation: the label leads the signed encoding, so a branch\n * signature can never be replayed as a workspace signature (or any later\n * scheme's) under a shared secret. `WorkspaceShare` has always led with its\n * own label; a one-sided label protects one direction, and the branch side is\n * the one anonymous share-link holders reach.\n */\nconst schemeLabel = \"@smthrs/sync/BranchShare/v2\"\n\n/** Length-prefixed so no two distinct claim sets share an encoding. */\nconst canonical = (claims: ShareClaims): string =>\n shareSigner.lengthPrefixed([\n schemeLabel,\n claims.kid,\n claims.branchId,\n claims.capabilityId,\n claims.access,\n String(claims.issuedAtMs),\n String(claims.expiresAtMs)\n ])\n\n/**\n * The claim fields, copied out of the capability the caller owns.\n *\n * `Schema.Class` instances are not frozen, and `verify` awaits Web Crypto\n * between signing the claims and authorizing them. Reading the caller's object\n * again after the await let an in-process holder of the same instance widen\n * `access` \u2014 or move `expiresAtMs` \u2014 between the signature that was checked\n * and the checks that follow it.\n */\nconst snapshot = (claims: ShareClaims): ShareClaims =>\n new ShareClaims({\n kid: claims.kid,\n branchId: claims.branchId,\n capabilityId: claims.capabilityId,\n access: claims.access,\n issuedAtMs: claims.issuedAtMs,\n expiresAtMs: claims.expiresAtMs\n })\n\n/**\n * One named signing key. The secret is `Redacted` so a keyring never renders\n * it into logs or inspection output.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Key = shareSigner.Key\n\n/**\n * A keyring: the key that signs new capabilities plus every key still\n * accepted for verification. Rotation adds a new active key and keeps the\n * retired one in `keys` until its outstanding links expire, so rotating the\n * branch secret no longer breaks every share link that is already out.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Keyring = shareSigner.Keyring\n\n/**\n * Constructs the HMAC-SHA-256 share authority over a keyring.\n *\n * Every key is imported up front, so a misconfigured keyring \u2014 an unknown\n * active kid, a duplicate kid, or a key Web Crypto refuses \u2014 fails at\n * construction rather than at the first request. Secrets enter as `Redacted`\n * values: an authority never holds a plain string that a log, a span, or an\n * inspection of the keyring could render.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeHmac = (keyring: Keyring): Effect.Effect<Service, SyncError> =>\n Effect.map(\n shareSigner.importKeyring(keyring, \"branch\"),\n (ring) => {\n const mint = Effect.fn(\"BranchShare.mint\")(function*(request: MintRequest) {\n // Decoded, not read: the declared parameter type admits an empty\n // branch id, an empty capability id and a non-positive ttl that the\n // schema forbids, and reading them straight into `new ShareClaims`\n // turned a caller's bad argument into a defect out of an operation\n // whose type promises a `SyncError`. A `ttlMs` of 0 minted an\n // already-expired capability instead of being refused.\n const admitted = yield* Admission.decode(MintRequest, request, \"invalid_request\")\n yield* Effect.annotateCurrentSpan({ branchId: admitted.branchId, access: admitted.access })\n const issuedAtMs = yield* Clock.currentTimeMillis\n const maxExpiresAtMs = admitted.maxExpiresAtMs ?? Infinity\n if (issuedAtMs >= maxExpiresAtMs) {\n return yield* Effect.fail(denied(\"The share capability has expired\"))\n }\n const claims = new ShareClaims({\n kid: ring.activeKid,\n branchId: admitted.branchId,\n capabilityId: admitted.capabilityId,\n access: admitted.access,\n issuedAtMs,\n expiresAtMs: Math.min(issuedAtMs + admitted.ttlMs, maxExpiresAtMs)\n })\n return new ShareCapability({\n claims,\n signature: yield* shareSigner.signHmac(ring.active, canonical(claims))\n })\n })\n\n const verify = Effect.fn(\"BranchShare.verify\")(function*(\n capability: ShareCapability,\n request: AuthorizeRequest\n ) {\n yield* Effect.annotateCurrentSpan({ branchId: request.branchId, access: request.access })\n // Everything authorized below is read from these locals, never from\n // the caller's objects, which may change while Web Crypto is awaited.\n const claims = snapshot(capability.claims)\n const signature = capability.signature\n const branchId = request.branchId\n const key = ring.verification.get(claims.kid)\n if (key === undefined) {\n return yield* Effect.fail(denied(\"The share capability names an unknown signing key\"))\n }\n yield* shareSigner.verifyClaims({\n key,\n canonical: canonical(claims),\n signature,\n expiresAtMs: claims.expiresAtMs,\n granted: claims.access,\n requested: request.access,\n subject: \"The share capability\",\n // The one check this authority adds, run after the signature so a\n // forged claim set is refused as a bad signature and never reported\n // back as the branch it names.\n scope: claims.branchId === branchId\n ? Effect.void\n : Effect.fail(denied(`The share capability is scoped to branch ${claims.branchId}`))\n })\n return claims\n })\n\n return BranchShare.of({ mint, verify })\n }\n )\n\n/**\n * Provides the HMAC-SHA-256 share authority over a keyring.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerHmac = (keyring: Keyring): Layer.Layer<BranchShare, SyncError> =>\n Layer.effect(BranchShare, makeHmac(keyring))\n\n/**\n * Provides the branch share authority from configuration: the secret from\n * `SMITHERS_SYNC_BRANCH_SECRET` (read as `Redacted`, never logged) and the key\n * name from `SMITHERS_SYNC_BRANCH_KEY_ID`, defaulting to `primary`. Rotation\n * beyond one configured key uses {@link layerHmac} with an explicit keyring.\n *\n * The names are the branch authority's own, not the workspace's: the scheme\n * label makes one secret in both authorities survivable, not advisable, and a\n * deployment that rotates one credential should not silently invalidate the\n * other's outstanding capabilities.\n *\n * There is deliberately no default secret: a deployment that configures\n * neither name fails to construct the authority, and every branch operation\n * stays closed.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layerConfig: Layer.Layer<BranchShare, SyncError | Config.ConfigError> = Layer.effect(\n BranchShare,\n Effect.gen(function*() {\n const secret = yield* Config.Redacted(\"SMITHERS_SYNC_BRANCH_SECRET\")\n const kid = yield* Config.String(\"SMITHERS_SYNC_BRANCH_KEY_ID\").pipe(Config.withDefault(\"primary\"))\n return yield* makeHmac({ activeKid: kid, keys: [{ kid, secret }] })\n })\n)\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Workspace run enumeration for synchronization.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import type { JournalEvent } from "@smthrs/journal";
7
+ import * as Context from "effect/Context";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Layer from "effect/Layer";
10
+ import type * as Scope from "effect/Scope";
11
+ import * as Stream from "effect/Stream";
12
+ import type { SyncError } from "./SyncError.ts";
13
+ /**
14
+ * Workspace run enumeration operations.
15
+ *
16
+ * `list` is the AUTHORITATIVE run set and `changes` is only a low-latency
17
+ * wake: a subscriber that misses an announcement re-lists rather than losing
18
+ * state, and a subscriber whose covered set must stay accurate re-lists on a
19
+ * cadence of its own. Every implementation returns a fresh array from `list`,
20
+ * so a caller may retain and sort what it receives without disturbing another
21
+ * reader.
22
+ *
23
+ * `list` names each run AT MOST ONCE. Every implementation here deduplicates,
24
+ * and `SyncServer` deduplicates again at the seam rather than trusting a
25
+ * host's catalog, because both fan-out paths key a run's served position by
26
+ * run id: a run named twice is read twice from the same position and its
27
+ * entries are served twice.
28
+ *
29
+ * The catalog is supplied by the host rather than derived from the journal:
30
+ * `@smthrs/journal` has no workspace-wide list or watch contract, and
31
+ * `@smthrs/engine-store` owns the durable run set that {@link makePolling}
32
+ * reads.
33
+ *
34
+ * @category models
35
+ * @since 0.1.0
36
+ */
37
+ export interface Service {
38
+ readonly list: Effect.Effect<ReadonlyArray<JournalEvent.RunId>>;
39
+ readonly changes: Stream.Stream<JournalEvent.RunId>;
40
+ }
41
+ declare const RunCatalog_base: Context.ServiceClass<RunCatalog, "@smthrs/sync/RunCatalog", Service>;
42
+ /**
43
+ * Host-provided workspace run catalog.
44
+ *
45
+ * @category services
46
+ * @since 0.1.0
47
+ */
48
+ export declare class RunCatalog extends RunCatalog_base {
49
+ }
50
+ /**
51
+ * Provides an immutable run catalog.
52
+ *
53
+ * `list` answers with a fresh array each time, matching {@link makeMemory} and
54
+ * {@link makePolling}. Handing every caller the same instance let one reader
55
+ * sort or splice the catalog in place for every other reader, which is a
56
+ * defect a `ReadonlyArray` type hides from TypeScript and not from JavaScript.
57
+ *
58
+ * @category layers
59
+ * @since 0.1.0
60
+ */
61
+ export declare const layerStatic: (ids: ReadonlyArray<JournalEvent.RunId>) => Layer.Layer<RunCatalog>;
62
+ /**
63
+ * Announcements a stalled {@link Service.changes} subscriber may fall behind
64
+ * by before the oldest are dropped.
65
+ *
66
+ * `changes` is a notification feed, not a log: a follower reads it to learn
67
+ * that the workspace gained a run and then reads the run itself. Retaining an
68
+ * unbounded backlog for a subscriber that has stopped pulling — a disconnected
69
+ * tab whose socket has not timed out yet, a follower blocked behind a slow
70
+ * consumer — is therefore pure cost, and the workspace's whole run history is
71
+ * the upper bound on it. A subscriber that falls further behind than this
72
+ * bound loses the oldest announcements; `list` still names every run, so a
73
+ * follower that reconnects or re-lists converges.
74
+ *
75
+ * @category constants
76
+ * @since 0.1.0
77
+ */
78
+ export declare const defaultChangesCapacity = 1024;
79
+ /**
80
+ * Options for the in-memory run catalog.
81
+ *
82
+ * @category models
83
+ * @since 0.1.0
84
+ */
85
+ export interface MemoryOptions {
86
+ /**
87
+ * Announcements a stalled subscriber may fall behind by. Defaults to
88
+ * {@link defaultChangesCapacity}.
89
+ */
90
+ readonly changesCapacity?: number | undefined;
91
+ }
92
+ /**
93
+ * Constructs a mutable in-memory run catalog.
94
+ *
95
+ * A run is published exactly once, when it is first registered. The
96
+ * announcement feed slides: registering never waits on a stalled subscriber,
97
+ * and never grows the process on its behalf.
98
+ *
99
+ * Fails with `invalid_request` when `changesCapacity` is not a positive safe
100
+ * integer, rather than handing it to `PubSub.sliding` as a capacity.
101
+ *
102
+ * @category constructors
103
+ * @since 0.1.0
104
+ */
105
+ export declare const makeMemory: (options?: MemoryOptions) => Effect.Effect<{
106
+ readonly catalog: Service;
107
+ readonly register: (runId: JournalEvent.RunId) => Effect.Effect<void>;
108
+ }, SyncError>;
109
+ /**
110
+ * How often {@link makePolling} re-reads its source when the caller names no
111
+ * interval, in milliseconds.
112
+ *
113
+ * A workspace follower learns of a new run within one interval of it being
114
+ * created, and the read costs one bounded query per interval per composition,
115
+ * not per subscriber. A second is short enough that a run appears in a UI
116
+ * while the operator is still looking at the command that started it, and
117
+ * long enough that an idle workspace is close to free.
118
+ *
119
+ * @category constants
120
+ * @since 0.1.0
121
+ */
122
+ export declare const defaultPollIntervalMs = 1000;
123
+ /**
124
+ * Options for the polling run catalog.
125
+ *
126
+ * @category models
127
+ * @since 0.1.0
128
+ */
129
+ export interface PollingOptions<E, R> {
130
+ /**
131
+ * Reads the workspace's current run set. `@smthrs/engine-store` supplies
132
+ * the durable one over `flows_runs`; anything that answers the same
133
+ * question works, and the catalog holds no assumption about which.
134
+ */
135
+ readonly read: Effect.Effect<ReadonlyArray<JournalEvent.RunId>, E, R>;
136
+ /** Milliseconds between reads. Defaults to {@link defaultPollIntervalMs}. */
137
+ readonly intervalMs?: number | undefined;
138
+ /**
139
+ * Announcements a stalled subscriber may fall behind by. Defaults to
140
+ * {@link defaultChangesCapacity}.
141
+ */
142
+ readonly changesCapacity?: number | undefined;
143
+ }
144
+ /**
145
+ * Constructs a run catalog that polls a durable source.
146
+ *
147
+ * Polling, not waking. rc.0 has no cross-process event delivery, so a
148
+ * follower learns of a run another engine created by asking the workspace
149
+ * again, and the interval is the whole of the policy.
150
+ *
151
+ * The catalog primes itself before it returns, so the first read a follower
152
+ * makes is already current and a workspace that cannot be read at all fails
153
+ * the composition instead of serving an empty run set. After that a failed
154
+ * read is a warning and nothing else: the previous view stands, the interval
155
+ * holds, and no subscription attached to the catalog is torn down for it.
156
+ * The warning is written once per outage, with the first failure's cause, and
157
+ * one info line marks the read that ends it; the polls in between are silent.
158
+ *
159
+ * A run is announced once, when it first appears. A run the read stops
160
+ * naming, which is what retention collecting it looks like, leaves the view;
161
+ * `list` is the workspace's run set, not a log of every run it ever had.
162
+ *
163
+ * `list` answers with a fresh array each time, matching {@link layerStatic} and
164
+ * {@link makeMemory}. Handing back the stored snapshot let a caller sort or
165
+ * splice the authoritative run set in place under the poll fiber that owns it,
166
+ * which is a defect a `ReadonlyArray` type hides from TypeScript and not from
167
+ * JavaScript.
168
+ *
169
+ * The poll fiber belongs to the caller's scope. Closing the scope stops it.
170
+ *
171
+ * Both numeric options are validated here, before the first read: an
172
+ * `intervalMs` of `NaN` or zero reached `Effect.delay` as the whole of the
173
+ * poll cadence, which is a loop with no interval rather than a refusal.
174
+ *
175
+ * @category constructors
176
+ * @since 0.1.0
177
+ */
178
+ export declare const makePolling: <E, R>(options: PollingOptions<E, R>) => Effect.Effect<Service, E | SyncError, R | Scope.Scope>;
179
+ /**
180
+ * Provides a run catalog that polls a durable source. The poll fiber lives as
181
+ * long as the layer's scope.
182
+ *
183
+ * @category layers
184
+ * @since 0.1.0
185
+ */
186
+ export declare const layerPolling: <E, R>(options: PollingOptions<E, R>) => Layer.Layer<RunCatalog, E | SyncError, R>;
187
+ /**
188
+ * Provides the mutable in-memory run catalog, matching how every other
189
+ * implementation here is provided. Fails with `invalid_request` when
190
+ * `changesCapacity` is not a positive safe integer.
191
+ *
192
+ * The layer provides the catalog alone. A composition that also registers runs
193
+ * builds {@link makeMemory} itself and keeps its `register`.
194
+ *
195
+ * @category layers
196
+ * @since 1.0.0-rc.0
197
+ */
198
+ export declare const layerMemory: (options?: MemoryOptions) => Layer.Layer<RunCatalog, SyncError>;
199
+ /**
200
+ * Provides an empty run catalog.
201
+ *
202
+ * @category layers
203
+ * @since 0.1.0
204
+ */
205
+ export declare const layerNoop: Layer.Layer<RunCatalog>;
206
+ export {};
207
+ //# sourceMappingURL=RunCatalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RunCatalog.d.ts","sourceRoot":"","sources":["../../src/RunCatalog.ts"],"names":[],"mappings":"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"}