effect 4.0.0-beta.44 → 4.0.0-beta.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/Equal.d.ts.map +1 -1
  2. package/dist/Equal.js +16 -0
  3. package/dist/Equal.js.map +1 -1
  4. package/dist/Hash.js +1 -1
  5. package/dist/Hash.js.map +1 -1
  6. package/dist/Semaphore.d.ts +1 -1
  7. package/dist/Semaphore.d.ts.map +1 -1
  8. package/dist/Semaphore.js +1 -3
  9. package/dist/Semaphore.js.map +1 -1
  10. package/dist/unstable/ai/McpServer.d.ts.map +1 -1
  11. package/dist/unstable/ai/McpServer.js +24 -21
  12. package/dist/unstable/ai/McpServer.js.map +1 -1
  13. package/dist/unstable/eventlog/Event.d.ts +0 -6
  14. package/dist/unstable/eventlog/Event.d.ts.map +1 -1
  15. package/dist/unstable/eventlog/Event.js +0 -5
  16. package/dist/unstable/eventlog/Event.js.map +1 -1
  17. package/dist/unstable/eventlog/EventGroup.d.ts +0 -2
  18. package/dist/unstable/eventlog/EventGroup.d.ts.map +1 -1
  19. package/dist/unstable/eventlog/EventGroup.js +0 -2
  20. package/dist/unstable/eventlog/EventGroup.js.map +1 -1
  21. package/dist/unstable/eventlog/EventJournal.d.ts +22 -5
  22. package/dist/unstable/eventlog/EventJournal.d.ts.map +1 -1
  23. package/dist/unstable/eventlog/EventJournal.js +126 -67
  24. package/dist/unstable/eventlog/EventJournal.js.map +1 -1
  25. package/dist/unstable/eventlog/EventLog.d.ts +88 -34
  26. package/dist/unstable/eventlog/EventLog.d.ts.map +1 -1
  27. package/dist/unstable/eventlog/EventLog.js +215 -141
  28. package/dist/unstable/eventlog/EventLog.js.map +1 -1
  29. package/dist/unstable/eventlog/EventLogEncryption.d.ts +9 -7
  30. package/dist/unstable/eventlog/EventLogEncryption.d.ts.map +1 -1
  31. package/dist/unstable/eventlog/EventLogEncryption.js +13 -15
  32. package/dist/unstable/eventlog/EventLogEncryption.js.map +1 -1
  33. package/dist/unstable/eventlog/EventLogMessage.d.ts +228 -0
  34. package/dist/unstable/eventlog/EventLogMessage.d.ts.map +1 -0
  35. package/dist/unstable/eventlog/EventLogMessage.js +214 -0
  36. package/dist/unstable/eventlog/EventLogMessage.js.map +1 -0
  37. package/dist/unstable/eventlog/EventLogRemote.d.ts +109 -194
  38. package/dist/unstable/eventlog/EventLogRemote.d.ts.map +1 -1
  39. package/dist/unstable/eventlog/EventLogRemote.js +165 -320
  40. package/dist/unstable/eventlog/EventLogRemote.js.map +1 -1
  41. package/dist/unstable/eventlog/EventLogServer.d.ts +25 -47
  42. package/dist/unstable/eventlog/EventLogServer.d.ts.map +1 -1
  43. package/dist/unstable/eventlog/EventLogServer.js +127 -198
  44. package/dist/unstable/eventlog/EventLogServer.js.map +1 -1
  45. package/dist/unstable/eventlog/EventLogServerEncrypted.d.ts +60 -0
  46. package/dist/unstable/eventlog/EventLogServerEncrypted.d.ts.map +1 -0
  47. package/dist/unstable/eventlog/EventLogServerEncrypted.js +166 -0
  48. package/dist/unstable/eventlog/EventLogServerEncrypted.js.map +1 -0
  49. package/dist/unstable/eventlog/EventLogServerUnencrypted.d.ts +183 -0
  50. package/dist/unstable/eventlog/EventLogServerUnencrypted.d.ts.map +1 -0
  51. package/dist/unstable/eventlog/EventLogServerUnencrypted.js +461 -0
  52. package/dist/unstable/eventlog/EventLogServerUnencrypted.js.map +1 -0
  53. package/dist/unstable/eventlog/EventLogSessionAuth.d.ts +117 -0
  54. package/dist/unstable/eventlog/EventLogSessionAuth.d.ts.map +1 -0
  55. package/dist/unstable/eventlog/EventLogSessionAuth.js +284 -0
  56. package/dist/unstable/eventlog/EventLogSessionAuth.js.map +1 -0
  57. package/dist/unstable/eventlog/{SqlEventLogJournal.d.ts → SqlEventJournal.d.ts} +2 -2
  58. package/dist/unstable/eventlog/SqlEventJournal.d.ts.map +1 -0
  59. package/dist/unstable/eventlog/{SqlEventLogJournal.js → SqlEventJournal.js} +20 -14
  60. package/dist/unstable/eventlog/SqlEventJournal.js.map +1 -0
  61. package/dist/unstable/eventlog/{SqlEventLogServer.d.ts → SqlEventLogServerEncrypted.d.ts} +5 -5
  62. package/dist/unstable/eventlog/SqlEventLogServerEncrypted.d.ts.map +1 -0
  63. package/dist/unstable/eventlog/{SqlEventLogServer.js → SqlEventLogServerEncrypted.js} +65 -24
  64. package/dist/unstable/eventlog/SqlEventLogServerEncrypted.js.map +1 -0
  65. package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.d.ts +25 -0
  66. package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.d.ts.map +1 -0
  67. package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.js +354 -0
  68. package/dist/unstable/eventlog/SqlEventLogServerUnencrypted.js.map +1 -0
  69. package/dist/unstable/eventlog/index.d.ts +22 -2
  70. package/dist/unstable/eventlog/index.d.ts.map +1 -1
  71. package/dist/unstable/eventlog/index.js +22 -2
  72. package/dist/unstable/eventlog/index.js.map +1 -1
  73. package/dist/unstable/eventlog/internal/identityRootSecretDerivation.d.ts +2 -0
  74. package/dist/unstable/eventlog/internal/identityRootSecretDerivation.d.ts.map +1 -0
  75. package/dist/unstable/eventlog/internal/identityRootSecretDerivation.js +89 -0
  76. package/dist/unstable/eventlog/internal/identityRootSecretDerivation.js.map +1 -0
  77. package/dist/unstable/reactivity/AtomHttpApi.d.ts +1 -2
  78. package/dist/unstable/reactivity/AtomHttpApi.d.ts.map +1 -1
  79. package/dist/unstable/reactivity/AtomHttpApi.js +2 -2
  80. package/dist/unstable/reactivity/AtomHttpApi.js.map +1 -1
  81. package/dist/unstable/reactivity/AtomRpc.d.ts +1 -2
  82. package/dist/unstable/reactivity/AtomRpc.d.ts.map +1 -1
  83. package/dist/unstable/reactivity/AtomRpc.js +3 -3
  84. package/dist/unstable/reactivity/AtomRpc.js.map +1 -1
  85. package/dist/unstable/rpc/Rpc.d.ts +25 -4
  86. package/dist/unstable/rpc/Rpc.d.ts.map +1 -1
  87. package/dist/unstable/rpc/Rpc.js +26 -0
  88. package/dist/unstable/rpc/Rpc.js.map +1 -1
  89. package/dist/unstable/rpc/RpcClient.d.ts +3 -13
  90. package/dist/unstable/rpc/RpcClient.d.ts.map +1 -1
  91. package/dist/unstable/rpc/RpcClient.js +47 -23
  92. package/dist/unstable/rpc/RpcClient.js.map +1 -1
  93. package/dist/unstable/rpc/RpcGroup.d.ts +1 -1
  94. package/dist/unstable/rpc/RpcGroup.d.ts.map +1 -1
  95. package/dist/unstable/rpc/RpcMiddleware.d.ts +2 -2
  96. package/dist/unstable/rpc/RpcMiddleware.d.ts.map +1 -1
  97. package/dist/unstable/rpc/RpcServer.d.ts.map +1 -1
  98. package/dist/unstable/rpc/RpcServer.js +3 -2
  99. package/dist/unstable/rpc/RpcServer.js.map +1 -1
  100. package/dist/unstable/rpc/Utils.d.ts +6 -0
  101. package/dist/unstable/rpc/Utils.d.ts.map +1 -1
  102. package/dist/unstable/rpc/Utils.js +44 -0
  103. package/dist/unstable/rpc/Utils.js.map +1 -1
  104. package/dist/unstable/schema/Model.d.ts +2 -2
  105. package/dist/unstable/schema/Model.d.ts.map +1 -1
  106. package/dist/unstable/schema/Model.js +2 -4
  107. package/dist/unstable/schema/Model.js.map +1 -1
  108. package/dist/unstable/schema/VariantSchema.d.ts +1 -1
  109. package/dist/unstable/schema/VariantSchema.d.ts.map +1 -1
  110. package/dist/unstable/schema/VariantSchema.js +1 -12
  111. package/dist/unstable/schema/VariantSchema.js.map +1 -1
  112. package/dist/unstable/workers/Transferable.d.ts +1 -1
  113. package/dist/unstable/workers/Transferable.d.ts.map +1 -1
  114. package/dist/unstable/workers/Transferable.js +1 -1
  115. package/dist/unstable/workers/Transferable.js.map +1 -1
  116. package/package.json +1 -1
  117. package/src/Equal.ts +17 -0
  118. package/src/Hash.ts +2 -2
  119. package/src/Semaphore.ts +2 -4
  120. package/src/unstable/ai/McpServer.ts +24 -22
  121. package/src/unstable/eventlog/Event.ts +0 -8
  122. package/src/unstable/eventlog/EventGroup.ts +0 -4
  123. package/src/unstable/eventlog/EventJournal.ts +144 -76
  124. package/src/unstable/eventlog/EventLog.ts +342 -221
  125. package/src/unstable/eventlog/EventLogEncryption.ts +16 -30
  126. package/src/unstable/eventlog/EventLogMessage.ts +277 -0
  127. package/src/unstable/eventlog/EventLogRemote.ts +261 -408
  128. package/src/unstable/eventlog/EventLogServer.ts +182 -274
  129. package/src/unstable/eventlog/EventLogServerEncrypted.ts +206 -0
  130. package/src/unstable/eventlog/EventLogServerUnencrypted.ts +749 -0
  131. package/src/unstable/eventlog/EventLogSessionAuth.ts +437 -0
  132. package/src/unstable/eventlog/{SqlEventLogJournal.ts → SqlEventJournal.ts} +26 -18
  133. package/src/unstable/eventlog/{SqlEventLogServer.ts → SqlEventLogServerEncrypted.ts} +102 -40
  134. package/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts +500 -0
  135. package/src/unstable/eventlog/index.ts +27 -2
  136. package/src/unstable/eventlog/internal/identityRootSecretDerivation.ts +153 -0
  137. package/src/unstable/reactivity/AtomHttpApi.ts +23 -8
  138. package/src/unstable/reactivity/AtomRpc.ts +16 -5
  139. package/src/unstable/rpc/Rpc.ts +42 -4
  140. package/src/unstable/rpc/RpcClient.ts +59 -24
  141. package/src/unstable/rpc/RpcGroup.ts +1 -1
  142. package/src/unstable/rpc/RpcMiddleware.ts +2 -2
  143. package/src/unstable/rpc/RpcServer.ts +5 -3
  144. package/src/unstable/rpc/Utils.ts +59 -0
  145. package/src/unstable/schema/Model.ts +4 -6
  146. package/src/unstable/schema/VariantSchema.ts +4 -17
  147. package/src/unstable/workers/Transferable.ts +9 -11
  148. package/dist/unstable/eventlog/SqlEventLogJournal.d.ts.map +0 -1
  149. package/dist/unstable/eventlog/SqlEventLogJournal.js.map +0 -1
  150. package/dist/unstable/eventlog/SqlEventLogServer.d.ts.map +0 -1
  151. 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 type * as SqlError from "../sql/SqlError.ts"
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
- void,
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 brackets: ReadonlyArray<RemoteBracket> = options.compact
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
- : [[uncommitted.map((remoteEntry) => remoteEntry.entry), uncommitted] as const]
172
+ : uncommitted.map((remoteEntry) => remoteEntry.entry)
170
173
 
171
- for (const [compacted] of brackets) {
172
- for (const entry of compacted) {
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
- Effect.flatMap(decodeEntryRows),
182
- Effect.map(toEntries)
183
- )
184
- yield* options.effect({ entry, conflicts })
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