@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,70 @@
1
+ /**
2
+ * Execution-time validation of the numeric policies this package's
3
+ * constructors accept.
4
+ *
5
+ * Every `Options` field is typed `number`, and TypeScript admits `NaN`,
6
+ * `Infinity`, zero, and a negative under that type. Each of them disables the
7
+ * thing it configures rather than tightening it: `maxFrameBytes: NaN` makes
8
+ * every `bytes > maxFrameBytes` comparison false, so the ceiling silently
9
+ * stops existing; `concurrency: 0` reaches a bounded `Stream.flatMap`
10
+ * unchecked. A policy is checked once, where it enters, and a bad one is a
11
+ * typed refusal rather than a bound that quietly does nothing.
12
+ *
13
+ * @since 1.0.0-rc.0
14
+ */
15
+ import * as Effect from "effect/Effect";
16
+ import { SyncError } from "../SyncError.js";
17
+ /**
18
+ * The caller's value when it is a positive safe integer, the fallback when the
19
+ * caller supplied none, and a typed `invalid_request` refusal otherwise. The
20
+ * message names the option so the offending field is identifiable without the
21
+ * error carrying anything but the scalar it was given.
22
+ *
23
+ * @category validation
24
+ * @since 1.0.0-rc.0
25
+ */
26
+ export const positiveInt = (name, value, fallback) => {
27
+ if (value === undefined)
28
+ return Effect.succeed(fallback);
29
+ return Number.isSafeInteger(value) && value > 0 ? Effect.succeed(value) : Effect.fail(new SyncError({
30
+ code: "invalid_request",
31
+ message: `${name} must be a positive safe integer, not ${value}`
32
+ }));
33
+ };
34
+ /**
35
+ * A count carried by a REQUEST rather than by a policy: floored at one,
36
+ * ceilinged at what the wire schema allows, and refused otherwise.
37
+ *
38
+ * `SyncProtocol` already bounds `limit` and `credit` on both sides, so a
39
+ * decoded request cannot carry a bad one. An in-process caller that builds the
40
+ * request value directly never meets that schema, and the TypeScript type it
41
+ * does meet is `number`: `NaN`, zero, and a negative all satisfy it. Each of
42
+ * them disables the bound instead of tightening it — `Math.min(NaN, ceiling)`
43
+ * is `NaN`, every `length >= NaN` comparison answers false, and the value
44
+ * reaches the journal as a page size — so the floor is checked here, where
45
+ * both callers share one refusal and one sentence.
46
+ *
47
+ * @category validation
48
+ * @since 1.0.0-rc.0
49
+ */
50
+ export const requestCount = (name, value, ceiling) => Number.isSafeInteger(value) && value > 0 ? Effect.succeed(Math.min(value, ceiling)) : Effect.fail(new SyncError({
51
+ code: "invalid_request",
52
+ message: `${name} must be a positive safe integer, not ${value}`
53
+ }));
54
+ /**
55
+ * {@link positiveInt} plus a ceiling, for a policy the WIRE also bounds.
56
+ *
57
+ * A value the local schema would accept and the wire schema would refuse is
58
+ * not a transport failure, but that is what it looked like: the RPC client's
59
+ * own decode failure arrived through the same channel a dropped connection
60
+ * does, and the follow retried it under backoff forever. The refusal belongs
61
+ * here, before a request is built.
62
+ *
63
+ * @category validation
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export const boundedInt = (name, value, fallback, maximum) => Effect.flatMap(positiveInt(name, value, fallback), (accepted) => accepted <= maximum ? Effect.succeed(accepted) : Effect.fail(new SyncError({
67
+ code: "invalid_request",
68
+ message: `${name} must be at most ${maximum}, not ${accepted}`
69
+ })));
70
+ //# sourceMappingURL=PolicyOptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PolicyOptions.js","sourceRoot":"","sources":["../../../src/internal/PolicyOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,IAAY,EACZ,KAAyB,EACzB,QAAgB,EACkB,EAAE;IACpC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACxD,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CACnF,IAAI,SAAS,CAAC;QACZ,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,GAAG,IAAI,yCAAyC,KAAK,EAAE;KACjE,CAAC,CACH,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,IAAY,EACZ,KAAa,EACb,OAAe,EACmB,EAAE,CACpC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAC/F,IAAI,SAAS,CAAC;IACZ,IAAI,EAAE,iBAAiB;IACvB,OAAO,EAAE,GAAG,IAAI,yCAAyC,KAAK,EAAE;CACjE,CAAC,CACH,CAAA;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,IAAY,EACZ,KAAyB,EACzB,QAAgB,EAChB,OAAe,EACmB,EAAE,CACpC,MAAM,CAAC,OAAO,CACZ,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,EAClC,CAAC,QAAQ,EAAE,EAAE,CACX,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAC1D,IAAI,SAAS,CAAC;IACZ,IAAI,EAAE,iBAAiB;IACvB,OAAO,EAAE,GAAG,IAAI,oBAAoB,OAAO,SAAS,QAAQ,EAAE;CAC/D,CAAC,CACH,CACJ,CAAA","sourcesContent":["/**\n * Execution-time validation of the numeric policies this package's\n * constructors accept.\n *\n * Every `Options` field is typed `number`, and TypeScript admits `NaN`,\n * `Infinity`, zero, and a negative under that type. Each of them disables the\n * thing it configures rather than tightening it: `maxFrameBytes: NaN` makes\n * every `bytes > maxFrameBytes` comparison false, so the ceiling silently\n * stops existing; `concurrency: 0` reaches a bounded `Stream.flatMap`\n * unchecked. A policy is checked once, where it enters, and a bad one is a\n * typed refusal rather than a bound that quietly does nothing.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport { SyncError } from \"../SyncError.ts\"\n\n/**\n * The caller's value when it is a positive safe integer, the fallback when the\n * caller supplied none, and a typed `invalid_request` refusal otherwise. The\n * message names the option so the offending field is identifiable without the\n * error carrying anything but the scalar it was given.\n *\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const positiveInt = (\n name: string,\n value: number | undefined,\n fallback: number\n): Effect.Effect<number, SyncError> => {\n if (value === undefined) return Effect.succeed(fallback)\n return Number.isSafeInteger(value) && value > 0 ? Effect.succeed(value) : Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: `${name} must be a positive safe integer, not ${value}`\n })\n )\n}\n\n/**\n * A count carried by a REQUEST rather than by a policy: floored at one,\n * ceilinged at what the wire schema allows, and refused otherwise.\n *\n * `SyncProtocol` already bounds `limit` and `credit` on both sides, so a\n * decoded request cannot carry a bad one. An in-process caller that builds the\n * request value directly never meets that schema, and the TypeScript type it\n * does meet is `number`: `NaN`, zero, and a negative all satisfy it. Each of\n * them disables the bound instead of tightening it — `Math.min(NaN, ceiling)`\n * is `NaN`, every `length >= NaN` comparison answers false, and the value\n * reaches the journal as a page size — so the floor is checked here, where\n * both callers share one refusal and one sentence.\n *\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const requestCount = (\n name: string,\n value: number,\n ceiling: number\n): Effect.Effect<number, SyncError> =>\n Number.isSafeInteger(value) && value > 0 ? Effect.succeed(Math.min(value, ceiling)) : Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: `${name} must be a positive safe integer, not ${value}`\n })\n )\n\n/**\n * {@link positiveInt} plus a ceiling, for a policy the WIRE also bounds.\n *\n * A value the local schema would accept and the wire schema would refuse is\n * not a transport failure, but that is what it looked like: the RPC client's\n * own decode failure arrived through the same channel a dropped connection\n * does, and the follow retried it under backoff forever. The refusal belongs\n * here, before a request is built.\n *\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const boundedInt = (\n name: string,\n value: number | undefined,\n fallback: number,\n maximum: number\n): Effect.Effect<number, SyncError> =>\n Effect.flatMap(\n positiveInt(name, value, fallback),\n (accepted) =>\n accepted <= maximum ? Effect.succeed(accepted) : Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: `${name} must be at most ${maximum}, not ${accepted}`\n })\n )\n )\n"]}
@@ -0,0 +1,17 @@
1
+ /** The one protocol-version comparison every sync boundary answers with.
2
+ * @since 1.0.0-rc.0
3
+ */
4
+ import * as Effect from "effect/Effect";
5
+ import { SyncError } from "../SyncError.ts";
6
+ /** Refuse a version this revision does not speak, on every path, with one code.
7
+ *
8
+ * Read, subscribe and snapshot all decode a mismatched version so the refusal
9
+ * is this typed `protocol_violation` rather than three different decode
10
+ * failures, and the accepted number is only ever
11
+ * {@link SyncProtocol.protocolVersion}.
12
+ *
13
+ * @category validation
14
+ * @since 1.0.0-rc.0
15
+ */
16
+ export declare const requireVersion: (version: number | undefined) => Effect.Effect<void, SyncError>;
17
+ //# sourceMappingURL=ProtocolVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProtocolVersion.d.ts","sourceRoot":"","sources":["../../../src/internal/ProtocolVersion.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAG3C;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,GAAG,SAAS,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CASrF,CAAA"}
@@ -0,0 +1,24 @@
1
+ /** The one protocol-version comparison every sync boundary answers with.
2
+ * @since 1.0.0-rc.0
3
+ */
4
+ import * as Effect from "effect/Effect";
5
+ import { SyncError } from "../SyncError.js";
6
+ import { protocolVersion } from "../SyncProtocol.js";
7
+ /** Refuse a version this revision does not speak, on every path, with one code.
8
+ *
9
+ * Read, subscribe and snapshot all decode a mismatched version so the refusal
10
+ * is this typed `protocol_violation` rather than three different decode
11
+ * failures, and the accepted number is only ever
12
+ * {@link SyncProtocol.protocolVersion}.
13
+ *
14
+ * @category validation
15
+ * @since 1.0.0-rc.0
16
+ */
17
+ export const requireVersion = (version) => version === protocolVersion
18
+ ? Effect.void
19
+ : Effect.fail(new SyncError({
20
+ code: "protocol_violation",
21
+ message: `Expected sync protocol version ${protocolVersion}; received ${version}`,
22
+ cause: "protocol_version_mismatch"
23
+ }));
24
+ //# sourceMappingURL=ProtocolVersion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProtocolVersion.js","sourceRoot":"","sources":["../../../src/internal/ProtocolVersion.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAA2B,EAAkC,EAAE,CAC5F,OAAO,KAAK,eAAe;IACzB,CAAC,CAAC,MAAM,CAAC,IAAI;IACb,CAAC,CAAC,MAAM,CAAC,IAAI,CACX,IAAI,SAAS,CAAC;QACZ,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,kCAAkC,eAAe,cAAc,OAAO,EAAE;QACjF,KAAK,EAAE,2BAA2B;KACnC,CAAC,CACH,CAAA","sourcesContent":["/** The one protocol-version comparison every sync boundary answers with.\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport { SyncError } from \"../SyncError.ts\"\nimport { protocolVersion } from \"../SyncProtocol.ts\"\n\n/** Refuse a version this revision does not speak, on every path, with one code.\n *\n * Read, subscribe and snapshot all decode a mismatched version so the refusal\n * is this typed `protocol_violation` rather than three different decode\n * failures, and the accepted number is only ever\n * {@link SyncProtocol.protocolVersion}.\n *\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const requireVersion = (version: number | undefined): Effect.Effect<void, SyncError> =>\n version === protocolVersion\n ? Effect.void\n : Effect.fail(\n new SyncError({\n code: \"protocol_violation\",\n message: `Expected sync protocol version ${protocolVersion}; received ${version}`,\n cause: \"protocol_version_mismatch\"\n })\n )\n"]}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * HMAC-SHA-256 signing primitives shared by the branch and workspace share
3
+ * authorities.
4
+ *
5
+ * Both authorities sign a length-prefixed encoding of their claims: without
6
+ * length prefixes a field ending in the separator could be re-cut into a
7
+ * different, still-validly-signed claim set. Web Crypto is used directly so
8
+ * the same module runs in the browser and on node, and signature comparison
9
+ * is length-independent so a check leaks no prefix length.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ import * as Effect from "effect/Effect";
14
+ import * as Redacted from "effect/Redacted";
15
+ import type { Access } from "../BranchProtocol.ts";
16
+ import { SyncError } from "../SyncError.ts";
17
+ /**
18
+ * Length-prefixes each field so no two distinct field sequences share an
19
+ * encoding.
20
+ *
21
+ * The prefix counts UTF-8 BYTES, not UTF-16 code units, because the bytes are
22
+ * what {@link signHmac} covers. Counting code units let a two-byte field claim
23
+ * length one, so the prefix no longer separated the fields the signature was
24
+ * taken over.
25
+ *
26
+ * @category encoding
27
+ * @since 0.1.0
28
+ */
29
+ export declare const lengthPrefixed: (fields: ReadonlyArray<string>) => string;
30
+ /**
31
+ * Whether a string is exactly what its own UTF-8 bytes decode back to.
32
+ *
33
+ * `TextEncoder` replaces every unpaired surrogate with U+FFFD, so two claim
34
+ * sets differing only in a lone surrogate sign to identical bytes: a
35
+ * capability minted for a lone-surrogate branch id verifies after its
36
+ * `branchId` is rewritten to U+FFFD, which names a different branch. A length
37
+ * prefix cannot separate them either, because both encode to the same three
38
+ * bytes. The only correct answer is to refuse such a claim set before it is
39
+ * signed, and the round trip is the exact question — does this string survive
40
+ * the encoding the signature is taken over — rather than a proxy for it.
41
+ *
42
+ * @category encoding
43
+ * @since 1.0.0-rc.0
44
+ */
45
+ export declare const utf8RoundTrips: (value: string, bytes: Uint8Array) => boolean;
46
+ /**
47
+ * Renders signature bytes as lowercase hex.
48
+ *
49
+ * @category encoding
50
+ * @since 0.1.0
51
+ */
52
+ export declare const hex: (bytes: Uint8Array) => string;
53
+ /**
54
+ * Length-independent comparison, so a signature check leaks no prefix length.
55
+ *
56
+ * @category comparison
57
+ * @since 0.1.0
58
+ */
59
+ export declare const constantTimeEquals: (left: string, right: string) => boolean;
60
+ /**
61
+ * The fewest UTF-8 bytes a signing secret may carry: the HMAC-SHA-256 output
62
+ * length, the minimum RFC 2104 section 3 recommends.
63
+ *
64
+ * Every capability carries its claims and their signature in the clear, so one
65
+ * observed capability is an offline oracle for the secret. A short secret such
66
+ * as `a` falls to exhaustive search in seconds, and the attacker then mints
67
+ * branch and workspace capabilities of their own.
68
+ *
69
+ * @category constants
70
+ * @since 1.0.0-rc.1
71
+ */
72
+ export declare const minSecretBytes = 32;
73
+ /**
74
+ * Imports a raw secret as a non-extractable Web Crypto HMAC-SHA-256 signing
75
+ * key. Fails with `invalid_request` when the secret does not survive UTF-8
76
+ * (an unpaired surrogate) or is shorter than {@link minSecretBytes}, and with
77
+ * a `SyncError` carrying the rejection as `cause` when Web Crypto refuses the
78
+ * import.
79
+ *
80
+ * @category crypto
81
+ * @since 0.1.0
82
+ */
83
+ export declare const importHmacKey: (secret: string) => Effect.Effect<CryptoKey, SyncError>;
84
+ /**
85
+ * Signs a canonical claim encoding, returning the signature as lowercase hex.
86
+ *
87
+ * @category crypto
88
+ * @since 0.1.0
89
+ */
90
+ export declare const signHmac: (key: CryptoKey, canonical: string) => Effect.Effect<string, SyncError>;
91
+ /**
92
+ * The refusal both authorities issue. Declared once so a message added to one
93
+ * authority's verification cannot land under a different error code in the
94
+ * other.
95
+ *
96
+ * @category errors
97
+ * @since 1.0.0-rc.0
98
+ */
99
+ export declare const unauthorized: (message: string) => SyncError;
100
+ /**
101
+ * One named signing key. The secret is `Redacted` so a keyring never renders
102
+ * it into logs or inspection output.
103
+ *
104
+ * @category models
105
+ * @since 1.0.0-rc.0
106
+ */
107
+ export interface Key {
108
+ readonly kid: string;
109
+ readonly secret: Redacted.Redacted<string>;
110
+ }
111
+ /**
112
+ * A keyring: the key that signs new capabilities plus every key still
113
+ * accepted for verification. Rotation adds a new active key and keeps the
114
+ * retired one in `keys` until its outstanding capabilities expire.
115
+ *
116
+ * @category models
117
+ * @since 1.0.0-rc.0
118
+ */
119
+ export interface Keyring {
120
+ readonly activeKid: string;
121
+ readonly keys: ReadonlyArray<Key>;
122
+ }
123
+ /**
124
+ * A keyring whose every secret is already an imported Web Crypto key: the
125
+ * signer for new capabilities, and the map a verifier selects a retired key
126
+ * from by `kid`.
127
+ *
128
+ * @category models
129
+ * @since 1.0.0-rc.0
130
+ */
131
+ export interface ImportedKeyring {
132
+ readonly activeKid: string;
133
+ readonly active: CryptoKey;
134
+ readonly verification: ReadonlyMap<string, CryptoKey>;
135
+ }
136
+ /**
137
+ * Imports every key in a keyring, so a misconfigured ring — an unknown active
138
+ * kid, a duplicate kid, or a key Web Crypto refuses — fails at construction
139
+ * rather than at the first request. `subject` names the authority in the
140
+ * refusal ("branch", "workspace").
141
+ *
142
+ * @category crypto
143
+ * @since 1.0.0-rc.0
144
+ */
145
+ export declare const importKeyring: (keyring: Keyring, subject: string) => Effect.Effect<ImportedKeyring, SyncError>;
146
+ /**
147
+ * The authorization both capability authorities run once their signing key is
148
+ * selected: the signature, then the expiry, then the requested access.
149
+ *
150
+ * The order is the contract. A capability is authenticated before anything it
151
+ * claims is reported back, so a forged claim set is refused as a bad signature
152
+ * and never as a scope or an expiry. `scope` is the one check an authority
153
+ * adds for itself — the branch authority's cross-branch refusal — and it runs
154
+ * after the signature for exactly that reason.
155
+ *
156
+ * Both authorities called this sequence out by hand, which left a fix to the
157
+ * expiry boundary or the read-only refusal landing in one and not the other.
158
+ *
159
+ * @category crypto
160
+ * @since 1.0.0-rc.0
161
+ */
162
+ export declare const verifyClaims: (options: {
163
+ readonly key: CryptoKey;
164
+ readonly canonical: string;
165
+ readonly signature: string;
166
+ readonly expiresAtMs: number;
167
+ readonly granted: Access;
168
+ readonly requested: Access;
169
+ /** The refused thing, named as it appears in the message: "The share capability". */
170
+ readonly subject: string;
171
+ readonly scope?: Effect.Effect<void, SyncError>;
172
+ }) => Effect.Effect<void, SyncError>;
173
+ //# sourceMappingURL=ShareSigner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShareSigner.d.ts","sourceRoot":"","sources":["../../../src/internal/ShareSigner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAK3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,aAAa,CAAC,MAAM,CAAC,KAAG,MACa,CAAA;AAI5E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,OAAO,UAAU,KAAG,OAA0C,CAAA;AAE5G;;;;;GAKG;AACH,eAAO,MAAM,GAAG,GAAI,OAAO,UAAU,KAAG,MACkC,CAAA;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,OAQhE,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,KAAK,CAAA;AAEhC;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAiChF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,SAAS,EAAE,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAsB3F,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,SAA6D,CAAA;AAE5G;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;CAC3C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;CACtD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,EAAE,SAAS,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,SAAS,CA6BtG,CAAA;AAEJ;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS;IACpC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;CAChD,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAc7B,CAAA"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * HMAC-SHA-256 signing primitives shared by the branch and workspace share
3
+ * authorities.
4
+ *
5
+ * Both authorities sign a length-prefixed encoding of their claims: without
6
+ * length prefixes a field ending in the separator could be re-cut into a
7
+ * different, still-validly-signed claim set. Web Crypto is used directly so
8
+ * the same module runs in the browser and on node, and signature comparison
9
+ * is length-independent so a check leaks no prefix length.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ import * as Clock from "effect/Clock";
14
+ import * as Effect from "effect/Effect";
15
+ import * as Redacted from "effect/Redacted";
16
+ import { SyncError } from "../SyncError.js";
17
+ import { causeText } from "./CauseText.js";
18
+ const encoder = new TextEncoder();
19
+ /**
20
+ * Length-prefixes each field so no two distinct field sequences share an
21
+ * encoding.
22
+ *
23
+ * The prefix counts UTF-8 BYTES, not UTF-16 code units, because the bytes are
24
+ * what {@link signHmac} covers. Counting code units let a two-byte field claim
25
+ * length one, so the prefix no longer separated the fields the signature was
26
+ * taken over.
27
+ *
28
+ * @category encoding
29
+ * @since 0.1.0
30
+ */
31
+ export const lengthPrefixed = (fields) => fields.map((field) => `${encoder.encode(field).length}:${field}`).join("");
32
+ const decoder = new TextDecoder();
33
+ /**
34
+ * Whether a string is exactly what its own UTF-8 bytes decode back to.
35
+ *
36
+ * `TextEncoder` replaces every unpaired surrogate with U+FFFD, so two claim
37
+ * sets differing only in a lone surrogate sign to identical bytes: a
38
+ * capability minted for a lone-surrogate branch id verifies after its
39
+ * `branchId` is rewritten to U+FFFD, which names a different branch. A length
40
+ * prefix cannot separate them either, because both encode to the same three
41
+ * bytes. The only correct answer is to refuse such a claim set before it is
42
+ * signed, and the round trip is the exact question — does this string survive
43
+ * the encoding the signature is taken over — rather than a proxy for it.
44
+ *
45
+ * @category encoding
46
+ * @since 1.0.0-rc.0
47
+ */
48
+ export const utf8RoundTrips = (value, bytes) => decoder.decode(bytes) === value;
49
+ /**
50
+ * Renders signature bytes as lowercase hex.
51
+ *
52
+ * @category encoding
53
+ * @since 0.1.0
54
+ */
55
+ export const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
56
+ /**
57
+ * Length-independent comparison, so a signature check leaks no prefix length.
58
+ *
59
+ * @category comparison
60
+ * @since 0.1.0
61
+ */
62
+ export const constantTimeEquals = (left, right) => {
63
+ let difference = left.length ^ right.length;
64
+ // `charCodeAt` past the end is NaN, and `NaN | 0` is 0, so the loop reads
65
+ // both strings to the longer length without an early return.
66
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
67
+ difference |= (left.charCodeAt(index) | 0) ^ (right.charCodeAt(index) | 0);
68
+ }
69
+ return difference === 0;
70
+ };
71
+ /**
72
+ * The fewest UTF-8 bytes a signing secret may carry: the HMAC-SHA-256 output
73
+ * length, the minimum RFC 2104 section 3 recommends.
74
+ *
75
+ * Every capability carries its claims and their signature in the clear, so one
76
+ * observed capability is an offline oracle for the secret. A short secret such
77
+ * as `a` falls to exhaustive search in seconds, and the attacker then mints
78
+ * branch and workspace capabilities of their own.
79
+ *
80
+ * @category constants
81
+ * @since 1.0.0-rc.1
82
+ */
83
+ export const minSecretBytes = 32;
84
+ /**
85
+ * Imports a raw secret as a non-extractable Web Crypto HMAC-SHA-256 signing
86
+ * key. Fails with `invalid_request` when the secret does not survive UTF-8
87
+ * (an unpaired surrogate) or is shorter than {@link minSecretBytes}, and with
88
+ * a `SyncError` carrying the rejection as `cause` when Web Crypto refuses the
89
+ * import.
90
+ *
91
+ * @category crypto
92
+ * @since 0.1.0
93
+ */
94
+ export const importHmacKey = (secret) => {
95
+ // An unpaired surrogate encodes to U+FFFD's bytes, so two distinct
96
+ // secrets would import the same key and rotating between them revokes
97
+ // nothing. Refused here, as `signHmac` refuses such claims.
98
+ const bytes = encoder.encode(secret);
99
+ if (!utf8RoundTrips(secret, bytes)) {
100
+ return Effect.fail(new SyncError({
101
+ code: "invalid_request",
102
+ message: "The HMAC signing secret carries an unpaired surrogate"
103
+ }));
104
+ }
105
+ if (bytes.length < minSecretBytes) {
106
+ return Effect.fail(new SyncError({
107
+ code: "invalid_request",
108
+ message: `The HMAC signing secret must be at least ${minSecretBytes} bytes of UTF-8`
109
+ }));
110
+ }
111
+ return Effect.tryPromise({
112
+ try: () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, [
113
+ "sign"
114
+ ]),
115
+ catch: (cause) => new SyncError({
116
+ code: "unknown",
117
+ message: "Web Crypto could not import the HMAC signing key",
118
+ cause: causeText(cause)
119
+ })
120
+ });
121
+ };
122
+ /**
123
+ * Signs a canonical claim encoding, returning the signature as lowercase hex.
124
+ *
125
+ * @category crypto
126
+ * @since 0.1.0
127
+ */
128
+ export const signHmac = (key, canonical) => {
129
+ const bytes = encoder.encode(canonical);
130
+ if (!utf8RoundTrips(canonical, bytes)) {
131
+ return Effect.fail(new SyncError({
132
+ code: "invalid_request",
133
+ message: "Share claims carry an unpaired surrogate and cannot be signed"
134
+ }));
135
+ }
136
+ return Effect.map(Effect.tryPromise({
137
+ try: () => crypto.subtle.sign("HMAC", key, bytes),
138
+ catch: (cause) => new SyncError({
139
+ code: "unknown",
140
+ message: "Web Crypto could not sign the share claims",
141
+ cause: causeText(cause)
142
+ })
143
+ }), (signature) => hex(new Uint8Array(signature)));
144
+ };
145
+ /**
146
+ * The refusal both authorities issue. Declared once so a message added to one
147
+ * authority's verification cannot land under a different error code in the
148
+ * other.
149
+ *
150
+ * @category errors
151
+ * @since 1.0.0-rc.0
152
+ */
153
+ export const unauthorized = (message) => new SyncError({ code: "unauthorized", message });
154
+ /**
155
+ * Imports every key in a keyring, so a misconfigured ring — an unknown active
156
+ * kid, a duplicate kid, or a key Web Crypto refuses — fails at construction
157
+ * rather than at the first request. `subject` names the authority in the
158
+ * refusal ("branch", "workspace").
159
+ *
160
+ * @category crypto
161
+ * @since 1.0.0-rc.0
162
+ */
163
+ export const importKeyring = (keyring, subject) => Effect.gen(function* () {
164
+ // Copied BEFORE the first import, for the reason snapshots exist on the
165
+ // verify path: the keyring belongs to the caller and every import below is
166
+ // an await, so a `for..of` over the caller's own array re-reads it between
167
+ // them. A splice landing in that window changed which keys the authority
168
+ // ended up holding, and reading `activeKid` only after the loop let it name
169
+ // a key the ring was validated with rather than the one the caller passed.
170
+ const activeKid = keyring.activeKid;
171
+ const keys = keyring.keys.map((key) => ({ kid: key.kid, secret: key.secret }));
172
+ const verification = new Map();
173
+ for (const key of keys) {
174
+ if (verification.has(key.kid)) {
175
+ return yield* Effect.fail(new SyncError({ code: "invalid_request", message: `The ${subject} keyring names kid ${key.kid} twice` }));
176
+ }
177
+ verification.set(key.kid, yield* importHmacKey(Redacted.value(key.secret)));
178
+ }
179
+ const active = verification.get(activeKid);
180
+ if (active === undefined) {
181
+ return yield* Effect.fail(new SyncError({
182
+ code: "invalid_request",
183
+ message: `The ${subject} keyring's active kid names no key in the ring`
184
+ }));
185
+ }
186
+ return { activeKid, active, verification };
187
+ });
188
+ /**
189
+ * The authorization both capability authorities run once their signing key is
190
+ * selected: the signature, then the expiry, then the requested access.
191
+ *
192
+ * The order is the contract. A capability is authenticated before anything it
193
+ * claims is reported back, so a forged claim set is refused as a bad signature
194
+ * and never as a scope or an expiry. `scope` is the one check an authority
195
+ * adds for itself — the branch authority's cross-branch refusal — and it runs
196
+ * after the signature for exactly that reason.
197
+ *
198
+ * Both authorities called this sequence out by hand, which left a fix to the
199
+ * expiry boundary or the read-only refusal landing in one and not the other.
200
+ *
201
+ * @category crypto
202
+ * @since 1.0.0-rc.0
203
+ */
204
+ export const verifyClaims = (options) => Effect.gen(function* () {
205
+ const expected = yield* signHmac(options.key, options.canonical);
206
+ if (!constantTimeEquals(expected, options.signature)) {
207
+ return yield* Effect.fail(unauthorized(`${options.subject} signature is invalid`));
208
+ }
209
+ if (options.scope !== undefined)
210
+ yield* options.scope;
211
+ const nowMs = yield* Clock.currentTimeMillis;
212
+ if (nowMs >= options.expiresAtMs) {
213
+ return yield* Effect.fail(unauthorized(`${options.subject} has expired`));
214
+ }
215
+ if (options.requested === "write" && options.granted !== "write") {
216
+ return yield* Effect.fail(unauthorized(`${options.subject} is read-only`));
217
+ }
218
+ });
219
+ //# sourceMappingURL=ShareSigner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShareSigner.js","sourceRoot":"","sources":["../../../src/internal/ShareSigner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * HMAC-SHA-256 signing primitives shared by the branch and workspace share\n * authorities.\n *\n * Both authorities sign a length-prefixed encoding of their claims: without\n * length prefixes a field ending in the separator could be re-cut into a\n * different, still-validly-signed claim set. Web Crypto is used directly so\n * the same module runs in the browser and on node, and signature comparison\n * is length-independent so a check leaks no prefix length.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport * as Effect from \"effect/Effect\"\nimport * as Redacted from \"effect/Redacted\"\nimport type { Access } from \"../BranchProtocol.ts\"\nimport { SyncError } from \"../SyncError.ts\"\nimport { causeText } from \"./CauseText.ts\"\n\nconst encoder = new TextEncoder()\n\n/**\n * Length-prefixes each field so no two distinct field sequences share an\n * encoding.\n *\n * The prefix counts UTF-8 BYTES, not UTF-16 code units, because the bytes are\n * what {@link signHmac} covers. Counting code units let a two-byte field claim\n * length one, so the prefix no longer separated the fields the signature was\n * taken over.\n *\n * @category encoding\n * @since 0.1.0\n */\nexport const lengthPrefixed = (fields: ReadonlyArray<string>): string =>\n fields.map((field) => `${encoder.encode(field).length}:${field}`).join(\"\")\n\nconst decoder = new TextDecoder()\n\n/**\n * Whether a string is exactly what its own UTF-8 bytes decode back to.\n *\n * `TextEncoder` replaces every unpaired surrogate with U+FFFD, so two claim\n * sets differing only in a lone surrogate sign to identical bytes: a\n * capability minted for a lone-surrogate branch id verifies after its\n * `branchId` is rewritten to U+FFFD, which names a different branch. A length\n * prefix cannot separate them either, because both encode to the same three\n * bytes. The only correct answer is to refuse such a claim set before it is\n * signed, and the round trip is the exact question — does this string survive\n * the encoding the signature is taken over — rather than a proxy for it.\n *\n * @category encoding\n * @since 1.0.0-rc.0\n */\nexport const utf8RoundTrips = (value: string, bytes: Uint8Array): boolean => decoder.decode(bytes) === value\n\n/**\n * Renders signature bytes as lowercase hex.\n *\n * @category encoding\n * @since 0.1.0\n */\nexport const hex = (bytes: Uint8Array): string =>\n Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\")\n\n/**\n * Length-independent comparison, so a signature check leaks no prefix length.\n *\n * @category comparison\n * @since 0.1.0\n */\nexport const constantTimeEquals = (left: string, right: string): boolean => {\n let difference = left.length ^ right.length\n // `charCodeAt` past the end is NaN, and `NaN | 0` is 0, so the loop reads\n // both strings to the longer length without an early return.\n for (let index = 0; index < Math.max(left.length, right.length); index += 1) {\n difference |= (left.charCodeAt(index) | 0) ^ (right.charCodeAt(index) | 0)\n }\n return difference === 0\n}\n\n/**\n * The fewest UTF-8 bytes a signing secret may carry: the HMAC-SHA-256 output\n * length, the minimum RFC 2104 section 3 recommends.\n *\n * Every capability carries its claims and their signature in the clear, so one\n * observed capability is an offline oracle for the secret. A short secret such\n * as `a` falls to exhaustive search in seconds, and the attacker then mints\n * branch and workspace capabilities of their own.\n *\n * @category constants\n * @since 1.0.0-rc.1\n */\nexport const minSecretBytes = 32\n\n/**\n * Imports a raw secret as a non-extractable Web Crypto HMAC-SHA-256 signing\n * key. Fails with `invalid_request` when the secret does not survive UTF-8\n * (an unpaired surrogate) or is shorter than {@link minSecretBytes}, and with\n * a `SyncError` carrying the rejection as `cause` when Web Crypto refuses the\n * import.\n *\n * @category crypto\n * @since 0.1.0\n */\nexport const importHmacKey = (secret: string): Effect.Effect<CryptoKey, SyncError> => {\n // An unpaired surrogate encodes to U+FFFD's bytes, so two distinct\n // secrets would import the same key and rotating between them revokes\n // nothing. Refused here, as `signHmac` refuses such claims.\n const bytes = encoder.encode(secret)\n if (!utf8RoundTrips(secret, bytes)) {\n return Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: \"The HMAC signing secret carries an unpaired surrogate\"\n })\n )\n }\n if (bytes.length < minSecretBytes) {\n return Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: `The HMAC signing secret must be at least ${minSecretBytes} bytes of UTF-8`\n })\n )\n }\n return Effect.tryPromise({\n try: () =>\n crypto.subtle.importKey(\"raw\", bytes, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\n \"sign\"\n ]),\n catch: (cause) =>\n new SyncError({\n code: \"unknown\",\n message: \"Web Crypto could not import the HMAC signing key\",\n cause: causeText(cause)\n })\n })\n}\n\n/**\n * Signs a canonical claim encoding, returning the signature as lowercase hex.\n *\n * @category crypto\n * @since 0.1.0\n */\nexport const signHmac = (key: CryptoKey, canonical: string): Effect.Effect<string, SyncError> => {\n const bytes = encoder.encode(canonical)\n if (!utf8RoundTrips(canonical, bytes)) {\n return Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: \"Share claims carry an unpaired surrogate and cannot be signed\"\n })\n )\n }\n return Effect.map(\n Effect.tryPromise({\n try: () => crypto.subtle.sign(\"HMAC\", key, bytes),\n catch: (cause) =>\n new SyncError({\n code: \"unknown\",\n message: \"Web Crypto could not sign the share claims\",\n cause: causeText(cause)\n })\n }),\n (signature) => hex(new Uint8Array(signature))\n )\n}\n\n/**\n * The refusal both authorities issue. Declared once so a message added to one\n * authority's verification cannot land under a different error code in the\n * other.\n *\n * @category errors\n * @since 1.0.0-rc.0\n */\nexport const unauthorized = (message: string): SyncError => new SyncError({ code: \"unauthorized\", message })\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 interface Key {\n readonly kid: string\n readonly secret: Redacted.Redacted<string>\n}\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 capabilities expire.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Keyring {\n readonly activeKid: string\n readonly keys: ReadonlyArray<Key>\n}\n\n/**\n * A keyring whose every secret is already an imported Web Crypto key: the\n * signer for new capabilities, and the map a verifier selects a retired key\n * from by `kid`.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface ImportedKeyring {\n readonly activeKid: string\n readonly active: CryptoKey\n readonly verification: ReadonlyMap<string, CryptoKey>\n}\n\n/**\n * Imports every key in a keyring, so a misconfigured ring — an unknown active\n * kid, a duplicate kid, or a key Web Crypto refuses — fails at construction\n * rather than at the first request. `subject` names the authority in the\n * refusal (\"branch\", \"workspace\").\n *\n * @category crypto\n * @since 1.0.0-rc.0\n */\nexport const importKeyring = (keyring: Keyring, subject: string): Effect.Effect<ImportedKeyring, SyncError> =>\n Effect.gen(function*() {\n // Copied BEFORE the first import, for the reason snapshots exist on the\n // verify path: the keyring belongs to the caller and every import below is\n // an await, so a `for..of` over the caller's own array re-reads it between\n // them. A splice landing in that window changed which keys the authority\n // ended up holding, and reading `activeKid` only after the loop let it name\n // a key the ring was validated with rather than the one the caller passed.\n const activeKid = keyring.activeKid\n const keys = keyring.keys.map((key): Key => ({ kid: key.kid, secret: key.secret }))\n const verification = new Map<string, CryptoKey>()\n for (const key of keys) {\n if (verification.has(key.kid)) {\n return yield* Effect.fail(\n new SyncError({ code: \"invalid_request\", message: `The ${subject} keyring names kid ${key.kid} twice` })\n )\n }\n verification.set(key.kid, yield* importHmacKey(Redacted.value(key.secret)))\n }\n const active = verification.get(activeKid)\n if (active === undefined) {\n return yield* Effect.fail(\n new SyncError({\n code: \"invalid_request\",\n message: `The ${subject} keyring's active kid names no key in the ring`\n })\n )\n }\n return { activeKid, active, verification }\n })\n\n/**\n * The authorization both capability authorities run once their signing key is\n * selected: the signature, then the expiry, then the requested access.\n *\n * The order is the contract. A capability is authenticated before anything it\n * claims is reported back, so a forged claim set is refused as a bad signature\n * and never as a scope or an expiry. `scope` is the one check an authority\n * adds for itself — the branch authority's cross-branch refusal — and it runs\n * after the signature for exactly that reason.\n *\n * Both authorities called this sequence out by hand, which left a fix to the\n * expiry boundary or the read-only refusal landing in one and not the other.\n *\n * @category crypto\n * @since 1.0.0-rc.0\n */\nexport const verifyClaims = (options: {\n readonly key: CryptoKey\n readonly canonical: string\n readonly signature: string\n readonly expiresAtMs: number\n readonly granted: Access\n readonly requested: Access\n /** The refused thing, named as it appears in the message: \"The share capability\". */\n readonly subject: string\n readonly scope?: Effect.Effect<void, SyncError>\n}): Effect.Effect<void, SyncError> =>\n Effect.gen(function*() {\n const expected = yield* signHmac(options.key, options.canonical)\n if (!constantTimeEquals(expected, options.signature)) {\n return yield* Effect.fail(unauthorized(`${options.subject} signature is invalid`))\n }\n if (options.scope !== undefined) yield* options.scope\n const nowMs = yield* Clock.currentTimeMillis\n if (nowMs >= options.expiresAtMs) {\n return yield* Effect.fail(unauthorized(`${options.subject} has expired`))\n }\n if (options.requested === \"write\" && options.granted !== \"write\") {\n return yield* Effect.fail(unauthorized(`${options.subject} is read-only`))\n }\n })\n"]}
@@ -0,0 +1,17 @@
1
+ /** Snapshot validation shared by the hosted and browser-side boundaries.
2
+ * @since 1.0.0-rc.0
3
+ */
4
+ import * as Effect from "effect/Effect";
5
+ import { SyncError } from "../SyncError.ts";
6
+ import * as Protocol from "../SyncProtocol.ts";
7
+ /** Decode a detached request before callbacks can mutate its expected identity.
8
+ * @category validation
9
+ * @since 1.0.0-rc.0
10
+ */
11
+ export declare const request: (value: unknown) => Effect.Effect<Protocol.SnapshotRequest, SyncError>;
12
+ /** Copy JSON bytes once, bound them, then check the exact requested projection.
13
+ * @category validation
14
+ * @since 1.0.0-rc.0
15
+ */
16
+ export declare const response: (expected: Protocol.SnapshotRequest, value: unknown, maxBytes: number) => Effect.Effect<Protocol.Snapshot, SyncError>;
17
+ //# sourceMappingURL=SnapshotBoundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnapshotBoundary.d.ts","sourceRoot":"","sources":["../../../src/internal/SnapshotBoundary.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,QAAQ,MAAM,oBAAoB,CAAA;AAI9C;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAUvF,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,CAAC,eAAe,EAClC,OAAO,OAAO,EACd,UAAU,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAuDzC,CAAA"}