clanka 0.2.47 → 0.2.49

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.
@@ -0,0 +1,493 @@
1
+ const spec = `# EventLog Rotating Session Identity Proof
2
+
3
+ ## Summary
4
+
5
+ Add a session-scoped rotating challenge proof to §EventLogRemote§ and §EventLogServerUnencrypted§ so a client must prove continuity of possession of its identity secret, not just knowledge of the public key.
6
+
7
+ The protocol becomes a breaking change:
8
+
9
+ - §Hello§ includes a fresh server challenge.
10
+ - the client must complete a dedicated §Authenticate§ handshake once per socket session before normal reads or writes are allowed.
11
+ - the server stores the proof generated from the current session's challenge and requires the previously stored proof on the next session.
12
+ - proof failures introduced by this feature use §Forbidden§.
13
+ - there is no legacy public-key-only fallback.
14
+
15
+ This design deliberately follows the existing identity model in the repository: the so-called private key is currently a 32-byte symmetric secret, not an asymmetric signing key. Because the server does not know that secret, the proof is based on deterministic keyed challenge proofs and trust-on-first-use (TOFU) for the first session of a given public key.
16
+
17
+ ## Scope clarification
18
+
19
+ This repository currently exposes:
20
+
21
+ - two client transport constructors:
22
+ - §EventLogRemote.fromSocket§
23
+ - §EventLogRemote.fromSocketUnencrypted§
24
+ - one server-side handler/runtime path:
25
+ - §EventLogServerUnencrypted.make§ / §EventLogServerUnencrypted.makeHandler§
26
+
27
+ For this change:
28
+
29
+ - the wire protocol additions must be shared across both client transport variants.
30
+ - concrete server-side enforcement lands in §EventLogServerUnencrypted.makeHandler§ and the shared runtime created by §EventLogServerUnencrypted.make§.
31
+ - storage implementations, including SQL-backed storage, inherit the same runtime auth behavior automatically and must not reimplement protocol authentication separately.
32
+ - if a dedicated encrypted server handler is added later, it must reuse the same handshake semantics and protocol message shapes defined here.
33
+
34
+ ## Background
35
+
36
+ Current behavior:
37
+
38
+ - §EventLogRemote.fromSocket§ and §EventLogRemote.fromSocketUnencrypted§ send the caller's §publicKey§ on each request.
39
+ - §EventLogServerUnencrypted.makeHandler§ accepts requests based on the §publicKey§ in the message and delegates read / write authorization to §EventLogServerAuth§.
40
+ - there is no proof that the caller also possesses the matching secret key material.
41
+ - §EventLog.Identity.privateKey§ is already a raw 32-byte secret used by §EventLogEncryption§ as an AES key.
42
+
43
+ Relevant code today:
44
+
45
+ - §packages/effect/src/unstable/eventlog/EventLog.ts§ defines §Identity§.
46
+ - §packages/effect/src/unstable/eventlog/EventLogEncryption.ts§ shows the current symmetric-key model.
47
+ - §packages/effect/src/unstable/eventlog/EventLogRemote.ts§ defines both encrypted and unencrypted wire protocols and client transports.
48
+ - §packages/effect/src/unstable/eventlog/EventLogServerUnencrypted.ts§ defines the current server handler and authorization hooks.
49
+
50
+ ## Goals
51
+
52
+ 1. Require a session-level proof of identity beyond §publicKey§.
53
+ 2. Apply the protocol/authentication design to both remote transport variants:
54
+ - §EventLogRemote.fromSocket§
55
+ - §EventLogRemote.fromSocketUnencrypted§
56
+ 3. Enforce the handshake in §EventLogServerUnencrypted.makeHandler§.
57
+ 4. Keep read/write authorization semantics separate from identity proof.
58
+ 5. Reuse the existing symmetric identity secret instead of introducing a new asymmetric key system.
59
+ 6. Rotate the proof material from a fresh server-issued challenge for each new socket session.
60
+ 7. Keep first-session bootstrap intentionally TOFU, per user direction.
61
+ 8. Use §Forbidden§ for new proof-related failures.
62
+
63
+ ## Non-goals
64
+
65
+ - Replacing the identity model with asymmetric signatures in this change.
66
+ - Preserving backward compatibility with legacy clients or servers.
67
+ - Changing store mapping rules or eventlog persistence semantics.
68
+ - Changing the public shape of §EventLogServerAuth.authorizeRead§ / §authorizeWrite§.
69
+ - Adding proof fields to normal read/write requests after the session is authenticated.
70
+ - Solving distributed proof persistence across multiple server instances or durable client storage across application restarts.
71
+ - Cryptographically verifying the very first session for a previously unseen public key.
72
+
73
+ ## User-confirmed decisions
74
+
75
+ - Scope: apply the design across both protocol variants.
76
+ - Compatibility: breaking change, no legacy fallback.
77
+ - Auth frequency: once per socket session.
78
+ - First auth attempt: the server does not verify it; it stores the first proof and begins the rotation cycle.
79
+ - Reads/writes: unchanged at the message level; proof is only for establishing identity.
80
+ - New proof failures: use §Forbidden§.
81
+
82
+ ## Design overview
83
+
84
+ ### Session model
85
+
86
+ Authentication becomes a separate handshake step that happens once per socket session:
87
+
88
+ 1. server sends §Hello§ with §remoteId§ and a fresh §challenge§.
89
+ 2. client computes a deterministic proof from that challenge and its identity secret.
90
+ 3. client sends §Authenticate§ before any application-level request.
91
+ 4. server either:
92
+ - bootstraps trust for a previously unseen public key by storing the proof, or
93
+ - compares the caller's §previousProof§ with the stored proof for that public key.
94
+ 5. on success, server marks the socket session as authenticated for exactly one public key and stores the new proof for the next session.
95
+ 6. after that, existing §WriteEntries§ / §RequestChanges§ flows continue unchanged.
96
+
97
+ A socket session is bound to one identity:
98
+
99
+ - once authenticated, every request on that socket must use the same §publicKey§.
100
+ - attempting to use a different §publicKey§ on the same authenticated socket is §Forbidden§.
101
+ - if multiple first operations race on the same unauthenticated socket, the first identity that begins authentication binds the socket; concurrent or later operations for a different public key fail.
102
+
103
+ ### Challenge-proof algorithm
104
+
105
+ Use a deterministic keyed proof derived from the existing secret key material:
106
+
107
+ - algorithm: §HMAC-SHA-256§
108
+ - HMAC key: raw bytes from §Redacted.value(identity.privateKey)§
109
+ - input: the raw §Hello.challenge§ bytes
110
+ - output: 32-byte proof (§Uint8Array§)
111
+
112
+ Rationale:
113
+
114
+ - the repository's current identity model is symmetric, not asymmetric.
115
+ - the server cannot independently compute the proof because it does not know the secret.
116
+ - deterministic output is required because the server verifies the next session by exact proof comparison.
117
+ - §AES-GCM§ is intentionally not used for this proof because its randomized IV makes the output unsuitable for stable equality comparison across sessions.
118
+
119
+ Implementation details:
120
+
121
+ - proof comparison on the server must use a constant-time byte loop.
122
+ - store and compare raw bytes, not encoded strings.
123
+ - cloned copies of proof arrays must be stored so later mutation of request buffers cannot affect server state.
124
+ - the server challenge should be 32 random bytes from §globalThis.crypto.getRandomValues§.
125
+
126
+ ### Proof rotation semantics
127
+
128
+ For each public key, the server maintains the most recent accepted proof and one fallback proof for robustness:
129
+
130
+ - §currentProof§: the proof most recently stored for that public key.
131
+ - §fallbackProof§: the immediately previous proof, accepted for one extra session to reduce client/server desynchronization risk.
132
+
133
+ Rotation rules:
134
+
135
+ 1. first successful bootstrap session for §publicKey§:
136
+ - no stored proof exists
137
+ - server does not verify §previousProof§
138
+ - server stores §currentProof = Authenticate.currentProof§
139
+ - §fallbackProof§ remains empty
140
+ 2. later sessions:
141
+ - the client sends §previousProof§ whenever it has one cached locally
142
+ - server accepts it if it matches either stored §currentProof§ or stored §fallbackProof§
143
+ - on success, server rotates to:
144
+ - new §fallbackProof = old currentProof§
145
+ - new §currentProof = Authenticate.currentProof§
146
+ 3. if stored proof state exists and neither stored proof matches §previousProof§, authentication fails with §Forbidden§.
147
+
148
+ This fallback window is intentionally limited to one prior proof. It improves recovery from lost auth acknowledgements or one-step client cache lag without permanently accepting stale proofs.
149
+
150
+ ### Proof-state ownership and atomicity
151
+
152
+ Server proof state is shared across all sockets created from the same eventlog runtime.
153
+
154
+ Requirements:
155
+
156
+ - proof-state updates for a given public key must be atomic.
157
+ - concurrent bootstrap or rotation attempts for the same public key must not corrupt the stored proof chain.
158
+ - the implementation may use a single in-memory map plus a synchronization primitive, or a per-public-key synchronized structure, as long as reads and writes observe consistent state.
159
+
160
+ ## Protocol changes
161
+
162
+ ### §Hello§
163
+
164
+ Extend §Hello§ with:
165
+
166
+ - §challenge: Schema.Uint8Array§
167
+
168
+ §Hello.remoteId§ remains unchanged.
169
+
170
+ ### New request: §Authenticate§
171
+
172
+ Add a new protocol request class shared by encrypted and unencrypted transports:
173
+
174
+ - §_tag: "Authenticate"§
175
+ - §publicKey: Schema.String§
176
+ - §currentProof: Schema.Uint8Array§
177
+ - §previousProof: Schema.optional(Schema.Uint8Array)§
178
+
179
+ Semantics:
180
+
181
+ - §currentProof§ is the HMAC of the current session's §Hello.challenge§.
182
+ - §previousProof§ is the proof last accepted for this public key, if the client has one.
183
+ - because the client cannot know whether the server already has proof state, the client should send §previousProof§ whenever it has a cached value.
184
+ - when the server is bootstrapping a public key with no stored proof state, any provided §previousProof§ is ignored.
185
+
186
+ ### New response: §Authenticated§
187
+
188
+ Add a new protocol response class shared by encrypted and unencrypted transports:
189
+
190
+ - §_tag: "Authenticated"§
191
+ - §publicKey: Schema.String§
192
+
193
+ Semantics:
194
+
195
+ - emitted once the session has been authenticated successfully.
196
+ - indicates the socket is now bound to that public key for the rest of the session.
197
+
198
+ ### Shared error response: §ProtocolError§
199
+
200
+ Introduce a shared protocol error response usable by both protocol variants:
201
+
202
+ - §_tag: "Error"§
203
+ - §requestTag: Schema.String§
204
+ - §id: Schema.optional(Schema.Number)§
205
+ - §publicKey: Schema.optional(Schema.String)§
206
+ - §code: Schema.Literals(["Unauthorized", "Forbidden", "NotFound", "InvalidRequest", "InternalServerError"])§
207
+ - §message: Schema.String§
208
+
209
+ Requirements:
210
+
211
+ - replace the unencrypted-only error shape with a shared response class used in both unions.
212
+ - §Authenticate§ failures produced by this feature must use:
213
+ - §requestTag: "Authenticate"§
214
+ - §code: "Forbidden"§
215
+ - if an unauthenticated client sends §WriteEntries§ / §RequestChanges§ / §StopChanges§ before completing auth, the server returns §Forbidden§ for the attempted request tag.
216
+ - server-side error emission must explicitly support §Authenticate§ and §StopChanges§ in addition to the existing request tags.
217
+
218
+ ### Union updates
219
+
220
+ Update both protocol unions in §EventLogRemote.ts§:
221
+
222
+ - §ProtocolRequest§ includes §Authenticate§.
223
+ - §ProtocolRequestUnencrypted§ includes §Authenticate§.
224
+ - §ProtocolResponse§ includes §Authenticated§ and §ProtocolError§.
225
+ - §ProtocolResponseUnencrypted§ includes §Authenticated§ and §ProtocolError§.
226
+
227
+ No proof fields are added to §WriteEntries§, §WriteEntriesUnencrypted§, or §RequestChanges§.
228
+
229
+ ## Client behavior specification
230
+
231
+ ### Shared client rules
232
+
233
+ Both remote transport constructors must implement the same session-auth model:
234
+
235
+ - keep per-socket session auth state separate from long-lived proof cache state.
236
+ - cache the last locally accepted proof by §remoteId + publicKey§ for the lifetime of the remote instance.
237
+ - serialize auth attempts per socket.
238
+ - once auth begins for one public key, concurrent attempts using a different public key fail instead of piggybacking on the in-flight auth effect.
239
+
240
+ Receiving a new §Hello§ must:
241
+
242
+ - update §remoteId§.
243
+ - replace the current session challenge.
244
+ - clear any per-socket authenticated identity marker.
245
+ - clear any in-flight auth state associated with the previous socket session.
246
+
247
+ After an auth failure with §Forbidden§:
248
+
249
+ - the current socket session is treated as terminal for identity-bound operations.
250
+ - the client does not auto-retry auth on that same socket.
251
+ - the cached local proof remains unchanged.
252
+
253
+ ### §EventLogRemote.fromSocketUnencrypted§
254
+
255
+ Add session-auth state to the remote instance:
256
+
257
+ - §helloChallenge§ from the latest §Hello§
258
+ - §authenticatedPublicKey§ for the current socket session, initially empty
259
+ - per-remote proof cache keyed by §remoteId + publicKey§ holding the last locally accepted proof
260
+ - a serialized in-flight auth effect so concurrent first operations do not send duplicate §Authenticate§ requests
261
+
262
+ Required behavior:
263
+
264
+ 1. receiving §Hello§ stores §remoteId§ and §challenge§.
265
+ 2. the first §write(identity, entries)§ or §changes(identity, startSequence)§ on an unauthenticated session must:
266
+ - compute §currentProof§ from the §Hello.challenge§
267
+ - load cached §previousProof§ for \`(remoteId, identity.publicKey)\` if present
268
+ - send §Authenticate§ and wait for §Authenticated§
269
+ - on success, cache §currentProof§ as the latest local proof for \`(remoteId, publicKey)\` and bind the session to that §publicKey§
270
+ 3. once authenticated, normal write/change requests proceed exactly as today.
271
+ 4. if the caller uses a different §publicKey§ after the session is authenticated, fail without silently switching identities.
272
+ 5. auth failure must surface as §EventLogRemoteError§ with §method: "authenticate"§.
273
+ 6. a failed auth attempt must not replace the cached local proof.
274
+ 7. §StopChanges§ should only be sent for authenticated sessions that actually created a subscription.
275
+ 8. the remote must decode §ProtocolError§ and fail pending auth, write, or change operations appropriately.
276
+
277
+ ### §EventLogRemote.fromSocket§
278
+
279
+ Apply the same session-auth flow as above before encrypted read/write traffic:
280
+
281
+ - same §Hello.challenge§ handling
282
+ - same §Authenticate§ request/§Authenticated§ response flow
283
+ - same single-public-key-per-socket rule
284
+ - same local proof cache semantics
285
+ - same §ProtocolError§ handling and §EventLogRemoteError§ mapping for auth failures
286
+
287
+ This change must not alter the existing event payload encryption behavior.
288
+
289
+ ### Concurrency and retry rules
290
+
291
+ - session auth must be serialized per socket.
292
+ - concurrent first write/read calls for the same identity wait on the same auth effect.
293
+ - concurrent first write/read calls for different identities on the same socket fail for the loser of the race.
294
+ - if auth fails with §Forbidden§, pending callers fail; the remote does not retry automatically.
295
+ - after a successful auth, reconnecting or recreating the socket starts a new auth session using the latest locally cached proof.
296
+
297
+ ## Server behavior specification
298
+
299
+ ### Runtime state
300
+
301
+ Add proof state to §EventLogServerUnencrypted.make§ so it is shared across handler instances created from the same runtime:
302
+
303
+ - §Map<publicKey, { currentProof: Uint8Array; fallbackProof?: Uint8Array }>§
304
+
305
+ Add per-socket session state inside §makeHandler§:
306
+
307
+ - §sessionChallenge: Uint8Array§ generated when the socket opens
308
+ - §authenticatedPublicKey: string | undefined§
309
+ - optional session marker that auth has completed
310
+
311
+ ### Socket startup and reset
312
+
313
+ On each accepted socket:
314
+
315
+ 1. generate a fresh random challenge.
316
+ 2. send §Hello({ remoteId, challenge })§.
317
+ 3. do not process application requests until the session authenticates.
318
+
319
+ If the underlying socket reconnects and a new §Hello§ is emitted, that begins a new session and any previous authenticated identity binding no longer applies.
320
+
321
+ ### §Authenticate§ handling
322
+
323
+ Required behavior:
324
+
325
+ 1. if the socket is already authenticated:
326
+ - if §request.publicKey§ matches the bound session public key, return §Authenticated§ again without rotating anything.
327
+ - otherwise return §Forbidden§.
328
+ 2. if this is the first auth message on the socket:
329
+ - look up stored proof state for §request.publicKey§.
330
+ 3. if no proof state exists for that public key:
331
+ - accept this as bootstrap / TOFU
332
+ - ignore §previousProof§ if it was provided
333
+ - store a cloned copy of \`{ currentProof: request.currentProof }\`
334
+ - mark the socket authenticated for §request.publicKey§
335
+ - respond §Authenticated§
336
+ 4. if proof state exists:
337
+ - compare the provided §previousProof§, if any, against both stored §currentProof§ and stored §fallbackProof§
338
+ - if neither matches, return §Forbidden§
339
+ - if it matches, rotate stored proofs to cloned copies of:
340
+ - §fallbackProof = old currentProof§
341
+ - §currentProof = request.currentProof§
342
+ - mark the socket authenticated for §request.publicKey§
343
+ - respond §Authenticated§
344
+
345
+ ### Request gating and ordering
346
+
347
+ Before session auth completes:
348
+
349
+ - §Ping§ remains allowed.
350
+ - §Authenticate§ is allowed.
351
+ - §WriteEntries§, §RequestChanges§, and §StopChanges§ must fail with §Forbidden§.
352
+ - if a chunked message resolves to one of those gated request tags, it must also fail with §Forbidden§.
353
+
354
+ After session auth completes, the handler must enforce this order for read/write requests:
355
+
356
+ 1. verify the session is authenticated
357
+ 2. verify the request §publicKey§ matches the session-bound §authenticatedPublicKey§
358
+ 3. only then resolve the store and call §authorizeRead§ / §authorizeWrite§
359
+
360
+ This ordering ensures the new feature remains strictly an identity-proof gate and does not leak store-resolution or authorization behavior before identity has been established.
361
+
362
+ ### Interaction with §EventLogServerAuth§
363
+
364
+ Keep the auth service focused on authorization:
365
+
366
+ - do not add proof fields to §authorizeWrite§ or §authorizeRead§.
367
+ - run session-proof checks before invoking authorization.
368
+ - after a session is authenticated, authorization behavior is unchanged.
369
+ - existing authorization failures keep their current semantics; only the new proof handshake failures introduced here are mandated to use §Forbidden§.
370
+
371
+ ## Behavioral edge cases
372
+
373
+ 1. **First ever session for a public key**
374
+ - succeeds without verification
375
+ - stores the first proof
376
+ 2. **Bootstrap request that includes a stale or spurious §previousProof§**
377
+ - still succeeds when the server has no proof state for that public key
378
+ - the provided §previousProof§ is ignored
379
+ 3. **Second session with matching previous proof**
380
+ - succeeds
381
+ - rotates the stored proof
382
+ 4. **Second session without previous proof**
383
+ - §Forbidden§ when the server already has stored proof state
384
+ 5. **Session with stale proof older than the single fallback window**
385
+ - §Forbidden§
386
+ 6. **Session authenticated as one public key but request uses another**
387
+ - §Forbidden§
388
+ 7. **Write/read attempted before auth**
389
+ - §Forbidden§
390
+ 8. **Unauthenticated §StopChanges§**
391
+ - §Forbidden§
392
+ 9. **Auth ack lost but client still only has the previous local proof**
393
+ - next session still succeeds once because the server accepts the single fallback proof
394
+ 10. **Server restart or new runtime instance**
395
+ - stored proof map resets
396
+ - the next session for any public key bootstraps again
397
+ 11. **Client-side proof cache loss while the server still retains a newer proof**
398
+ - auth fails with §Forbidden§ after the one-proof fallback window
399
+ - durable client proof persistence is out of scope for this change
400
+
401
+ ## Acceptance criteria
402
+
403
+ The feature is complete when:
404
+
405
+ 1. §Hello§ includes a challenge in both protocol variants.
406
+ 2. a new §Authenticate§ / §Authenticated§ handshake exists in both protocol variants.
407
+ 3. a shared §ProtocolError§ response exists in both protocol variants.
408
+ 4. §EventLogRemote.fromSocket§ authenticates once per socket session before encrypted requests and handles auth failures through §ProtocolError§.
409
+ 5. §EventLogRemote.fromSocketUnencrypted§ authenticates once per socket session before unencrypted requests and handles auth failures through §ProtocolError§.
410
+ 6. §EventLogServerUnencrypted.makeHandler§ forbids reads/writes before auth.
411
+ 7. the first session for a public key bootstraps by storing proof without verification.
412
+ 8. bootstrap ignores any provided §previousProof§ when no server-side proof state exists.
413
+ 9. the next session for the same public key must supply a previous proof acceptable under the current/fallback rotation rule and a new current proof.
414
+ 10. the server rotates stored proofs and accepts the immediately previous proof as a one-step fallback.
415
+ 11. a socket session is bound to a single public key.
416
+ 12. proof-state updates per public key are atomic.
417
+ 13. read/write authorization remains separate from session identity proof.
418
+ 14. proof-related failures introduced by this feature use §Forbidden§.
419
+ 15. existing request payload schemas for writes/reads are otherwise unchanged.
420
+
421
+ ## Testing strategy
422
+
423
+ ### Existing test files to extend
424
+
425
+ - §packages/effect/test/unstable/eventlog/EventLogRemote.test.ts§
426
+ - §packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts§
427
+
428
+ ### Required new test coverage
429
+
430
+ #### Remote client tests
431
+
432
+ 1. §Hello§ decoding captures the challenge.
433
+ 2. first §write§ on an unauthenticated unencrypted socket sends §Authenticate§ before §WriteEntries§.
434
+ 3. first §changes§ on an unauthenticated unencrypted socket sends §Authenticate§ before §RequestChanges§.
435
+ 4. encrypted §fromSocket§ performs the same handshake before §WriteEntries§ / §RequestChanges§.
436
+ 5. concurrent first operations for the same identity share one auth handshake.
437
+ 6. concurrent first operations for different identities on the same socket do not both succeed.
438
+ 7. auth §Forbidden§ surfaces as §EventLogRemoteError§ with §method: "authenticate"§.
439
+ 8. once authenticated, subsequent operations on the same socket do not send another §Authenticate§.
440
+ 9. a failed auth does not update the local proof cache.
441
+ 10. receiving a new §Hello§ resets per-session auth state.
442
+ 11. encrypted §fromSocket§ also handles §ProtocolError§ auth failures.
443
+
444
+ #### Server handler tests
445
+
446
+ 1. §Hello§ includes a challenge.
447
+ 2. non-auth requests sent before §Authenticate§ receive §Forbidden§.
448
+ 3. unauthenticated §StopChanges§ receives §Forbidden§.
449
+ 4. first-session §Authenticate§ bootstraps proof state and allows later requests on that socket.
450
+ 5. bootstrap ignores a provided §previousProof§ when no proof state exists.
451
+ 6. second session for the same public key requires a matching §previousProof§.
452
+ 7. missing or mismatched §previousProof§ is §Forbidden§.
453
+ 8. successful re-auth rotates the stored proof.
454
+ 9. the immediate prior proof is accepted once as fallback.
455
+ 10. a socket authenticated for one public key rejects read/write requests using another public key.
456
+ 11. §authorizeWrite§ / §authorizeRead§ are not called before session auth succeeds.
457
+ 12. once session auth succeeds, existing read/write authorization behavior still works.
458
+ 13. concurrent proof-state updates for the same public key do not corrupt rotation state.
459
+
460
+ ### Test helper guidance
461
+
462
+ - follow the existing socket harness style already used in both eventlog test files.
463
+ - add a small deterministic helper for producing test proofs from a known identity and challenge.
464
+ - keep tests in the §it.effect§ style used by this repository.
465
+
466
+ ## Implementation plan
467
+
468
+ 1. **Implement the end-to-end rotating session auth protocol in one atomic change**
469
+ - add §Hello.challenge§, §Authenticate§, §Authenticated§, and shared §ProtocolError§ protocol types and codec updates
470
+ - add the shared deterministic HMAC proof helper and proof-comparison utilities
471
+ - update both §EventLogRemote.fromSocket§ and §EventLogRemote.fromSocketUnencrypted§ to perform session authentication, bind one public key per socket, cache local proofs, and surface auth failures
472
+ - update §EventLogServerUnencrypted.make§ / §makeHandler§ to maintain shared proof state, gate unauthenticated requests, enforce identity binding, and rotate proofs atomically
473
+ - update/add the affected tests in §EventLogRemote.test.ts§ and §EventLogServerUnencrypted.test.ts§ so the new wire protocol passes end-to-end
474
+
475
+ 2. **Run validation and cleanup**
476
+ - run §pnpm lint-fix§
477
+ - run §pnpm test packages/effect/test/unstable/eventlog/EventLogRemote.test.ts§
478
+ - run §pnpm test packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts§
479
+ - run §pnpm check:tsgo§
480
+ - run §pnpm docgen§
481
+ - update any affected protocol documentation or inline comments discovered during implementation
482
+
483
+ ## Validation checklist for implementation
484
+
485
+ - §pnpm lint-fix§
486
+ - §pnpm test packages/effect/test/unstable/eventlog/EventLogRemote.test.ts§
487
+ - §pnpm test packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts§
488
+ - §pnpm check:tsgo§
489
+ - §pnpm docgen§
490
+ `;
491
+ const content = spec.replaceAll('§', '`');
492
+ await writeFile({ path: '.specs/eventlog-rotating-session-auth.md', content });
493
+ console.log(content);