@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.
- package/CHANGELOG.md +208 -0
- package/LICENSE +21 -0
- package/README.md +208 -2
- package/dist/cjs/BranchCommands.d.ts +228 -0
- package/dist/cjs/BranchCommands.d.ts.map +1 -0
- package/dist/cjs/BranchCommands.js +316 -0
- package/dist/cjs/BranchCommands.js.map +7 -0
- package/dist/cjs/BranchIds.d.ts +65 -0
- package/dist/cjs/BranchIds.d.ts.map +1 -0
- package/dist/cjs/BranchIds.js +53 -0
- package/dist/cjs/BranchIds.js.map +7 -0
- package/dist/cjs/BranchPresence.d.ts +195 -0
- package/dist/cjs/BranchPresence.d.ts.map +1 -0
- package/dist/cjs/BranchPresence.js +276 -0
- package/dist/cjs/BranchPresence.js.map +7 -0
- package/dist/cjs/BranchProjection.d.ts +179 -0
- package/dist/cjs/BranchProjection.d.ts.map +1 -0
- package/dist/cjs/BranchProjection.js +197 -0
- package/dist/cjs/BranchProjection.js.map +7 -0
- package/dist/cjs/BranchProtocol.d.ts +415 -0
- package/dist/cjs/BranchProtocol.d.ts.map +1 -0
- package/dist/cjs/BranchProtocol.js +172 -0
- package/dist/cjs/BranchProtocol.js.map +7 -0
- package/dist/cjs/BranchRpcs.d.ts +177 -0
- package/dist/cjs/BranchRpcs.d.ts.map +1 -0
- package/dist/cjs/BranchRpcs.js +85 -0
- package/dist/cjs/BranchRpcs.js.map +7 -0
- package/dist/cjs/BranchServer.d.ts +55 -0
- package/dist/cjs/BranchServer.d.ts.map +1 -0
- package/dist/cjs/BranchServer.js +147 -0
- package/dist/cjs/BranchServer.js.map +7 -0
- package/dist/cjs/BranchShare.d.ts +159 -0
- package/dist/cjs/BranchShare.d.ts.map +1 -0
- package/dist/cjs/BranchShare.js +147 -0
- package/dist/cjs/BranchShare.js.map +7 -0
- package/dist/cjs/RunCatalog.d.ts +207 -0
- package/dist/cjs/RunCatalog.d.ts.map +1 -0
- package/dist/cjs/RunCatalog.js +122 -0
- package/dist/cjs/RunCatalog.js.map +7 -0
- package/dist/cjs/SyncAuth.d.ts +70 -0
- package/dist/cjs/SyncAuth.d.ts.map +1 -0
- package/dist/cjs/SyncAuth.js +109 -0
- package/dist/cjs/SyncAuth.js.map +7 -0
- package/dist/cjs/SyncClient.d.ts +274 -0
- package/dist/cjs/SyncClient.d.ts.map +1 -0
- package/dist/cjs/SyncClient.js +474 -0
- package/dist/cjs/SyncClient.js.map +7 -0
- package/dist/cjs/SyncError.d.ts +107 -0
- package/dist/cjs/SyncError.d.ts.map +1 -0
- package/dist/cjs/SyncError.js +110 -0
- package/dist/cjs/SyncError.js.map +7 -0
- package/dist/cjs/SyncPrincipal.d.ts +100 -0
- package/dist/cjs/SyncPrincipal.d.ts.map +1 -0
- package/dist/cjs/SyncPrincipal.js +48 -0
- package/dist/cjs/SyncPrincipal.js.map +7 -0
- package/dist/cjs/SyncProtocol.d.ts +592 -0
- package/dist/cjs/SyncProtocol.d.ts.map +1 -0
- package/dist/cjs/SyncProtocol.js +165 -0
- package/dist/cjs/SyncProtocol.js.map +7 -0
- package/dist/cjs/SyncRpcs.d.ts +92 -0
- package/dist/cjs/SyncRpcs.d.ts.map +1 -0
- package/dist/cjs/SyncRpcs.js +35 -0
- package/dist/cjs/SyncRpcs.js.map +7 -0
- package/dist/cjs/SyncServer.d.ts +184 -0
- package/dist/cjs/SyncServer.d.ts.map +1 -0
- package/dist/cjs/SyncServer.js +513 -0
- package/dist/cjs/SyncServer.js.map +7 -0
- package/dist/cjs/WorkspaceShare.d.ts +174 -0
- package/dist/cjs/WorkspaceShare.d.ts.map +1 -0
- package/dist/cjs/WorkspaceShare.js +149 -0
- package/dist/cjs/WorkspaceShare.js.map +7 -0
- package/dist/cjs/index.d.ts +91 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +67 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Admission.d.ts +43 -0
- package/dist/cjs/internal/Admission.d.ts.map +1 -0
- package/dist/cjs/internal/Admission.js +90 -0
- package/dist/cjs/internal/Admission.js.map +7 -0
- package/dist/cjs/internal/CauseText.d.ts +55 -0
- package/dist/cjs/internal/CauseText.d.ts.map +1 -0
- package/dist/cjs/internal/CauseText.js +38 -0
- package/dist/cjs/internal/CauseText.js.map +7 -0
- package/dist/cjs/internal/JournalErrorCode.d.ts +23 -0
- package/dist/cjs/internal/JournalErrorCode.d.ts.map +1 -0
- package/dist/cjs/internal/JournalErrorCode.js +34 -0
- package/dist/cjs/internal/JournalErrorCode.js.map +7 -0
- package/dist/cjs/internal/PolicyOptions.d.ts +57 -0
- package/dist/cjs/internal/PolicyOptions.d.ts.map +1 -0
- package/dist/cjs/internal/PolicyOptions.js +62 -0
- package/dist/cjs/internal/PolicyOptions.js.map +7 -0
- package/dist/cjs/internal/ProtocolVersion.d.ts +17 -0
- package/dist/cjs/internal/ProtocolVersion.d.ts.map +1 -0
- package/dist/cjs/internal/ProtocolVersion.js +44 -0
- package/dist/cjs/internal/ProtocolVersion.js.map +7 -0
- package/dist/cjs/internal/ShareSigner.d.ts +173 -0
- package/dist/cjs/internal/ShareSigner.d.ts.map +1 -0
- package/dist/cjs/internal/ShareSigner.js +150 -0
- package/dist/cjs/internal/ShareSigner.js.map +7 -0
- package/dist/cjs/internal/SnapshotBoundary.d.ts +17 -0
- package/dist/cjs/internal/SnapshotBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/SnapshotBoundary.js +97 -0
- package/dist/cjs/internal/SnapshotBoundary.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestSocket.d.ts +55 -0
- package/dist/cjs/test/TestSocket.d.ts.map +1 -0
- package/dist/cjs/test/TestSocket.js +145 -0
- package/dist/cjs/test/TestSocket.js.map +7 -0
- package/dist/cjs/test/TestSync.d.ts +54 -0
- package/dist/cjs/test/TestSync.d.ts.map +1 -0
- package/dist/cjs/test/TestSync.js +96 -0
- package/dist/cjs/test/TestSync.js.map +7 -0
- package/dist/esm/BranchCommands.d.ts +228 -0
- package/dist/esm/BranchCommands.d.ts.map +1 -0
- package/dist/esm/BranchCommands.js +496 -0
- package/dist/esm/BranchCommands.js.map +1 -0
- package/dist/esm/BranchIds.d.ts +65 -0
- package/dist/esm/BranchIds.d.ts.map +1 -0
- package/dist/esm/BranchIds.js +61 -0
- package/dist/esm/BranchIds.js.map +1 -0
- package/dist/esm/BranchPresence.d.ts +195 -0
- package/dist/esm/BranchPresence.d.ts.map +1 -0
- package/dist/esm/BranchPresence.js +361 -0
- package/dist/esm/BranchPresence.js.map +1 -0
- package/dist/esm/BranchProjection.d.ts +179 -0
- package/dist/esm/BranchProjection.d.ts.map +1 -0
- package/dist/esm/BranchProjection.js +269 -0
- package/dist/esm/BranchProjection.js.map +1 -0
- package/dist/esm/BranchProtocol.d.ts +415 -0
- package/dist/esm/BranchProtocol.d.ts.map +1 -0
- package/dist/esm/BranchProtocol.js +323 -0
- package/dist/esm/BranchProtocol.js.map +1 -0
- package/dist/esm/BranchRpcs.d.ts +177 -0
- package/dist/esm/BranchRpcs.d.ts.map +1 -0
- package/dist/esm/BranchRpcs.js +103 -0
- package/dist/esm/BranchRpcs.js.map +1 -0
- package/dist/esm/BranchServer.d.ts +55 -0
- package/dist/esm/BranchServer.d.ts.map +1 -0
- package/dist/esm/BranchServer.js +168 -0
- package/dist/esm/BranchServer.js.map +1 -0
- package/dist/esm/BranchShare.d.ts +159 -0
- package/dist/esm/BranchShare.d.ts.map +1 -0
- package/dist/esm/BranchShare.js +213 -0
- package/dist/esm/BranchShare.js.map +1 -0
- package/dist/esm/RunCatalog.d.ts +207 -0
- package/dist/esm/RunCatalog.d.ts.map +1 -0
- package/dist/esm/RunCatalog.js +196 -0
- package/dist/esm/RunCatalog.js.map +1 -0
- package/dist/esm/SyncAuth.d.ts +70 -0
- package/dist/esm/SyncAuth.d.ts.map +1 -0
- package/dist/esm/SyncAuth.js +119 -0
- package/dist/esm/SyncAuth.js.map +1 -0
- package/dist/esm/SyncClient.d.ts +274 -0
- package/dist/esm/SyncClient.d.ts.map +1 -0
- package/dist/esm/SyncClient.js +586 -0
- package/dist/esm/SyncClient.js.map +1 -0
- package/dist/esm/SyncError.d.ts +107 -0
- package/dist/esm/SyncError.d.ts.map +1 -0
- package/dist/esm/SyncError.js +133 -0
- package/dist/esm/SyncError.js.map +1 -0
- package/dist/esm/SyncPrincipal.d.ts +100 -0
- package/dist/esm/SyncPrincipal.d.ts.map +1 -0
- package/dist/esm/SyncPrincipal.js +58 -0
- package/dist/esm/SyncPrincipal.js.map +1 -0
- package/dist/esm/SyncProtocol.d.ts +592 -0
- package/dist/esm/SyncProtocol.d.ts.map +1 -0
- package/dist/esm/SyncProtocol.js +434 -0
- package/dist/esm/SyncProtocol.js.map +1 -0
- package/dist/esm/SyncRpcs.d.ts +92 -0
- package/dist/esm/SyncRpcs.d.ts.map +1 -0
- package/dist/esm/SyncRpcs.js +35 -0
- package/dist/esm/SyncRpcs.js.map +1 -0
- package/dist/esm/SyncServer.d.ts +184 -0
- package/dist/esm/SyncServer.d.ts.map +1 -0
- package/dist/esm/SyncServer.js +897 -0
- package/dist/esm/SyncServer.js.map +1 -0
- package/dist/esm/WorkspaceShare.d.ts +174 -0
- package/dist/esm/WorkspaceShare.d.ts.map +1 -0
- package/dist/esm/WorkspaceShare.js +208 -0
- package/dist/esm/WorkspaceShare.js.map +1 -0
- package/dist/esm/index.d.ts +91 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +91 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Admission.d.ts +43 -0
- package/dist/esm/internal/Admission.d.ts.map +1 -0
- package/dist/esm/internal/Admission.js +80 -0
- package/dist/esm/internal/Admission.js.map +1 -0
- package/dist/esm/internal/CauseText.d.ts +55 -0
- package/dist/esm/internal/CauseText.d.ts.map +1 -0
- package/dist/esm/internal/CauseText.js +67 -0
- package/dist/esm/internal/CauseText.js.map +1 -0
- package/dist/esm/internal/JournalErrorCode.d.ts +23 -0
- package/dist/esm/internal/JournalErrorCode.d.ts.map +1 -0
- package/dist/esm/internal/JournalErrorCode.js +44 -0
- package/dist/esm/internal/JournalErrorCode.js.map +1 -0
- package/dist/esm/internal/PolicyOptions.d.ts +57 -0
- package/dist/esm/internal/PolicyOptions.d.ts.map +1 -0
- package/dist/esm/internal/PolicyOptions.js +70 -0
- package/dist/esm/internal/PolicyOptions.js.map +1 -0
- package/dist/esm/internal/ProtocolVersion.d.ts +17 -0
- package/dist/esm/internal/ProtocolVersion.d.ts.map +1 -0
- package/dist/esm/internal/ProtocolVersion.js +24 -0
- package/dist/esm/internal/ProtocolVersion.js.map +1 -0
- package/dist/esm/internal/ShareSigner.d.ts +173 -0
- package/dist/esm/internal/ShareSigner.d.ts.map +1 -0
- package/dist/esm/internal/ShareSigner.js +219 -0
- package/dist/esm/internal/ShareSigner.js.map +1 -0
- package/dist/esm/internal/SnapshotBoundary.d.ts +17 -0
- package/dist/esm/internal/SnapshotBoundary.d.ts.map +1 -0
- package/dist/esm/internal/SnapshotBoundary.js +68 -0
- package/dist/esm/internal/SnapshotBoundary.js.map +1 -0
- package/dist/esm/test/TestSocket.d.ts +55 -0
- package/dist/esm/test/TestSocket.d.ts.map +1 -0
- package/dist/esm/test/TestSocket.js +128 -0
- package/dist/esm/test/TestSocket.js.map +1 -0
- package/dist/esm/test/TestSync.d.ts +54 -0
- package/dist/esm/test/TestSync.d.ts.map +1 -0
- package/dist/esm/test/TestSync.js +76 -0
- package/dist/esm/test/TestSync.js.map +1 -0
- package/docs/README.md +156 -0
- package/docs/api.md +328 -0
- package/docs/concepts/authorization.md +117 -0
- package/docs/concepts/branches.md +195 -0
- package/docs/concepts/compaction.md +144 -0
- package/docs/concepts/replay-then-follow.md +129 -0
- package/docs/concepts/scopes-and-cursors.md +135 -0
- package/docs/guides/authorize-a-connection.md +174 -0
- package/docs/guides/follow-a-run.md +174 -0
- package/docs/guides/handle-a-compacted-run.md +158 -0
- package/docs/guides/list-workspace-runs.md +120 -0
- package/docs/guides/run-a-long-soak.md +80 -0
- package/docs/guides/serve-the-read-path.md +146 -0
- package/docs/guides/test-a-follower.md +273 -0
- package/docs/installation.md +92 -0
- package/docs/protocol.md +183 -0
- package/docs/quickstart.md +159 -0
- package/docs/troubleshooting.md +234 -0
- package/package.json +304 -3
- package/src/BranchCommands.ts +684 -0
- package/src/BranchIds.ts +76 -0
- package/src/BranchPresence.ts +512 -0
- package/src/BranchProjection.ts +308 -0
- package/src/BranchProtocol.ts +411 -0
- package/src/BranchRpcs.ts +163 -0
- package/src/BranchServer.ts +202 -0
- package/src/BranchShare.ts +297 -0
- package/src/RunCatalog.ts +308 -0
- package/src/SyncAuth.ts +146 -0
- package/src/SyncClient.ts +902 -0
- package/src/SyncError.ts +147 -0
- package/src/SyncPrincipal.ts +115 -0
- package/src/SyncProtocol.ts +594 -0
- package/src/SyncRpcs.ts +40 -0
- package/src/SyncServer.ts +1184 -0
- package/src/WorkspaceShare.ts +292 -0
- package/src/index.ts +107 -0
- package/src/internal/Admission.ts +119 -0
- package/src/internal/CauseText.ts +74 -0
- package/src/internal/JournalErrorCode.ts +46 -0
- package/src/internal/PolicyOptions.ts +97 -0
- package/src/internal/ProtocolVersion.ts +28 -0
- package/src/internal/ShareSigner.ts +300 -0
- package/src/internal/SnapshotBoundary.ts +91 -0
- package/src/test/TestSocket.ts +192 -0
- 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"}
|