1import { SqliteClient } from "@effect/sql-sqlite-node"
2import { assert, describe, it } from "@effect/vitest"
3import { Effect, Layer, Redacted } from "effect"
4import * as SqlEventLogServerUnencryptedStorageTest from "effect-test/eventlog/SqlEventLogServerUnencryptedStorageTest"
5import * as EventJournal from "effect/eventlog/EventJournal"
6import * as EventLog from "effect/eventlog/EventLog"
7import * as EventLogEncryption from "effect/eventlog/EventLogEncryption"
8import * as EventLogMessage from "effect/eventlog/EventLogMessage"
9import * as EventLogServerUnencrypted from "effect/eventlog/EventLogServerUnencrypted"
10import * as EventLogSessionAuth from "effect/eventlog/EventLogSessionAuth"
11import { makeGetIdentityRootSecretMaterial } from "effect/eventlog/internal/identityRootSecretDerivation"
12import * as SqlEventLogServerUnencrypted from "effect/eventlog/SqlEventLogServerUnencrypted"
13import { Reactivity } from "effect/reactivity"
14import * as RpcTest from "effect/rpc/RpcTest"
15import * as SqlClient from "effect/sql/SqlClient"
16
17SqlEventLogServerUnencryptedStorageTest.suite(
18 "sql-sqlite-node",
19 SqliteClient.layer({
20 filename: ":memory:"
21 })
22)
23
24const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto)
25const storeId = EventLogMessage.StoreId.make("store-a")
26
27const makeEntry = (value: number) =>
28 new EventJournal.Entry({
29 id: EventJournal.makeEntryIdUnsafe(),
30 event: "UserCreated",
31 primaryKey: `user-${value}`,
32 payload: new Uint8Array([value])
33 }, { disableChecks: true })
34
35const makeAuthenticateRequest = Effect.fnUntraced(function*(options: {
36 readonly identity: EventLog.Identity["Service"]
37 readonly challenge: Uint8Array
38 readonly remoteId: EventJournal.RemoteId
39}) {
40 const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity)
41 const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
42 remoteId: options.remoteId,
43 challenge: options.challenge,
44 publicKey: options.identity.publicKey,
45 signingPublicKey: rootSecretMaterial.signingPublicKey,
46 signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
47 })
48 return new EventLogMessage.Authenticate({
49 publicKey: options.identity.publicKey,
50 signingPublicKey: rootSecretMaterial.signingPublicKey,
51 signature,
52 algorithm: "Ed25519"
53 })
54})
55
56describe("SqlEventLogServerUnencrypted (sql-sqlite-node)", () => {
57 it.effect("forbids writing entries for another identity", () =>
58 Effect.gen(function*() {
59 const sql = yield* SqliteClient.make({ filename: ":memory:" })
60 const storage = yield* SqlEventLogServerUnencrypted.makeStorage().pipe(
61 Effect.provideService(SqlClient.SqlClient, sql),
62 Effect.orDie
63 )
64 const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
65 Effect.provide(
66 EventLogServerUnencrypted.layerRpcHandlers.pipe(
67 Layer.provideMerge(EventLog.layerRegistry),
68 Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)),
69 Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, {
70 resolve: ({ storeId }) => Effect.succeed(storeId),
71 hasStore: () => Effect.succeed(true)
72 })),
73 Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, {
74 authorizeWrite: () => Effect.void,
75 authorizeRead: () => Effect.void,
76 authorizeIdentity: () => Effect.void
77 }))
78 )
79 )
80 )
81 const identityA = yield* EventLog.makeIdentity
82 const identityB = yield* EventLog.makeIdentity
83 const hello = yield* rpcClient["EventLog.Hello"]()
84 yield* rpcClient["EventLog.Authenticate"](
85 yield* makeAuthenticateRequest({
86 identity: identityA,
87 challenge: hello.challenge,
88 remoteId: hello.remoteId
89 })
90 )
91 const data = yield* new EventLogMessage.WriteEntriesUnencrypted({
92 publicKey: identityB.publicKey,
93 storeId,
94 entries: [makeEntry(1)]
95 }).encoded
96
97 const error = yield* rpcClient["EventLog.WriteSingle"]({ data }).pipe(Effect.flip)
98
99 assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
100 assert.strictEqual(error.code, "Forbidden")
101 const written = yield* storage.write(storeId, [makeEntry(2)])
102 assert.deepStrictEqual(written.map((entry) => entry.remoteSequence), [1])
103 }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
104
105 it.effect("rejects session-auth rebinding for an existing publicKey", () =>
106 Effect.gen(function*() {
107 const sql = yield* SqliteClient.make({ filename: ":memory:" })
108 const storage = yield* SqlEventLogServerUnencrypted.makeStorage().pipe(
109 Effect.provideService(SqlClient.SqlClient, sql),
110 Effect.orDie
111 )
112
113 const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
114 Effect.provide(
115 EventLogServerUnencrypted.layerRpcHandlers.pipe(
116 Layer.provideMerge(EventLog.layerRegistry),
117 Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)),
118 Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, {
119 resolve: ({ storeId }) => Effect.succeed(storeId),
120 hasStore: () => Effect.succeed(true)
121 })),
122 Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, {
123 authorizeWrite: () => Effect.void,
124 authorizeRead: () => Effect.void,
125 authorizeIdentity: () => Effect.void
126 }))
127 )
128 )
129 )
130
131 const firstIdentity = yield* EventLog.makeIdentity
132 const secondIdentitySeed = yield* EventLog.makeIdentity
133 const secondIdentity: EventLog.Identity["Service"] = {
134 publicKey: firstIdentity.publicKey,
135 privateKey: secondIdentitySeed.privateKey
136 }
137
138 const firstMaterial = yield* getIdentityRootSecretMaterial(firstIdentity)
139 const secondMaterial = yield* getIdentityRootSecretMaterial(secondIdentity)
140 const sameSigningPublicKey =
141 firstMaterial.signingPublicKey.byteLength === secondMaterial.signingPublicKey.byteLength &&
142 firstMaterial.signingPublicKey.every((byte, index) => byte === secondMaterial.signingPublicKey[index])
143 assert.strictEqual(sameSigningPublicKey, false)
144
145 const firstHello = yield* rpcClient["EventLog.Hello"]()
146 yield* rpcClient["EventLog.Authenticate"](
147 yield* makeAuthenticateRequest({
148 identity: firstIdentity,
149 challenge: firstHello.challenge,
150 remoteId: firstHello.remoteId
151 })
152 )
153
154 const secondHello = yield* rpcClient["EventLog.Hello"]()
155 const error = yield* rpcClient["EventLog.Authenticate"](
156 yield* makeAuthenticateRequest({
157 identity: secondIdentity,
158 challenge: secondHello.challenge,
159 remoteId: secondHello.remoteId
160 })
161 ).pipe(Effect.flip)
162
163 assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
164 assert.strictEqual(error.code, "Forbidden")
165 assert.strictEqual(error.message, "Session auth signature verification failed")
166 }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
167})