@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,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace share capabilities: the credential a sync connection presents to
|
|
3
|
+
* read the workspace's non-branch runs.
|
|
4
|
+
*
|
|
5
|
+
* The scheme extends the branch share shape rather than inventing a parallel
|
|
6
|
+
* one: a claim set and an HMAC-SHA-256 signature over its length-prefixed
|
|
7
|
+
* canonical encoding, verified with a constant-time comparison. Two things
|
|
8
|
+
* are added. A `kid` names the signing key inside the claims, so keys rotate
|
|
9
|
+
* without invalidating capabilities minted under a retired key that is still
|
|
10
|
+
* in the verification keyring. And the canonical encoding starts with a
|
|
11
|
+
* scheme label, so a workspace capability can never verify as a branch
|
|
12
|
+
* capability even if one secret is misconfigured into both authorities.
|
|
13
|
+
*
|
|
14
|
+
* Secrets enter through {@link Key} as `Redacted` values — a keyring never
|
|
15
|
+
* holds a plain string — and {@link layerConfig} provisions the keyring from
|
|
16
|
+
* configuration without any secret appearing in code.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as Clock from "effect/Clock"
|
|
22
|
+
import * as Config from "effect/Config"
|
|
23
|
+
import * as Context from "effect/Context"
|
|
24
|
+
import * as Effect from "effect/Effect"
|
|
25
|
+
import * as Layer from "effect/Layer"
|
|
26
|
+
import * as Schema from "effect/Schema"
|
|
27
|
+
import { Access } from "./BranchProtocol.ts"
|
|
28
|
+
import * as Admission from "./internal/Admission.ts"
|
|
29
|
+
import * as shareSigner from "./internal/ShareSigner.ts"
|
|
30
|
+
import type { SyncError } from "./SyncError.ts"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Schema for the claims a workspace capability carries.
|
|
34
|
+
*
|
|
35
|
+
* `kid` names the key that signed the claims and is itself signed, so a
|
|
36
|
+
* verifier both selects the right key and refuses a capability whose key
|
|
37
|
+
* name was swapped after minting.
|
|
38
|
+
*
|
|
39
|
+
* @category schemas
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export class WorkspaceClaims extends Schema.Class<WorkspaceClaims>("@smthrs/sync/WorkspaceShare/WorkspaceClaims")({
|
|
43
|
+
kid: Schema.NonEmptyString,
|
|
44
|
+
capabilityId: Schema.NonEmptyString,
|
|
45
|
+
access: Access,
|
|
46
|
+
issuedAtMs: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
47
|
+
expiresAtMs: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
48
|
+
}) {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Schema for a signed, expiring, workspace-scoped capability.
|
|
52
|
+
*
|
|
53
|
+
* @category schemas
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export class WorkspaceCapability extends Schema.Class<WorkspaceCapability>(
|
|
57
|
+
"@smthrs/sync/WorkspaceShare/WorkspaceCapability"
|
|
58
|
+
)({
|
|
59
|
+
claims: WorkspaceClaims,
|
|
60
|
+
signature: Schema.String
|
|
61
|
+
}) {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The access one authorization request needs.
|
|
65
|
+
*
|
|
66
|
+
* @category models
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
export interface AuthorizeRequest {
|
|
70
|
+
readonly access: Access
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a freshly minted workspace capability grants.
|
|
75
|
+
*
|
|
76
|
+
* @category models
|
|
77
|
+
* @since 0.1.0
|
|
78
|
+
*/
|
|
79
|
+
export const MintRequest = Schema.Struct({
|
|
80
|
+
capabilityId: Schema.NonEmptyString,
|
|
81
|
+
access: Access,
|
|
82
|
+
ttlMs: Schema.Int.check(Schema.isGreaterThan(0))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What a freshly minted workspace capability grants.
|
|
87
|
+
*
|
|
88
|
+
* @category models
|
|
89
|
+
* @since 0.1.0
|
|
90
|
+
*/
|
|
91
|
+
export type MintRequest = typeof MintRequest.Type
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Workspace share capability operations.
|
|
95
|
+
*
|
|
96
|
+
* `mint` fails with a `SyncError` when the Web Crypto signing operation
|
|
97
|
+
* rejects; `verify` fails with a `SyncError` when signing rejects or the
|
|
98
|
+
* capability is refused.
|
|
99
|
+
*
|
|
100
|
+
* @category models
|
|
101
|
+
* @since 0.1.0
|
|
102
|
+
*/
|
|
103
|
+
export interface Service {
|
|
104
|
+
readonly mint: (request: MintRequest) => Effect.Effect<WorkspaceCapability, SyncError>
|
|
105
|
+
readonly verify: (
|
|
106
|
+
capability: WorkspaceCapability,
|
|
107
|
+
request: AuthorizeRequest
|
|
108
|
+
) => Effect.Effect<WorkspaceClaims, SyncError>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The workspace share-capability authority.
|
|
113
|
+
*
|
|
114
|
+
* @category services
|
|
115
|
+
* @since 0.1.0
|
|
116
|
+
*/
|
|
117
|
+
export class WorkspaceShare extends Context.Service<WorkspaceShare, Service>()("@smthrs/sync/WorkspaceShare") {}
|
|
118
|
+
|
|
119
|
+
const denied = shareSigner.unauthorized
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Constructs a workspace share authority that mints nothing and trusts
|
|
123
|
+
* nothing.
|
|
124
|
+
*
|
|
125
|
+
* @category constructors
|
|
126
|
+
* @since 0.1.0
|
|
127
|
+
*/
|
|
128
|
+
export const makeNoop = (overrides: Partial<Service> = {}): Service =>
|
|
129
|
+
WorkspaceShare.of({
|
|
130
|
+
// Both operations FAIL. `mint` used to die, and this is the authority the
|
|
131
|
+
// shipped CLI gateway wires, so a consumer handling `SyncError` on that
|
|
132
|
+
// composition got a crash where the type promised a refusal.
|
|
133
|
+
mint: () => Effect.fail(denied("Workspace sharing is unavailable")),
|
|
134
|
+
verify: () => Effect.fail(denied("Workspace sharing is unavailable")),
|
|
135
|
+
...overrides
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Provides a workspace share authority that refuses every capability.
|
|
140
|
+
*
|
|
141
|
+
* @category layers
|
|
142
|
+
* @since 0.1.0
|
|
143
|
+
*/
|
|
144
|
+
export const layerNoop: Layer.Layer<WorkspaceShare> = Layer.succeed(WorkspaceShare, makeNoop())
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One named signing key. The secret is `Redacted` so a keyring never renders
|
|
148
|
+
* it into logs or inspection output.
|
|
149
|
+
*
|
|
150
|
+
* @category models
|
|
151
|
+
* @since 0.1.0
|
|
152
|
+
*/
|
|
153
|
+
export type Key = shareSigner.Key
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A keyring: the key that signs new capabilities plus every key still
|
|
157
|
+
* accepted for verification. Rotation adds a new active key and keeps the
|
|
158
|
+
* retired one in `keys` until its outstanding capabilities expire.
|
|
159
|
+
*
|
|
160
|
+
* @category models
|
|
161
|
+
* @since 0.1.0
|
|
162
|
+
*/
|
|
163
|
+
export type Keyring = shareSigner.Keyring
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Domain separation: the label leads the signed encoding, so a workspace
|
|
167
|
+
* signature can never be replayed as any other scheme's signature under a
|
|
168
|
+
* shared secret.
|
|
169
|
+
*/
|
|
170
|
+
const schemeLabel = "@smthrs/sync/WorkspaceShare/v1"
|
|
171
|
+
|
|
172
|
+
const canonical = (claims: WorkspaceClaims): string =>
|
|
173
|
+
shareSigner.lengthPrefixed([
|
|
174
|
+
schemeLabel,
|
|
175
|
+
claims.kid,
|
|
176
|
+
claims.capabilityId,
|
|
177
|
+
claims.access,
|
|
178
|
+
String(claims.issuedAtMs),
|
|
179
|
+
String(claims.expiresAtMs)
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The claim fields, copied out of the capability the caller owns.
|
|
184
|
+
*
|
|
185
|
+
* `Schema.Class` instances are not frozen, and `verify` awaits Web Crypto
|
|
186
|
+
* between signing the claims and authorizing them, so the checks must read a
|
|
187
|
+
* snapshot rather than an object the caller can still mutate.
|
|
188
|
+
*/
|
|
189
|
+
const snapshot = (claims: WorkspaceClaims): WorkspaceClaims =>
|
|
190
|
+
new WorkspaceClaims({
|
|
191
|
+
kid: claims.kid,
|
|
192
|
+
capabilityId: claims.capabilityId,
|
|
193
|
+
access: claims.access,
|
|
194
|
+
issuedAtMs: claims.issuedAtMs,
|
|
195
|
+
expiresAtMs: claims.expiresAtMs
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Constructs the HMAC-SHA-256 workspace share authority over a keyring.
|
|
200
|
+
*
|
|
201
|
+
* Every key is imported up front, so a misconfigured keyring — an unknown
|
|
202
|
+
* active kid, a duplicate kid, or a key Web Crypto refuses — fails at
|
|
203
|
+
* construction rather than at the first request.
|
|
204
|
+
*
|
|
205
|
+
* @category constructors
|
|
206
|
+
* @since 0.1.0
|
|
207
|
+
*/
|
|
208
|
+
export const makeHmac = (keyring: Keyring): Effect.Effect<Service, SyncError> =>
|
|
209
|
+
Effect.map(
|
|
210
|
+
shareSigner.importKeyring(keyring, "workspace"),
|
|
211
|
+
(ring) => {
|
|
212
|
+
const mint = Effect.fn("WorkspaceShare.mint")(function*(request: MintRequest) {
|
|
213
|
+
// Decoded, not read: the declared parameter type admits an empty
|
|
214
|
+
// capability id and a `ttlMs` of 0 or NaN that the schema forbids, and
|
|
215
|
+
// reading them straight into `new WorkspaceClaims` turned a caller's
|
|
216
|
+
// bad argument into a defect out of an operation whose type promises a
|
|
217
|
+
// `SyncError`.
|
|
218
|
+
const admitted = yield* Admission.decode(MintRequest, request, "invalid_request")
|
|
219
|
+
yield* Effect.annotateCurrentSpan({ access: admitted.access })
|
|
220
|
+
const issuedAtMs = yield* Clock.currentTimeMillis
|
|
221
|
+
const claims = new WorkspaceClaims({
|
|
222
|
+
kid: ring.activeKid,
|
|
223
|
+
capabilityId: admitted.capabilityId,
|
|
224
|
+
access: admitted.access,
|
|
225
|
+
issuedAtMs,
|
|
226
|
+
expiresAtMs: issuedAtMs + admitted.ttlMs
|
|
227
|
+
})
|
|
228
|
+
return new WorkspaceCapability({
|
|
229
|
+
claims,
|
|
230
|
+
signature: yield* shareSigner.signHmac(ring.active, canonical(claims))
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const verify = Effect.fn("WorkspaceShare.verify")(function*(
|
|
235
|
+
capability: WorkspaceCapability,
|
|
236
|
+
request: AuthorizeRequest
|
|
237
|
+
) {
|
|
238
|
+
yield* Effect.annotateCurrentSpan({ access: request.access })
|
|
239
|
+
// Everything authorized below is read from these locals, never from the
|
|
240
|
+
// caller's objects, which may change while Web Crypto is awaited.
|
|
241
|
+
const claims = snapshot(capability.claims)
|
|
242
|
+
const signature = capability.signature
|
|
243
|
+
const key = ring.verification.get(claims.kid)
|
|
244
|
+
if (key === undefined) {
|
|
245
|
+
return yield* Effect.fail(denied("The workspace capability names an unknown signing key"))
|
|
246
|
+
}
|
|
247
|
+
yield* shareSigner.verifyClaims({
|
|
248
|
+
key,
|
|
249
|
+
canonical: canonical(claims),
|
|
250
|
+
signature,
|
|
251
|
+
expiresAtMs: claims.expiresAtMs,
|
|
252
|
+
granted: claims.access,
|
|
253
|
+
requested: request.access,
|
|
254
|
+
subject: "The workspace capability"
|
|
255
|
+
})
|
|
256
|
+
return claims
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return WorkspaceShare.of({ mint, verify })
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Provides the HMAC-SHA-256 workspace share authority over a keyring.
|
|
265
|
+
*
|
|
266
|
+
* @category layers
|
|
267
|
+
* @since 0.1.0
|
|
268
|
+
*/
|
|
269
|
+
export const layerHmac = (keyring: Keyring): Layer.Layer<WorkspaceShare, SyncError> =>
|
|
270
|
+
Layer.effect(WorkspaceShare, makeHmac(keyring))
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Provides the workspace share authority from configuration: the secret from
|
|
274
|
+
* `SMITHERS_SYNC_SECRET` (read as `Redacted`, never logged) and the key name
|
|
275
|
+
* from `SMITHERS_SYNC_KEY_ID`, defaulting to `primary`. Rotation beyond one
|
|
276
|
+
* configured key uses {@link layerHmac} with an explicit keyring.
|
|
277
|
+
*
|
|
278
|
+
* There is deliberately no default secret: a deployment that configures
|
|
279
|
+
* neither name fails to construct the authority, and the read path stays
|
|
280
|
+
* closed.
|
|
281
|
+
*
|
|
282
|
+
* @category layers
|
|
283
|
+
* @since 0.1.0
|
|
284
|
+
*/
|
|
285
|
+
export const layerConfig: Layer.Layer<WorkspaceShare, SyncError | Config.ConfigError> = Layer.effect(
|
|
286
|
+
WorkspaceShare,
|
|
287
|
+
Effect.gen(function*() {
|
|
288
|
+
const secret = yield* Config.Redacted("SMITHERS_SYNC_SECRET")
|
|
289
|
+
const kid = yield* Config.String("SMITHERS_SYNC_KEY_ID").pipe(Config.withDefault("primary"))
|
|
290
|
+
return yield* makeHmac({ activeKid: kid, keys: [{ kid, secret }] })
|
|
291
|
+
})
|
|
292
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only journal synchronization services for flows.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @category errors
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
export * as SyncError from "./SyncError.ts"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @category protocol
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export * as SyncProtocol from "./SyncProtocol.ts"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @category protocol
|
|
21
|
+
* @since 0.1.0
|
|
22
|
+
*/
|
|
23
|
+
export * as BranchProtocol from "./BranchProtocol.ts"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @category projections
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
*/
|
|
29
|
+
export * as BranchProjection from "./BranchProjection.ts"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @category rpc
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
export * as SyncRpcs from "./SyncRpcs.ts"
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @category rpc
|
|
39
|
+
* @since 0.1.0
|
|
40
|
+
*/
|
|
41
|
+
export * as BranchRpcs from "./BranchRpcs.ts"
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @category services
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export * as RunCatalog from "./RunCatalog.ts"
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @category services
|
|
51
|
+
* @since 0.1.0
|
|
52
|
+
*/
|
|
53
|
+
export * as SyncServer from "./SyncServer.ts"
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @category services
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
*/
|
|
59
|
+
export * as SyncClient from "./SyncClient.ts"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @category services
|
|
63
|
+
* @since 0.1.0
|
|
64
|
+
*/
|
|
65
|
+
export * as BranchShare from "./BranchShare.ts"
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @category services
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
export * as WorkspaceShare from "./WorkspaceShare.ts"
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @category services
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export * as SyncPrincipal from "./SyncPrincipal.ts"
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @category rpc
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export * as SyncAuth from "./SyncAuth.ts"
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @category services
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
*/
|
|
89
|
+
export * as BranchPresence from "./BranchPresence.ts"
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @category services
|
|
93
|
+
* @since 0.1.0
|
|
94
|
+
*/
|
|
95
|
+
export * as BranchCommands from "./BranchCommands.ts"
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @category services
|
|
99
|
+
* @since 0.1.0
|
|
100
|
+
*/
|
|
101
|
+
export * as BranchServer from "./BranchServer.ts"
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @category services
|
|
105
|
+
* @since 0.1.0
|
|
106
|
+
*/
|
|
107
|
+
export * as BranchIds from "./BranchIds.ts"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Admission of host and transport values before any progress is committed.
|
|
2
|
+
* @since 1.0.0-rc.0
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as JournalEvent from "@smthrs/journal/JournalEvent"
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import { BranchId, branchOfRunId, CommandEvent, CommandEventPayload } from "../BranchProtocol.ts"
|
|
9
|
+
import { SyncError } from "../SyncError.ts"
|
|
10
|
+
import { causeCode } from "./CauseText.ts"
|
|
11
|
+
|
|
12
|
+
/** Decode a boundary value while retaining a safe cause classification.
|
|
13
|
+
* @category validation
|
|
14
|
+
* @since 1.0.0-rc.0
|
|
15
|
+
*/
|
|
16
|
+
export const decode = <S extends Schema.Top & { readonly DecodingServices: never }>(
|
|
17
|
+
schema: S,
|
|
18
|
+
value: unknown,
|
|
19
|
+
code: "decode_failed" | "invalid_request" = "decode_failed"
|
|
20
|
+
): Effect.Effect<S["Type"], SyncError> =>
|
|
21
|
+
Effect.try({
|
|
22
|
+
try: () => Schema.decodeUnknownSync(schema)(value),
|
|
23
|
+
catch: (cause) => new SyncError({ code, message: "Invalid sync envelope", cause: causeCode(cause) })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const decodeEntries = Schema.decodeUnknownSync(Schema.Array(JournalEvent.Entry))
|
|
27
|
+
const decodeJson = Schema.decodeUnknownSync(Schema.Struct({ payload: Schema.Json, meta: Schema.Json }))
|
|
28
|
+
const decodeCommand = Schema.decodeUnknownSync(CommandEventPayload)
|
|
29
|
+
const decodeBranch = Schema.decodeUnknownSync(Schema.Struct({ branchId: Schema.optional(BranchId) }))
|
|
30
|
+
|
|
31
|
+
/** One admitted entry beside the command payload admission decoded from it.
|
|
32
|
+
* `command` is `undefined` for every event type other than `CommandEvent`.
|
|
33
|
+
* @category models
|
|
34
|
+
* @since 1.0.0-rc.0
|
|
35
|
+
*/
|
|
36
|
+
export interface Admitted {
|
|
37
|
+
readonly entry: JournalEvent.Entry
|
|
38
|
+
readonly command: CommandEventPayload | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The one validation `records`, `withCommands`, and `entries` share. It keeps
|
|
42
|
+
* each command's decoded payload beside its entry rather than discarding it.
|
|
43
|
+
*/
|
|
44
|
+
const admit = (values: ReadonlyArray<JournalEvent.Entry>): Effect.Effect<ReadonlyArray<Admitted>, SyncError> =>
|
|
45
|
+
Effect.try({
|
|
46
|
+
// Capture getters once and detach nested payloads before any application
|
|
47
|
+
// callback can mutate the producer's next entry after admission.
|
|
48
|
+
try: () =>
|
|
49
|
+
decodeEntries(structuredClone(values)).map((entry): Admitted => {
|
|
50
|
+
decodeJson(entry)
|
|
51
|
+
if (entry.eventType !== CommandEvent) return { entry, command: undefined }
|
|
52
|
+
const command = decodeCommand(entry.payload)
|
|
53
|
+
const identity = decodeBranch(entry.payload)
|
|
54
|
+
if (identity.branchId !== undefined && identity.branchId !== branchOfRunId(entry.runId)) {
|
|
55
|
+
throw new SyncError({
|
|
56
|
+
code: "protocol_violation",
|
|
57
|
+
message: "Command belongs to another branch",
|
|
58
|
+
cause: "foreign_branch"
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return { entry, command }
|
|
62
|
+
}),
|
|
63
|
+
catch: (cause) =>
|
|
64
|
+
cause instanceof SyncError
|
|
65
|
+
? cause
|
|
66
|
+
: new SyncError({ code: "decode_failed", message: "Invalid sync record", cause: causeCode(cause) })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const entriesOf = (admitted: ReadonlyArray<Admitted>): ReadonlyArray<JournalEvent.Entry> =>
|
|
70
|
+
admitted.map(({ entry }) => entry)
|
|
71
|
+
|
|
72
|
+
/** Known branch records fail closed; unknown namespaces remain open JSON envelopes.
|
|
73
|
+
* Defaults on persisted command args/target keep their existing additive schema meaning.
|
|
74
|
+
* @category validation
|
|
75
|
+
* @since 1.0.0-rc.0
|
|
76
|
+
*/
|
|
77
|
+
export const records = (
|
|
78
|
+
values: ReadonlyArray<JournalEvent.Entry>
|
|
79
|
+
): Effect.Effect<ReadonlyArray<JournalEvent.Entry>, SyncError> => Effect.map(admit(values), entriesOf)
|
|
80
|
+
|
|
81
|
+
/** Admit an entire run batch before its caller publishes or updates a cursor,
|
|
82
|
+
* handing back each command beside the payload admission decoded, so a caller
|
|
83
|
+
* that needs a command field reads it instead of decoding the payload again.
|
|
84
|
+
* @category validation
|
|
85
|
+
* @since 1.0.0-rc.0
|
|
86
|
+
*/
|
|
87
|
+
export const withCommands = (
|
|
88
|
+
values: ReadonlyArray<JournalEvent.Entry>,
|
|
89
|
+
runId: JournalEvent.RunId,
|
|
90
|
+
after: number
|
|
91
|
+
): Effect.Effect<ReadonlyArray<Admitted>, SyncError> =>
|
|
92
|
+
Effect.gen(function*() {
|
|
93
|
+
const admitted = yield* admit(values)
|
|
94
|
+
let previous = after
|
|
95
|
+
for (const { entry } of admitted) {
|
|
96
|
+
if (entry.runId !== runId || entry.seq <= previous) {
|
|
97
|
+
return yield* Effect.fail(
|
|
98
|
+
new SyncError({
|
|
99
|
+
code: "protocol_violation",
|
|
100
|
+
message: "Journal batch must belong to the requested run and ascend strictly above its cursor",
|
|
101
|
+
cause: entry.runId !== runId ? "foreign_run" : "non_monotonic_sequence"
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
previous = entry.seq
|
|
106
|
+
}
|
|
107
|
+
return admitted
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** Admit an entire run batch before its caller publishes or updates a cursor.
|
|
111
|
+
* @category validation
|
|
112
|
+
* @since 1.0.0-rc.0
|
|
113
|
+
*/
|
|
114
|
+
export const entries = (
|
|
115
|
+
values: ReadonlyArray<JournalEvent.Entry>,
|
|
116
|
+
runId: JournalEvent.RunId,
|
|
117
|
+
after: number
|
|
118
|
+
): Effect.Effect<ReadonlyArray<JournalEvent.Entry>, SyncError> =>
|
|
119
|
+
Effect.map(withCommands(values, runId, after), entriesOf)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a host failure crosses the sync boundary: what a follower is told about
|
|
3
|
+
* it, and what it is not.
|
|
4
|
+
*
|
|
5
|
+
* `SyncError.cause` crosses the wire to a follower that may hold nothing but a
|
|
6
|
+
* branch share link, so it carries a short string rather than the host object
|
|
7
|
+
* that failed. An arbitrary `unknown` had three problems at once: it counted
|
|
8
|
+
* against no ceiling, a class instance or a cyclic value has no defined wire
|
|
9
|
+
* form, and whatever the host threw — a driver message with SQL text, a
|
|
10
|
+
* rejected credential — went out verbatim.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.0.0-rc.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Longest cause rendering that crosses the wire. Long enough to name a failure
|
|
17
|
+
* and short enough that an error can never be the largest thing a follower
|
|
18
|
+
* receives.
|
|
19
|
+
*/
|
|
20
|
+
const maxCauseTextLength = 512
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Clamps one rendering to {@link maxCauseTextLength}.
|
|
24
|
+
*
|
|
25
|
+
* Every string this module produces leaves through here, and so does every
|
|
26
|
+
* other string a `SyncError` carries out of a host failure: `message` is
|
|
27
|
+
* `Schema.String` and is as unbounded as `cause` was.
|
|
28
|
+
*
|
|
29
|
+
* @category encoding
|
|
30
|
+
* @since 1.0.0-rc.0
|
|
31
|
+
*/
|
|
32
|
+
export const boundedText = (text: string): string =>
|
|
33
|
+
text.length <= maxCauseTextLength ? text : `${text.slice(0, maxCauseTextLength)} (truncated)`
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders one host failure as a bounded string.
|
|
37
|
+
*
|
|
38
|
+
* An `Error` renders as `name: message`; anything else renders as its type
|
|
39
|
+
* alone, because a non-`Error` value is arbitrary host data and its contents
|
|
40
|
+
* are not this boundary's to publish. Nothing is retained beyond the returned
|
|
41
|
+
* string, so an oversized input does not keep its subject alive.
|
|
42
|
+
*
|
|
43
|
+
* @category encoding
|
|
44
|
+
* @since 1.0.0-rc.0
|
|
45
|
+
*/
|
|
46
|
+
export const causeText = (cause: unknown): string =>
|
|
47
|
+
boundedText(
|
|
48
|
+
cause instanceof Error ? `${cause.name}: ${cause.message}` : Object.prototype.toString.call(cause)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Renders one failure as its TYPE alone, with no message.
|
|
53
|
+
*
|
|
54
|
+
* The journal's message is the SQLite driver's, and it routinely carries SQL
|
|
55
|
+
* text, table and column names, and constraint identifiers. A follower may
|
|
56
|
+
* hold nothing but a branch share link, so what it learns about a storage
|
|
57
|
+
* fault is the stable code the journal enumerates, never the sentence the
|
|
58
|
+
* driver wrote. `decodeCapability` already refuses to be a parsing oracle;
|
|
59
|
+
* this is the same rule for the read and write paths.
|
|
60
|
+
*
|
|
61
|
+
* The result is bounded like {@link causeText}: a `Journal` is a host seam, so
|
|
62
|
+
* the `name` and `code` this reads are as much the host's to choose as the
|
|
63
|
+
* message it refuses to publish.
|
|
64
|
+
*
|
|
65
|
+
* @category encoding
|
|
66
|
+
* @since 1.0.0-rc.0
|
|
67
|
+
*/
|
|
68
|
+
export const causeCode = (cause: unknown): string => {
|
|
69
|
+
if (cause instanceof Error) {
|
|
70
|
+
const code = (cause as { readonly code?: unknown }).code
|
|
71
|
+
return boundedText(typeof code === "string" ? `${cause.name}(${code})` : cause.name)
|
|
72
|
+
}
|
|
73
|
+
return boundedText(Object.prototype.toString.call(cause))
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a journal failure's stable code maps onto the sync boundary's error
|
|
3
|
+
* vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* @since 1.0.0-rc.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The journal codes this boundary can state in its own vocabulary.
|
|
10
|
+
*
|
|
11
|
+
* Only codes whose meaning is identical on both sides are here. A journal that
|
|
12
|
+
* is shut down and a sync boundary that is closed are the same fact; an
|
|
13
|
+
* admission queue that overflowed and backpressure are the same fact; a
|
|
14
|
+
* payload the journal could not decode and one this boundary could not decode
|
|
15
|
+
* are the same fact. Everything else — a fence lost, a sequence conflict, a
|
|
16
|
+
* projection fault — is a storage-layer distinction a follower cannot act on,
|
|
17
|
+
* and inventing a sync code for it would say more than is known.
|
|
18
|
+
*/
|
|
19
|
+
const journalCodes: Readonly<Record<string, "backpressure" | "closed" | "decode_failed">> = {
|
|
20
|
+
decode_failed: "decode_failed",
|
|
21
|
+
journal_closed: "closed",
|
|
22
|
+
queue_overflow: "backpressure"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Projects one journal failure's stable code onto this boundary's vocabulary,
|
|
27
|
+
* or `unknown` when it has no counterpart.
|
|
28
|
+
*
|
|
29
|
+
* `SyncError.ErrorCode` already declares `closed`, `backpressure`, and
|
|
30
|
+
* `decode_failed`, so collapsing every journal failure but `compacted` into
|
|
31
|
+
* `unknown` threw away a classification the wire could already carry, and left
|
|
32
|
+
* a follower unable to tell a shut-down journal from an unexplained fault.
|
|
33
|
+
* Nothing here widens what a follower learns: `causeCode` already publishes
|
|
34
|
+
* the journal's own enumerated code, and the journal's MESSAGE stays refused
|
|
35
|
+
* on every path.
|
|
36
|
+
*
|
|
37
|
+
* @category encoding
|
|
38
|
+
* @since 1.0.0-rc.0
|
|
39
|
+
*/
|
|
40
|
+
export const journalErrorCode = (
|
|
41
|
+
cause: unknown
|
|
42
|
+
): "backpressure" | "closed" | "decode_failed" | "unknown" => {
|
|
43
|
+
if (!(cause instanceof Error)) return "unknown"
|
|
44
|
+
const code = (cause as { readonly code?: unknown }).code
|
|
45
|
+
return typeof code === "string" ? journalCodes[code] ?? "unknown" : "unknown"
|
|
46
|
+
}
|