@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.
- package/dist/cjs/chain/engine.js +1 -1
- package/dist/cjs/chain/recordStore.js +3 -3
- package/dist/cjs/chain/verify.js +8 -4
- package/dist/cjs/envelope/signingInput.js +12 -1
- package/dist/cjs/identity/resolver.js +2 -1
- package/dist/cjs/node/didWebResolver.js +1 -3
- package/dist/cjs/node/nodeApp.js +9 -3
- package/dist/cjs/node/nodeClient.js +7 -2
- package/dist/cjs/platform/platform.js +1 -1
- package/dist/cjs/secp256k1.js +25 -26
- package/dist/cjs/sign.js +17 -0
- package/dist/esm/chain/engine.js +1 -1
- package/dist/esm/chain/recordStore.js +3 -3
- package/dist/esm/chain/verify.js +8 -4
- package/dist/esm/envelope/sign.js +1 -1
- package/dist/esm/envelope/signingInput.js +12 -1
- package/dist/esm/identity/resolver.js +2 -1
- package/dist/esm/node/didWebResolver.js +1 -3
- package/dist/esm/node/nodeApp.js +9 -3
- package/dist/esm/node/nodeClient.js +8 -3
- package/dist/esm/platform/platform.js +1 -1
- package/dist/esm/secp256k1.js +26 -27
- package/dist/esm/sign.js +10 -0
- package/dist/types/chain/engine.d.ts +1 -1
- package/dist/types/chain/recordStore.d.ts +3 -3
- package/dist/types/chain/verify.d.ts +3 -2
- package/dist/types/sign.d.ts +10 -0
- package/package.json +21 -22
- package/src/__tests__/canonicalJson.test.ts +2 -6
- package/src/__tests__/chain.test.ts +42 -11
- package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
- package/src/__tests__/didWebResolver.test.ts +36 -7
- package/src/__tests__/nodeApp.test.ts +76 -21
- package/src/__tests__/nodeClient.test.ts +30 -7
- package/src/__tests__/nodeHarness.ts +7 -3
- package/src/__tests__/optionalNativePeers.test.ts +8 -6
- package/src/__tests__/rateLimit.test.ts +2 -1
- package/src/__tests__/registrationPow.test.ts +35 -37
- package/src/__tests__/secp256k1.test.ts +41 -70
- package/src/__tests__/transparency.test.ts +4 -1
- package/src/auth/registrationPow.ts +5 -1
- package/src/chain/engine.ts +1 -1
- package/src/chain/recordStore.ts +3 -3
- package/src/chain/verify.ts +11 -4
- package/src/envelope/recordId.ts +1 -4
- package/src/envelope/sign.ts +6 -6
- package/src/envelope/signingInput.ts +12 -1
- package/src/identity/resolver.ts +9 -2
- package/src/node/didWebResolver.ts +2 -7
- package/src/node/nodeApp.ts +85 -67
- package/src/node/nodeClient.ts +35 -16
- package/src/platform/crypto.native.ts +9 -2
- package/src/platform/platform.ts +3 -1
- package/src/secp256k1.ts +33 -58
- package/src/sign.ts +16 -0
- package/dist/cjs/.tsbuildinfo +0 -1
- package/dist/esm/.tsbuildinfo +0 -1
- package/dist/types/.tsbuildinfo +0 -1
package/dist/cjs/chain/engine.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* engine owns the verification + ordering policy.
|
|
11
11
|
*
|
|
12
12
|
* Storage and identity are both injected, so the engine has zero knowledge of
|
|
13
|
-
*
|
|
13
|
+
* the database, Oxy DIDs, or any app's lexicon — exactly what makes it reusable.
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.verifyAndAppend = verifyAndAppend;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* The engine ({@link ./verify}, {@link ./engine}) is storage-agnostic: it owns
|
|
6
6
|
* the verification state machine and continuity logic, and delegates EVERY read
|
|
7
7
|
* and write to an injected {@link RecordStore}. An app supplies a store over its
|
|
8
|
-
* own backend (oxy-api over
|
|
9
|
-
* Mention over its own
|
|
8
|
+
* own backend (oxy-api over Postgres `signed_records`/`repo_heads`, a node over
|
|
9
|
+
* SQLite, Mention over its own database) without the engine knowing anything Oxy- or
|
|
10
10
|
* app-specific.
|
|
11
11
|
*
|
|
12
12
|
* All methods are **subject-keyed**: `subject` is the chain's subject DID
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `append` MUST be atomic (record insert + head advance in one unit) and MUST
|
|
19
19
|
* translate a duplicate-key collision on the unique `(subject, seq)` /
|
|
20
20
|
* `recordId` index — i.e. a concurrent writer that already took this `seq` — into
|
|
21
|
-
* `{ ok: false, reason: 'chain_conflict' }` (
|
|
21
|
+
* `{ ok: false, reason: 'chain_conflict' }` (Postgres `23505` / SQLite
|
|
22
22
|
* `SQLITE_CONSTRAINT`). That is the real multi-writer race guard; the engine's
|
|
23
23
|
* pre-append continuity check is only the fast-path rejection.
|
|
24
24
|
*/
|
package/dist/cjs/chain/verify.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* - the {@link RecordStore} owns the monotonicity frontier + the chain head.
|
|
12
12
|
*
|
|
13
13
|
* The ordered checks (first failure wins):
|
|
14
|
-
* 1. **shape** — the base `
|
|
14
|
+
* 1. **shape** — the base `signedRecordEnvelopeCheck`, the Zod-free twin of
|
|
15
|
+
* `signedRecordEnvelopeSchema` (open `type`, opaque
|
|
15
16
|
* `record`). An app re-narrows `type` to its own set in its adapter.
|
|
16
17
|
* 2. **signature** — `verifyEnvelopeSignature` recomputes the canonical signing
|
|
17
18
|
* input and checks the secp256k1 signature against the embedded `publicKey`.
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
30
|
exports.DEFAULT_CLOCK_SKEW_MS = void 0;
|
|
30
31
|
exports.verifyEnvelope = verifyEnvelope;
|
|
31
|
-
const
|
|
32
|
+
const client_1 = require("@oxy.so/contracts/client");
|
|
32
33
|
const sign_1 = require("../envelope/sign");
|
|
33
34
|
const resolver_1 = require("../identity/resolver");
|
|
34
35
|
const continuity_1 = require("./continuity");
|
|
@@ -42,7 +43,7 @@ exports.DEFAULT_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
|
42
43
|
async function verifyEnvelope(store, resolver, env, opts = {}) {
|
|
43
44
|
// 1. Base envelope shape (open `type`, `record` opaque). An app's adapter
|
|
44
45
|
// re-narrows `type` to its own accepted set before/around this call.
|
|
45
|
-
if (
|
|
46
|
+
if ((0, client_1.checkContract)(client_1.signedRecordEnvelopeCheck, env) === null) {
|
|
46
47
|
return { ok: false, reason: 'invalid_envelope' };
|
|
47
48
|
}
|
|
48
49
|
// 2. Signature is internally consistent with the embedded `publicKey`. Cheap,
|
|
@@ -51,7 +52,10 @@ async function verifyEnvelope(store, resolver, env, opts = {}) {
|
|
|
51
52
|
return { ok: false, reason: 'bad_signature' };
|
|
52
53
|
}
|
|
53
54
|
// Enforce that v2 envelopes strictly contain required chain fields.
|
|
54
|
-
if (env.version === 2 &&
|
|
55
|
+
if (env.version === 2 &&
|
|
56
|
+
(typeof env.seq !== 'number' ||
|
|
57
|
+
typeof env.collection !== 'string' ||
|
|
58
|
+
typeof env.rkey !== 'string')) {
|
|
55
59
|
return { ok: false, reason: 'invalid_envelope' };
|
|
56
60
|
}
|
|
57
61
|
// 3. The signing key is an authorized writer for the issuer (self-issued ⇒ a
|
|
@@ -26,7 +26,18 @@ function signedRecordSigningInput(fields) {
|
|
|
26
26
|
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
27
27
|
if (version === 2) {
|
|
28
28
|
const { seq, prev, collection, rkey } = fields;
|
|
29
|
-
return (0, canonicalJson_1.canonicalize)({
|
|
29
|
+
return (0, canonicalJson_1.canonicalize)({
|
|
30
|
+
version,
|
|
31
|
+
type,
|
|
32
|
+
subject,
|
|
33
|
+
issuer,
|
|
34
|
+
record,
|
|
35
|
+
issuedAt,
|
|
36
|
+
seq,
|
|
37
|
+
prev,
|
|
38
|
+
collection,
|
|
39
|
+
rkey,
|
|
40
|
+
});
|
|
30
41
|
}
|
|
31
42
|
return (0, canonicalJson_1.canonicalize)({ version, type, subject, issuer, record, issuedAt });
|
|
32
43
|
}
|
|
@@ -42,7 +42,8 @@ function isAuthorizedKey(resolved, env) {
|
|
|
42
42
|
if (resolved !== null &&
|
|
43
43
|
resolved.custodialIssuer !== undefined &&
|
|
44
44
|
env.issuer === resolved.custodialIssuer) {
|
|
45
|
-
return resolved.custodialPublicKey !== undefined &&
|
|
45
|
+
return resolved.custodialPublicKey !== undefined &&
|
|
46
|
+
env.publicKey === resolved.custodialPublicKey
|
|
46
47
|
? { ok: true }
|
|
47
48
|
: { ok: false, reason: 'public_key_not_a_current_verification_method' };
|
|
48
49
|
}
|
|
@@ -64,9 +64,7 @@ function isSecp256k1Vm(vm) {
|
|
|
64
64
|
* signatures verify against the hex key.
|
|
65
65
|
*/
|
|
66
66
|
function collectCurrentPublicKeys(doc) {
|
|
67
|
-
const byId = new Map(doc.verificationMethod
|
|
68
|
-
.filter(isSecp256k1Vm)
|
|
69
|
-
.map((vm) => [vm.id, vm.publicKeyHex]));
|
|
67
|
+
const byId = new Map(doc.verificationMethod.filter(isSecp256k1Vm).map((vm) => [vm.id, vm.publicKeyHex]));
|
|
70
68
|
const keys = [];
|
|
71
69
|
for (const id of doc.assertionMethod) {
|
|
72
70
|
const key = byId.get(id);
|
package/dist/cjs/node/nodeApp.js
CHANGED
|
@@ -153,7 +153,9 @@ function createNodeApp(deps) {
|
|
|
153
153
|
mode: config.mode,
|
|
154
154
|
version: config.protocolId,
|
|
155
155
|
serviceType: config.serviceType,
|
|
156
|
-
head: head && head.headRecordId !== null
|
|
156
|
+
head: head && head.headRecordId !== null
|
|
157
|
+
? { seq: head.seq, headRecordId: head.headRecordId }
|
|
158
|
+
: null,
|
|
157
159
|
});
|
|
158
160
|
}
|
|
159
161
|
catch (error) {
|
|
@@ -180,7 +182,9 @@ function createNodeApp(deps) {
|
|
|
180
182
|
const since = firstQueryValue(req.query.since);
|
|
181
183
|
const limit = parseLimit(req.query.limit);
|
|
182
184
|
const head = await store.getHead(subject);
|
|
183
|
-
const headWire = head && head.headRecordId !== null
|
|
185
|
+
const headWire = head && head.headRecordId !== null
|
|
186
|
+
? { seq: head.seq, headRecordId: head.headRecordId }
|
|
187
|
+
: null;
|
|
184
188
|
const sinceSeq = await resolveSinceSeq(store, subject, since);
|
|
185
189
|
if (sinceSeq === null) {
|
|
186
190
|
res.json({ records: [], count: 0, head: headWire });
|
|
@@ -225,7 +229,9 @@ function createNodeApp(deps) {
|
|
|
225
229
|
app.post(constants_1.NODE_SYNC_PUSH_PATH, writeRateLimit, jsonParser, async (req, res, next) => {
|
|
226
230
|
try {
|
|
227
231
|
const body = req.body;
|
|
228
|
-
const items = typeof body === 'object' &&
|
|
232
|
+
const items = typeof body === 'object' &&
|
|
233
|
+
body !== null &&
|
|
234
|
+
Array.isArray(body.records)
|
|
229
235
|
? body.records
|
|
230
236
|
: null;
|
|
231
237
|
if (!items) {
|
|
@@ -28,7 +28,9 @@ class NodeClientError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
exports.NodeClientError = NodeClientError;
|
|
30
30
|
function readError(body) {
|
|
31
|
-
if (typeof body === 'object' &&
|
|
31
|
+
if (typeof body === 'object' &&
|
|
32
|
+
body !== null &&
|
|
33
|
+
typeof body.error === 'string') {
|
|
32
34
|
return body.error;
|
|
33
35
|
}
|
|
34
36
|
return undefined;
|
|
@@ -108,7 +110,10 @@ class NodeClient {
|
|
|
108
110
|
headRaw !== null &&
|
|
109
111
|
typeof headRaw.seq === 'number' &&
|
|
110
112
|
typeof headRaw.headRecordId === 'string'
|
|
111
|
-
? {
|
|
113
|
+
? {
|
|
114
|
+
seq: headRaw.seq,
|
|
115
|
+
headRecordId: headRaw.headRecordId,
|
|
116
|
+
}
|
|
112
117
|
: null;
|
|
113
118
|
return { records, count: records.length, head };
|
|
114
119
|
}
|
|
@@ -29,5 +29,5 @@ function isReactNative() {
|
|
|
29
29
|
* `randomBytes`) and the `await import('node:crypto')` loader.
|
|
30
30
|
*/
|
|
31
31
|
function isNodeJS() {
|
|
32
|
-
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
32
|
+
return (typeof process !== 'undefined' && process.versions != null && process.versions.node != null);
|
|
33
33
|
}
|
package/dist/cjs/secp256k1.js
CHANGED
|
@@ -23,9 +23,9 @@ const HEX = /^[0-9a-fA-F]+$/;
|
|
|
23
23
|
const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
24
24
|
const UNCOMPRESSED_PUBLIC_KEY = /^04[0-9a-fA-F]{128}$/;
|
|
25
25
|
function bytesToHex(bytes) {
|
|
26
|
-
let result =
|
|
26
|
+
let result = '';
|
|
27
27
|
for (const byte of bytes) {
|
|
28
|
-
result += byte.toString(16).padStart(2,
|
|
28
|
+
result += byte.toString(16).padStart(2, '0');
|
|
29
29
|
}
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
@@ -44,16 +44,16 @@ function hexToBytes(value, label) {
|
|
|
44
44
|
* The scalar must be within the secp256k1 order; zero and overflow are rejected.
|
|
45
45
|
*/
|
|
46
46
|
function normalizeSecp256k1PrivateKey(privateKeyHex) {
|
|
47
|
-
if (typeof privateKeyHex !==
|
|
47
|
+
if (typeof privateKeyHex !== 'string' ||
|
|
48
48
|
privateKeyHex.length === 0 ||
|
|
49
49
|
privateKeyHex.length > 64 ||
|
|
50
50
|
!HEX.test(privateKeyHex)) {
|
|
51
|
-
throw new Error(
|
|
51
|
+
throw new Error('secp256k1 private key must contain 1 to 64 hexadecimal characters');
|
|
52
52
|
}
|
|
53
|
-
const normalized = privateKeyHex.toLowerCase().padStart(64,
|
|
54
|
-
const privateKey = hexToBytes(normalized,
|
|
53
|
+
const normalized = privateKeyHex.toLowerCase().padStart(64, '0');
|
|
54
|
+
const privateKey = hexToBytes(normalized, 'secp256k1 private key');
|
|
55
55
|
if (!secp256k1_js_1.secp256k1.utils.isValidSecretKey(privateKey)) {
|
|
56
|
-
throw new Error(
|
|
56
|
+
throw new Error('secp256k1 private key is outside the valid scalar range');
|
|
57
57
|
}
|
|
58
58
|
return normalized;
|
|
59
59
|
}
|
|
@@ -68,24 +68,23 @@ function isValidSecp256k1PrivateKey(privateKeyHex) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
function parsePrivateKey(privateKeyHex) {
|
|
71
|
-
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex),
|
|
71
|
+
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), 'secp256k1 private key');
|
|
72
72
|
}
|
|
73
73
|
function parsePublicKey(publicKeyHex) {
|
|
74
|
-
if (typeof publicKeyHex !==
|
|
75
|
-
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
|
|
76
|
-
|
|
77
|
-
throw new Error("secp256k1 public key must be a compressed or uncompressed SEC1 hex key");
|
|
74
|
+
if (typeof publicKeyHex !== 'string' ||
|
|
75
|
+
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) && !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
|
|
76
|
+
throw new Error('secp256k1 public key must be a compressed or uncompressed SEC1 hex key');
|
|
78
77
|
}
|
|
79
|
-
const publicKey = hexToBytes(publicKeyHex,
|
|
78
|
+
const publicKey = hexToBytes(publicKeyHex, 'secp256k1 public key');
|
|
80
79
|
// Parsing validates the SEC1 prefix, coordinate range, and curve equation.
|
|
81
80
|
secp256k1_js_1.secp256k1.Point.fromBytes(publicKey);
|
|
82
81
|
return publicKey;
|
|
83
82
|
}
|
|
84
83
|
function parseDigest(digestHex) {
|
|
85
|
-
if (typeof digestHex !==
|
|
86
|
-
throw new Error(
|
|
84
|
+
if (typeof digestHex !== 'string' || digestHex.length !== 64) {
|
|
85
|
+
throw new Error('secp256k1 digest must be exactly 32 bytes of hexadecimal data');
|
|
87
86
|
}
|
|
88
|
-
return hexToBytes(digestHex,
|
|
87
|
+
return hexToBytes(digestHex, 'secp256k1 digest');
|
|
89
88
|
}
|
|
90
89
|
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
91
90
|
function isValidSecp256k1PublicKey(publicKeyHex) {
|
|
@@ -125,17 +124,17 @@ function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
|
|
|
125
124
|
const signature = secp256k1_js_1.secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
|
|
126
125
|
lowS: options.lowS ?? false,
|
|
127
126
|
});
|
|
128
|
-
return signature.toHex(
|
|
127
|
+
return signature.toHex('der');
|
|
129
128
|
}
|
|
130
129
|
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
131
130
|
function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
|
|
132
131
|
const publicKey = parsePublicKey(publicKeyHex);
|
|
133
132
|
const digest = parseDigest(digestHex);
|
|
134
|
-
const signature = hexToBytes(signatureDerHex,
|
|
133
|
+
const signature = hexToBytes(signatureDerHex, 'secp256k1 DER signature');
|
|
135
134
|
// Parse once up front so malformed or non-DER input is rejected explicitly.
|
|
136
|
-
secp256k1_js_1.secp256k1.Signature.fromBytes(signature,
|
|
135
|
+
secp256k1_js_1.secp256k1.Signature.fromBytes(signature, 'der');
|
|
137
136
|
return secp256k1_js_1.secp256k1.verify(signature, digest, publicKey, {
|
|
138
|
-
format:
|
|
137
|
+
format: 'der',
|
|
139
138
|
lowS: false,
|
|
140
139
|
});
|
|
141
140
|
}
|
|
@@ -149,24 +148,24 @@ function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
|
|
|
149
148
|
function tweakAddSecp256k1PrivateKey(privateKeyHex, tweakHex) {
|
|
150
149
|
const n = secp256k1_js_1.secp256k1.Point.Fn.ORDER;
|
|
151
150
|
const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
|
|
152
|
-
if (typeof tweakHex !==
|
|
153
|
-
throw new Error(
|
|
151
|
+
if (typeof tweakHex !== 'string' || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
|
|
152
|
+
throw new Error('secp256k1 tweak must be exactly 32 bytes of hexadecimal data');
|
|
154
153
|
}
|
|
155
154
|
const tweak = BigInt(`0x${tweakHex}`);
|
|
156
155
|
if (tweak >= n) {
|
|
157
|
-
throw new Error(
|
|
156
|
+
throw new Error('secp256k1 tweak is not below the curve order');
|
|
158
157
|
}
|
|
159
158
|
const child = (key + tweak) % n;
|
|
160
159
|
if (child === 0n) {
|
|
161
|
-
throw new Error(
|
|
160
|
+
throw new Error('secp256k1 tweaked private key is zero');
|
|
162
161
|
}
|
|
163
|
-
return child.toString(16).padStart(64,
|
|
162
|
+
return child.toString(16).padStart(64, '0');
|
|
164
163
|
}
|
|
165
164
|
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
166
165
|
function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
|
|
167
166
|
const encodedPoint = secp256k1_js_1.secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);
|
|
168
167
|
if (encodedPoint.length !== 33) {
|
|
169
|
-
throw new Error(
|
|
168
|
+
throw new Error('secp256k1 ECDH returned an unexpected point encoding');
|
|
170
169
|
}
|
|
171
170
|
return encodedPoint.slice(1);
|
|
172
171
|
}
|
package/dist/cjs/sign.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@oxy.so/protocol/sign` — message and envelope signatures (secp256k1).
|
|
4
|
+
*
|
|
5
|
+
* The same functions the root entry exports, on their own entry so a client
|
|
6
|
+
* can load the signer (and `@noble/curves` under it) where it signs — inside
|
|
7
|
+
* `@oxy.so/core/crypto`, which apps load on first use — while the root entry
|
|
8
|
+
* it imports at startup for hashing and platform loaders stays free of the
|
|
9
|
+
* curve library once a bundler drops the root's unused re-exports.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.verifySignature = exports.verifyEnvelopeSignature = exports.signMessage = exports.signEnvelope = void 0;
|
|
13
|
+
var sign_1 = require("./envelope/sign");
|
|
14
|
+
Object.defineProperty(exports, "signEnvelope", { enumerable: true, get: function () { return sign_1.signEnvelope; } });
|
|
15
|
+
Object.defineProperty(exports, "signMessage", { enumerable: true, get: function () { return sign_1.signMessage; } });
|
|
16
|
+
Object.defineProperty(exports, "verifyEnvelopeSignature", { enumerable: true, get: function () { return sign_1.verifyEnvelopeSignature; } });
|
|
17
|
+
Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return sign_1.verifySignature; } });
|
package/dist/esm/chain/engine.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
8
|
-
* Mention over its own
|
|
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' }` (
|
|
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
|
*/
|
package/dist/esm/chain/verify.js
CHANGED
|
@@ -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 `
|
|
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 {
|
|
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 (
|
|
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 &&
|
|
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
|
|
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({
|
|
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 &&
|
|
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);
|
package/dist/esm/node/nodeApp.js
CHANGED
|
@@ -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
|
|
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
|
|
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' &&
|
|
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
|
|
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' &&
|
|
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
|
-
? {
|
|
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
|
}
|