effect 4.0.0-beta.44 → 4.0.0-beta.45
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/dist/Equal.d.ts.map +1 -1
- package/dist/Equal.js +16 -0
- package/dist/Equal.js.map +1 -1
- package/dist/Hash.js +1 -1
- package/dist/Hash.js.map +1 -1
- package/dist/Semaphore.d.ts +1 -1
- package/dist/Semaphore.d.ts.map +1 -1
- package/dist/Semaphore.js +1 -3
- package/dist/Semaphore.js.map +1 -1
- package/dist/unstable/ai/McpServer.d.ts.map +1 -1
- package/dist/unstable/ai/McpServer.js +24 -21
- package/dist/unstable/ai/McpServer.js.map +1 -1
- package/dist/unstable/eventlog/Event.d.ts +0 -6
- package/dist/unstable/eventlog/Event.d.ts.map +1 -1
- package/dist/unstable/eventlog/Event.js +0 -5
- package/dist/unstable/eventlog/Event.js.map +1 -1
- package/dist/unstable/eventlog/EventGroup.d.ts +0 -2
- package/dist/unstable/eventlog/EventGroup.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventGroup.js +0 -2
- package/dist/unstable/eventlog/EventGroup.js.map +1 -1
- package/dist/unstable/eventlog/EventJournal.d.ts +22 -5
- package/dist/unstable/eventlog/EventJournal.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventJournal.js +126 -67
- package/dist/unstable/eventlog/EventJournal.js.map +1 -1
- package/dist/unstable/eventlog/EventLog.d.ts +88 -34
- package/dist/unstable/eventlog/EventLog.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventLog.js +215 -141
- package/dist/unstable/eventlog/EventLog.js.map +1 -1
- package/dist/unstable/eventlog/EventLogEncryption.d.ts +9 -7
- package/dist/unstable/eventlog/EventLogEncryption.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventLogEncryption.js +13 -15
- package/dist/unstable/eventlog/EventLogEncryption.js.map +1 -1
- package/dist/unstable/eventlog/EventLogMessage.d.ts +228 -0
- package/dist/unstable/eventlog/EventLogMessage.d.ts.map +1 -0
- package/dist/unstable/eventlog/EventLogMessage.js +214 -0
- package/dist/unstable/eventlog/EventLogMessage.js.map +1 -0
- package/dist/unstable/eventlog/EventLogRemote.d.ts +109 -194
- package/dist/unstable/eventlog/EventLogRemote.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventLogRemote.js +165 -320
- package/dist/unstable/eventlog/EventLogRemote.js.map +1 -1
- package/dist/unstable/eventlog/EventLogServer.d.ts +25 -47
- package/dist/unstable/eventlog/EventLogServer.d.ts.map +1 -1
- package/dist/unstable/eventlog/EventLogServer.js +127 -198
- package/dist/unstable/eventlog/EventLogServer.js.map +1 -1
- package/dist/unstable/eventlog/EventLogServerEncrypted.d.ts +60 -0
- package/dist/unstable/eventlog/EventLogServerEncrypted.d.ts.map +1 -0
- package/dist/unstable/eventlog/EventLogServerEncrypted.js +166 -0
- package/dist/unstable/eventlog/EventLogServerEncrypted.js.map +1 -0
- package/dist/unstable/eventlog/EventLogServerUnencrypted.d.ts +183 -0
- package/dist/unstable/eventlog/EventLogServerUnencrypted.d.ts.map +1 -0
- package/dist/unstable/eventlog/EventLogServerUnencrypted.js +461 -0
- package/dist/unstable/eventlog/EventLogServerUnencrypted.js.map +1 -0
- package/dist/unstable/eventlog/EventLogSessionAuth.d.ts +117 -0
- package/dist/unstable/eventlog/EventLogSessionAuth.d.ts.map +1 -0
- package/dist/unstable/eventlog/EventLogSessionAuth.js +284 -0
- package/dist/unstable/eventlog/EventLogSessionAuth.js.map +1 -0
- package/dist/unstable/eventlog/{SqlEventLogJournal.d.ts → SqlEventJournal.d.ts} +2 -2
- package/dist/unstable/eventlog/SqlEventJournal.d.ts.map +1 -0
- package/dist/unstable/eventlog/{SqlEventLogJournal.js → SqlEventJournal.js} +20 -14
- package/dist/unstable/eventlog/SqlEventJournal.js.map +1 -0
- package/dist/unstable/eventlog/{SqlEventLogServer.d.ts → SqlEventLogServerEncrypted.d.ts} +5 -5
- package/dist/unstable/eventlog/SqlEventLogServerEncrypted.d.ts.map +1 -0
- package/dist/unstable/eventlog/{SqlEventLogServer.js → SqlEventLogServerEncrypted.js} +65 -24
- package/dist/unstable/eventlog/SqlEventLogServerEncrypted.js.map +1 -0
- package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.d.ts +25 -0
- package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.d.ts.map +1 -0
- package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.js +354 -0
- package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.js.map +1 -0
- package/dist/unstable/eventlog/index.d.ts +22 -2
- package/dist/unstable/eventlog/index.d.ts.map +1 -1
- package/dist/unstable/eventlog/index.js +22 -2
- package/dist/unstable/eventlog/index.js.map +1 -1
- package/dist/unstable/eventlog/internal/identityRootSecretDerivation.d.ts +2 -0
- package/dist/unstable/eventlog/internal/identityRootSecretDerivation.d.ts.map +1 -0
- package/dist/unstable/eventlog/internal/identityRootSecretDerivation.js +89 -0
- package/dist/unstable/eventlog/internal/identityRootSecretDerivation.js.map +1 -0
- package/dist/unstable/reactivity/AtomHttpApi.d.ts +1 -2
- package/dist/unstable/reactivity/AtomHttpApi.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomHttpApi.js +2 -2
- package/dist/unstable/reactivity/AtomHttpApi.js.map +1 -1
- package/dist/unstable/reactivity/AtomRpc.d.ts +1 -2
- package/dist/unstable/reactivity/AtomRpc.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomRpc.js +3 -3
- package/dist/unstable/reactivity/AtomRpc.js.map +1 -1
- package/dist/unstable/rpc/Rpc.d.ts +25 -4
- package/dist/unstable/rpc/Rpc.d.ts.map +1 -1
- package/dist/unstable/rpc/Rpc.js +26 -0
- package/dist/unstable/rpc/Rpc.js.map +1 -1
- package/dist/unstable/rpc/RpcClient.d.ts +3 -13
- package/dist/unstable/rpc/RpcClient.d.ts.map +1 -1
- package/dist/unstable/rpc/RpcClient.js +47 -23
- package/dist/unstable/rpc/RpcClient.js.map +1 -1
- package/dist/unstable/rpc/RpcGroup.d.ts +1 -1
- package/dist/unstable/rpc/RpcGroup.d.ts.map +1 -1
- package/dist/unstable/rpc/RpcMiddleware.d.ts +2 -2
- package/dist/unstable/rpc/RpcMiddleware.d.ts.map +1 -1
- package/dist/unstable/rpc/RpcServer.d.ts.map +1 -1
- package/dist/unstable/rpc/RpcServer.js +3 -2
- package/dist/unstable/rpc/RpcServer.js.map +1 -1
- package/dist/unstable/rpc/Utils.d.ts +6 -0
- package/dist/unstable/rpc/Utils.d.ts.map +1 -1
- package/dist/unstable/rpc/Utils.js +44 -0
- package/dist/unstable/rpc/Utils.js.map +1 -1
- package/dist/unstable/schema/Model.d.ts +2 -2
- package/dist/unstable/schema/Model.d.ts.map +1 -1
- package/dist/unstable/schema/Model.js +2 -4
- package/dist/unstable/schema/Model.js.map +1 -1
- package/dist/unstable/schema/VariantSchema.d.ts +1 -1
- package/dist/unstable/schema/VariantSchema.d.ts.map +1 -1
- package/dist/unstable/schema/VariantSchema.js +1 -12
- package/dist/unstable/schema/VariantSchema.js.map +1 -1
- package/dist/unstable/workers/Transferable.d.ts +1 -1
- package/dist/unstable/workers/Transferable.d.ts.map +1 -1
- package/dist/unstable/workers/Transferable.js +1 -1
- package/dist/unstable/workers/Transferable.js.map +1 -1
- package/package.json +1 -1
- package/src/Equal.ts +17 -0
- package/src/Hash.ts +2 -2
- package/src/Semaphore.ts +2 -4
- package/src/unstable/ai/McpServer.ts +24 -22
- package/src/unstable/eventlog/Event.ts +0 -8
- package/src/unstable/eventlog/EventGroup.ts +0 -4
- package/src/unstable/eventlog/EventJournal.ts +144 -76
- package/src/unstable/eventlog/EventLog.ts +342 -221
- package/src/unstable/eventlog/EventLogEncryption.ts +16 -30
- package/src/unstable/eventlog/EventLogMessage.ts +277 -0
- package/src/unstable/eventlog/EventLogRemote.ts +261 -408
- package/src/unstable/eventlog/EventLogServer.ts +182 -274
- package/src/unstable/eventlog/EventLogServerEncrypted.ts +206 -0
- package/src/unstable/eventlog/EventLogServerUnencrypted.ts +749 -0
- package/src/unstable/eventlog/EventLogSessionAuth.ts +437 -0
- package/src/unstable/eventlog/{SqlEventLogJournal.ts → SqlEventJournal.ts} +26 -18
- package/src/unstable/eventlog/{SqlEventLogServer.ts → SqlEventLogServerEncrypted.ts} +102 -40
- package/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts +500 -0
- package/src/unstable/eventlog/index.ts +27 -2
- package/src/unstable/eventlog/internal/identityRootSecretDerivation.ts +153 -0
- package/src/unstable/reactivity/AtomHttpApi.ts +23 -8
- package/src/unstable/reactivity/AtomRpc.ts +16 -5
- package/src/unstable/rpc/Rpc.ts +42 -4
- package/src/unstable/rpc/RpcClient.ts +59 -24
- package/src/unstable/rpc/RpcGroup.ts +1 -1
- package/src/unstable/rpc/RpcMiddleware.ts +2 -2
- package/src/unstable/rpc/RpcServer.ts +5 -3
- package/src/unstable/rpc/Utils.ts +59 -0
- package/src/unstable/schema/Model.ts +4 -6
- package/src/unstable/schema/VariantSchema.ts +4 -17
- package/src/unstable/workers/Transferable.ts +9 -11
- package/dist/unstable/eventlog/SqlEventLogJournal.d.ts.map +0 -1
- package/dist/unstable/eventlog/SqlEventLogJournal.js.map +0 -1
- package/dist/unstable/eventlog/SqlEventLogServer.d.ts.map +0 -1
- package/dist/unstable/eventlog/SqlEventLogServer.js.map +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 4.0.0
|
|
3
|
+
*/
|
|
4
|
+
import * as Data from "../../Data.ts"
|
|
5
|
+
import * as Effect from "../../Effect.ts"
|
|
6
|
+
|
|
7
|
+
const textEncoder = new TextEncoder()
|
|
8
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true })
|
|
9
|
+
|
|
10
|
+
const constLengthPrefixBytes = 4
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @since 4.0.0
|
|
14
|
+
* @category constants
|
|
15
|
+
*/
|
|
16
|
+
export const AuthPayloadContext = "eventlog-auth-v1"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @since 4.0.0
|
|
20
|
+
* @category constants
|
|
21
|
+
*/
|
|
22
|
+
export const Ed25519PublicKeyLength = 32
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @since 4.0.0
|
|
26
|
+
* @category constants
|
|
27
|
+
*/
|
|
28
|
+
export const Ed25519SignatureLength = 64
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @since 4.0.0
|
|
32
|
+
* @category constants
|
|
33
|
+
*/
|
|
34
|
+
export const SessionAuthChallengeLength = 32
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @since 4.0.0
|
|
38
|
+
* @category constants
|
|
39
|
+
*/
|
|
40
|
+
export const SessionAuthChallengeTimeToLiveMillis = 30_000
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @since 4.0.0
|
|
44
|
+
* @category model
|
|
45
|
+
*/
|
|
46
|
+
export interface SessionAuthPayload {
|
|
47
|
+
readonly remoteId: string | Uint8Array
|
|
48
|
+
readonly challenge: Uint8Array
|
|
49
|
+
readonly publicKey: string
|
|
50
|
+
readonly signingPublicKey: Uint8Array
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @since 4.0.0
|
|
55
|
+
* @category errors
|
|
56
|
+
*/
|
|
57
|
+
export class EventLogSessionAuthError extends Data.TaggedError("EventLogSessionAuthError")<{
|
|
58
|
+
readonly reason:
|
|
59
|
+
| "InvalidPayload"
|
|
60
|
+
| "InvalidContext"
|
|
61
|
+
| "InvalidAlgorithm"
|
|
62
|
+
| "InvalidSigningPublicKeyLength"
|
|
63
|
+
| "InvalidSignatureLength"
|
|
64
|
+
| "InvalidSigningPrivateKey"
|
|
65
|
+
| "CryptoUnavailable"
|
|
66
|
+
| "CryptoFailure"
|
|
67
|
+
readonly message: string
|
|
68
|
+
readonly cause?: unknown
|
|
69
|
+
}> {}
|
|
70
|
+
|
|
71
|
+
const toArrayBuffer = (data: Uint8Array): ArrayBuffer => {
|
|
72
|
+
const copy = new Uint8Array(data.byteLength)
|
|
73
|
+
copy.set(data)
|
|
74
|
+
return copy.buffer
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const decodeUtf8 = (bytes: Uint8Array) =>
|
|
78
|
+
Effect.try({
|
|
79
|
+
try: () => textDecoder.decode(bytes),
|
|
80
|
+
catch: (cause) =>
|
|
81
|
+
new EventLogSessionAuthError({
|
|
82
|
+
reason: "InvalidPayload",
|
|
83
|
+
message: "Session auth payload contains invalid UTF-8 bytes",
|
|
84
|
+
cause
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const assertSigningPublicKeyLength = (signingPublicKey: Uint8Array): Effect.Effect<void, EventLogSessionAuthError> => {
|
|
89
|
+
if (signingPublicKey.byteLength === Ed25519PublicKeyLength) return Effect.void
|
|
90
|
+
return Effect.fail(
|
|
91
|
+
new EventLogSessionAuthError({
|
|
92
|
+
reason: "InvalidSigningPublicKeyLength",
|
|
93
|
+
message:
|
|
94
|
+
`Expected signingPublicKey length to be ${Ed25519PublicKeyLength} bytes, received ${signingPublicKey.byteLength}`
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const assertSignatureLength = (signature: Uint8Array): Effect.Effect<void, EventLogSessionAuthError> => {
|
|
100
|
+
if (signature.byteLength === Ed25519SignatureLength) return Effect.void
|
|
101
|
+
return Effect.fail(
|
|
102
|
+
new EventLogSessionAuthError({
|
|
103
|
+
reason: "InvalidSignatureLength",
|
|
104
|
+
message: `Expected signature length to be ${Ed25519SignatureLength} bytes, received ${signature.byteLength}`
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const getSubtle = Effect.suspend(() => {
|
|
110
|
+
const subtle = globalThis.crypto?.subtle
|
|
111
|
+
if (subtle === undefined) {
|
|
112
|
+
return Effect.fail(
|
|
113
|
+
new EventLogSessionAuthError({
|
|
114
|
+
reason: "CryptoUnavailable",
|
|
115
|
+
message: "globalThis.crypto.subtle is not available"
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return Effect.succeed(subtle)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const getCrypto = Effect.suspend(() => {
|
|
123
|
+
const crypto = globalThis.crypto
|
|
124
|
+
if (crypto === undefined) {
|
|
125
|
+
return Effect.fail(
|
|
126
|
+
new EventLogSessionAuthError({
|
|
127
|
+
reason: "CryptoUnavailable",
|
|
128
|
+
message: "globalThis.crypto is not available"
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return Effect.succeed(crypto)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const writeLength = (
|
|
136
|
+
target: Uint8Array,
|
|
137
|
+
offset: number,
|
|
138
|
+
length: number
|
|
139
|
+
): Effect.Effect<number, EventLogSessionAuthError> => {
|
|
140
|
+
if (length < 0 || length > 0xffff_ffff) {
|
|
141
|
+
return Effect.fail(
|
|
142
|
+
new EventLogSessionAuthError({
|
|
143
|
+
reason: "InvalidPayload",
|
|
144
|
+
message: `Invalid canonical field length: ${length}`
|
|
145
|
+
})
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
target[offset] = (length >>> 24) & 0xff
|
|
150
|
+
target[offset + 1] = (length >>> 16) & 0xff
|
|
151
|
+
target[offset + 2] = (length >>> 8) & 0xff
|
|
152
|
+
target[offset + 3] = length & 0xff
|
|
153
|
+
|
|
154
|
+
return Effect.succeed(offset + constLengthPrefixBytes)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const readLength = (source: Uint8Array, offset: number): number =>
|
|
158
|
+
(
|
|
159
|
+
(source[offset]! << 24) |
|
|
160
|
+
(source[offset + 1]! << 16) |
|
|
161
|
+
(source[offset + 2]! << 8) |
|
|
162
|
+
source[offset + 3]!
|
|
163
|
+
) >>> 0
|
|
164
|
+
|
|
165
|
+
const readField = (
|
|
166
|
+
payload: Uint8Array,
|
|
167
|
+
state: { offset: number }
|
|
168
|
+
): Effect.Effect<Uint8Array, EventLogSessionAuthError> => {
|
|
169
|
+
if (state.offset + constLengthPrefixBytes > payload.byteLength) {
|
|
170
|
+
return Effect.fail(
|
|
171
|
+
new EventLogSessionAuthError({
|
|
172
|
+
reason: "InvalidPayload",
|
|
173
|
+
message: "Session auth payload is truncated before field length"
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const length = readLength(payload, state.offset)
|
|
179
|
+
state.offset += constLengthPrefixBytes
|
|
180
|
+
|
|
181
|
+
if (state.offset + length > payload.byteLength) {
|
|
182
|
+
return Effect.fail(
|
|
183
|
+
new EventLogSessionAuthError({
|
|
184
|
+
reason: "InvalidPayload",
|
|
185
|
+
message: "Session auth payload is truncated inside a field"
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const field = payload.slice(state.offset, state.offset + length)
|
|
191
|
+
state.offset += length
|
|
192
|
+
return Effect.succeed(field)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const bytesToHex = (bytes: Uint8Array): string => {
|
|
196
|
+
let hex = ""
|
|
197
|
+
for (const byte of bytes) {
|
|
198
|
+
hex += byte.toString(16).padStart(2, "0")
|
|
199
|
+
}
|
|
200
|
+
return hex
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const encodeRemoteIdField = (remoteId: string | Uint8Array): Uint8Array =>
|
|
204
|
+
typeof remoteId === "string"
|
|
205
|
+
? textEncoder.encode(remoteId)
|
|
206
|
+
: textEncoder.encode(bytesToHex(remoteId))
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Canonical payload format uses ordered big-endian length-prefixed fields:
|
|
210
|
+
*
|
|
211
|
+
* 1. context (fixed: eventlog-auth-v1)
|
|
212
|
+
* 2. remoteId
|
|
213
|
+
* 3. challenge bytes
|
|
214
|
+
* 4. publicKey
|
|
215
|
+
* 5. signingPublicKey bytes
|
|
216
|
+
*
|
|
217
|
+
* @since 4.0.0
|
|
218
|
+
* @category encoding
|
|
219
|
+
*/
|
|
220
|
+
export const encodeSessionAuthPayload = Effect.fnUntraced(function*(payload: SessionAuthPayload) {
|
|
221
|
+
yield* assertSigningPublicKeyLength(payload.signingPublicKey)
|
|
222
|
+
|
|
223
|
+
const fields = [
|
|
224
|
+
textEncoder.encode(AuthPayloadContext),
|
|
225
|
+
encodeRemoteIdField(payload.remoteId),
|
|
226
|
+
payload.challenge,
|
|
227
|
+
textEncoder.encode(payload.publicKey),
|
|
228
|
+
payload.signingPublicKey
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
const totalLength = fields.reduce(
|
|
232
|
+
(total, field) => total + constLengthPrefixBytes + field.byteLength,
|
|
233
|
+
0
|
|
234
|
+
)
|
|
235
|
+
const encoded = new Uint8Array(totalLength)
|
|
236
|
+
|
|
237
|
+
let offset = 0
|
|
238
|
+
for (const field of fields) {
|
|
239
|
+
offset = yield* writeLength(encoded, offset, field.byteLength)
|
|
240
|
+
encoded.set(field, offset)
|
|
241
|
+
offset += field.byteLength
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return encoded
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @since 4.0.0
|
|
249
|
+
* @category encoding
|
|
250
|
+
*/
|
|
251
|
+
export const decodeSessionAuthPayload = Effect.fnUntraced(
|
|
252
|
+
function*(payload: Uint8Array): Effect.fn.Return<SessionAuthPayload, EventLogSessionAuthError> {
|
|
253
|
+
const state = { offset: 0 }
|
|
254
|
+
const context = yield* decodeUtf8(yield* readField(payload, state))
|
|
255
|
+
|
|
256
|
+
if (context !== AuthPayloadContext) {
|
|
257
|
+
return yield* new EventLogSessionAuthError({
|
|
258
|
+
reason: "InvalidContext",
|
|
259
|
+
message: `Invalid session auth payload context: ${context}`
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const remoteId = yield* decodeUtf8(yield* readField(payload, state))
|
|
264
|
+
const challenge = yield* readField(payload, state)
|
|
265
|
+
const publicKey = yield* decodeUtf8(yield* readField(payload, state))
|
|
266
|
+
const signingPublicKey = yield* readField(payload, state)
|
|
267
|
+
yield* assertSigningPublicKeyLength(signingPublicKey)
|
|
268
|
+
|
|
269
|
+
if (state.offset !== payload.byteLength) {
|
|
270
|
+
return yield* new EventLogSessionAuthError({
|
|
271
|
+
reason: "InvalidPayload",
|
|
272
|
+
message: "Session auth payload contains trailing bytes"
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
remoteId,
|
|
278
|
+
challenge,
|
|
279
|
+
publicKey,
|
|
280
|
+
signingPublicKey
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @since 4.0.0
|
|
287
|
+
* @category signing
|
|
288
|
+
*/
|
|
289
|
+
export const signSessionAuthPayloadBytes = Effect.fnUntraced(function*(options: {
|
|
290
|
+
readonly payload: Uint8Array
|
|
291
|
+
readonly signingPrivateKey: Uint8Array
|
|
292
|
+
}): Effect.fn.Return<Uint8Array<ArrayBuffer>, EventLogSessionAuthError> {
|
|
293
|
+
yield* decodeSessionAuthPayload(options.payload)
|
|
294
|
+
|
|
295
|
+
const subtle = yield* getSubtle
|
|
296
|
+
let privateKey = yield* Effect.tryPromise({
|
|
297
|
+
try: () =>
|
|
298
|
+
subtle.importKey(
|
|
299
|
+
"pkcs8",
|
|
300
|
+
toArrayBuffer(options.signingPrivateKey),
|
|
301
|
+
"Ed25519",
|
|
302
|
+
false,
|
|
303
|
+
["sign"]
|
|
304
|
+
),
|
|
305
|
+
catch: (cause) =>
|
|
306
|
+
new EventLogSessionAuthError({
|
|
307
|
+
reason: "InvalidSigningPrivateKey",
|
|
308
|
+
message: "Failed to import Ed25519 signing private key (expected PKCS#8 bytes)",
|
|
309
|
+
cause
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const signature = yield* Effect.tryPromise({
|
|
314
|
+
try: () => subtle.sign("Ed25519", privateKey, toArrayBuffer(options.payload)),
|
|
315
|
+
catch: (cause) =>
|
|
316
|
+
new EventLogSessionAuthError({
|
|
317
|
+
reason: "CryptoFailure",
|
|
318
|
+
message: "Failed to sign canonical session auth payload",
|
|
319
|
+
cause
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
return new Uint8Array(signature)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @since 4.0.0
|
|
327
|
+
* @category verification
|
|
328
|
+
*/
|
|
329
|
+
export const verifySessionAuthPayloadBytes = Effect.fnUntraced(function*(options: {
|
|
330
|
+
readonly payload: Uint8Array
|
|
331
|
+
readonly signingPublicKey: Uint8Array
|
|
332
|
+
readonly signature: Uint8Array
|
|
333
|
+
}) {
|
|
334
|
+
yield* decodeSessionAuthPayload(options.payload)
|
|
335
|
+
yield* assertSigningPublicKeyLength(options.signingPublicKey)
|
|
336
|
+
yield* assertSignatureLength(options.signature)
|
|
337
|
+
|
|
338
|
+
const subtle = yield* getSubtle
|
|
339
|
+
const publicKey = yield* Effect.tryPromise({
|
|
340
|
+
try: () => subtle.importKey("raw", toArrayBuffer(options.signingPublicKey), "Ed25519", false, ["verify"]),
|
|
341
|
+
catch: (cause) =>
|
|
342
|
+
new EventLogSessionAuthError({
|
|
343
|
+
reason: "InvalidSigningPublicKeyLength",
|
|
344
|
+
message: "Failed to import Ed25519 signing public key",
|
|
345
|
+
cause
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
return yield* Effect.tryPromise({
|
|
350
|
+
try: () => subtle.verify("Ed25519", publicKey, toArrayBuffer(options.signature), toArrayBuffer(options.payload)),
|
|
351
|
+
catch: (cause) =>
|
|
352
|
+
new EventLogSessionAuthError({
|
|
353
|
+
reason: "CryptoFailure",
|
|
354
|
+
message: "Failed to verify canonical session auth payload signature",
|
|
355
|
+
cause
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @since 4.0.0
|
|
362
|
+
* @category signing
|
|
363
|
+
*/
|
|
364
|
+
export const signSessionAuthPayload = (
|
|
365
|
+
options: SessionAuthPayload & {
|
|
366
|
+
readonly signingPrivateKey: Uint8Array
|
|
367
|
+
}
|
|
368
|
+
) =>
|
|
369
|
+
encodeSessionAuthPayload(options).pipe(
|
|
370
|
+
Effect.flatMap((payload) =>
|
|
371
|
+
signSessionAuthPayloadBytes({
|
|
372
|
+
payload,
|
|
373
|
+
signingPrivateKey: options.signingPrivateKey
|
|
374
|
+
})
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @since 4.0.0
|
|
380
|
+
* @category verification
|
|
381
|
+
*/
|
|
382
|
+
export const verifySessionAuthPayload = (
|
|
383
|
+
options: SessionAuthPayload & {
|
|
384
|
+
readonly signature: Uint8Array
|
|
385
|
+
}
|
|
386
|
+
) =>
|
|
387
|
+
encodeSessionAuthPayload(options).pipe(
|
|
388
|
+
Effect.flatMap((payload) =>
|
|
389
|
+
verifySessionAuthPayloadBytes({
|
|
390
|
+
payload,
|
|
391
|
+
signingPublicKey: options.signingPublicKey,
|
|
392
|
+
signature: options.signature
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @since 4.0.0
|
|
399
|
+
* @category challenge
|
|
400
|
+
*/
|
|
401
|
+
export const makeSessionAuthChallenge: Effect.Effect<
|
|
402
|
+
Uint8Array<ArrayBuffer>,
|
|
403
|
+
EventLogSessionAuthError
|
|
404
|
+
> = Effect.gen(function*() {
|
|
405
|
+
const crypto = yield* getCrypto
|
|
406
|
+
const challenge = new Uint8Array(SessionAuthChallengeLength)
|
|
407
|
+
crypto.getRandomValues(challenge)
|
|
408
|
+
return challenge
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @since 4.0.0
|
|
413
|
+
* @category verification
|
|
414
|
+
*/
|
|
415
|
+
export const verifySessionAuthenticateRequest = Effect.fnUntraced(function*(options: {
|
|
416
|
+
readonly remoteId: string | Uint8Array
|
|
417
|
+
readonly challenge: Uint8Array
|
|
418
|
+
readonly publicKey: string
|
|
419
|
+
readonly signingPublicKey: Uint8Array
|
|
420
|
+
readonly signature: Uint8Array
|
|
421
|
+
readonly algorithm: string
|
|
422
|
+
}) {
|
|
423
|
+
if (options.algorithm !== "Ed25519") {
|
|
424
|
+
return yield* new EventLogSessionAuthError({
|
|
425
|
+
reason: "InvalidAlgorithm",
|
|
426
|
+
message: `Unsupported session auth algorithm: ${options.algorithm}`
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return yield* verifySessionAuthPayload({
|
|
431
|
+
remoteId: options.remoteId,
|
|
432
|
+
challenge: options.challenge,
|
|
433
|
+
publicKey: options.publicKey,
|
|
434
|
+
signingPublicKey: options.signingPublicKey,
|
|
435
|
+
signature: options.signature
|
|
436
|
+
})
|
|
437
|
+
})
|
|
@@ -7,14 +7,12 @@ import * as Layer from "../../Layer.ts"
|
|
|
7
7
|
import * as PubSub from "../../PubSub.ts"
|
|
8
8
|
import * as Schema from "../../Schema.ts"
|
|
9
9
|
import * as SqlClient from "../sql/SqlClient.ts"
|
|
10
|
-
import
|
|
10
|
+
import * as SqlError from "../sql/SqlError.ts"
|
|
11
11
|
import * as SqlSchema from "../sql/SqlSchema.ts"
|
|
12
12
|
import * as EventJournal from "./EventJournal.ts"
|
|
13
13
|
|
|
14
14
|
type WriteFromRemoteOptions = Parameters<EventJournal.EventJournal["Service"]["writeFromRemote"]>[0]
|
|
15
15
|
|
|
16
|
-
type RemoteBracket = readonly [ReadonlyArray<EventJournal.Entry>, ReadonlyArray<EventJournal.RemoteEntry>]
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* @since 4.0.0
|
|
20
18
|
* @category constructors
|
|
@@ -129,7 +127,9 @@ export const make = (options?: {
|
|
|
129
127
|
const pubsub = yield* PubSub.unbounded<EventJournal.Entry>()
|
|
130
128
|
|
|
131
129
|
const writeFromRemote = Effect.fnUntraced(function*(options: WriteFromRemoteOptions): Effect.fn.Return<
|
|
132
|
-
|
|
130
|
+
{
|
|
131
|
+
readonly duplicateEntries: ReadonlyArray<EventJournal.Entry>
|
|
132
|
+
},
|
|
133
133
|
EventJournal.EventJournalError | Schema.SchemaError | SqlError.SqlError
|
|
134
134
|
> {
|
|
135
135
|
const entries = options.entries.map((remoteEntry) => remoteEntry.entry)
|
|
@@ -164,13 +164,15 @@ export const make = (options?: {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const uncommitted = options.entries.filter((entry) => !existingIds.has(entry.entry.idString))
|
|
167
|
-
const
|
|
167
|
+
const duplicateEntries = options.entries
|
|
168
|
+
.filter((entry) => existingIds.has(entry.entry.idString))
|
|
169
|
+
.map((entry) => entry.entry)
|
|
170
|
+
const compacted = options.compact
|
|
168
171
|
? yield* options.compact(uncommitted)
|
|
169
|
-
:
|
|
172
|
+
: uncommitted.map((remoteEntry) => remoteEntry.entry)
|
|
170
173
|
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
const conflicts = yield* sql`
|
|
174
|
+
for (const entry of compacted) {
|
|
175
|
+
const conflicts = yield* sql`
|
|
174
176
|
SELECT *
|
|
175
177
|
FROM ${entryTableSql}
|
|
176
178
|
WHERE event = ${entry.event} AND
|
|
@@ -178,11 +180,14 @@ export const make = (options?: {
|
|
|
178
180
|
timestamp >= ${entry.createdAtMillis}
|
|
179
181
|
ORDER BY timestamp ASC
|
|
180
182
|
`.pipe(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
Effect.flatMap(decodeEntryRows),
|
|
184
|
+
Effect.map(toEntries)
|
|
185
|
+
)
|
|
186
|
+
yield* options.effect({ entry, conflicts })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
duplicateEntries
|
|
186
191
|
}
|
|
187
192
|
})
|
|
188
193
|
|
|
@@ -205,12 +210,10 @@ export const make = (options?: {
|
|
|
205
210
|
yield* PubSub.publish(pubsub, entry)
|
|
206
211
|
return value
|
|
207
212
|
},
|
|
208
|
-
sql.withTransaction,
|
|
209
213
|
Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "write" }))
|
|
210
214
|
),
|
|
211
215
|
writeFromRemote: (options) =>
|
|
212
216
|
writeFromRemote(options).pipe(
|
|
213
|
-
sql.withTransaction,
|
|
214
217
|
Effect.catchIf(
|
|
215
218
|
(e) => e._tag !== "EventJournalError",
|
|
216
219
|
(cause) => Effect.fail(new EventJournal.EventJournalError({ cause, method: "writeFromRemote" }))
|
|
@@ -229,7 +232,6 @@ export const make = (options?: {
|
|
|
229
232
|
)
|
|
230
233
|
return yield* f(entries)
|
|
231
234
|
},
|
|
232
|
-
sql.withTransaction,
|
|
233
235
|
Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "withRemoteUncommited" }))
|
|
234
236
|
),
|
|
235
237
|
nextRemoteSequence: (remoteId) =>
|
|
@@ -248,7 +250,13 @@ export const make = (options?: {
|
|
|
248
250
|
yield* sql`DROP TABLE ${remotesTableSql}`
|
|
249
251
|
}).pipe(
|
|
250
252
|
Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "destroy" }))
|
|
251
|
-
)
|
|
253
|
+
),
|
|
254
|
+
withLock(_storeId) {
|
|
255
|
+
return (effect) =>
|
|
256
|
+
sql.withTransaction(effect).pipe(
|
|
257
|
+
Effect.catchIf(SqlError.isSqlError, Effect.die)
|
|
258
|
+
)
|
|
259
|
+
}
|
|
252
260
|
})
|
|
253
261
|
})
|
|
254
262
|
|