@oxy.so/protocol 1.2.2 → 1.3.1

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 (58) hide show
  1. package/dist/cjs/chain/engine.js +1 -1
  2. package/dist/cjs/chain/recordStore.js +3 -3
  3. package/dist/cjs/chain/verify.js +8 -4
  4. package/dist/cjs/envelope/signingInput.js +12 -1
  5. package/dist/cjs/identity/resolver.js +2 -1
  6. package/dist/cjs/node/didWebResolver.js +1 -3
  7. package/dist/cjs/node/nodeApp.js +9 -3
  8. package/dist/cjs/node/nodeClient.js +7 -2
  9. package/dist/cjs/platform/platform.js +1 -1
  10. package/dist/cjs/secp256k1.js +25 -26
  11. package/dist/cjs/sign.js +17 -0
  12. package/dist/esm/chain/engine.js +1 -1
  13. package/dist/esm/chain/recordStore.js +3 -3
  14. package/dist/esm/chain/verify.js +8 -4
  15. package/dist/esm/envelope/sign.js +1 -1
  16. package/dist/esm/envelope/signingInput.js +12 -1
  17. package/dist/esm/identity/resolver.js +2 -1
  18. package/dist/esm/node/didWebResolver.js +1 -3
  19. package/dist/esm/node/nodeApp.js +9 -3
  20. package/dist/esm/node/nodeClient.js +8 -3
  21. package/dist/esm/platform/platform.js +1 -1
  22. package/dist/esm/secp256k1.js +26 -27
  23. package/dist/esm/sign.js +10 -0
  24. package/dist/types/chain/engine.d.ts +1 -1
  25. package/dist/types/chain/recordStore.d.ts +3 -3
  26. package/dist/types/chain/verify.d.ts +3 -2
  27. package/dist/types/sign.d.ts +10 -0
  28. package/package.json +21 -22
  29. package/src/__tests__/canonicalJson.test.ts +2 -6
  30. package/src/__tests__/chain.test.ts +42 -11
  31. package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
  32. package/src/__tests__/didWebResolver.test.ts +36 -7
  33. package/src/__tests__/nodeApp.test.ts +76 -21
  34. package/src/__tests__/nodeClient.test.ts +30 -7
  35. package/src/__tests__/nodeHarness.ts +7 -3
  36. package/src/__tests__/optionalNativePeers.test.ts +8 -6
  37. package/src/__tests__/rateLimit.test.ts +2 -1
  38. package/src/__tests__/registrationPow.test.ts +35 -37
  39. package/src/__tests__/secp256k1.test.ts +41 -70
  40. package/src/__tests__/transparency.test.ts +4 -1
  41. package/src/auth/registrationPow.ts +5 -1
  42. package/src/chain/engine.ts +1 -1
  43. package/src/chain/recordStore.ts +3 -3
  44. package/src/chain/verify.ts +11 -4
  45. package/src/envelope/recordId.ts +1 -4
  46. package/src/envelope/sign.ts +6 -6
  47. package/src/envelope/signingInput.ts +12 -1
  48. package/src/identity/resolver.ts +9 -2
  49. package/src/node/didWebResolver.ts +2 -7
  50. package/src/node/nodeApp.ts +85 -67
  51. package/src/node/nodeClient.ts +35 -16
  52. package/src/platform/crypto.native.ts +9 -2
  53. package/src/platform/platform.ts +3 -1
  54. package/src/secp256k1.ts +33 -58
  55. package/src/sign.ts +16 -0
  56. package/dist/cjs/.tsbuildinfo +0 -1
  57. package/dist/esm/.tsbuildinfo +0 -1
  58. package/dist/types/.tsbuildinfo +0 -1
