@kubun/credential 0.17.4 → 0.18.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 (51) hide show
  1. package/lib/api.d.ts +61 -30
  2. package/lib/api.js +455 -111
  3. package/lib/authority.d.ts +4 -0
  4. package/lib/authority.js +3 -0
  5. package/lib/branch.d.ts +4 -0
  6. package/lib/branch.js +24 -0
  7. package/lib/builder.d.ts +5 -0
  8. package/lib/builder.js +8 -6
  9. package/lib/canonical-token.d.ts +2 -0
  10. package/lib/canonical-token.js +10 -0
  11. package/lib/errors.d.ts +39 -1
  12. package/lib/errors.js +77 -0
  13. package/lib/fork-repair.d.ts +19 -0
  14. package/lib/fork-repair.js +3 -0
  15. package/lib/grantor-authority.d.ts +26 -0
  16. package/lib/grantor-authority.js +43 -0
  17. package/lib/index.d.ts +15 -6
  18. package/lib/index.js +7 -2
  19. package/lib/key-cache.d.ts +4 -3
  20. package/lib/key-cache.js +22 -15
  21. package/lib/manager-context.d.ts +50 -0
  22. package/lib/manager-context.js +82 -0
  23. package/lib/manager-entries.d.ts +3 -0
  24. package/lib/manager-entries.js +585 -0
  25. package/lib/manager-keys.d.ts +3 -0
  26. package/lib/manager-keys.js +413 -0
  27. package/lib/manager-open.d.ts +45 -0
  28. package/lib/manager-open.js +421 -0
  29. package/lib/manager-records.d.ts +94 -0
  30. package/lib/manager-records.js +291 -0
  31. package/lib/manager-removal-recovery.d.ts +7 -0
  32. package/lib/manager-removal-recovery.js +211 -0
  33. package/lib/manager-rotate.d.ts +15 -0
  34. package/lib/manager-rotate.js +536 -0
  35. package/lib/manager-types.d.ts +337 -0
  36. package/lib/manager-types.js +1 -0
  37. package/lib/manager.d.ts +2 -293
  38. package/lib/manager.js +38 -1073
  39. package/lib/manifest-token.d.ts +31 -0
  40. package/lib/manifest-token.js +51 -0
  41. package/lib/provider.d.ts +15 -2
  42. package/lib/provider.js +39 -0
  43. package/lib/record-types.d.ts +65 -0
  44. package/lib/record-types.js +865 -0
  45. package/lib/refreshing-credential-provider.d.ts +6 -2
  46. package/lib/refreshing-credential-provider.js +76 -13
  47. package/lib/suite.d.ts +5 -4
  48. package/lib/suite.js +2 -2
  49. package/package.json +16 -15
  50. package/lib/op.d.ts +0 -128
  51. package/lib/op.js +0 -304
