@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
@@ -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
  import { computeRecordId } from '../envelope/recordId.js';
15
15
  import { verifyEnvelope } from './verify.js';
@@ -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`.
@@ -24,7 +25,7 @@
24
25
  * subject?") is intentionally NOT here — it is an adapter-policy decision the
25
26
  * caller makes before invoking the engine, not a property of the envelope.
26
27
  */
27
- import { signedRecordEnvelopeSchema } from '@oxy.so/contracts';
28
+ import { checkContract, signedRecordEnvelopeCheck } from '@oxy.so/contracts/client';
28
29
  import { verifyEnvelopeSignature } from '../envelope/sign.js';
29
30
  import { isAuthorizedKey } from '../identity/resolver.js';
30
31
  import { checkContinuity } from './continuity.js';
@@ -38,7 +39,7 @@ export const DEFAULT_CLOCK_SKEW_MS = 5 * 60 * 1000;
38
39
  export async function verifyEnvelope(store, resolver, env, opts = {}) {
39
40
  // 1. Base envelope shape (open `type`, `record` opaque). An app's adapter
40
41
  // re-narrows `type` to its own accepted set before/around this call.
41
- if (!signedRecordEnvelopeSchema.safeParse(env).success) {
42
+ if (checkContract(signedRecordEnvelopeCheck, env) === null) {
42
43
  return { ok: false, reason: 'invalid_envelope' };
43
44
  }
44
45
  // 2. Signature is internally consistent with the embedded `publicKey`. Cheap,
@@ -47,7 +48,10 @@ export async function verifyEnvelope(store, resolver, env, opts = {}) {
47
48
  return { ok: false, reason: 'bad_signature' };
48
49
  }
49
50
  // Enforce that v2 envelopes strictly contain required chain fields.
50
- if (env.version === 2 && (typeof env.seq !== 'number' || typeof env.collection !== 'string' || typeof env.rkey !== 'string')) {
51
+ if (env.version === 2 &&
52
+ (typeof env.seq !== 'number' ||
53
+ typeof env.collection !== 'string' ||
54
+ typeof env.rkey !== 'string')) {
51
55
  return { ok: false, reason: 'invalid_envelope' };
52
56
  }
53
57
  // 3. The signing key is an authorized writer for the issuer (self-issued ⇒ a
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { signedRecordSigningInput } from './signingInput.js';
10
10
  import { sha256 } from './recordId.js';
11
- import { deriveSecp256k1PublicKey, signSecp256k1Digest, verifySecp256k1Digest, } from '../secp256k1.js';
11
+ import { deriveSecp256k1PublicKey, signSecp256k1Digest, verifySecp256k1Digest } from '../secp256k1.js';
12
12
  /** The one signature algorithm identifier the protocol emits. */
13
13
  const ALG = 'ES256K-DER-SHA256';
14
14
  /**
@@ -23,7 +23,18 @@ export function signedRecordSigningInput(fields) {
23
23
  const { version, type, subject, issuer, record, issuedAt } = fields;
24
24
  if (version === 2) {
25
25
  const { seq, prev, collection, rkey } = fields;
26
- return canonicalize({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
26
+ return canonicalize({
27
+ version,
28
+ type,
29
+ subject,
30
+ issuer,
31
+ record,
32
+ issuedAt,
33
+ seq,
34
+ prev,
35
+ collection,
36
+ rkey,
37
+ });
27
38
  }
28
39
  return canonicalize({ version, type, subject, issuer, record, issuedAt });
29
40
  }
@@ -39,7 +39,8 @@ export function isAuthorizedKey(resolved, env) {
39
39
  if (resolved !== null &&
40
40
  resolved.custodialIssuer !== undefined &&
41
41
  env.issuer === resolved.custodialIssuer) {
42
- return resolved.custodialPublicKey !== undefined && env.publicKey === resolved.custodialPublicKey
42
+ return resolved.custodialPublicKey !== undefined &&
43
+ env.publicKey === resolved.custodialPublicKey
43
44
  ? { ok: true }
44
45
  : { ok: false, reason: 'public_key_not_a_current_verification_method' };
45
46
  }
@@ -60,9 +60,7 @@ function isSecp256k1Vm(vm) {
60
60
  * signatures verify against the hex key.
61
61
  */
62
62
  function collectCurrentPublicKeys(doc) {
63
- const byId = new Map(doc.verificationMethod
64
- .filter(isSecp256k1Vm)
65
- .map((vm) => [vm.id, vm.publicKeyHex]));
63
+ const byId = new Map(doc.verificationMethod.filter(isSecp256k1Vm).map((vm) => [vm.id, vm.publicKeyHex]));
66
64
  const keys = [];
67
65
  for (const id of doc.assertionMethod) {
68
66
  const key = byId.get(id);
@@ -145,7 +145,9 @@ export function createNodeApp(deps) {
145
145
  mode: config.mode,
146
146
  version: config.protocolId,
147
147
  serviceType: config.serviceType,
148
- head: head && head.headRecordId !== null ? { seq: head.seq, headRecordId: head.headRecordId } : null,
148
+ head: head && head.headRecordId !== null
149
+ ? { seq: head.seq, headRecordId: head.headRecordId }
150
+ : null,
149
151
  });
150
152
  }
151
153
  catch (error) {
@@ -172,7 +174,9 @@ export function createNodeApp(deps) {
172
174
  const since = firstQueryValue(req.query.since);
173
175
  const limit = parseLimit(req.query.limit);
174
176
  const head = await store.getHead(subject);
175
- const headWire = head && head.headRecordId !== null ? { seq: head.seq, headRecordId: head.headRecordId } : null;
177
+ const headWire = head && head.headRecordId !== null
178
+ ? { seq: head.seq, headRecordId: head.headRecordId }
179
+ : null;
176
180
  const sinceSeq = await resolveSinceSeq(store, subject, since);
177
181
  if (sinceSeq === null) {
178
182
  res.json({ records: [], count: 0, head: headWire });
@@ -217,7 +221,9 @@ export function createNodeApp(deps) {
217
221
  app.post(NODE_SYNC_PUSH_PATH, writeRateLimit, jsonParser, async (req, res, next) => {
218
222
  try {
219
223
  const body = req.body;
220
- const items = typeof body === 'object' && body !== null && Array.isArray(body.records)
224
+ const items = typeof body === 'object' &&
225
+ body !== null &&
226
+ Array.isArray(body.records)
221
227
  ? body.records
222
228
  : null;
223
229
  if (!items) {
@@ -11,7 +11,7 @@
11
11
  * response body is read with a hard byte ceiling, so a node cannot stream an
12
12
  * unbounded body into the caller.
13
13
  */
14
- import { readBoundedBytes, readBoundedJson, } from './httpFetch.js';
14
+ import { readBoundedBytes, readBoundedJson } from './httpFetch.js';
15
15
  import { DEFAULT_CLIENT_MAX_REDIRECTS, DEFAULT_CLIENT_TIMEOUT_MS, DEFAULT_HEAD_MAX_BYTES, DEFAULT_LOG_MAX_BYTES, DEFAULT_MAX_BLOB_BYTES, DEFAULT_WRITE_RESPONSE_MAX_BYTES, NODE_BLOBS_PATH, NODE_HEAD_PATH, NODE_LOG_PATH, NODE_RECORDS_PATH, NODE_SYNC_PUSH_PATH, OWNER_AUTH_HEADERS, } from './constants.js';
16
16
  /** A non-2xx node response (or a node that returned a malformed body). */
17
17
  export class NodeClientError extends Error {
@@ -23,7 +23,9 @@ export class NodeClientError extends Error {
23
23
  }
24
24
  }
25
25
  function readError(body) {
26
- if (typeof body === 'object' && body !== null && typeof body.error === 'string') {
26
+ if (typeof body === 'object' &&
27
+ body !== null &&
28
+ typeof body.error === 'string') {
27
29
  return body.error;
28
30
  }
29
31
  return undefined;
@@ -103,7 +105,10 @@ export class NodeClient {
103
105
  headRaw !== null &&
104
106
  typeof headRaw.seq === 'number' &&
105
107
  typeof headRaw.headRecordId === 'string'
106
- ? { seq: headRaw.seq, headRecordId: headRaw.headRecordId }
108
+ ? {
109
+ seq: headRaw.seq,
110
+ headRecordId: headRaw.headRecordId,
111
+ }
107
112
  : null;
108
113
  return { records, count: records.length, head };
109
114
  }
@@ -25,5 +25,5 @@ export function isReactNative() {
25
25
  * `randomBytes`) and the `await import('node:crypto')` loader.
26
26
  */
27
27
  export function isNodeJS() {
28
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
28
+ return (typeof process !== 'undefined' && process.versions != null && process.versions.node != null);
29
29
  }
@@ -6,14 +6,14 @@
6
6
  * RFC 6979 deterministic ECDSA signatures encoded as DER, and the 32-byte
7
7
  * ECDH x-coordinate. Callers never receive a library-specific key object.
8
8
  */
9
- import { secp256k1 } from "@noble/curves/secp256k1.js";
9
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
10
10
  const HEX = /^[0-9a-fA-F]+$/;
11
11
  const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
12
12
  const UNCOMPRESSED_PUBLIC_KEY = /^04[0-9a-fA-F]{128}$/;
13
13
  function bytesToHex(bytes) {
14
- let result = "";
14
+ let result = '';
15
15
  for (const byte of bytes) {
16
- result += byte.toString(16).padStart(2, "0");
16
+ result += byte.toString(16).padStart(2, '0');
17
17
  }
18
18
  return result;
19
19
  }
@@ -32,16 +32,16 @@ function hexToBytes(value, label) {
32
32
  * The scalar must be within the secp256k1 order; zero and overflow are rejected.
33
33
  */
34
34
  export function normalizeSecp256k1PrivateKey(privateKeyHex) {
35
- if (typeof privateKeyHex !== "string" ||
35
+ if (typeof privateKeyHex !== 'string' ||
36
36
  privateKeyHex.length === 0 ||
37
37
  privateKeyHex.length > 64 ||
38
38
  !HEX.test(privateKeyHex)) {
39
- throw new Error("secp256k1 private key must contain 1 to 64 hexadecimal characters");
39
+ throw new Error('secp256k1 private key must contain 1 to 64 hexadecimal characters');
40
40
  }
41
- const normalized = privateKeyHex.toLowerCase().padStart(64, "0");
42
- const privateKey = hexToBytes(normalized, "secp256k1 private key");
41
+ const normalized = privateKeyHex.toLowerCase().padStart(64, '0');
42
+ const privateKey = hexToBytes(normalized, 'secp256k1 private key');
43
43
  if (!secp256k1.utils.isValidSecretKey(privateKey)) {
44
- throw new Error("secp256k1 private key is outside the valid scalar range");
44
+ throw new Error('secp256k1 private key is outside the valid scalar range');
45
45
  }
46
46
  return normalized;
47
47
  }
@@ -56,24 +56,23 @@ export function isValidSecp256k1PrivateKey(privateKeyHex) {
56
56
  }
57
57
  }
58
58
  function parsePrivateKey(privateKeyHex) {
59
- return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), "secp256k1 private key");
59
+ return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), 'secp256k1 private key');
60
60
  }
61
61
  function parsePublicKey(publicKeyHex) {
62
- if (typeof publicKeyHex !== "string" ||
63
- (!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
64
- !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
65
- throw new Error("secp256k1 public key must be a compressed or uncompressed SEC1 hex key");
62
+ if (typeof publicKeyHex !== 'string' ||
63
+ (!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) && !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
64
+ throw new Error('secp256k1 public key must be a compressed or uncompressed SEC1 hex key');
66
65
  }
67
- const publicKey = hexToBytes(publicKeyHex, "secp256k1 public key");
66
+ const publicKey = hexToBytes(publicKeyHex, 'secp256k1 public key');
68
67
  // Parsing validates the SEC1 prefix, coordinate range, and curve equation.
69
68
  secp256k1.Point.fromBytes(publicKey);
70
69
  return publicKey;
71
70
  }
72
71
  function parseDigest(digestHex) {
73
- if (typeof digestHex !== "string" || digestHex.length !== 64) {
74
- throw new Error("secp256k1 digest must be exactly 32 bytes of hexadecimal data");
72
+ if (typeof digestHex !== 'string' || digestHex.length !== 64) {
73
+ throw new Error('secp256k1 digest must be exactly 32 bytes of hexadecimal data');
75
74
  }
76
- return hexToBytes(digestHex, "secp256k1 digest");
75
+ return hexToBytes(digestHex, 'secp256k1 digest');
77
76
  }
78
77
  /** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
79
78
  export function isValidSecp256k1PublicKey(publicKeyHex) {
@@ -113,17 +112,17 @@ export function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
113
112
  const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
114
113
  lowS: options.lowS ?? false,
115
114
  });
116
- return signature.toHex("der");
115
+ return signature.toHex('der');
117
116
  }
118
117
  /** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
119
118
  export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
120
119
  const publicKey = parsePublicKey(publicKeyHex);
121
120
  const digest = parseDigest(digestHex);
122
- const signature = hexToBytes(signatureDerHex, "secp256k1 DER signature");
121
+ const signature = hexToBytes(signatureDerHex, 'secp256k1 DER signature');
123
122
  // Parse once up front so malformed or non-DER input is rejected explicitly.
124
- secp256k1.Signature.fromBytes(signature, "der");
123
+ secp256k1.Signature.fromBytes(signature, 'der');
125
124
  return secp256k1.verify(signature, digest, publicKey, {
126
- format: "der",
125
+ format: 'der',
127
126
  lowS: false,
128
127
  });
129
128
  }
@@ -137,24 +136,24 @@ export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex)
137
136
  export function tweakAddSecp256k1PrivateKey(privateKeyHex, tweakHex) {
138
137
  const n = secp256k1.Point.Fn.ORDER;
139
138
  const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
140
- if (typeof tweakHex !== "string" || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
141
- throw new Error("secp256k1 tweak must be exactly 32 bytes of hexadecimal data");
139
+ if (typeof tweakHex !== 'string' || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
140
+ throw new Error('secp256k1 tweak must be exactly 32 bytes of hexadecimal data');
142
141
  }
143
142
  const tweak = BigInt(`0x${tweakHex}`);
144
143
  if (tweak >= n) {
145
- throw new Error("secp256k1 tweak is not below the curve order");
144
+ throw new Error('secp256k1 tweak is not below the curve order');
146
145
  }
147
146
  const child = (key + tweak) % n;
148
147
  if (child === 0n) {
149
- throw new Error("secp256k1 tweaked private key is zero");
148
+ throw new Error('secp256k1 tweaked private key is zero');
150
149
  }
151
- return child.toString(16).padStart(64, "0");
150
+ return child.toString(16).padStart(64, '0');
152
151
  }
153
152
  /** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
154
153
  export function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
155
154
  const encodedPoint = secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);
156
155
  if (encodedPoint.length !== 33) {
157
- throw new Error("secp256k1 ECDH returned an unexpected point encoding");
156
+ throw new Error('secp256k1 ECDH returned an unexpected point encoding');
158
157
  }
159
158
  return encodedPoint.slice(1);
160
159
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@oxy.so/protocol/sign` — message and envelope signatures (secp256k1).
3
+ *
4
+ * The same functions the root entry exports, on their own entry so a client
5
+ * can load the signer (and `@noble/curves` under it) where it signs — inside
6
+ * `@oxy.so/core/crypto`, which apps load on first use — while the root entry
7
+ * it imports at startup for hashing and platform loaders stays free of the
8
+ * curve library once a bundler drops the root's unused re-exports.
9
+ */
10
+ export { signEnvelope, signMessage, verifyEnvelopeSignature, verifySignature, } from './envelope/sign.js';