@@ -91,7 +91,11 @@ describe('createNodeApp', () => {
91
91
  const empty = await request(app).get('/oxy/head');
92
92
  expect(empty.body).toEqual({ seq: null, headRecordId: null, recordCount: 0 });
93
93
 
94
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
94
+ const envelope = await buildSignedEnvelope({
95
+ privateKey: owner.privateKey,
96
+ seq: 0,
97
+ prev: null,
98
+ });
95
99
  const recordId = await computeRecordId(envelope);
96
100
  const write = await request(app).post('/records').send(envelope);
97
101
  expect(write.status).toBe(201);
@@ -107,7 +111,12 @@ describe('createNodeApp', () => {
107
111
  const genesisId = await computeRecordId(genesis);
108
112
  await request(app).post('/records').send(genesis).expect(201);
109
113
 
110
- const second = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 1, prev: genesisId, record: { step: 2 } });
114
+ const second = await buildSignedEnvelope({
115
+ privateKey: owner.privateKey,
116
+ seq: 1,
117
+ prev: genesisId,
118
+ record: { step: 2 },
119
+ });
111
120
  const secondId = await computeRecordId(second);
112
121
  const res = await request(app).post('/records').send(second);
113
122
  expect(res.status).toBe(201);
@@ -117,7 +126,11 @@ describe('createNodeApp', () => {
117
126
  it('POST /records rejects a record signed by a NON-owner key (403 not_owner)', async () => {
118
127
  const { app } = buildApp(owner);
119
128
  const attacker = generateKeyPair();
120
- const envelope = await buildSignedEnvelope({ privateKey: attacker.privateKey, seq: 0, prev: null });
129
+ const envelope = await buildSignedEnvelope({
130
+ privateKey: attacker.privateKey,
131
+ seq: 0,
132
+ prev: null,
133
+ });
121
134
  const res = await request(app).post('/records').send(envelope);
122
135
  expect(res.status).toBe(403);
123
136
  expect(res.body).toEqual({ error: 'not_owner' });
@@ -132,7 +145,11 @@ describe('createNodeApp', () => {
132
145
 
133
146
  it('POST /records rejects a chain gap (422 chain_gap)', async () => {
134
147
  const { app } = buildApp(owner);
135
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 3, prev: null });
148
+ const envelope = await buildSignedEnvelope({
149
+ privateKey: owner.privateKey,
150
+ seq: 3,
151
+ prev: null,
152
+ });
136
153
  const res = await request(app).post('/records').send(envelope);
137
154
  expect(res.status).toBe(422);
138
155
  expect(res.body).toEqual({ error: 'chain_gap' });
@@ -162,8 +179,14 @@ describe('createNodeApp', () => {
162
179
  it('POST /sync/push verifies + appends a batch in order', async () => {
163
180
  const { app } = buildApp(owner);
164
181
  const genesis = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
165
- const second = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 1, prev: await computeRecordId(genesis) });
166
- const res = await request(app).post('/sync/push').send({ records: [genesis, second] });
182
+ const second = await buildSignedEnvelope({
183
+ privateKey: owner.privateKey,
184
+ seq: 1,
185
+ prev: await computeRecordId(genesis),
186
+ });
187
+ const res = await request(app)
188
+ .post('/sync/push')
189
+ .send({ records: [genesis, second] });
167
190
  expect(res.status).toBe(200);
168
191
  expect(res.body.accepted).toBe(2);
169
192
  expect(res.body.results.every((r: { ok: boolean }) => r.ok)).toBe(true);
@@ -174,7 +197,12 @@ describe('createNodeApp', () => {
174
197
  let prev: string | null = null;
175
198
  const ids: string[] = [];
176
199
  for (let seq = 0; seq < 4; seq += 1) {
177
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq, prev, record: { seq } });
200
+ const envelope = await buildSignedEnvelope({
201
+ privateKey: owner.privateKey,
202
+ seq,
203
+ prev,
204
+ record: { seq },
205
+ });
178
206
  const id = await computeRecordId(envelope);
179
207
  await request(app).post('/records').send(envelope).expect(201);
180
208
  ids.push(id);
@@ -193,7 +221,9 @@ describe('createNodeApp', () => {
193
221
  const sinceCursor = await request(app).get('/oxy/log').query({ since: ids[1] });
194
222
  expect(sinceCursor.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([2, 3]);
195
223
 
196
- const unknownCursor = await request(app).get('/oxy/log').query({ since: 'f'.repeat(64) });
224
+ const unknownCursor = await request(app)
225
+ .get('/oxy/log')
226
+ .query({ since: 'f'.repeat(64) });
197
227
  expect(unknownCursor.body.records).toEqual([]);
198
228
  });
199
229
 
@@ -215,11 +245,14 @@ describe('createNodeApp', () => {
215
245
  expect(put.status).toBe(201);
216
246
  expect(put.body).toEqual({ hash, size: bytes.length });
217
247
 
218
- const get = await request(app).get(`/blobs/${hash}`).buffer(true).parse((res, cb) => {
219
- const chunks: Buffer[] = [];
220
- res.on('data', (chunk: Buffer) => chunks.push(chunk));
221
- res.on('end', () => cb(null, Buffer.concat(chunks)));
222
- });
248
+ const get = await request(app)
249
+ .get(`/blobs/${hash}`)
250
+ .buffer(true)
251
+ .parse((res, cb) => {
252
+ const chunks: Buffer[] = [];
253
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
254
+ res.on('end', () => cb(null, Buffer.concat(chunks)));
255
+ });
223
256
  expect(get.status).toBe(200);
224
257
  expect(Buffer.isBuffer(get.body) ? get.body : Buffer.from(get.body)).toEqual(bytes);
225
258
  });
@@ -269,7 +302,9 @@ describe('createNodeApp', () => {
269
302
  const auth = await signBlobPin(hash, owner);
270
303
 
271
304
  const server = createServer(app);
272
- await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve); });
305
+ await new Promise<void>((resolve) => {
306
+ server.listen(0, '127.0.0.1', resolve);
307
+ });
273
308
  const { port } = server.address() as AddressInfo;
274
309
 
275
310
  const raw = await new Promise<string>((resolve, reject) => {
@@ -284,12 +319,14 @@ describe('createNodeApp', () => {
284
319
  'Connection: close',
285
320
  '',
286
321
  '',
287
- ].join('\r\n')
322
+ ].join('\r\n'),
288
323
  );
289
324
  });
290
325
  let received = '';
291
326
  socket.setEncoding('utf8');
292
- socket.on('data', (chunk: string) => { received += chunk; });
327
+ socket.on('data', (chunk: string) => {
328
+ received += chunk;
329
+ });
293
330
  socket.on('error', reject);
294
331
  socket.on('end', () => resolve(received));
295
332
  });
@@ -360,7 +397,11 @@ describe('createNodeApp', () => {
360
397
  // within the window is rejected 429 BEFORE the handler runs, regardless of
361
398
  // each request's chain outcome.
362
399
  const { app } = buildApp(owner, [], { windowMs: 60_000, max: 2 });
363
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
400
+ const envelope = await buildSignedEnvelope({
401
+ privateKey: owner.privateKey,
402
+ seq: 0,
403
+ prev: null,
404
+ });
364
405
 
365
406
  const first = await request(app).post('/records').send(envelope);
366
407
  expect(first.status).toBe(201);
@@ -379,7 +420,12 @@ describe('createNodeApp', () => {
379
420
  let prev: string | null = null;
380
421
  const ids: string[] = [];
381
422
  for (let seq = 0; seq < 3; seq += 1) {
382
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq, prev, record: { seq } });
423
+ const envelope = await buildSignedEnvelope({
424
+ privateKey: owner.privateKey,
425
+ seq,
426
+ prev,
427
+ record: { seq },
428
+ });
383
429
  const id = await computeRecordId(envelope);
384
430
  await request(app).post('/records').send(envelope).expect(201);
385
431
  ids.push(id);
@@ -388,7 +434,9 @@ describe('createNodeApp', () => {
388
434
 
389
435
  // `?since[]=<id1>&since[]=<id2>` arrives as a string[] — the handler must
390
436
  // take the FIRST value (a tampered array can never cause type confusion).
391
- const res = await request(app).get('/oxy/log').query({ 'since[]': [ids[0], ids[2]] });
437
+ const res = await request(app)
438
+ .get('/oxy/log')
439
+ .query({ 'since[]': [ids[0], ids[2]] });
392
440
  expect(res.status).toBe(200);
393
441
  // Cursor resolves off ids[0] → records strictly after seq 0 → [1, 2].
394
442
  expect(res.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([1, 2]);
@@ -398,12 +446,19 @@ describe('createNodeApp', () => {
398
446
  const { app } = buildApp(owner);
399
447
  let prev: string | null = null;
400
448
  for (let seq = 0; seq < 4; seq += 1) {
401
- const envelope = await buildSignedEnvelope({ privateKey: owner.privateKey, seq, prev, record: { seq } });
449
+ const envelope = await buildSignedEnvelope({
450
+ privateKey: owner.privateKey,
451
+ seq,
452
+ prev,
453
+ record: { seq },
454
+ });
402
455
  await request(app).post('/records').send(envelope).expect(201);
403
456
  prev = await computeRecordId(envelope);
404
457
  }
405
458
 
406
- const res = await request(app).get('/oxy/log').query({ 'limit[]': ['2', '99'] });
459
+ const res = await request(app)
460
+ .get('/oxy/log')
461
+ .query({ 'limit[]': ['2', '99'] });
407
462
  expect(res.status).toBe(200);
408
463
  expect(res.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([0, 1]);
409
464
  });
@@ -71,7 +71,9 @@ describe('trimTrailingSlashes', () => {
71
71
  });
72
72
 
73
73
  it('preserves interior slashes and the empty string', () => {
74
- expect(trimTrailingSlashes('https://node.example/oxy/log')).toBe('https://node.example/oxy/log');
74
+ expect(trimTrailingSlashes('https://node.example/oxy/log')).toBe(
75
+ 'https://node.example/oxy/log',
76
+ );
75
77
  expect(trimTrailingSlashes('')).toBe('');
76
78
  expect(trimTrailingSlashes('///')).toBe('');
77
79
  });
@@ -127,7 +129,11 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
127
129
  const genesisId = await computeRecordId(genesis);
128
130
  await client.writeRecord(genesis);
129
131
 
130
- const second = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 1, prev: genesisId });
132
+ const second = await buildSignedEnvelope({
133
+ privateKey: owner.privateKey,
134
+ seq: 1,
135
+ prev: genesisId,
136
+ });
131
137
  const secondId = await computeRecordId(second);
132
138
  expect(await client.writeRecord(second)).toEqual({ recordId: secondId, seq: 1 });
133
139
 
@@ -143,7 +149,10 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
143
149
 
144
150
  it('writeRecord surfaces a chain gap as a NodeClientError (422 chain_gap)', async () => {
145
151
  const gap = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 3, prev: null });
146
- await expect(client.writeRecord(gap)).rejects.toMatchObject({ status: 422, reason: 'chain_gap' });
152
+ await expect(client.writeRecord(gap)).rejects.toMatchObject({
153
+ status: 422,
154
+ reason: 'chain_gap',
155
+ });
147
156
  await expect(client.writeRecord(gap)).rejects.toBeInstanceOf(NodeClientError);
148
157
  });
149
158
 
@@ -151,14 +160,28 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
151
160
  const genesis = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
152
161
  await client.writeRecord(genesis);
153
162
 
154
- const fork = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 1, prev: 'f'.repeat(64) });
155
- await expect(client.writeRecord(fork)).rejects.toMatchObject({ status: 422, reason: 'chain_fork' });
163
+ const fork = await buildSignedEnvelope({
164
+ privateKey: owner.privateKey,
165
+ seq: 1,
166
+ prev: 'f'.repeat(64),
167
+ });
168
+ await expect(client.writeRecord(fork)).rejects.toMatchObject({
169
+ status: 422,
170
+ reason: 'chain_fork',
171
+ });
156
172
  });
157
173
 
158
174
  it('writeRecord surfaces a non-owner write as a NodeClientError (403 not_owner)', async () => {
159
175
  const attacker = generateKeyPair();
160
- const envelope = await buildSignedEnvelope({ privateKey: attacker.privateKey, seq: 0, prev: null });
161
- await expect(client.writeRecord(envelope)).rejects.toMatchObject({ status: 403, reason: 'not_owner' });
176
+ const envelope = await buildSignedEnvelope({
177
+ privateKey: attacker.privateKey,
178
+ seq: 0,
179
+ prev: null,
180
+ });
181
+ await expect(client.writeRecord(envelope)).rejects.toMatchObject({
182
+ status: 403,
183
+ reason: 'not_owner',
184
+ });
162
185
  });
163
186
 
164
187
  it('putBlob then getBlob round-trips; getBlob returns null when absent', async () => {
@@ -95,14 +95,18 @@ export function createInMemoryNodeStore(): RecordStore & BlobStore {
95
95
  },
96
96
  async resolveCursorSeq(_subject, recordId): Promise<number | null> {
97
97
  const found = records.find((r) => r.recordId === recordId);
98
- return found ? found.env.seq ?? -1 : null;
98
+ return found ? (found.env.seq ?? -1) : null;
99
99
  },
100
100
  async materializeCurrent(_subject, collection, rkey): Promise<SignedRecordEnvelope | null> {
101
- const matching = records.filter((r) => r.env.collection === collection && r.env.rkey === rkey);
101
+ const matching = records.filter(
102
+ (r) => r.env.collection === collection && r.env.rkey === rkey,
103
+ );
102
104
  return matching.length ? matching[matching.length - 1].env : null;
103
105
  },
104
106
  async latestIssuedAtForKey(_subject, env): Promise<number | null> {
105
- const matching = records.filter((r) => r.env.collection === env.collection && r.env.rkey === env.rkey);
107
+ const matching = records.filter(
108
+ (r) => r.env.collection === env.collection && r.env.rkey === env.rkey,
109
+ );
106
110
  return matching.length ? matching[matching.length - 1].env.issuedAt : null;
107
111
  },
108
112
  async putBlob(hash, bytes): Promise<void> {
@@ -26,9 +26,7 @@ import { dirname, join, resolve } from 'node:path';
26
26
  const SRC_DIR = resolve(__dirname, '..');
27
27
  const ROOT_ENTRY = join(SRC_DIR, 'index.ts');
28
28
 
29
- const packageJson = JSON.parse(
30
- readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8'),
31
- ) as {
29
+ const packageJson = JSON.parse(readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8')) as {
32
30
  peerDependenciesMeta?: Record<string, { optional?: boolean }>;
33
31
  };
34
32
 
@@ -218,9 +216,13 @@ describe('crypto.native optional-peer degradation', () => {
218
216
  });
219
217
  jest.doMock('expo-crypto', () => fakeCrypto, { virtual: true });
220
218
  jest.doMock('expo-secure-store', () => fakeSecureStore, { virtual: true });
221
- jest.doMock('@react-native-async-storage/async-storage', () => ({ default: fakeAsyncStorage }), {
222
- virtual: true,
223
- });
219
+ jest.doMock(
220
+ '@react-native-async-storage/async-storage',
221
+ () => ({ default: fakeAsyncStorage }),
222
+ {
223
+ virtual: true,
224
+ },
225
+ );
224
226
 
225
227
  const mod = require(MODULE_PATH) as typeof import('../platform/crypto.native');
226
228
 
@@ -209,7 +209,8 @@ describe('createRateLimiter', () => {
209
209
  await jest.isolateModulesAsync(async () => {
210
210
  const reloaded = await import('../node/rateLimit');
211
211
  afterRestart = reloaded.clientRateLimitKey({ ip: ADDRESS } as Request);
212
- stableAfterRestart = reloaded.clientRateLimitKey({ ip: ADDRESS } as Request) === afterRestart;
212
+ stableAfterRestart =
213
+ reloaded.clientRateLimitKey({ ip: ADDRESS } as Request) === afterRestart;
213
214
  });
214
215
 
215
216
  expect(afterRestart).not.toBe(before);
@@ -3,70 +3,68 @@ import {
3
3
  meetsRegistrationPowDifficulty,
4
4
  registrationPowMessage,
5
5
  REGISTRATION_POW_DIFFICULTY_BITS,
6
- } from "../auth/registrationPow";
6
+ } from '../auth/registrationPow';
7
7
 
8
- describe("registrationPowMessage", () => {
9
- test("binds publicKey, timestamp and nonce, distinct from the registration signature message", () => {
10
- expect(registrationPowMessage("pk", 1700000000000, "42")).toBe(
11
- "oxy:register-pow:pk:1700000000000:42",
8
+ describe('registrationPowMessage', () => {
9
+ test('binds publicKey, timestamp and nonce, distinct from the registration signature message', () => {
10
+ expect(registrationPowMessage('pk', 1700000000000, '42')).toBe(
11
+ 'oxy:register-pow:pk:1700000000000:42',
12
12
  );
13
13
  });
14
14
 
15
- test("a different nonce is a different message", () => {
16
- expect(registrationPowMessage("pk", 1, "a")).not.toBe(
17
- registrationPowMessage("pk", 1, "b"),
18
- );
15
+ test('a different nonce is a different message', () => {
16
+ expect(registrationPowMessage('pk', 1, 'a')).not.toBe(registrationPowMessage('pk', 1, 'b'));
19
17
  });
20
18
  });
21
19
 
22
- describe("countLeadingZeroBits", () => {
20
+ describe('countLeadingZeroBits', () => {
23
21
  test.each([
24
- ["", 0],
25
- ["f", 0],
26
- ["8000", 0],
27
- ["0", 4],
28
- ["00", 8],
29
- ["01", 7],
30
- ["0f", 4],
31
- ["1f", 3],
32
- ["2f", 2],
33
- ["4f", 1],
34
- ["0000f", 16],
22
+ ['', 0],
23
+ ['f', 0],
24
+ ['8000', 0],
25
+ ['0', 4],
26
+ ['00', 8],
27
+ ['01', 7],
28
+ ['0f', 4],
29
+ ['1f', 3],
30
+ ['2f', 2],
31
+ ['4f', 1],
32
+ ['0000f', 16],
35
33
  // Case-insensitive: the digest producers this compares against are
36
34
  // lowercase, but the counter must not silently under-count an uppercase one.
37
- ["00F", 8],
38
- ])("counts %s as %i leading zero bits", (hex, expected) => {
35
+ ['00F', 8],
36
+ ])('counts %s as %i leading zero bits', (hex, expected) => {
39
37
  expect(countLeadingZeroBits(hex)).toBe(expected);
40
38
  });
41
39
 
42
- test("stops at the first non-zero nibble rather than scanning the whole digest", () => {
43
- expect(countLeadingZeroBits(`00f${"0".repeat(60)}`)).toBe(8);
40
+ test('stops at the first non-zero nibble rather than scanning the whole digest', () => {
41
+ expect(countLeadingZeroBits(`00f${'0'.repeat(60)}`)).toBe(8);
44
42
  });
45
43
 
46
- test("a non-hex character ends the count instead of throwing", () => {
47
- expect(countLeadingZeroBits("0z")).toBe(4);
48
- expect(() => countLeadingZeroBits("not-hex-at-all")).not.toThrow();
44
+ test('a non-hex character ends the count instead of throwing', () => {
45
+ expect(countLeadingZeroBits('0z')).toBe(4);
46
+ expect(() => countLeadingZeroBits('not-hex-at-all')).not.toThrow();
49
47
  });
50
48
  });
51
49
 
52
- describe("meetsRegistrationPowDifficulty", () => {
53
- test("passes a digest that clears the bound", () => {
54
- expect(meetsRegistrationPowDifficulty("00f0", 8)).toBe(true);
55
- expect(meetsRegistrationPowDifficulty("00f0", 9)).toBe(false);
50
+ describe('meetsRegistrationPowDifficulty', () => {
51
+ test('passes a digest that clears the bound', () => {
52
+ expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
53
+ expect(meetsRegistrationPowDifficulty('00f0', 9)).toBe(false);
56
54
  });
57
55
 
58
- test("an exact match at the bound passes", () => {
59
- expect(meetsRegistrationPowDifficulty("00f0", 8)).toBe(true);
56
+ test('an exact match at the bound passes', () => {
57
+ expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
60
58
  });
61
59
 
62
- test("the exported difficulty constant is a defensible, positive bound", () => {
60
+ test('the exported difficulty constant is a defensible, positive bound', () => {
63
61
  // Pinned as a regression check on the constant itself: a change here is a
64
62
  // deliberate retune, not an accidental one, and shows up in review.
65
63
  expect(REGISTRATION_POW_DIFFICULTY_BITS).toBe(16);
66
- expect(meetsRegistrationPowDifficulty("0".repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
64
+ expect(meetsRegistrationPowDifficulty('0'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
67
65
  true,
68
66
  );
69
- expect(meetsRegistrationPowDifficulty("f".repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
67
+ expect(meetsRegistrationPowDifficulty('f'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
70
68
  false,
71
69
  );
72
70
  });
@@ -8,111 +8,82 @@ import {
8
8
  normalizeSecp256k1PublicKey,
9
9
  signSecp256k1Digest,
10
10
  verifySecp256k1Digest,
11
- } from "../secp256k1";
11
+ } from '../secp256k1';
12
12
 
13
- const PRIVATE_KEY_ONE = `${"0".repeat(63)}1`;
14
- const PRIVATE_KEY_TWO = `${"0".repeat(63)}2`;
15
- const DIGEST_ONE = `${"0".repeat(63)}1`;
13
+ const PRIVATE_KEY_ONE = `${'0'.repeat(63)}1`;
14
+ const PRIVATE_KEY_TWO = `${'0'.repeat(63)}2`;
15
+ const DIGEST_ONE = `${'0'.repeat(63)}1`;
16
16
 
17
17
  const PUBLIC_KEY_ONE =
18
- "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" +
19
- "483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8";
18
+ '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' +
19
+ '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8';
20
20
  const COMPRESSED_PUBLIC_KEY_ONE =
21
- "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
21
+ '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
22
22
  const PUBLIC_KEY_TWO =
23
- "04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" +
24
- "1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a";
23
+ '04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5' +
24
+ '1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a';
25
25
 
26
26
  // Exact DER output produced by elliptic 6.6.1 for this RFC 6979 vector.
27
27
  const HISTORICAL_HIGH_S_SIGNATURE =
28
- "304502206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898" +
29
- "022100b3e568e9ad1f52577fedf107fda18f5ebb8e5c220badf23532bf6fbcc67fb4b8";
28
+ '304502206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
29
+ '022100b3e568e9ad1f52577fedf107fda18f5ebb8e5c220badf23532bf6fbcc67fb4b8';
30
30
  const LOW_S_SIGNATURE =
31
- "304402206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898" +
32
- "02204c1a971652e0ada880120ef8025e709fff2080c4a39aae068d12eed009b68c89";
31
+ '304402206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
32
+ '02204c1a971652e0ada880120ef8025e709fff2080c4a39aae068d12eed009b68c89';
33
33
 
34
- describe("secp256k1 primitives", () => {
35
- test("preserves the canonical private/public key formats", () => {
36
- expect(normalizeSecp256k1PrivateKey("1")).toBe(PRIVATE_KEY_ONE);
34
+ describe('secp256k1 primitives', () => {
35
+ test('preserves the canonical private/public key formats', () => {
36
+ expect(normalizeSecp256k1PrivateKey('1')).toBe(PRIVATE_KEY_ONE);
37
37
  expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
38
- expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(
39
- COMPRESSED_PUBLIC_KEY_ONE,
40
- );
41
- expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(
42
- PUBLIC_KEY_ONE,
43
- );
44
- expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(
45
- COMPRESSED_PUBLIC_KEY_ONE,
46
- );
38
+ expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
39
+ expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
40
+ expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
47
41
  });
48
42
 
49
- test("matches the historical deterministic DER wire format", () => {
43
+ test('matches the historical deterministic DER wire format', () => {
50
44
  const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE);
51
45
  expect(signature).toBe(HISTORICAL_HIGH_S_SIGNATURE);
52
- expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
53
- true,
54
- );
55
- expect(
56
- verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature),
57
- ).toBe(true);
46
+ expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
47
+ expect(verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
58
48
  });
59
49
 
60
- test("supports explicit low-S signatures while accepting both historical forms", () => {
50
+ test('supports explicit low-S signatures while accepting both historical forms', () => {
61
51
  const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE, {
62
52
  lowS: true,
63
53
  });
64
54
  expect(signature).toBe(LOW_S_SIGNATURE);
65
- expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
55
+ expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
56
+ expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, HISTORICAL_HIGH_S_SIGNATURE)).toBe(
66
57
  true,
67
58
  );
68
- expect(
69
- verifySecp256k1Digest(
70
- PUBLIC_KEY_ONE,
71
- DIGEST_ONE,
72
- HISTORICAL_HIGH_S_SIGNATURE,
73
- ),
74
- ).toBe(true);
75
59
  });
76
60
 
77
- test("derives the fixed-width ECDH x-coordinate symmetrically", () => {
78
- const expected =
79
- "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5";
80
- const oneToTwo = deriveSecp256k1SharedSecret(
81
- PRIVATE_KEY_ONE,
82
- PUBLIC_KEY_TWO,
83
- );
84
- const twoToOne = deriveSecp256k1SharedSecret(
85
- PRIVATE_KEY_TWO,
86
- PUBLIC_KEY_ONE,
87
- );
61
+ test('derives the fixed-width ECDH x-coordinate symmetrically', () => {
62
+ const expected = 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5';
63
+ const oneToTwo = deriveSecp256k1SharedSecret(PRIVATE_KEY_ONE, PUBLIC_KEY_TWO);
64
+ const twoToOne = deriveSecp256k1SharedSecret(PRIVATE_KEY_TWO, PUBLIC_KEY_ONE);
88
65
 
89
- expect(Buffer.from(oneToTwo).toString("hex")).toBe(expected);
66
+ expect(Buffer.from(oneToTwo).toString('hex')).toBe(expected);
90
67
  expect(oneToTwo).toEqual(twoToOne);
91
68
  expect(oneToTwo).toHaveLength(32);
92
69
  });
93
70
 
94
- test("generates canonical key pairs that round-trip", () => {
71
+ test('generates canonical key pairs that round-trip', () => {
95
72
  const keyPair = generateSecp256k1KeyPair();
96
73
  expect(keyPair.privateKey).toMatch(/^[0-9a-f]{64}$/);
97
74
  expect(keyPair.publicKey).toMatch(/^04[0-9a-f]{128}$/);
98
- expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(
99
- keyPair.publicKey,
100
- );
75
+ expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(keyPair.publicKey);
101
76
  });
102
77
 
103
- test("strictly rejects malformed scalars, points, digests, and DER", () => {
104
- expect(isValidSecp256k1PrivateKey("0")).toBe(false);
105
- expect(isValidSecp256k1PrivateKey("zz")).toBe(false);
106
- expect(isValidSecp256k1PrivateKey("f".repeat(66))).toBe(false);
107
- expect(isValidSecp256k1PublicKey(`04${"0".repeat(128)}`)).toBe(false);
108
- expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(
109
- false,
110
- );
111
- expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, "00")).toThrow(
112
- "digest must be exactly 32 bytes",
78
+ test('strictly rejects malformed scalars, points, digests, and DER', () => {
79
+ expect(isValidSecp256k1PrivateKey('0')).toBe(false);
80
+ expect(isValidSecp256k1PrivateKey('zz')).toBe(false);
81
+ expect(isValidSecp256k1PrivateKey('f'.repeat(66))).toBe(false);
82
+ expect(isValidSecp256k1PublicKey(`04${'0'.repeat(128)}`)).toBe(false);
83
+ expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(false);
84
+ expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, '00')).toThrow(
85
+ 'digest must be exactly 32 bytes',
113
86
  );
114
- expect(() =>
115
- verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, "00"),
116
- ).toThrow();
87
+ expect(() => verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, '00')).toThrow();
117
88
  });
118
89
  });
@@ -216,7 +216,10 @@ describe('verifyInclusionProof', () => {
216
216
  const entries = heads(5);
217
217
  const tree = await buildTransparencyTreeFromHeads(entries);
218
218
  const proof = inclusionProof(tree, 2);
219
- const tamperedLeaf = await transparencyLeafHash({ ...entries[2], headRecordId: 'f'.repeat(64) });
219
+ const tamperedLeaf = await transparencyLeafHash({
220
+ ...entries[2],
221
+ headRecordId: 'f'.repeat(64),
222
+ });
220
223
  await expect(
221
224
  verifyInclusionProof({ leaf: tamperedLeaf, index: 2, treeSize: 5, proof, root: tree.root }),
222
225
  ).resolves.toBe(false);
@@ -89,7 +89,11 @@ const NIBBLE_LEADING_ZERO_BITS: Readonly<Record<string, number>> = {
89
89
  * vice versa — the two are unrelated preimages even for the same
90
90
  * `publicKey`/`timestamp` pair.
91
91
  */
92
- export function registrationPowMessage(publicKey: string, timestamp: number, nonce: string): string {
92
+ export function registrationPowMessage(
93
+ publicKey: string,
94
+ timestamp: number,
95
+ nonce: string,
96
+ ): string {
93
97
  return `oxy:register-pow:${publicKey}:${timestamp}:${nonce}`;
94
98
  }
95
99
 
@@ -9,7 +9,7 @@
9
9
  * engine owns the verification + ordering policy.
10
10
  *
11
11
  * Storage and identity are both injected, so the engine has zero knowledge of
12
- * Mongo/SQLite, Oxy DIDs, or any app's lexicon — exactly what makes it reusable.
12
+ * the database, Oxy DIDs, or any app's lexicon — exactly what makes it reusable.
13
13
  */
14
14
 
15
15
  import type { SignedRecordEnvelope } from '@oxy.so/contracts';
@@ -4,8 +4,8 @@
4
4
  * The engine ({@link ./verify}, {@link ./engine}) is storage-agnostic: it owns
5
5
  * the verification state machine and continuity logic, and delegates EVERY read
6
6
  * and write to an injected {@link RecordStore}. An app supplies a store over its
7
- * own backend (oxy-api over Mongo `SignedRecord`/`RepoHead`, a node over SQLite,
8
- * Mention over its own Mongo) without the engine knowing anything Oxy- or
7
+ * own backend (oxy-api over Postgres `signed_records`/`repo_heads`, a node over
8
+ * SQLite, Mention over its own database) without the engine knowing anything Oxy- or
9
9
  * app-specific.
10
10
  *
11
11
  * All methods are **subject-keyed**: `subject` is the chain's subject DID
@@ -17,7 +17,7 @@
17
17
  * `append` MUST be atomic (record insert + head advance in one unit) and MUST
18
18
  * translate a duplicate-key collision on the unique `(subject, seq)` /
19
19
  * `recordId` index — i.e. a concurrent writer that already took this `seq` — into
20
- * `{ ok: false, reason: 'chain_conflict' }` (Mongo E11000 / SQLite
20
+ * `{ ok: false, reason: 'chain_conflict' }` (Postgres `23505` / SQLite
21
21
  * `SQLITE_CONSTRAINT`). That is the real multi-writer race guard; the engine's
22
22
  * pre-append continuity check is only the fast-path rejection.
23
23
  */