@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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-side handlers of {@link BranchRpcs}: a thin projection of the
|
|
3
|
+
* branch services onto the wire.
|
|
4
|
+
*
|
|
5
|
+
* Five procedures forward to the service that owns their boundary and add
|
|
6
|
+
* nothing: `Branch.Submit` to {@link BranchCommands}, and `Branch.Announce`,
|
|
7
|
+
* `Branch.Leave`, `Branch.Roster`, and `Branch.WatchRoster` to
|
|
8
|
+
* {@link BranchPresence}. Two enforce policy here, on top of what
|
|
9
|
+
* {@link BranchShare} checks: `Branch.CreateBranch` refuses any principal that
|
|
10
|
+
* is not an authenticated workspace, and `Branch.MintShare` verifies write
|
|
11
|
+
* access, refuses an expired parent, and caps the child at the parent's
|
|
12
|
+
* expiry. `BranchShare.mint` performs none of those checks, so an in-process
|
|
13
|
+
* host that mints through the service directly must gate the principal and
|
|
14
|
+
* the parent capability itself.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
import type * as Layer from "effect/Layer";
|
|
19
|
+
import type * as Rpc from "effect/unstable/rpc/Rpc";
|
|
20
|
+
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";
|
|
21
|
+
import * as BranchCommands from "./BranchCommands.ts";
|
|
22
|
+
import * as BranchIds from "./BranchIds.ts";
|
|
23
|
+
import * as BranchPresence from "./BranchPresence.ts";
|
|
24
|
+
import { BranchRpcs } from "./BranchRpcs.ts";
|
|
25
|
+
import * as BranchShare from "./BranchShare.ts";
|
|
26
|
+
/**
|
|
27
|
+
* Provides the branch RPC handlers over the branch services.
|
|
28
|
+
*
|
|
29
|
+
* With {@link BranchPresence.layerNoop}, roster reads and watches fail with
|
|
30
|
+
* `unsupported`: unavailable presence cannot establish an empty roster.
|
|
31
|
+
*
|
|
32
|
+
* A roster watch emits the roster as of subscription, then re-lists on every
|
|
33
|
+
* presence change for the branch AND at least once per second, emitting
|
|
34
|
+
* only when the roster it read differs from the one it last sent.
|
|
35
|
+
*
|
|
36
|
+
* All three of those are triggers into ONE sequential reader. The roster is
|
|
37
|
+
* mutable state behind an effect, so concurrent readers can complete out of
|
|
38
|
+
* order, and a watcher that receives an older roster after a newer one keeps
|
|
39
|
+
* the older one: `changesWith` compares against the previous emission, not
|
|
40
|
+
* against the freshest state. Serializing the reads is what makes the last
|
|
41
|
+
* value a watcher holds the most recent one that was read.
|
|
42
|
+
*
|
|
43
|
+
* The periodic re-list is what makes expiry observable. A lapsed lease
|
|
44
|
+
* publishes nothing — the roster drops it the next time somebody lists — so a
|
|
45
|
+
* watch driven by change events alone never saw the LAST participant leave,
|
|
46
|
+
* and a burst of announcements on unrelated branches could slide the one
|
|
47
|
+
* notification this watch needed out of the shared feed. Both are bounded by
|
|
48
|
+
* one second: a departure is visible within 1 s of lease expiry,
|
|
49
|
+
* whether or not a survivor reports it.
|
|
50
|
+
*
|
|
51
|
+
* @category layers
|
|
52
|
+
* @since 0.1.0
|
|
53
|
+
*/
|
|
54
|
+
export declare const layerHandlers: Layer.Layer<Rpc.ToHandler<RpcGroup.Rpcs<typeof BranchRpcs>>, never, BranchShare.BranchShare | BranchPresence.BranchPresence | BranchCommands.BranchCommands | BranchIds.BranchIds>;
|
|
55
|
+
//# sourceMappingURL=BranchServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchServer.d.ts","sourceRoot":"","sources":["../../src/BranchServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAE1C,OAAO,KAAK,KAAK,GAAG,MAAM,yBAAyB,CAAA;AACnD,OAAO,KAAK,KAAK,QAAQ,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAwC/C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,KAAK,CACrC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC,CAAC,EAC/C,KAAK,EACH,WAAW,CAAC,WAAW,GACvB,cAAc,CAAC,cAAc,GAC7B,cAAc,CAAC,cAAc,GAC7B,SAAS,CAAC,SAAS,CAkGtB,CAAA"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-side handlers of {@link BranchRpcs}: a thin projection of the
|
|
3
|
+
* branch services onto the wire.
|
|
4
|
+
*
|
|
5
|
+
* Five procedures forward to the service that owns their boundary and add
|
|
6
|
+
* nothing: `Branch.Submit` to {@link BranchCommands}, and `Branch.Announce`,
|
|
7
|
+
* `Branch.Leave`, `Branch.Roster`, and `Branch.WatchRoster` to
|
|
8
|
+
* {@link BranchPresence}. Two enforce policy here, on top of what
|
|
9
|
+
* {@link BranchShare} checks: `Branch.CreateBranch` refuses any principal that
|
|
10
|
+
* is not an authenticated workspace, and `Branch.MintShare` verifies write
|
|
11
|
+
* access, refuses an expired parent, and caps the child at the parent's
|
|
12
|
+
* expiry. `BranchShare.mint` performs none of those checks, so an in-process
|
|
13
|
+
* host that mints through the service directly must gate the principal and
|
|
14
|
+
* the parent capability itself.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
import * as Clock from "effect/Clock";
|
|
19
|
+
import * as Effect from "effect/Effect";
|
|
20
|
+
import * as Stream from "effect/Stream";
|
|
21
|
+
import * as BranchCommands from "./BranchCommands.js";
|
|
22
|
+
import * as BranchIds from "./BranchIds.js";
|
|
23
|
+
import * as BranchPresence from "./BranchPresence.js";
|
|
24
|
+
import { BranchId } from "./BranchProtocol.js";
|
|
25
|
+
import { BranchRpcs } from "./BranchRpcs.js";
|
|
26
|
+
import * as BranchShare from "./BranchShare.js";
|
|
27
|
+
import * as Admission from "./internal/Admission.js";
|
|
28
|
+
import { SyncError } from "./SyncError.js";
|
|
29
|
+
import * as SyncPrincipal from "./SyncPrincipal.js";
|
|
30
|
+
/**
|
|
31
|
+
* Whether two roster emissions say the same thing.
|
|
32
|
+
*
|
|
33
|
+
* `list` returns participants sorted by id, so the comparison is positional.
|
|
34
|
+
* The lease itself is part of the key: a heartbeat that only extends a lease
|
|
35
|
+
* IS a change the watcher wants, because it is the evidence the participant is
|
|
36
|
+
* still there.
|
|
37
|
+
*
|
|
38
|
+
* The rendering is JSON rather than delimiter-joined fields. Participant ids
|
|
39
|
+
* and display names are `NonEmptyString` with no charset constraint, so every
|
|
40
|
+
* separator is a character they may carry, including the control bytes this
|
|
41
|
+
* used to join on. Two rosters differing only in where a field boundary falls
|
|
42
|
+
* then rendered identically and a watcher never observed the change. JSON
|
|
43
|
+
* quotes and escapes each field, so distinct rosters render distinctly.
|
|
44
|
+
*/
|
|
45
|
+
const rosterKey = (participants) => JSON.stringify(participants.map((participant) => [
|
|
46
|
+
participant.participantId,
|
|
47
|
+
participant.displayName,
|
|
48
|
+
participant.sessionId,
|
|
49
|
+
participant.kind,
|
|
50
|
+
participant.where,
|
|
51
|
+
participant.watching,
|
|
52
|
+
participant.for_member,
|
|
53
|
+
participant.leaseExpiresAtMs,
|
|
54
|
+
participant.cursor === null ? null : [participant.cursor.cardId, participant.cursor.offset]
|
|
55
|
+
]));
|
|
56
|
+
const sameRoster = (left, right) => rosterKey(left) === rosterKey(right);
|
|
57
|
+
/**
|
|
58
|
+
* Provides the branch RPC handlers over the branch services.
|
|
59
|
+
*
|
|
60
|
+
* With {@link BranchPresence.layerNoop}, roster reads and watches fail with
|
|
61
|
+
* `unsupported`: unavailable presence cannot establish an empty roster.
|
|
62
|
+
*
|
|
63
|
+
* A roster watch emits the roster as of subscription, then re-lists on every
|
|
64
|
+
* presence change for the branch AND at least once per second, emitting
|
|
65
|
+
* only when the roster it read differs from the one it last sent.
|
|
66
|
+
*
|
|
67
|
+
* All three of those are triggers into ONE sequential reader. The roster is
|
|
68
|
+
* mutable state behind an effect, so concurrent readers can complete out of
|
|
69
|
+
* order, and a watcher that receives an older roster after a newer one keeps
|
|
70
|
+
* the older one: `changesWith` compares against the previous emission, not
|
|
71
|
+
* against the freshest state. Serializing the reads is what makes the last
|
|
72
|
+
* value a watcher holds the most recent one that was read.
|
|
73
|
+
*
|
|
74
|
+
* The periodic re-list is what makes expiry observable. A lapsed lease
|
|
75
|
+
* publishes nothing — the roster drops it the next time somebody lists — so a
|
|
76
|
+
* watch driven by change events alone never saw the LAST participant leave,
|
|
77
|
+
* and a burst of announcements on unrelated branches could slide the one
|
|
78
|
+
* notification this watch needed out of the shared feed. Both are bounded by
|
|
79
|
+
* one second: a departure is visible within 1 s of lease expiry,
|
|
80
|
+
* whether or not a survivor reports it.
|
|
81
|
+
*
|
|
82
|
+
* @category layers
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
export const layerHandlers = BranchRpcs.toLayer(Effect.gen(function* () {
|
|
86
|
+
const share = yield* BranchShare.BranchShare;
|
|
87
|
+
const presence = yield* BranchPresence.BranchPresence;
|
|
88
|
+
const commands = yield* BranchCommands.BranchCommands;
|
|
89
|
+
const ids = yield* BranchIds.BranchIds;
|
|
90
|
+
return BranchRpcs.of({
|
|
91
|
+
"Branch.CreateBranch": ({ ttlMs }) => Effect.gen(function* () {
|
|
92
|
+
const principal = yield* SyncPrincipal.SyncPrincipal;
|
|
93
|
+
if (!SyncPrincipal.isWorkspace(principal)) {
|
|
94
|
+
return yield* new SyncError({
|
|
95
|
+
code: "unauthorized",
|
|
96
|
+
message: "Creating a branch requires an authenticated workspace principal"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Decoded, not branded: `BranchIds` is a host port whose `fresh` is
|
|
100
|
+
// typed `Effect<string>`, so a host id source yielding "" turned an
|
|
101
|
+
// asserted `BranchId` into a schema defect inside `mint` rather than
|
|
102
|
+
// the `SyncError` this handler declares.
|
|
103
|
+
const branchId = yield* Admission.decode(BranchId, yield* ids.fresh, "invalid_request");
|
|
104
|
+
const capability = yield* share.mint({
|
|
105
|
+
branchId,
|
|
106
|
+
capabilityId: yield* ids.fresh,
|
|
107
|
+
access: "write",
|
|
108
|
+
ttlMs
|
|
109
|
+
});
|
|
110
|
+
return { branchId: capability.claims.branchId, capability };
|
|
111
|
+
}),
|
|
112
|
+
"Branch.MintShare": ({ capability, access, ttlMs }) => Effect.gen(function* () {
|
|
113
|
+
const claims = yield* share.verify(capability, {
|
|
114
|
+
branchId: capability.claims.branchId,
|
|
115
|
+
access: "write"
|
|
116
|
+
});
|
|
117
|
+
const capabilityId = yield* ids.fresh;
|
|
118
|
+
const nowMs = yield* Clock.currentTimeMillis;
|
|
119
|
+
// ID generation may outlast the parent. Refuse before minting, and
|
|
120
|
+
// pass the absolute ceiling so the signing clock cannot extend it.
|
|
121
|
+
if (nowMs >= claims.expiresAtMs) {
|
|
122
|
+
return yield* new SyncError({
|
|
123
|
+
code: "unauthorized",
|
|
124
|
+
message: "The share capability has expired"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// A link never outlives the capability it was minted from.
|
|
128
|
+
return yield* share.mint({
|
|
129
|
+
branchId: claims.branchId,
|
|
130
|
+
capabilityId,
|
|
131
|
+
access,
|
|
132
|
+
ttlMs,
|
|
133
|
+
maxExpiresAtMs: claims.expiresAtMs
|
|
134
|
+
});
|
|
135
|
+
}),
|
|
136
|
+
"Branch.Submit": (payload) => commands.submit(payload),
|
|
137
|
+
"Branch.Announce": (payload) => presence.announce(payload),
|
|
138
|
+
"Branch.Leave": (payload) => Effect.as(presence.leave(payload), null),
|
|
139
|
+
"Branch.Roster": (payload) => presence.list(payload),
|
|
140
|
+
"Branch.PresenceOn": (payload) => presence.presenceOn(payload),
|
|
141
|
+
"Branch.WatchRoster": (payload) => Stream.merge(
|
|
142
|
+
// The subscription's own snapshot is a TRIGGER like the other two,
|
|
143
|
+
// not a fourth reader. Reading it here concurrently with them let
|
|
144
|
+
// the roster a change produced overtake it: `presence.list` is an
|
|
145
|
+
// effect against mutable state, three of them ran at once, and the
|
|
146
|
+
// slowest one won the merge. `changesWith` compares an emission only
|
|
147
|
+
// against the one before it, so the older roster passed the filter
|
|
148
|
+
// and the watcher's LAST value said a participant who had already
|
|
149
|
+
// left was still present, until the next change or lease tick.
|
|
150
|
+
Stream.succeed(undefined), Stream.merge(presence.changes.pipe(Stream.filter((branchId) => branchId === payload.branchId), Stream.map(() => undefined)),
|
|
151
|
+
// A lapsed lease publishes nothing, so a watch driven only by
|
|
152
|
+
// change events never observes the LAST participant leaving, and
|
|
153
|
+
// an unrelated branch's burst can slide the one notification this
|
|
154
|
+
// watch needed out of the shared feed. Re-listing at least once per second
|
|
155
|
+
// bounds both: a departure is visible within 1 s of expiry, whether or not anyone reports it.
|
|
156
|
+
Stream.fromEffectRepeat(Effect.sleep(Math.min(presence.leaseMs, 1_000))).pipe(Stream.map(() => undefined)))).pipe(
|
|
157
|
+
// ONE reader, sequential by default, so a roster is read in the
|
|
158
|
+
// order its trigger arrived and every emission is at least as fresh
|
|
159
|
+
// as the one before it.
|
|
160
|
+
Stream.mapEffect(() => presence.list(payload)),
|
|
161
|
+
// Deduplicated over the WHOLE stream, initial emission included.
|
|
162
|
+
// Applied to the change half alone, its first value always passed,
|
|
163
|
+
// so a change that left the roster identical still sent the watcher
|
|
164
|
+
// a second copy of what it had just been given.
|
|
165
|
+
Stream.changesWith(sameRoster), Stream.map((participants) => ({ participants })))
|
|
166
|
+
});
|
|
167
|
+
}));
|
|
168
|
+
//# sourceMappingURL=BranchServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchServer.js","sourceRoot":"","sources":["../../src/BranchServer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The server-side handlers of {@link BranchRpcs}: a thin projection of the\n * branch services onto the wire.\n *\n * Five procedures forward to the service that owns their boundary and add\n * nothing: `Branch.Submit` to {@link BranchCommands}, and `Branch.Announce`,\n * `Branch.Leave`, `Branch.Roster`, and `Branch.WatchRoster` to\n * {@link BranchPresence}. Two enforce policy here, on top of what\n * {@link BranchShare} checks: `Branch.CreateBranch` refuses any principal that\n * is not an authenticated workspace, and `Branch.MintShare` verifies write\n * access, refuses an expired parent, and caps the child at the parent's\n * expiry. `BranchShare.mint` performs none of those checks, so an in-process\n * host that mints through the service directly must gate the principal and\n * the parent capability itself.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Rpc from \"effect/unstable/rpc/Rpc\"\nimport type * as RpcGroup from \"effect/unstable/rpc/RpcGroup\"\nimport * as BranchCommands from \"./BranchCommands.ts\"\nimport * as BranchIds from \"./BranchIds.ts\"\nimport * as BranchPresence from \"./BranchPresence.ts\"\nimport { BranchId, type Participant } from \"./BranchProtocol.ts\"\nimport { BranchRpcs } from \"./BranchRpcs.ts\"\nimport * as BranchShare from \"./BranchShare.ts\"\nimport * as Admission from \"./internal/Admission.ts\"\nimport { SyncError } from \"./SyncError.ts\"\nimport * as SyncPrincipal from \"./SyncPrincipal.ts\"\n\n/**\n * Whether two roster emissions say the same thing.\n *\n * `list` returns participants sorted by id, so the comparison is positional.\n * The lease itself is part of the key: a heartbeat that only extends a lease\n * IS a change the watcher wants, because it is the evidence the participant is\n * still there.\n *\n * The rendering is JSON rather than delimiter-joined fields. Participant ids\n * and display names are `NonEmptyString` with no charset constraint, so every\n * separator is a character they may carry, including the control bytes this\n * used to join on. Two rosters differing only in where a field boundary falls\n * then rendered identically and a watcher never observed the change. JSON\n * quotes and escapes each field, so distinct rosters render distinctly.\n */\nconst rosterKey = (participants: ReadonlyArray<Participant>): string =>\n JSON.stringify(\n participants.map((participant) => [\n participant.participantId,\n participant.displayName,\n participant.sessionId,\n participant.kind,\n participant.where,\n participant.watching,\n participant.for_member,\n participant.leaseExpiresAtMs,\n participant.cursor === null ? null : [participant.cursor.cardId, participant.cursor.offset]\n ])\n )\n\nconst sameRoster = (\n left: ReadonlyArray<Participant>,\n right: ReadonlyArray<Participant>\n): boolean => rosterKey(left) === rosterKey(right)\n\n/**\n * Provides the branch RPC handlers over the branch services.\n *\n * With {@link BranchPresence.layerNoop}, roster reads and watches fail with\n * `unsupported`: unavailable presence cannot establish an empty roster.\n *\n * A roster watch emits the roster as of subscription, then re-lists on every\n * presence change for the branch AND at least once per second, emitting\n * only when the roster it read differs from the one it last sent.\n *\n * All three of those are triggers into ONE sequential reader. The roster is\n * mutable state behind an effect, so concurrent readers can complete out of\n * order, and a watcher that receives an older roster after a newer one keeps\n * the older one: `changesWith` compares against the previous emission, not\n * against the freshest state. Serializing the reads is what makes the last\n * value a watcher holds the most recent one that was read.\n *\n * The periodic re-list is what makes expiry observable. A lapsed lease\n * publishes nothing — the roster drops it the next time somebody lists — so a\n * watch driven by change events alone never saw the LAST participant leave,\n * and a burst of announcements on unrelated branches could slide the one\n * notification this watch needed out of the shared feed. Both are bounded by\n * one second: a departure is visible within 1 s of lease expiry,\n * whether or not a survivor reports it.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerHandlers: Layer.Layer<\n Rpc.ToHandler<RpcGroup.Rpcs<typeof BranchRpcs>>,\n never,\n | BranchShare.BranchShare\n | BranchPresence.BranchPresence\n | BranchCommands.BranchCommands\n | BranchIds.BranchIds\n> = BranchRpcs.toLayer(\n Effect.gen(function*() {\n const share = yield* BranchShare.BranchShare\n const presence = yield* BranchPresence.BranchPresence\n const commands = yield* BranchCommands.BranchCommands\n const ids = yield* BranchIds.BranchIds\n\n return BranchRpcs.of({\n \"Branch.CreateBranch\": ({ ttlMs }) =>\n Effect.gen(function*() {\n const principal = yield* SyncPrincipal.SyncPrincipal\n if (!SyncPrincipal.isWorkspace(principal)) {\n return yield* new SyncError({\n code: \"unauthorized\",\n message: \"Creating a branch requires an authenticated workspace principal\"\n })\n }\n // Decoded, not branded: `BranchIds` is a host port whose `fresh` is\n // typed `Effect<string>`, so a host id source yielding \"\" turned an\n // asserted `BranchId` into a schema defect inside `mint` rather than\n // the `SyncError` this handler declares.\n const branchId = yield* Admission.decode(BranchId, yield* ids.fresh, \"invalid_request\")\n const capability = yield* share.mint({\n branchId,\n capabilityId: yield* ids.fresh,\n access: \"write\",\n ttlMs\n })\n return { branchId: capability.claims.branchId, capability }\n }),\n \"Branch.MintShare\": ({ capability, access, ttlMs }) =>\n Effect.gen(function*() {\n const claims = yield* share.verify(capability, {\n branchId: capability.claims.branchId,\n access: \"write\"\n })\n const capabilityId = yield* ids.fresh\n const nowMs = yield* Clock.currentTimeMillis\n // ID generation may outlast the parent. Refuse before minting, and\n // pass the absolute ceiling so the signing clock cannot extend it.\n if (nowMs >= claims.expiresAtMs) {\n return yield* new SyncError({\n code: \"unauthorized\",\n message: \"The share capability has expired\"\n })\n }\n // A link never outlives the capability it was minted from.\n return yield* share.mint({\n branchId: claims.branchId,\n capabilityId,\n access,\n ttlMs,\n maxExpiresAtMs: claims.expiresAtMs\n })\n }),\n \"Branch.Submit\": (payload) => commands.submit(payload),\n \"Branch.Announce\": (payload) => presence.announce(payload),\n \"Branch.Leave\": (payload) => Effect.as(presence.leave(payload), null),\n \"Branch.Roster\": (payload) => presence.list(payload),\n \"Branch.PresenceOn\": (payload) => presence.presenceOn(payload),\n \"Branch.WatchRoster\": (payload) =>\n Stream.merge(\n // The subscription's own snapshot is a TRIGGER like the other two,\n // not a fourth reader. Reading it here concurrently with them let\n // the roster a change produced overtake it: `presence.list` is an\n // effect against mutable state, three of them ran at once, and the\n // slowest one won the merge. `changesWith` compares an emission only\n // against the one before it, so the older roster passed the filter\n // and the watcher's LAST value said a participant who had already\n // left was still present, until the next change or lease tick.\n Stream.succeed(undefined),\n Stream.merge(\n presence.changes.pipe(\n Stream.filter((branchId) => branchId === payload.branchId),\n Stream.map(() => undefined)\n ),\n // A lapsed lease publishes nothing, so a watch driven only by\n // change events never observes the LAST participant leaving, and\n // an unrelated branch's burst can slide the one notification this\n // watch needed out of the shared feed. Re-listing at least once per second\n // bounds both: a departure is visible within 1 s of expiry, whether or not anyone reports it.\n Stream.fromEffectRepeat(Effect.sleep(Math.min(presence.leaseMs, 1_000))).pipe(Stream.map(() => undefined))\n )\n ).pipe(\n // ONE reader, sequential by default, so a roster is read in the\n // order its trigger arrived and every emission is at least as fresh\n // as the one before it.\n Stream.mapEffect(() => presence.list(payload)),\n // Deduplicated over the WHOLE stream, initial emission included.\n // Applied to the change half alone, its first value always passed,\n // so a change that left the roster identical still sent the watcher\n // a second copy of what it had just been given.\n Stream.changesWith(sameRoster),\n Stream.map((participants) => ({ participants }))\n )\n })\n })\n)\n"]}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share capabilities: the authorization boundary of a shared branch.
|
|
3
|
+
*
|
|
4
|
+
* A share link carries a capability, not a session. The capability names
|
|
5
|
+
* exactly one branch, one access level, and one expiry, and it is signed, so
|
|
6
|
+
* the holder cannot widen it. Every branch operation authorizes through
|
|
7
|
+
* `verify`, which is therefore the single place cross-branch access and
|
|
8
|
+
* expired links are refused.
|
|
9
|
+
*
|
|
10
|
+
* The signature is HMAC-SHA-256 over a length-prefixed encoding of the claims,
|
|
11
|
+
* led by a scheme label. Length prefixes matter: without them a branch id
|
|
12
|
+
* ending in the separator could be re-cut into a different, still-validly-
|
|
13
|
+
* signed claim set. The label matters for the same reason across schemes: a
|
|
14
|
+
* branch capability can never verify as a workspace capability even if one
|
|
15
|
+
* secret is misconfigured into both authorities. Web Crypto is used directly
|
|
16
|
+
* so the same module runs in the browser and on node.
|
|
17
|
+
*
|
|
18
|
+
* A `kid` inside the claims names the key that signed them, so a branch secret
|
|
19
|
+
* rotates the way the workspace secret already did: the new key signs, the
|
|
20
|
+
* retired key stays in the verification keyring, and the share links already
|
|
21
|
+
* out keep working until they expire.
|
|
22
|
+
*
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
import * as Config from "effect/Config";
|
|
26
|
+
import * as Context from "effect/Context";
|
|
27
|
+
import * as Effect from "effect/Effect";
|
|
28
|
+
import * as Layer from "effect/Layer";
|
|
29
|
+
import * as Schema from "effect/Schema";
|
|
30
|
+
import { Access, BranchId, ShareCapability, ShareClaims } from "./BranchProtocol.ts";
|
|
31
|
+
import * as shareSigner from "./internal/ShareSigner.ts";
|
|
32
|
+
import type { SyncError } from "./SyncError.ts";
|
|
33
|
+
/**
|
|
34
|
+
* The branch and access one authorization request needs.
|
|
35
|
+
*
|
|
36
|
+
* @category models
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
export interface AuthorizeRequest {
|
|
40
|
+
readonly branchId: BranchId;
|
|
41
|
+
readonly access: Access;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a freshly minted capability grants.
|
|
45
|
+
*
|
|
46
|
+
* @category models
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
export declare const MintRequest: Schema.Struct<{
|
|
50
|
+
readonly branchId: Schema.brand<Schema.NonEmptyString, "@smthrs/sync/BranchProtocol/BranchId">;
|
|
51
|
+
readonly capabilityId: Schema.NonEmptyString;
|
|
52
|
+
readonly access: Schema.Literals<readonly ["read", "write"]>;
|
|
53
|
+
readonly ttlMs: Schema.Int;
|
|
54
|
+
/** Absolute expiry ceiling for a capability delegated from a parent. */
|
|
55
|
+
readonly maxExpiresAtMs: Schema.optionalKey<Schema.Int>;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* What a freshly minted capability grants.
|
|
59
|
+
*
|
|
60
|
+
* @category models
|
|
61
|
+
* @since 0.1.0
|
|
62
|
+
*/
|
|
63
|
+
export type MintRequest = typeof MintRequest.Type;
|
|
64
|
+
/**
|
|
65
|
+
* Share capability operations.
|
|
66
|
+
*
|
|
67
|
+
* `mint` fails with a `SyncError` when the Web Crypto signing operation
|
|
68
|
+
* rejects or the absolute expiry ceiling has elapsed; `verify` fails with a
|
|
69
|
+
* `SyncError` when signing rejects or the capability is refused.
|
|
70
|
+
*
|
|
71
|
+
* @category models
|
|
72
|
+
* @since 0.1.0
|
|
73
|
+
*/
|
|
74
|
+
export interface Service {
|
|
75
|
+
readonly mint: (request: MintRequest) => Effect.Effect<ShareCapability, SyncError>;
|
|
76
|
+
readonly verify: (capability: ShareCapability, request: AuthorizeRequest) => Effect.Effect<ShareClaims, SyncError>;
|
|
77
|
+
}
|
|
78
|
+
declare const BranchShare_base: Context.ServiceClass<BranchShare, "@smthrs/sync/BranchShare", Service>;
|
|
79
|
+
/**
|
|
80
|
+
* The branch share-capability authority.
|
|
81
|
+
*
|
|
82
|
+
* @category services
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
export declare class BranchShare extends BranchShare_base {
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Constructs a share authority that mints nothing and trusts nothing.
|
|
89
|
+
*
|
|
90
|
+
* @category constructors
|
|
91
|
+
* @since 0.1.0
|
|
92
|
+
*/
|
|
93
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
94
|
+
/**
|
|
95
|
+
* Provides a share authority that refuses every capability.
|
|
96
|
+
*
|
|
97
|
+
* @category layers
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export declare const layerNoop: Layer.Layer<BranchShare>;
|
|
101
|
+
/**
|
|
102
|
+
* One named signing key. The secret is `Redacted` so a keyring never renders
|
|
103
|
+
* it into logs or inspection output.
|
|
104
|
+
*
|
|
105
|
+
* @category models
|
|
106
|
+
* @since 1.0.0-rc.0
|
|
107
|
+
*/
|
|
108
|
+
export type Key = shareSigner.Key;
|
|
109
|
+
/**
|
|
110
|
+
* A keyring: the key that signs new capabilities plus every key still
|
|
111
|
+
* accepted for verification. Rotation adds a new active key and keeps the
|
|
112
|
+
* retired one in `keys` until its outstanding links expire, so rotating the
|
|
113
|
+
* branch secret no longer breaks every share link that is already out.
|
|
114
|
+
*
|
|
115
|
+
* @category models
|
|
116
|
+
* @since 1.0.0-rc.0
|
|
117
|
+
*/
|
|
118
|
+
export type Keyring = shareSigner.Keyring;
|
|
119
|
+
/**
|
|
120
|
+
* Constructs the HMAC-SHA-256 share authority over a keyring.
|
|
121
|
+
*
|
|
122
|
+
* Every key is imported up front, so a misconfigured keyring — an unknown
|
|
123
|
+
* active kid, a duplicate kid, or a key Web Crypto refuses — fails at
|
|
124
|
+
* construction rather than at the first request. Secrets enter as `Redacted`
|
|
125
|
+
* values: an authority never holds a plain string that a log, a span, or an
|
|
126
|
+
* inspection of the keyring could render.
|
|
127
|
+
*
|
|
128
|
+
* @category constructors
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
export declare const makeHmac: (keyring: Keyring) => Effect.Effect<Service, SyncError>;
|
|
132
|
+
/**
|
|
133
|
+
* Provides the HMAC-SHA-256 share authority over a keyring.
|
|
134
|
+
*
|
|
135
|
+
* @category layers
|
|
136
|
+
* @since 0.1.0
|
|
137
|
+
*/
|
|
138
|
+
export declare const layerHmac: (keyring: Keyring) => Layer.Layer<BranchShare, SyncError>;
|
|
139
|
+
/**
|
|
140
|
+
* Provides the branch share authority from configuration: the secret from
|
|
141
|
+
* `SMITHERS_SYNC_BRANCH_SECRET` (read as `Redacted`, never logged) and the key
|
|
142
|
+
* name from `SMITHERS_SYNC_BRANCH_KEY_ID`, defaulting to `primary`. Rotation
|
|
143
|
+
* beyond one configured key uses {@link layerHmac} with an explicit keyring.
|
|
144
|
+
*
|
|
145
|
+
* The names are the branch authority's own, not the workspace's: the scheme
|
|
146
|
+
* label makes one secret in both authorities survivable, not advisable, and a
|
|
147
|
+
* deployment that rotates one credential should not silently invalidate the
|
|
148
|
+
* other's outstanding capabilities.
|
|
149
|
+
*
|
|
150
|
+
* There is deliberately no default secret: a deployment that configures
|
|
151
|
+
* neither name fails to construct the authority, and every branch operation
|
|
152
|
+
* stays closed.
|
|
153
|
+
*
|
|
154
|
+
* @category layers
|
|
155
|
+
* @since 1.0.0-rc.0
|
|
156
|
+
*/
|
|
157
|
+
export declare const layerConfig: Layer.Layer<BranchShare, SyncError | Config.ConfigError>;
|
|
158
|
+
export {};
|
|
159
|
+
//# sourceMappingURL=BranchShare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchShare.d.ts","sourceRoot":"","sources":["../../src/BranchShare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEpF,OAAO,KAAK,WAAW,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;IAKtB,wEAAwE;;EAExE,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAA;IAClF,QAAQ,CAAC,MAAM,EAAE,CACf,UAAU,EAAE,eAAe,EAC3B,OAAO,EAAE,gBAAgB,KACtB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;CAC3C;;AAED;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,gBAAmE;CAAG;AAIvG;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,OAQxD,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAA0C,CAAA;AA0CzF;;;;;;GAMG;AACH,MAAM,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAA;AAEzC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAkEzE,CAAA;AAEH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,OAAO,KAAG,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,CACjC,CAAA;AAE9C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,WAAW,CAOhF,CAAA"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share capabilities: the authorization boundary of a shared branch.
|
|
3
|
+
*
|
|
4
|
+
* A share link carries a capability, not a session. The capability names
|
|
5
|
+
* exactly one branch, one access level, and one expiry, and it is signed, so
|
|
6
|
+
* the holder cannot widen it. Every branch operation authorizes through
|
|
7
|
+
* `verify`, which is therefore the single place cross-branch access and
|
|
8
|
+
* expired links are refused.
|
|
9
|
+
*
|
|
10
|
+
* The signature is HMAC-SHA-256 over a length-prefixed encoding of the claims,
|
|
11
|
+
* led by a scheme label. Length prefixes matter: without them a branch id
|
|
12
|
+
* ending in the separator could be re-cut into a different, still-validly-
|
|
13
|
+
* signed claim set. The label matters for the same reason across schemes: a
|
|
14
|
+
* branch capability can never verify as a workspace capability even if one
|
|
15
|
+
* secret is misconfigured into both authorities. Web Crypto is used directly
|
|
16
|
+
* so the same module runs in the browser and on node.
|
|
17
|
+
*
|
|
18
|
+
* A `kid` inside the claims names the key that signed them, so a branch secret
|
|
19
|
+
* rotates the way the workspace secret already did: the new key signs, the
|
|
20
|
+
* retired key stays in the verification keyring, and the share links already
|
|
21
|
+
* out keep working until they expire.
|
|
22
|
+
*
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
import * as Clock from "effect/Clock";
|
|
26
|
+
import * as Config from "effect/Config";
|
|
27
|
+
import * as Context from "effect/Context";
|
|
28
|
+
import * as Effect from "effect/Effect";
|
|
29
|
+
import * as Layer from "effect/Layer";
|
|
30
|
+
import * as Schema from "effect/Schema";
|
|
31
|
+
import { Access, BranchId, ShareCapability, ShareClaims } from "./BranchProtocol.js";
|
|
32
|
+
import * as Admission from "./internal/Admission.js";
|
|
33
|
+
import * as shareSigner from "./internal/ShareSigner.js";
|
|
34
|
+
/**
|
|
35
|
+
* What a freshly minted capability grants.
|
|
36
|
+
*
|
|
37
|
+
* @category models
|
|
38
|
+
* @since 0.1.0
|
|
39
|
+
*/
|
|
40
|
+
export const MintRequest = Schema.Struct({
|
|
41
|
+
branchId: BranchId,
|
|
42
|
+
capabilityId: Schema.NonEmptyString,
|
|
43
|
+
access: Access,
|
|
44
|
+
ttlMs: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
45
|
+
/** Absolute expiry ceiling for a capability delegated from a parent. */
|
|
46
|
+
maxExpiresAtMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* The branch share-capability authority.
|
|
50
|
+
*
|
|
51
|
+
* @category services
|
|
52
|
+
* @since 0.1.0
|
|
53
|
+
*/
|
|
54
|
+
export class BranchShare extends Context.Service()("@smthrs/sync/BranchShare") {
|
|
55
|
+
}
|
|
56
|
+
const denied = shareSigner.unauthorized;
|
|
57
|
+
/**
|
|
58
|
+
* Constructs a share authority that mints nothing and trusts nothing.
|
|
59
|
+
*
|
|
60
|
+
* @category constructors
|
|
61
|
+
* @since 0.1.0
|
|
62
|
+
*/
|
|
63
|
+
export const makeNoop = (overrides = {}) => BranchShare.of({
|
|
64
|
+
// Both operations FAIL. `mint` used to die, which contradicted its own
|
|
65
|
+
// declared `Effect<ShareCapability, SyncError>` and left a consumer
|
|
66
|
+
// wired to this authority unable to tell "sharing is off" from a bug.
|
|
67
|
+
mint: () => Effect.fail(denied("Branch sharing is unavailable")),
|
|
68
|
+
verify: () => Effect.fail(denied("Branch sharing is unavailable")),
|
|
69
|
+
...overrides
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Provides a share authority that refuses every capability.
|
|
73
|
+
*
|
|
74
|
+
* @category layers
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export const layerNoop = Layer.succeed(BranchShare, makeNoop());
|
|
78
|
+
/**
|
|
79
|
+
* Domain separation: the label leads the signed encoding, so a branch
|
|
80
|
+
* signature can never be replayed as a workspace signature (or any later
|
|
81
|
+
* scheme's) under a shared secret. `WorkspaceShare` has always led with its
|
|
82
|
+
* own label; a one-sided label protects one direction, and the branch side is
|
|
83
|
+
* the one anonymous share-link holders reach.
|
|
84
|
+
*/
|
|
85
|
+
const schemeLabel = "@smthrs/sync/BranchShare/v2";
|
|
86
|
+
/** Length-prefixed so no two distinct claim sets share an encoding. */
|
|
87
|
+
const canonical = (claims) => shareSigner.lengthPrefixed([
|
|
88
|
+
schemeLabel,
|
|
89
|
+
claims.kid,
|
|
90
|
+
claims.branchId,
|
|
91
|
+
claims.capabilityId,
|
|
92
|
+
claims.access,
|
|
93
|
+
String(claims.issuedAtMs),
|
|
94
|
+
String(claims.expiresAtMs)
|
|
95
|
+
]);
|
|
96
|
+
/**
|
|
97
|
+
* The claim fields, copied out of the capability the caller owns.
|
|
98
|
+
*
|
|
99
|
+
* `Schema.Class` instances are not frozen, and `verify` awaits Web Crypto
|
|
100
|
+
* between signing the claims and authorizing them. Reading the caller's object
|
|
101
|
+
* again after the await let an in-process holder of the same instance widen
|
|
102
|
+
* `access` — or move `expiresAtMs` — between the signature that was checked
|
|
103
|
+
* and the checks that follow it.
|
|
104
|
+
*/
|
|
105
|
+
const snapshot = (claims) => new ShareClaims({
|
|
106
|
+
kid: claims.kid,
|
|
107
|
+
branchId: claims.branchId,
|
|
108
|
+
capabilityId: claims.capabilityId,
|
|
109
|
+
access: claims.access,
|
|
110
|
+
issuedAtMs: claims.issuedAtMs,
|
|
111
|
+
expiresAtMs: claims.expiresAtMs
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Constructs the HMAC-SHA-256 share authority over a keyring.
|
|
115
|
+
*
|
|
116
|
+
* Every key is imported up front, so a misconfigured keyring — an unknown
|
|
117
|
+
* active kid, a duplicate kid, or a key Web Crypto refuses — fails at
|
|
118
|
+
* construction rather than at the first request. Secrets enter as `Redacted`
|
|
119
|
+
* values: an authority never holds a plain string that a log, a span, or an
|
|
120
|
+
* inspection of the keyring could render.
|
|
121
|
+
*
|
|
122
|
+
* @category constructors
|
|
123
|
+
* @since 0.1.0
|
|
124
|
+
*/
|
|
125
|
+
export const makeHmac = (keyring) => Effect.map(shareSigner.importKeyring(keyring, "branch"), (ring) => {
|
|
126
|
+
const mint = Effect.fn("BranchShare.mint")(function* (request) {
|
|
127
|
+
// Decoded, not read: the declared parameter type admits an empty
|
|
128
|
+
// branch id, an empty capability id and a non-positive ttl that the
|
|
129
|
+
// schema forbids, and reading them straight into `new ShareClaims`
|
|
130
|
+
// turned a caller's bad argument into a defect out of an operation
|
|
131
|
+
// whose type promises a `SyncError`. A `ttlMs` of 0 minted an
|
|
132
|
+
// already-expired capability instead of being refused.
|
|
133
|
+
const admitted = yield* Admission.decode(MintRequest, request, "invalid_request");
|
|
134
|
+
yield* Effect.annotateCurrentSpan({ branchId: admitted.branchId, access: admitted.access });
|
|
135
|
+
const issuedAtMs = yield* Clock.currentTimeMillis;
|
|
136
|
+
const maxExpiresAtMs = admitted.maxExpiresAtMs ?? Infinity;
|
|
137
|
+
if (issuedAtMs >= maxExpiresAtMs) {
|
|
138
|
+
return yield* Effect.fail(denied("The share capability has expired"));
|
|
139
|
+
}
|
|
140
|
+
const claims = new ShareClaims({
|
|
141
|
+
kid: ring.activeKid,
|
|
142
|
+
branchId: admitted.branchId,
|
|
143
|
+
capabilityId: admitted.capabilityId,
|
|
144
|
+
access: admitted.access,
|
|
145
|
+
issuedAtMs,
|
|
146
|
+
expiresAtMs: Math.min(issuedAtMs + admitted.ttlMs, maxExpiresAtMs)
|
|
147
|
+
});
|
|
148
|
+
return new ShareCapability({
|
|
149
|
+
claims,
|
|
150
|
+
signature: yield* shareSigner.signHmac(ring.active, canonical(claims))
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
const verify = Effect.fn("BranchShare.verify")(function* (capability, request) {
|
|
154
|
+
yield* Effect.annotateCurrentSpan({ branchId: request.branchId, access: request.access });
|
|
155
|
+
// Everything authorized below is read from these locals, never from
|
|
156
|
+
// the caller's objects, which may change while Web Crypto is awaited.
|
|
157
|
+
const claims = snapshot(capability.claims);
|
|
158
|
+
const signature = capability.signature;
|
|
159
|
+
const branchId = request.branchId;
|
|
160
|
+
const key = ring.verification.get(claims.kid);
|
|
161
|
+
if (key === undefined) {
|
|
162
|
+
return yield* Effect.fail(denied("The share capability names an unknown signing key"));
|
|
163
|
+
}
|
|
164
|
+
yield* shareSigner.verifyClaims({
|
|
165
|
+
key,
|
|
166
|
+
canonical: canonical(claims),
|
|
167
|
+
signature,
|
|
168
|
+
expiresAtMs: claims.expiresAtMs,
|
|
169
|
+
granted: claims.access,
|
|
170
|
+
requested: request.access,
|
|
171
|
+
subject: "The share capability",
|
|
172
|
+
// The one check this authority adds, run after the signature so a
|
|
173
|
+
// forged claim set is refused as a bad signature and never reported
|
|
174
|
+
// back as the branch it names.
|
|
175
|
+
scope: claims.branchId === branchId
|
|
176
|
+
? Effect.void
|
|
177
|
+
: Effect.fail(denied(`The share capability is scoped to branch ${claims.branchId}`))
|
|
178
|
+
});
|
|
179
|
+
return claims;
|
|
180
|
+
});
|
|
181
|
+
return BranchShare.of({ mint, verify });
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* Provides the HMAC-SHA-256 share authority over a keyring.
|
|
185
|
+
*
|
|
186
|
+
* @category layers
|
|
187
|
+
* @since 0.1.0
|
|
188
|
+
*/
|
|
189
|
+
export const layerHmac = (keyring) => Layer.effect(BranchShare, makeHmac(keyring));
|
|
190
|
+
/**
|
|
191
|
+
* Provides the branch share authority from configuration: the secret from
|
|
192
|
+
* `SMITHERS_SYNC_BRANCH_SECRET` (read as `Redacted`, never logged) and the key
|
|
193
|
+
* name from `SMITHERS_SYNC_BRANCH_KEY_ID`, defaulting to `primary`. Rotation
|
|
194
|
+
* beyond one configured key uses {@link layerHmac} with an explicit keyring.
|
|
195
|
+
*
|
|
196
|
+
* The names are the branch authority's own, not the workspace's: the scheme
|
|
197
|
+
* label makes one secret in both authorities survivable, not advisable, and a
|
|
198
|
+
* deployment that rotates one credential should not silently invalidate the
|
|
199
|
+
* other's outstanding capabilities.
|
|
200
|
+
*
|
|
201
|
+
* There is deliberately no default secret: a deployment that configures
|
|
202
|
+
* neither name fails to construct the authority, and every branch operation
|
|
203
|
+
* stays closed.
|
|
204
|
+
*
|
|
205
|
+
* @category layers
|
|
206
|
+
* @since 1.0.0-rc.0
|
|
207
|
+
*/
|
|
208
|
+
export const layerConfig = Layer.effect(BranchShare, Effect.gen(function* () {
|
|
209
|
+
const secret = yield* Config.Redacted("SMITHERS_SYNC_BRANCH_SECRET");
|
|
210
|
+
const kid = yield* Config.String("SMITHERS_SYNC_BRANCH_KEY_ID").pipe(Config.withDefault("primary"));
|
|
211
|
+
return yield* makeHmac({ activeKid: kid, keys: [{ kid, secret }] });
|
|
212
|
+
}));
|
|
213
|
+
//# sourceMappingURL=BranchShare.js.map
|