@oxy.so/protocol 1.2.1 → 1.3.0

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/.tsbuildinfo +1 -1
  2. package/dist/cjs/chain/engine.js +1 -1
  3. package/dist/cjs/chain/recordStore.js +3 -3
  4. package/dist/cjs/chain/verify.js +8 -4
  5. package/dist/cjs/envelope/signingInput.js +12 -1
  6. package/dist/cjs/identity/resolver.js +2 -1
  7. package/dist/cjs/node/didWebResolver.js +1 -3
  8. package/dist/cjs/node/nodeApp.js +9 -3
  9. package/dist/cjs/node/nodeClient.js +7 -2
  10. package/dist/cjs/platform/platform.js +1 -1
  11. package/dist/cjs/secp256k1.js +25 -26
  12. package/dist/cjs/sign.js +17 -0
  13. package/dist/esm/.tsbuildinfo +1 -1
  14. package/dist/esm/chain/engine.js +1 -1
  15. package/dist/esm/chain/recordStore.js +3 -3
  16. package/dist/esm/chain/verify.js +8 -4
  17. package/dist/esm/envelope/sign.js +1 -1
  18. package/dist/esm/envelope/signingInput.js +12 -1
  19. package/dist/esm/identity/resolver.js +2 -1
  20. package/dist/esm/node/didWebResolver.js +1 -3
  21. package/dist/esm/node/nodeApp.js +9 -3
  22. package/dist/esm/node/nodeClient.js +8 -3
  23. package/dist/esm/platform/platform.js +1 -1
  24. package/dist/esm/secp256k1.js +26 -27
  25. package/dist/esm/sign.js +10 -0
  26. package/dist/types/.tsbuildinfo +1 -1
  27. package/dist/types/chain/engine.d.ts +1 -1
  28. package/dist/types/chain/recordStore.d.ts +3 -3
  29. package/dist/types/chain/verify.d.ts +3 -2
  30. package/dist/types/sign.d.ts +10 -0
  31. package/package.json +22 -8
  32. package/src/__tests__/canonicalJson.test.ts +2 -6
  33. package/src/__tests__/chain.test.ts +42 -11
  34. package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
  35. package/src/__tests__/didWebResolver.test.ts +36 -7
  36. package/src/__tests__/nodeApp.test.ts +76 -21
  37. package/src/__tests__/nodeClient.test.ts +30 -7
  38. package/src/__tests__/nodeHarness.ts +7 -3
  39. package/src/__tests__/optionalNativePeers.test.ts +8 -6
  40. package/src/__tests__/rateLimit.test.ts +2 -1
  41. package/src/__tests__/registrationPow.test.ts +35 -37
  42. package/src/__tests__/secp256k1.test.ts +41 -70
  43. package/src/__tests__/transparency.test.ts +4 -1
  44. package/src/auth/registrationPow.ts +5 -1
  45. package/src/chain/engine.ts +1 -1
  46. package/src/chain/recordStore.ts +3 -3
  47. package/src/chain/verify.ts +11 -4
  48. package/src/envelope/recordId.ts +1 -4
  49. package/src/envelope/sign.ts +6 -6
  50. package/src/envelope/signingInput.ts +12 -1
  51. package/src/identity/resolver.ts +9 -2
  52. package/src/node/didWebResolver.ts +2 -7
  53. package/src/node/nodeApp.ts +85 -67
  54. package/src/node/nodeClient.ts +35 -16
  55. package/src/platform/crypto.native.ts +9 -2
  56. package/src/platform/platform.ts +3 -1
  57. package/src/secp256k1.ts +33 -58
  58. package/src/sign.ts +16 -0
@@ -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
  */
@@ -10,7 +10,8 @@
10
10
  * - the {@link RecordStore} owns the monotonicity frontier + the chain head.
11
11
  *
12
12
  * The ordered checks (first failure wins):