package/lib/op.js DELETED
@@ -1,304 +0,0 @@
1
- import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
2
- import { toB64U } from '@sozai/codec';
3
- import { credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts } from './digest.js';
4
- /** The digest of the bytes a row holds, per subject kind. */ export function credentialRowDigest(subject) {
5
- switch(subject.kind){
6
- case 'key':
7
- return credentialKeyDigest({
8
- ownerDID: subject.row.owner_did,
9
- suite: subject.row.suite,
10
- version: subject.row.version,
11
- state: subject.row.state
12
- });
13
- case 'wrapping':
14
- return credentialWrappingDigest({
15
- wrappingID: subject.row.wrapping_id,
16
- recipientDID: subject.row.recipient_did,
17
- iv: toB64U(new Uint8Array(subject.row.iv)),
18
- wrappedKey: toB64U(new Uint8Array(subject.row.wrapped_key)),
19
- factors: subject.row.factors
20
- });
21
- case 'entry':
22
- return credentialEntryDigest({
23
- entryID: subject.row.entry_id,
24
- iv: toB64U(new Uint8Array(subject.row.iv)),
25
- ciphertext: toB64U(new Uint8Array(subject.row.ciphertext)),
26
- hlc: subject.row.hlc
27
- });
28
- }
29
- }
30
- /** The digest of the bytes a row is about to be written with, per subject kind. */ export function credentialPutDigest(subject) {
31
- switch(subject.kind){
32
- case 'key':
33
- return credentialKeyDigest({
34
- ownerDID: subject.params.ownerDID,
35
- suite: subject.params.suite,
36
- version: subject.params.version,
37
- state: subject.params.state
38
- });
39
- case 'wrapping':
40
- return credentialWrappingDigest({
41
- wrappingID: subject.params.wrappingID,
42
- // An omitted recipient and an explicit null are the same wrapping, as
43
- // the signer folds them.
44
- recipientDID: subject.params.recipientDID ?? null,
45
- iv: toB64U(subject.params.iv),
46
- wrappedKey: toB64U(subject.params.wrappedKey),
47
- factors: subject.params.factors
48
- });
49
- case 'entry':
50
- return credentialEntryDigest({
51
- entryID: subject.params.entryID,
52
- iv: toB64U(subject.params.iv),
53
- ciphertext: toB64U(subject.params.ciphertext),
54
- hlc: subject.params.hlc
55
- });
56
- }
57
- }
58
- /**
59
- * Whether a verified claim speaks for exactly these bytes under exactly this
60
- * identity.
61
- *
62
- * The identifiers are checked along with the digest: a claim over another row's
63
- * content would otherwise authenticate this one as soon as the two held the same
64
- * bytes, which two wrappings of the same key routinely do not but two
65
- * empty-state keys would. `keyID` matters beyond that — it is the column the
66
- * sync lane buckets a vault by, so an op filed under another key's id would
67
- * travel with the wrong set.
68
- *
69
- * `stamp` is the row's own `hlc` where the row has one, which is entries only.
70
- * It has to equal the claim's: the op row keeps the claim's stamp and the entry
71
- * row keeps its own, and the two disagreeing would order a row differently from
72
- * the op that speaks for it.
73
- */ function matchesClaim(claim, subject, contentDigest) {
74
- return claim.subjectKind === subject.kind && claim.subjectID === subject.subjectID && claim.keyID === subject.keyID && claim.keyVersion === subject.keyVersion && (subject.stamp == null || claim.hlc === subject.stamp) && !claim.deleted && claim.contentDigest === contentDigest;
75
- }
76
- /** Whether a verified claim speaks for a stored row. */ export function credentialOpMatchesRow(claim, subject) {
77
- const identity = subject.kind === 'key' ? {
78
- kind: 'key',
79
- subjectID: subject.row.key_id,
80
- keyID: subject.row.key_id,
81
- keyVersion: subject.row.version
82
- } : subject.kind === 'wrapping' ? {
83
- kind: 'wrapping',
84
- subjectID: subject.row.wrapping_id,
85
- keyID: subject.row.key_id,
86
- keyVersion: subject.row.key_version
87
- } : {
88
- kind: 'entry',
89
- subjectID: subject.row.entry_id,
90
- keyID: subject.row.key_id,
91
- keyVersion: subject.row.key_version,
92
- stamp: subject.row.hlc
93
- };
94
- return matchesClaim(claim, identity, credentialRowDigest(subject));
95
- }
96
- /** Whether a verified claim speaks for a row about to be written. */ export function credentialOpMatchesPut(claim, subject) {
97
- const identity = subject.kind === 'key' ? {
98
- kind: 'key',
99
- subjectID: subject.params.keyID,
100
- keyID: subject.params.keyID,
101
- keyVersion: subject.params.version
102
- } : subject.kind === 'wrapping' ? {
103
- kind: 'wrapping',
104
- subjectID: subject.params.wrappingID,
105
- keyID: subject.params.keyID,
106
- keyVersion: subject.params.keyVersion
107
- } : {
108
- kind: 'entry',
109
- subjectID: subject.params.entryID,
110
- keyID: subject.params.keyID,
111
- keyVersion: subject.params.keyVersion,
112
- stamp: subject.params.hlc
113
- };
114
- return matchesClaim(claim, identity, credentialPutDigest(subject));
115
- }
116
- async function signOp(identity, claim) {
117
- const signed = await identity.signToken({
118
- ...claim
119
- }, {
120
- embedLongForm: true
121
- });
122
- const opJWT = stringifyToken(signed);
123
- return {
124
- keyID: claim.keyID,
125
- // Normalized, unlike a wrapping's recipient: this one is compared against a
126
- // key's owner and against a verified issuer, never resolved to a document.
127
- authorDID: normalizeDID(identity.id),
128
- hlc: claim.hlc,
129
- opJWT,
130
- // The token addresses itself, the way the graph mutation log addresses a
131
- // mutation by its hash.
132
- opHash: digestParts([
133
- opJWT
134
- ]),
135
- // A wrapping or entry mirrors the branch it signed; anything without one
136
- // gets the empty label. A key-op overrides this from its own `opHash`.
137
- branchID: claim.branchID ?? ''
138
- };
139
- }
140
- export function createCredentialOpSigner(identity) {
141
- async function tombstone(subject, hlc) {
142
- return await signOp(identity, {
143
- subjectKind: subject.subjectKind,
144
- subjectID: subject.subjectID,
145
- keyID: subject.keyID,
146
- keyVersion: subject.keyVersion,
147
- hlc,
148
- contentDigest: null,
149
- deleted: true
150
- });
151
- }
152
- return {
153
- async key (params, hlc) {
154
- const input = await signOp(identity, {
155
- subjectKind: 'key',
156
- subjectID: params.keyID,
157
- keyID: params.keyID,
158
- keyVersion: params.version,
159
- hlc,
160
- contentDigest: credentialKeyDigest({
161
- ownerDID: params.ownerDID,
162
- suite: params.suite,
163
- version: params.version,
164
- state: params.state
165
- }),
166
- deleted: false
167
- });
168
- // The key-op labels its own branch. It cannot claim this in the signed
169
- // token — the label folds `opHash`, which the signature produces — so it
170
- // rides on the input and a receiver recomputes it from the op's `hlc` and
171
- // `opHash`, which is what authenticates it.
172
- return {
173
- ...input,
174
- branchID: `${hlc}:${input.opHash}`
175
- };
176
- },
177
- async wrapping (params, hlc, branchID) {
178
- return await signOp(identity, {
179
- subjectKind: 'wrapping',
180
- subjectID: params.wrappingID,
181
- keyID: params.keyID,
182
- keyVersion: params.keyVersion,
183
- hlc,
184
- contentDigest: credentialWrappingDigest({
185
- wrappingID: params.wrappingID,
186
- // An omitted recipient and an explicit null are the same wrapping, and
187
- // the stored row keeps only one of the two spellings.
188
- recipientDID: params.recipientDID ?? null,
189
- iv: toB64U(params.iv),
190
- wrappedKey: toB64U(params.wrappedKey),
191
- factors: params.factors
192
- }),
193
- deleted: false,
194
- branchID
195
- });
196
- },
197
- async entry (params, branchID) {
198
- return await signOp(identity, {
199
- subjectKind: 'entry',
200
- subjectID: params.entryID,
201
- keyID: params.keyID,
202
- keyVersion: params.keyVersion,
203
- hlc: params.hlc,
204
- contentDigest: credentialEntryDigest({
205
- entryID: params.entryID,
206
- iv: toB64U(params.iv),
207
- ciphertext: toB64U(params.ciphertext),
208
- hlc: params.hlc
209
- }),
210
- deleted: false,
211
- branchID
212
- });
213
- },
214
- tombstone,
215
- async wrappingTombstone (subject, hlc) {
216
- return {
217
- ...await tombstone(subject, hlc),
218
- subjectID: subject.subjectID
219
- };
220
- }
221
- };
222
- }
223
- function isSubjectKind(value) {
224
- return value === 'key' || value === 'wrapping' || value === 'entry';
225
- }
226
- /**
227
- * Split a `branchID` into its `hlc` and `opHash` halves.
228
- *
229
- * The label is `${hlc}:${opHash}`, and both halves — an HLC and a DID nodeID —
230
- * carry their own colons, but `opHash` is base64url and carries none, so the
231
- * LAST colon is the boundary however many precede it.
232
- */ function splitBranchID(branchID) {
233
- const boundary = branchID.lastIndexOf(':');
234
- return boundary < 0 ? {
235
- hlc: branchID,
236
- opHash: ''
237
- } : {
238
- hlc: branchID.slice(0, boundary),
239
- opHash: branchID.slice(boundary + 1)
240
- };
241
- }
242
- /**
243
- * Order two branch labels, keyless and total.
244
- *
245
- * The `hlc` halves are compared as strings: the serialized HLC form is
246
- * lexically total-ordered by construction (fixed-width ISO wall time, then a
247
- * zero-padded counter, then the nodeID), the same property the store's
248
- * tombstone check leans on. Two concurrent rotations neither dominate, so the
249
- * `opHash` breaks the tie — also lexical, and distinct per op, which is what
250
- * makes the order total and identical on every device.
251
- */ export function compareBranchIDs(a, b) {
252
- const left = splitBranchID(a);
253
- const right = splitBranchID(b);
254
- if (left.hlc !== right.hlc) {
255
- return left.hlc < right.hlc ? -1 : 1;
256
- }
257
- return left.opHash < right.opHash ? -1 : left.opHash > right.opHash ? 1 : 0;
258
- }
259
- /**
260
- * The single branch a keyless party names as the winner: the one whose
261
- * introducing key-op dominates by HLC, `opHash` breaking genuine concurrency.
262
- * Order-independent — every device folds the same set to the same label.
263
- */ export function selectWinningBranch(branchIDs) {
264
- if (branchIDs.length === 0) {
265
- throw new RangeError('selectWinningBranch requires at least one branchID');
266
- }
267
- return branchIDs.reduce((winner, candidate)=>compareBranchIDs(candidate, winner) > 0 ? candidate : winner);
268
- }
269
- /**
270
- * Verify an op and extract its claim. Returns `null` (never throws) when the
271
- * token is unparseable, unsigned (`alg: 'none'`), or structurally malformed — an
272
- * attacker cannot forge an `iss` this way.
273
- */ export async function verifyCredentialOp(token) {
274
- let verified;
275
- try {
276
- verified = await verifyToken(token);
277
- } catch {
278
- return null;
279
- }
280
- if (!isVerifiedToken(verified)) {
281
- return null;
282
- }
283
- const { iss, subjectKind, subjectID, keyID, keyVersion, hlc, contentDigest, deleted, branchID } = verified.payload;
284
- if (!isSubjectKind(subjectKind) || typeof subjectID !== 'string' || typeof keyID !== 'string' || typeof keyVersion !== 'number' || typeof hlc !== 'string' || typeof deleted !== 'boolean' || // A live op with no digest would authenticate any bytes at all, so the two
285
- // fields are checked against each other rather than one at a time.
286
- (deleted ? contentDigest !== null : typeof contentDigest !== 'string') || // Optional — a key-op omits it — but a present one must be the signed string
287
- // a receiver reads as the row's branch label.
288
- branchID != null && typeof branchID !== 'string') {
289
- return null;
290
- }
291
- return {
292
- issuer: normalizeDID(iss),
293
- claim: {
294
- subjectKind,
295
- subjectID,
296
- keyID,
297
- keyVersion,
298
- hlc,
299
- contentDigest: contentDigest,
300
- deleted,
301
- branchID: branchID
302
- }
303
- };
304
- }