@oxyhq/core 3.11.0 → 3.13.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/signatureService.js +88 -2
- package/dist/cjs/index.js +19 -4
- package/dist/cjs/mixins/OxyServices.civic.js +611 -0
- package/dist/cjs/mixins/index.js +3 -0
- package/dist/cjs/utils/profileLinks.js +63 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/signatureService.js +87 -2
- package/dist/esm/index.js +11 -1
- package/dist/esm/mixins/OxyServices.civic.js +605 -0
- package/dist/esm/mixins/index.js +3 -0
- package/dist/esm/utils/profileLinks.js +60 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/signatureService.d.ts +54 -3
- package/dist/types/index.d.ts +5 -1
- package/dist/types/mixins/OxyServices.civic.d.ts +512 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/utils/profileLinks.d.ts +44 -0
- package/package.json +2 -2
- package/src/crypto/__tests__/signedRecord.test.ts +221 -1
- package/src/crypto/signatureService.ts +102 -3
- package/src/index.ts +29 -1
- package/src/mixins/OxyServices.civic.ts +956 -0
- package/src/mixins/__tests__/OxyServices.civic.test.ts +1097 -0
- package/src/mixins/index.ts +4 -0
- package/src/utils/__tests__/profileLinks.test.ts +180 -0
- package/src/utils/profileLinks.ts +91 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized profile link shape for display.
|
|
3
|
+
*
|
|
4
|
+
* `id` is a stable key for list rendering (the source entry's id when present,
|
|
5
|
+
* otherwise the source index as a string). `url` is always a non-empty string.
|
|
6
|
+
* `title`, `description`, and `image` are carried through from `linksMetadata`
|
|
7
|
+
* only when present; the legacy `links: string[]` path produces just
|
|
8
|
+
* `{ id, url }`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ProfileLink {
|
|
11
|
+
id: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
url: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
image?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Source shape of a single `User.linksMetadata` entry. */
|
|
18
|
+
export interface ProfileLinkMetadata {
|
|
19
|
+
url: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
image?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a user's profile links into a clean display shape.
|
|
27
|
+
*
|
|
28
|
+
* Pure, no side effects, no I/O.
|
|
29
|
+
*
|
|
30
|
+
* - Prefers `linksMetadata` when it is a non-empty array: maps each entry to
|
|
31
|
+
* `{ id, title, url, description, image }`, using `entry.id` when present and
|
|
32
|
+
* falling back to the entry index. `title`, `description`, and `image` are
|
|
33
|
+
* carried through only when they are present strings. Entries without a
|
|
34
|
+
* non-empty string `url` are dropped.
|
|
35
|
+
* - Otherwise falls back to the legacy `links` string array: maps each string to
|
|
36
|
+
* `{ id: <index>, url }` (no title/description/image). Empty/non-string values
|
|
37
|
+
* are dropped.
|
|
38
|
+
* - Returns `[]` when both are absent or empty (including when `linksMetadata`
|
|
39
|
+
* is present but every entry is dropped — it does NOT fall back to `links`).
|
|
40
|
+
*
|
|
41
|
+
* URLs are trimmed and blanks are filtered out. This does NOT add a scheme such
|
|
42
|
+
* as `https://`; prefixing is a display concern left to the caller.
|
|
43
|
+
*/
|
|
44
|
+
export declare function normalizeProfileLinks(linksMetadata?: ProfileLinkMetadata[], links?: string[]): ProfileLink[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@oxyhq/contracts": "
|
|
101
|
+
"@oxyhq/contracts": "workspace:*",
|
|
102
102
|
"bip39": "^3.1.0",
|
|
103
103
|
"buffer": "^6.0.3",
|
|
104
104
|
"elliptic": "^6.6.1",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { ec as EC } from 'elliptic';
|
|
12
12
|
import type { SignedRecordEnvelope } from '@oxyhq/contracts';
|
|
13
13
|
import { KeyManager } from '../keyManager';
|
|
14
|
-
import { SignatureService, signedRecordSigningInput } from '../signatureService';
|
|
14
|
+
import { SignatureService, signedRecordSigningInput, computeRecordId } from '../signatureService';
|
|
15
15
|
import { canonicalize } from '../canonicalJson';
|
|
16
16
|
|
|
17
17
|
const ec = new EC('secp256k1');
|
|
@@ -123,3 +123,223 @@ describe('SignatureService.signRecord / verifyRecord', () => {
|
|
|
123
123
|
).rejects.toThrow(/No identity found/);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Critical regression guard: the v1 signing input MUST be byte-identical to the
|
|
129
|
+
* original scheme. Every `identity`/`profile` record already in production was
|
|
130
|
+
* signed over exactly these bytes, so any change to {@link signedRecordSigningInput}
|
|
131
|
+
* for v1 would invalidate them all. This locks the exact canonical string.
|
|
132
|
+
*/
|
|
133
|
+
describe('signedRecordSigningInput — v1 byte stability (regression guard)', () => {
|
|
134
|
+
it('produces the exact original canonical bytes for a v1 record', () => {
|
|
135
|
+
const input = signedRecordSigningInput({
|
|
136
|
+
version: 1,
|
|
137
|
+
type: 'identity',
|
|
138
|
+
subject: 'did:web:oxy.so:u:u1',
|
|
139
|
+
issuer: 'did:web:oxy.so:u:u1',
|
|
140
|
+
record: { handle: '@nate' },
|
|
141
|
+
issuedAt: 1750000000000,
|
|
142
|
+
});
|
|
143
|
+
expect(input).toBe(
|
|
144
|
+
'{"issuedAt":1750000000000,"issuer":"did:web:oxy.so:u:u1","record":{"handle":"@nate"},"subject":"did:web:oxy.so:u:u1","type":"identity","version":1}',
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('does NOT include any v2 chain fields for a v1 record even if they are passed', () => {
|
|
149
|
+
const input = signedRecordSigningInput({
|
|
150
|
+
version: 1,
|
|
151
|
+
type: 'identity',
|
|
152
|
+
subject: 'did:web:oxy.so:u:u1',
|
|
153
|
+
issuer: 'did:web:oxy.so:u:u1',
|
|
154
|
+
record: { handle: '@nate' },
|
|
155
|
+
issuedAt: 1750000000000,
|
|
156
|
+
// These must be ignored by the v1 branch — proving v1 bytes are immutable.
|
|
157
|
+
seq: 5,
|
|
158
|
+
prev: 'deadbeef',
|
|
159
|
+
collection: 'app.oxy.identity',
|
|
160
|
+
rkey: 'self',
|
|
161
|
+
});
|
|
162
|
+
expect(input).not.toContain('seq');
|
|
163
|
+
expect(input).not.toContain('prev');
|
|
164
|
+
expect(input).not.toContain('collection');
|
|
165
|
+
expect(input).not.toContain('rkey');
|
|
166
|
+
expect(input).toBe(
|
|
167
|
+
'{"issuedAt":1750000000000,"issuer":"did:web:oxy.so:u:u1","record":{"handle":"@nate"},"subject":"did:web:oxy.so:u:u1","type":"identity","version":1}',
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('SignatureService.signRecordV2 / verifyRecord (v2 hash chain)', () => {
|
|
173
|
+
const keyPair = ec.genKeyPair();
|
|
174
|
+
const publicKey = keyPair.getPublic('hex');
|
|
175
|
+
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
jest.spyOn(KeyManager, 'getPublicKey').mockResolvedValue(publicKey);
|
|
178
|
+
jest.spyOn(KeyManager, 'getKeyPairObject').mockResolvedValue(keyPair);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
jest.restoreAllMocks();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const chain = {
|
|
186
|
+
seq: 0,
|
|
187
|
+
prev: null as string | null,
|
|
188
|
+
collection: 'app.oxy.reputation',
|
|
189
|
+
rkey: 'rt_1',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
it('builds a well-formed v2 envelope carrying the chain fields', async () => {
|
|
193
|
+
const subject = 'did:web:oxy.so:u:abc123';
|
|
194
|
+
const envelope = await SignatureService.signRecordV2(
|
|
195
|
+
'reputation_attestation',
|
|
196
|
+
subject,
|
|
197
|
+
{ points: 25 },
|
|
198
|
+
chain,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(envelope.version).toBe(2);
|
|
202
|
+
expect(envelope.type).toBe('reputation_attestation');
|
|
203
|
+
expect(envelope.subject).toBe(subject);
|
|
204
|
+
expect(envelope.issuer).toBe(subject); // self-issued
|
|
205
|
+
expect(envelope.seq).toBe(0);
|
|
206
|
+
expect(envelope.prev).toBeNull();
|
|
207
|
+
expect(envelope.collection).toBe('app.oxy.reputation');
|
|
208
|
+
expect(envelope.rkey).toBe('rt_1');
|
|
209
|
+
expect(envelope.publicKey).toBe(publicKey);
|
|
210
|
+
expect(envelope.alg).toBe('ES256K-DER-SHA256');
|
|
211
|
+
expect(typeof envelope.signature).toBe('string');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('round-trips: a freshly signed v2 record verifies', async () => {
|
|
215
|
+
const envelope = await SignatureService.signRecordV2(
|
|
216
|
+
'validation_verdict',
|
|
217
|
+
'did:web:oxy.so:u:v1',
|
|
218
|
+
{ verdict: 'approve' },
|
|
219
|
+
{ seq: 3, prev: 'a'.repeat(64), collection: 'app.oxy.validation', rkey: 'req_9' },
|
|
220
|
+
);
|
|
221
|
+
await expect(SignatureService.verifyRecord(envelope)).resolves.toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('covers the chain fields in the signed bytes (tampering breaks verify)', async () => {
|
|
225
|
+
const envelope = await SignatureService.signRecordV2(
|
|
226
|
+
'reputation_attestation',
|
|
227
|
+
'did:web:oxy.so:u:t1',
|
|
228
|
+
{ points: 8 },
|
|
229
|
+
chain,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await expect(
|
|
233
|
+
SignatureService.verifyRecord({ ...envelope, seq: 1 }),
|
|
234
|
+
).resolves.toBe(false);
|
|
235
|
+
await expect(
|
|
236
|
+
SignatureService.verifyRecord({ ...envelope, prev: 'b'.repeat(64) }),
|
|
237
|
+
).resolves.toBe(false);
|
|
238
|
+
await expect(
|
|
239
|
+
SignatureService.verifyRecord({ ...envelope, collection: 'app.evil' }),
|
|
240
|
+
).resolves.toBe(false);
|
|
241
|
+
await expect(
|
|
242
|
+
SignatureService.verifyRecord({ ...envelope, rkey: 'other' }),
|
|
243
|
+
).resolves.toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('a v2 envelope signs over DIFFERENT bytes than the same base fields as v1', async () => {
|
|
247
|
+
const subject = 'did:web:oxy.so:u:diff';
|
|
248
|
+
const record = { x: 1 };
|
|
249
|
+
const v2 = await SignatureService.signRecordV2('profile', subject, record, chain);
|
|
250
|
+
|
|
251
|
+
const v1Input = signedRecordSigningInput({
|
|
252
|
+
version: 1,
|
|
253
|
+
type: 'profile',
|
|
254
|
+
subject,
|
|
255
|
+
issuer: subject,
|
|
256
|
+
record,
|
|
257
|
+
issuedAt: v2.issuedAt,
|
|
258
|
+
});
|
|
259
|
+
const v2Input = signedRecordSigningInput(v2);
|
|
260
|
+
expect(v2Input).not.toBe(v1Input);
|
|
261
|
+
expect(v2Input).toContain('"seq":0');
|
|
262
|
+
expect(v2Input).toContain('"prev":null');
|
|
263
|
+
expect(v2Input).toContain('"collection":"app.oxy.reputation"');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('throws when no identity is stored', async () => {
|
|
267
|
+
jest.spyOn(KeyManager, 'getPublicKey').mockResolvedValue(null);
|
|
268
|
+
await expect(
|
|
269
|
+
SignatureService.signRecordV2('identity', 'did:web:oxy.so:u:u4', {}, chain),
|
|
270
|
+
).rejects.toThrow(/No identity found/);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('computeRecordId — deterministic content address', () => {
|
|
275
|
+
const base: SignedRecordEnvelope = {
|
|
276
|
+
version: 2,
|
|
277
|
+
type: 'reputation_attestation',
|
|
278
|
+
subject: 'did:web:oxy.so:u:rid',
|
|
279
|
+
issuer: 'did:web:oxy.so',
|
|
280
|
+
record: { points: 25 },
|
|
281
|
+
issuedAt: 1750000000000,
|
|
282
|
+
seq: 0,
|
|
283
|
+
prev: null,
|
|
284
|
+
collection: 'app.oxy.reputation',
|
|
285
|
+
rkey: 'rt_1',
|
|
286
|
+
publicKey: '03oxykey',
|
|
287
|
+
alg: 'ES256K-DER-SHA256',
|
|
288
|
+
signature: 'sig-is-not-part-of-the-id',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
it('is a 64-char lowercase hex SHA-256 digest', async () => {
|
|
292
|
+
const id = await computeRecordId(base);
|
|
293
|
+
expect(id).toMatch(/^[0-9a-f]{64}$/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('is deterministic for identical signing fields', async () => {
|
|
297
|
+
const a = await computeRecordId(base);
|
|
298
|
+
const b = await computeRecordId(base);
|
|
299
|
+
expect(a).toBe(b);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('equals sha256 of the canonical signing input (content address, excludes publicKey/signature)', async () => {
|
|
303
|
+
const id = await computeRecordId(base);
|
|
304
|
+
const expected = await SignatureService.hashMessage(signedRecordSigningInput(base));
|
|
305
|
+
expect(id).toBe(expected);
|
|
306
|
+
// Changing only signature/publicKey does NOT change the recordId.
|
|
307
|
+
const sameId = await computeRecordId({
|
|
308
|
+
...base,
|
|
309
|
+
signature: 'totally-different',
|
|
310
|
+
publicKey: 'different-key',
|
|
311
|
+
});
|
|
312
|
+
expect(sameId).toBe(id);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('changes when any signed field changes', async () => {
|
|
316
|
+
const id = await computeRecordId(base);
|
|
317
|
+
expect(await computeRecordId({ ...base, seq: 1 })).not.toBe(id);
|
|
318
|
+
expect(await computeRecordId({ ...base, record: { points: 26 } })).not.toBe(id);
|
|
319
|
+
expect(await computeRecordId({ ...base, prev: 'a'.repeat(64) })).not.toBe(id);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('a v1 record and a v2 record with the same base fields have different recordIds', async () => {
|
|
323
|
+
const v1Id = await computeRecordId({
|
|
324
|
+
version: 1,
|
|
325
|
+
type: 'profile',
|
|
326
|
+
subject: 'did:web:oxy.so:u:rid',
|
|
327
|
+
issuer: 'did:web:oxy.so:u:rid',
|
|
328
|
+
record: { points: 25 },
|
|
329
|
+
issuedAt: 1750000000000,
|
|
330
|
+
});
|
|
331
|
+
const v2Id = await computeRecordId({
|
|
332
|
+
version: 2,
|
|
333
|
+
type: 'profile',
|
|
334
|
+
subject: 'did:web:oxy.so:u:rid',
|
|
335
|
+
issuer: 'did:web:oxy.so:u:rid',
|
|
336
|
+
record: { points: 25 },
|
|
337
|
+
issuedAt: 1750000000000,
|
|
338
|
+
seq: 0,
|
|
339
|
+
prev: null,
|
|
340
|
+
collection: 'app.oxy.profile',
|
|
341
|
+
rkey: 'self',
|
|
342
|
+
});
|
|
343
|
+
expect(v1Id).not.toBe(v2Id);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -21,25 +21,58 @@ const ec = new EC('secp256k1');
|
|
|
21
21
|
* EXCEPT the `publicKey` and `signature`. Both the client (when signing) and
|
|
22
22
|
* the server (when verifying) canonicalize exactly these fields, so they agree
|
|
23
23
|
* on the bytes that the signature covers.
|
|
24
|
+
*
|
|
25
|
+
* The v2 chain fields (`seq`/`prev`/`collection`/`rkey`) are optional: a v1
|
|
26
|
+
* envelope omits them and is signed over only the base fields; a v2 envelope
|
|
27
|
+
* carries them and includes them in the signed bytes.
|
|
24
28
|
*/
|
|
25
29
|
export type SignedRecordSigningFields = Pick<
|
|
26
30
|
SignedRecordEnvelope,
|
|
27
31
|
'version' | 'type' | 'subject' | 'issuer' | 'record' | 'issuedAt'
|
|
28
|
-
|
|
32
|
+
> &
|
|
33
|
+
Partial<Pick<SignedRecordEnvelope, 'seq' | 'prev' | 'collection' | 'rkey'>>;
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
36
|
* Compute the canonical signing input for a signed-record envelope.
|
|
32
37
|
*
|
|
33
|
-
* This is the single definition of "what the signature covers"
|
|
34
|
-
* JSON of `{version, type, subject, issuer, record, issuedAt}`. `@oxyhq/core`
|
|
38
|
+
* This is the single definition of "what the signature covers". `@oxyhq/core`
|
|
35
39
|
* (client signing) and `@oxyhq/api` (server verification) both call this, so a
|
|
36
40
|
* record signed by a client and verified by the server cannot drift.
|
|
41
|
+
*
|
|
42
|
+
* - **v1**: the canonical JSON of `{version, type, subject, issuer, record,
|
|
43
|
+
* issuedAt}` — BYTE-IDENTICAL to the original scheme, so every signature
|
|
44
|
+
* already in production keeps verifying.
|
|
45
|
+
* - **v2**: the canonical JSON additionally includes the hash-chain fields
|
|
46
|
+
* `{seq, prev, collection, rkey}`. Because {@link canonicalize} sorts keys,
|
|
47
|
+
* the on-the-wire field order is irrelevant; the resulting canonical key
|
|
48
|
+
* order is `collection, issuedAt, issuer, prev, record, rkey, seq, subject,
|
|
49
|
+
* type, version`. `prev` is `null` at genesis (serialized as `null`, not
|
|
50
|
+
* omitted), so it is always part of the signed bytes.
|
|
37
51
|
*/
|
|
38
52
|
export function signedRecordSigningInput(fields: SignedRecordSigningFields): string {
|
|
39
53
|
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
54
|
+
if (version === 2) {
|
|
55
|
+
const { seq, prev, collection, rkey } = fields;
|
|
56
|
+
return canonicalize({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
|
|
57
|
+
}
|
|
40
58
|
return canonicalize({ version, type, subject, issuer, record, issuedAt });
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Compute the `recordId` (content address) of a signed record: the SHA-256 hex
|
|
63
|
+
* digest of its canonical {@link signedRecordSigningInput}.
|
|
64
|
+
*
|
|
65
|
+
* Deterministic and stable across runtimes (it reuses the same canonicalization
|
|
66
|
+
* + SHA-256 the signature itself is built on). The recordId is what `prev`
|
|
67
|
+
* references in the per-subject hash chain, so `@oxyhq/core` (client) and
|
|
68
|
+
* `@oxyhq/api` (server) MUST compute it identically — both call this function.
|
|
69
|
+
* It is taken over the SIGNING input (excluding `publicKey`/`signature`), so it
|
|
70
|
+
* is a pure content address of the record's meaning, independent of who signed.
|
|
71
|
+
*/
|
|
72
|
+
export async function computeRecordId(fields: SignedRecordSigningFields): Promise<string> {
|
|
73
|
+
return sha256(signedRecordSigningInput(fields));
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
/**
|
|
44
77
|
* Compute SHA-256 hash of a string
|
|
45
78
|
*/
|
|
@@ -419,6 +452,72 @@ export class SignatureService {
|
|
|
419
452
|
};
|
|
420
453
|
}
|
|
421
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Build a signed-record envelope (v2) carrying the per-subject hash-chain
|
|
457
|
+
* fields.
|
|
458
|
+
*
|
|
459
|
+
* Identical to {@link signRecord} (self-issued: `issuer === subject`; same
|
|
460
|
+
* `ES256K-DER-SHA256` scheme over {@link signedRecordSigningInput}) but
|
|
461
|
+
* `version` is `2` and the signed bytes additionally cover the chain fields:
|
|
462
|
+
*
|
|
463
|
+
* @param type - The record category.
|
|
464
|
+
* @param subject - The subject DID the record is about (also the issuer).
|
|
465
|
+
* @param record - The arbitrary record payload to attest to.
|
|
466
|
+
* @param chain - The hash-chain coordinates:
|
|
467
|
+
* - `seq` — strictly-increasing sequence number for this subject's chain.
|
|
468
|
+
* - `prev` — the `recordId` of the previous record, or `null` at genesis.
|
|
469
|
+
* - `collection` + `rkey` — the AtProto-style record key.
|
|
470
|
+
*
|
|
471
|
+
* The caller is responsible for fetching the current chain head (so `seq` /
|
|
472
|
+
* `prev` are correct) before signing. Requires a stored identity; throws if
|
|
473
|
+
* none exists.
|
|
474
|
+
*/
|
|
475
|
+
static async signRecordV2(
|
|
476
|
+
type: SignedRecordEnvelope['type'],
|
|
477
|
+
subject: string,
|
|
478
|
+
record: Record<string, unknown>,
|
|
479
|
+
chain: { seq: number; prev: string | null; collection: string; rkey: string },
|
|
480
|
+
): Promise<SignedRecordEnvelope> {
|
|
481
|
+
const publicKey = await KeyManager.getPublicKey();
|
|
482
|
+
if (!publicKey) {
|
|
483
|
+
throw new Error('No identity found. Please create or import an identity first.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const version = 2 as const;
|
|
487
|
+
const issuer = subject;
|
|
488
|
+
const issuedAt = Date.now();
|
|
489
|
+
const { seq, prev, collection, rkey } = chain;
|
|
490
|
+
const signingInput = signedRecordSigningInput({
|
|
491
|
+
version,
|
|
492
|
+
type,
|
|
493
|
+
subject,
|
|
494
|
+
issuer,
|
|
495
|
+
record,
|
|
496
|
+
issuedAt,
|
|
497
|
+
seq,
|
|
498
|
+
prev,
|
|
499
|
+
collection,
|
|
500
|
+
rkey,
|
|
501
|
+
});
|
|
502
|
+
const signature = await SignatureService.sign(signingInput);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
version,
|
|
506
|
+
type,
|
|
507
|
+
subject,
|
|
508
|
+
issuer,
|
|
509
|
+
record,
|
|
510
|
+
issuedAt,
|
|
511
|
+
seq,
|
|
512
|
+
prev,
|
|
513
|
+
collection,
|
|
514
|
+
rkey,
|
|
515
|
+
publicKey,
|
|
516
|
+
alg: 'ES256K-DER-SHA256',
|
|
517
|
+
signature,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
422
521
|
/**
|
|
423
522
|
* Verify a signed-record envelope: recompute the canonical signing input from
|
|
424
523
|
* the envelope's own fields and check the signature against the envelope's
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,8 @@ export {
|
|
|
89
89
|
getNormalizedUserHandle,
|
|
90
90
|
} from './utils/userHandle';
|
|
91
91
|
export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHandle';
|
|
92
|
+
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
93
|
+
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
92
94
|
|
|
93
95
|
// ---------------------------------------------------------------------------
|
|
94
96
|
// Applications (multi-user apps: membership, roles, credentials)
|
|
@@ -183,6 +185,32 @@ export type {
|
|
|
183
185
|
RemoveDomainResult,
|
|
184
186
|
} from './mixins/OxyServices.identity';
|
|
185
187
|
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Civic / Commons "Oxy ID" (public signed cards + Oxy ID QR payload) and Fase 2
|
|
190
|
+
// anti-gaming (real-life attestation QR + validator/jury). Wire shapes
|
|
191
|
+
// (PublicCard, SignedPublicCard, RealLifeAttestationResult,
|
|
192
|
+
// ValidationRequestSummary, ValidationVoteResult, ValidationVerdict, …) live in
|
|
193
|
+
// `@oxyhq/contracts` — import them from there. The SDK adds the client verdict
|
|
194
|
+
// wrapper, the QR payload parsers/builders, and the submit inputs/results.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
export {
|
|
197
|
+
parseIdPayload,
|
|
198
|
+
parseAttestPayload,
|
|
199
|
+
verifyPublicCardAttestation,
|
|
200
|
+
} from './mixins/OxyServices.civic';
|
|
201
|
+
export type {
|
|
202
|
+
CivicCardResult,
|
|
203
|
+
IdCardRef,
|
|
204
|
+
AttestQrPayload,
|
|
205
|
+
ParsedAttestPayload,
|
|
206
|
+
SubmitRealLifeAttestationInput,
|
|
207
|
+
DenyValidationResult,
|
|
208
|
+
VouchForPersonInput,
|
|
209
|
+
WithdrawVouchResult,
|
|
210
|
+
IssueCredentialInput,
|
|
211
|
+
RevokeCredentialResult,
|
|
212
|
+
} from './mixins/OxyServices.civic';
|
|
213
|
+
|
|
186
214
|
// ---------------------------------------------------------------------------
|
|
187
215
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
188
216
|
// ---------------------------------------------------------------------------
|
|
@@ -230,7 +258,7 @@ export {
|
|
|
230
258
|
IdentityPersistError,
|
|
231
259
|
} from './crypto/keyManager';
|
|
232
260
|
export type { KeyPair } from './crypto/keyManager';
|
|
233
|
-
export { SignatureService, signedRecordSigningInput } from './crypto/signatureService';
|
|
261
|
+
export { SignatureService, signedRecordSigningInput, computeRecordId } from './crypto/signatureService';
|
|
234
262
|
export type { SignedMessage, AuthChallenge, SignedRecordSigningFields } from './crypto/signatureService';
|
|
235
263
|
export { canonicalize } from './crypto/canonicalJson';
|
|
236
264
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase';
|