13
- * 1. **shape** — the base `signedRecordEnvelopeSchema` (open `type`, opaque
13
+ * 1. **shape** — the base `signedRecordEnvelopeCheck`, the Zod-free twin of
14
+ * `signedRecordEnvelopeSchema` (open `type`, opaque
14
15
  * `record`). An app re-narrows `type` to its own set in its adapter.
15
16
  * 2. **signature** — `verifyEnvelopeSignature` recomputes the canonical signing
16
17
  * input and checks the secp256k1 signature against the embedded `publicKey`.
@@ -25,7 +26,8 @@
25
26
  * caller makes before invoking the engine, not a property of the envelope.
26
27
  */
27
28
 
28
- import { signedRecordEnvelopeSchema, type SignedRecordEnvelope } from '@oxy.so/contracts';
29
+ import type { SignedRecordEnvelope } from '@oxy.so/contracts';
30
+ import { checkContract, signedRecordEnvelopeCheck } from '@oxy.so/contracts/client';
29
31
  import { verifyEnvelopeSignature } from '../envelope/sign';
30
32
  import { isAuthorizedKey, type VerificationMethodResolver } from '../identity/resolver';
31
33
  import { checkContinuity } from './continuity';
@@ -56,7 +58,7 @@ export async function verifyEnvelope(
56
58
  ): Promise<VerifyOutcome> {
57
59
  // 1. Base envelope shape (open `type`, `record` opaque). An app's adapter
58
60
  // re-narrows `type` to its own accepted set before/around this call.
59
- if (!signedRecordEnvelopeSchema.safeParse(env).success) {
61
+ if (checkContract(signedRecordEnvelopeCheck, env) === null) {
60
62
  return { ok: false, reason: 'invalid_envelope' };
61
63
  }
62
64
 
@@ -67,7 +69,12 @@ export async function verifyEnvelope(
67
69
  }
68
70
 
69
71
  // Enforce that v2 envelopes strictly contain required chain fields.
70
- if (env.version === 2 && (typeof env.seq !== 'number' || typeof env.collection !== 'string' || typeof env.rkey !== 'string')) {
72
+ if (
73
+ env.version === 2 &&
74
+ (typeof env.seq !== 'number' ||
75
+ typeof env.collection !== 'string' ||
76
+ typeof env.rkey !== 'string')
77
+ ) {
71
78
  return { ok: false, reason: 'invalid_envelope' };
72
79
  }
73
80
 
@@ -23,10 +23,7 @@ export async function sha256(message: string): Promise<string> {
23
23
  // In React Native, use expo-crypto
24
24
  if (isReactNative()) {
25
25
  const Crypto = await loadExpoCrypto();
26
- return Crypto.digestStringAsync(
27
- Crypto.CryptoDigestAlgorithm.SHA256,
28
- message,
29
- );
26
+ return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
30
27
  }
31
28
 
32
29
  if (isNodeJS()) {
@@ -10,11 +10,7 @@
10
10
  import type { SignedRecordEnvelope } from '@oxy.so/contracts';
11
11
  import { signedRecordSigningInput, type SignedRecordSigningFields } from './signingInput';
12
12
  import { sha256 } from './recordId';
13
- import {
14
- deriveSecp256k1PublicKey,
15
- signSecp256k1Digest,
16
- verifySecp256k1Digest,
17
- } from '../secp256k1';
13
+ import { deriveSecp256k1PublicKey, signSecp256k1Digest, verifySecp256k1Digest } from '../secp256k1';
18
14
 
19
15
  /** The one signature algorithm identifier the protocol emits. */
20
16
  const ALG = 'ES256K-DER-SHA256' as const;
@@ -82,5 +78,9 @@ export async function signEnvelope(
82
78
  * server / node owner check.
83
79
  */
84
80
  export async function verifyEnvelopeSignature(envelope: SignedRecordEnvelope): Promise<boolean> {
85
- return verifySignature(signedRecordSigningInput(envelope), envelope.signature, envelope.publicKey);
81
+ return verifySignature(
82
+ signedRecordSigningInput(envelope),
83
+ envelope.signature,
84
+ envelope.publicKey,
85
+ );
86
86
  }
@@ -42,7 +42,18 @@ export function signedRecordSigningInput(fields: SignedRecordSigningFields): str
42
42
  const { version, type, subject, issuer, record, issuedAt } = fields;
43
43
  if (version === 2) {
44
44
  const { seq, prev, collection, rkey } = fields;
45
- return canonicalize({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
45
+ return canonicalize({
46
+ version,
47
+ type,
48
+ subject,
49
+ issuer,
50
+ record,
51
+ issuedAt,
52
+ seq,
53
+ prev,
54
+ collection,
55
+ rkey,
56
+ });
46
57
  }
47
58
  return canonicalize({ version, type, subject, issuer, record, issuedAt });
48
59
  }
@@ -50,7 +50,13 @@ export interface VerificationMethodResolver {
50
50
  /** Verdict of the key-authorization decision (a subset of {@link RejectionReason}). */
51
51
  export type KeyAuthorization =
52
52
  | { ok: true }
53
- | { ok: false; reason: Extract<RejectionReason, 'public_key_not_a_current_verification_method' | 'untrusted_issuer'> };
53
+ | {
54
+ ok: false;
55
+ reason: Extract<
56
+ RejectionReason,
57
+ 'public_key_not_a_current_verification_method' | 'untrusted_issuer'
58
+ >;
59
+ };
54
60
 
55
61
  /**
56
62
  * Decide whether `env`'s signing key (`env.publicKey`) is authorized for its
@@ -81,7 +87,8 @@ export function isAuthorizedKey(
81
87
  resolved.custodialIssuer !== undefined &&
82
88
  env.issuer === resolved.custodialIssuer
83
89
  ) {
84
- return resolved.custodialPublicKey !== undefined && env.publicKey === resolved.custodialPublicKey
90
+ return resolved.custodialPublicKey !== undefined &&
91
+ env.publicKey === resolved.custodialPublicKey
85
92
  ? { ok: true }
86
93
  : { ok: false, reason: 'public_key_not_a_current_verification_method' };
87
94
  }
@@ -22,10 +22,7 @@ import {
22
22
  type DidDocument,
23
23
  type Secp256k1VerificationMethod,
24
24
  } from '@oxy.so/contracts';
25
- import type {
26
- ResolvedVerificationMethods,
27
- VerificationMethodResolver,
28
- } from '../identity/resolver';
25
+ import type { ResolvedVerificationMethods, VerificationMethodResolver } from '../identity/resolver';
29
26
  import { type NodeFetch, readBoundedJson } from './httpFetch';
30
27
  import {
31
28
  DEFAULT_CLIENT_MAX_REDIRECTS,
@@ -95,9 +92,7 @@ function isSecp256k1Vm(
95
92
  */
96
93
  function collectCurrentPublicKeys(doc: DidDocument): string[] {
97
94
  const byId = new Map(
98
- doc.verificationMethod
99
- .filter(isSecp256k1Vm)
100
- .map((vm) => [vm.id, vm.publicKeyHex] as const),
95
+ doc.verificationMethod.filter(isSecp256k1Vm).map((vm) => [vm.id, vm.publicKeyHex] as const),
101
96
  );
102
97
  const keys: string[] = [];
103
98
  for (const id of doc.assertionMethod) {