@kubun/credential 0.17.3 → 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 (53) 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 +16 -6
  18. package/lib/index.js +8 -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 -1072
  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/lib/wrappable.d.ts +14 -0
  50. package/lib/wrappable.js +15 -0
  51. package/package.json +16 -15
  52. package/lib/op.d.ts +0 -128
  53. package/lib/op.js +0 -304
@@ -0,0 +1,31 @@
1
+ import { type MethodRegistry, type SigningIdentity } from '@kokuin/token';
2
+ /**
3
+ * What a grantor signs to record the set of keys it granted a recipient: a
4
+ * claim the recipient checks its store against to prove completeness. Domain
5
+ * separation from other signed payloads comes from the `kind` marker plus the
6
+ * structural check in `verifyCredentialManifest` -- `signToken` has no
7
+ * domain override of its own.
8
+ */
9
+ export type CredentialManifestClaim = {
10
+ kind: 'credential/key-manifest';
11
+ ownerDID: string;
12
+ recipientDID: string;
13
+ grantorDID: string;
14
+ epoch: number;
15
+ sequence: number;
16
+ keys: Array<{
17
+ keyID: string;
18
+ version: number;
19
+ }>;
20
+ issuedAt: string;
21
+ };
22
+ /** The envelope carried on the wire and persisted is the signed token string itself. */
23
+ export type CredentialManifestEnvelope = string;
24
+ export declare function signCredentialManifest(identity: SigningIdentity, claim: Omit<CredentialManifestClaim, 'kind' | 'grantorDID'>): Promise<CredentialManifestEnvelope>;
25
+ /**
26
+ * Verify a credential-manifest envelope and extract its claim. Returns `null`
27
+ * (never throws) when the token is unparseable, unsigned (`alg: 'none'`), or
28
+ * structurally not a credential-manifest claim -- `grantorDID` is trusted
29
+ * only from the verified `iss`, never from a same-named field in the payload.
30
+ */
31
+ export declare function verifyCredentialManifest(envelope: CredentialManifestEnvelope, methods?: MethodRegistry): Promise<CredentialManifestClaim | null>;
@@ -0,0 +1,51 @@
1
+ import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
2
+ /**
3
+ * Check that a value is a finite, safe, positive integer (>= 1).
4
+ * Rejects NaN, Infinity, floats, negatives, and zero.
5
+ */ const isPosInt = (n)=>typeof n === 'number' && Number.isSafeInteger(n) && n >= 1;
6
+ export async function signCredentialManifest(identity, claim) {
7
+ const fullClaim = {
8
+ kind: 'credential/key-manifest',
9
+ grantorDID: identity.id,
10
+ ...claim
11
+ };
12
+ const signed = await identity.signToken(fullClaim, {
13
+ embedLongForm: true
14
+ });
15
+ return stringifyToken(signed);
16
+ }
17
+ function isValidKeysArray(value) {
18
+ return Array.isArray(value) && value.every((entry)=>entry != null && typeof entry === 'object' && typeof entry.keyID === 'string' && isPosInt(entry.version));
19
+ }
20
+ /**
21
+ * Verify a credential-manifest envelope and extract its claim. Returns `null`
22
+ * (never throws) when the token is unparseable, unsigned (`alg: 'none'`), or
23
+ * structurally not a credential-manifest claim -- `grantorDID` is trusted
24
+ * only from the verified `iss`, never from a same-named field in the payload.
25
+ */ export async function verifyCredentialManifest(envelope, methods) {
26
+ let verified;
27
+ try {
28
+ verified = await verifyToken(envelope, {
29
+ methods
30
+ });
31
+ } catch {
32
+ return null;
33
+ }
34
+ if (!isVerifiedToken(verified)) {
35
+ return null;
36
+ }
37
+ const { iss, kind, ownerDID, recipientDID, epoch, sequence, keys, issuedAt } = verified.payload;
38
+ if (kind !== 'credential/key-manifest' || typeof ownerDID !== 'string' || typeof recipientDID !== 'string' || !isPosInt(epoch) || !isPosInt(sequence) || typeof issuedAt !== 'string' || !isValidKeysArray(keys)) {
39
+ return null;
40
+ }
41
+ return {
42
+ kind: 'credential/key-manifest',
43
+ ownerDID,
44
+ recipientDID,
45
+ grantorDID: normalizeDID(iss),
46
+ epoch,
47
+ sequence,
48
+ keys,
49
+ issuedAt
50
+ };
51
+ }
package/lib/provider.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { AliasBindingCredentialProvider, Credential, CredentialProvider, EntryPairedCredentialProvider } from '@kubun/credential-types';
1
+ import type { AliasBindingCredentialProvider, Credential, CredentialProvider, EntryPairedCredentialProvider, GrantAddress, VersionedEntryCredentialProvider } from '@kubun/credential-types';
2
2
  import type { CredentialAPI } from './api.js';
3
3
  export declare function serializeCredential(credential: Credential): string;
4
4
  export declare function deserializeCredential(raw: string | Record<string, unknown>): Credential;
5
5
  export type DBCredentialProviderParams = {
6
6
  api: CredentialAPI;
7
7
  };
8
- export declare class DBCredentialProvider implements CredentialProvider, AliasBindingCredentialProvider, EntryPairedCredentialProvider {
8
+ export declare class DBCredentialProvider implements CredentialProvider, AliasBindingCredentialProvider, EntryPairedCredentialProvider, VersionedEntryCredentialProvider {
9
9
  #private;
10
10
  constructor(params: DBCredentialProviderParams);
11
11
  get(providerName: string, ownerDID: string): Promise<Credential | null>;
@@ -17,6 +17,7 @@ export declare class DBCredentialProvider implements CredentialProvider, AliasBi
17
17
  controllerWrappableDID?: string;
18
18
  }): Promise<void>;
19
19
  delete(providerName: string, ownerDID: string): Promise<void>;
20
+ update(address: GrantAddress, change: (current: Credential | null) => Credential): Promise<void>;
20
21
  bindAlias(providerName: string, ownerDID: string, entryID: string): Promise<void>;
21
22
  getCredentialRef(providerName: string, ownerDID: string): Promise<{
22
23
  credential: Credential;
@@ -28,4 +29,16 @@ export declare class DBCredentialProvider implements CredentialProvider, AliasBi
28
29
  entryID: string;
29
30
  credential: Credential;
30
31
  }): Promise<'updated' | 'absent'>;
32
+ getVersionedCredentialRef(providerName: string, ownerDID: string): Promise<{
33
+ credential: Credential;
34
+ entryID: string;
35
+ version: string;
36
+ } | null>;
37
+ setCredentialForEntryIfUnchanged(params: {
38
+ providerName: string;
39
+ ownerDID: string;
40
+ entryID: string;
41
+ credential: Credential;
42
+ expectedVersion: string;
43
+ }): Promise<'updated' | 'absent' | 'superseded'>;
31
44
  }
package/lib/provider.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { normalizeDID } from '@kokuin/token';
2
+ import { CredentialUpdateUnavailableError } from '@kubun/credential-types';
3
+ import { CredentialBindNotAuthorized } from './errors.js';
1
4
  export function serializeCredential(credential) {
2
5
  const serialized = {
3
6
  accessToken: credential.accessToken,
@@ -78,6 +81,24 @@ export class DBCredentialProvider {
78
81
  async delete(providerName, ownerDID) {
79
82
  await this.#api.deleteCredential(providerName, ownerDID);
80
83
  }
84
+ async update(address, change) {
85
+ if (normalizeDID(address.deviceDID) !== this.#api.getLocalDeviceDID()) {
86
+ throw new CredentialBindNotAuthorized(address.providerName, address.ownerDID);
87
+ }
88
+ for(;;){
89
+ const ref = await this.getVersionedCredentialRef(address.providerName, address.ownerDID);
90
+ if (ref == null || ref.version == null) throw new CredentialUpdateUnavailableError();
91
+ const credential = change(ref.credential);
92
+ const result = await this.setCredentialForEntryIfUnchanged({
93
+ ...address,
94
+ entryID: ref.entryID,
95
+ credential,
96
+ expectedVersion: ref.version
97
+ });
98
+ if (result === 'updated') return;
99
+ if (result === 'absent') throw new CredentialUpdateUnavailableError();
100
+ }
101
+ }
81
102
  bindAlias(providerName, ownerDID, entryID) {
82
103
  return this.#api.bindAlias(providerName, ownerDID, entryID);
83
104
  }
@@ -97,4 +118,22 @@ export class DBCredentialProvider {
97
118
  credential: serializeCredential(params.credential)
98
119
  });
99
120
  }
121
+ async getVersionedCredentialRef(providerName, ownerDID) {
122
+ const ref = await this.#api.getVersionedCredentialRef(providerName, ownerDID);
123
+ if (ref == null) return null;
124
+ return {
125
+ credential: deserializeCredential(ref.credential),
126
+ entryID: ref.entryID,
127
+ version: ref.version
128
+ };
129
+ }
130
+ async setCredentialForEntryIfUnchanged(params) {
131
+ return await this.#api.setCredentialForEntryIfUnchanged({
132
+ providerName: params.providerName,
133
+ ownerDID: params.ownerDID,
134
+ entryID: params.entryID,
135
+ credential: serializeCredential(params.credential),
136
+ expectedVersion: params.expectedVersion
137
+ });
138
+ }
100
139
  }
@@ -0,0 +1,65 @@
1
+ import { type MethodRegistry } from '@kokuin/token';
2
+ import { type RecordStoreAdapter, type RecordTypeDefinition, type VisibilityContext } from '@kubun/records';
3
+ import { type CredentialStoreAPI } from '@kubun/store-credential';
4
+ export type { CredentialRecordType } from '@kubun/store-credential';
5
+ /**
6
+ * What the credential record types check against on the receiving device.
7
+ *
8
+ * - `store` is the device's credential store. The hooks never read it: every read inside a
9
+ * decision goes through the store the decision runs against, since this one is bound
10
+ * outside the apply transaction.
11
+ * - `resolveController` answers the `did:kokuin` controller a device key acts for, or `null`.
12
+ * For controller-owned records, only devices at the verifier's current head act for it.
13
+ * - `verifyAdministerChain` checks, at present time, that `tokens` give `issuer` the
14
+ * `credential/administer` right over `owner`.
15
+ * - `verifyGrantorEnvelope` checks, at present time, that a grantor envelope is valid for the
16
+ * wrapping op `wrappingOpHash`, its administer chain included.
17
+ * - `selfDID` is this device. A wrapping, wrapping authority or manifest is only stored here
18
+ * when it is addressed to this device, or when this device acts for the record's owner.
19
+ */
20
+ export type CredentialRecordTypeDeps = {
21
+ store: CredentialStoreAPI;
22
+ resolveController: (did: string) => Promise<string | null>;
23
+ verifyAdministerChain: (tokens: ReadonlyArray<string>, owner: string, issuer: string) => Promise<boolean>;
24
+ verifyGrantorEnvelope: (token: string, wrappingOpHash: string) => Promise<boolean>;
25
+ selfDID: string;
26
+ methods?: MethodRegistry;
27
+ };
28
+ /** Thrown when a credential hook runs against a store it cannot reach the indexed tables of. */
29
+ export declare class CredentialRecordHookError extends Error {
30
+ constructor(message: string);
31
+ }
32
+ /**
33
+ * The id of the entry holding the OAuth grant `name` that `grantDeviceDID` spends for `owner`.
34
+ * Derived, so the device that may write an entry is a property of the entry's id, whatever
35
+ * ops a peer has or has not seen for it.
36
+ */
37
+ export declare function credentialGrantEntryID(owner: string, grantDeviceDID: string, name: string): string;
38
+ /** Revocation removes every indexed wrapping for that recipient, at every key version. */
39
+ export declare function credentialStoreForHooks(adapter: RecordStoreAdapter): CredentialStoreAPI;
40
+ /**
41
+ * The credential record types, in dependency order: how each merges, who may write it, who
42
+ * may receive it, what it needs first, and what applying it does to the indexed tables.
43
+ *
44
+ * Every authorizer decodes a live payload with the store's codec after checking it against the
45
+ * signed digest, binds the record to the owner of the key it belongs to, and accepts only an
46
+ * issuer that acts for that owner. None decrypts or unwraps anything.
47
+ */
48
+ export declare function credentialRecordTypes(deps: CredentialRecordTypeDeps): ReadonlyMap<string, RecordTypeDefinition>;
49
+ /**
50
+ * The record types that prove a requester holds a key: wrappings only. A revocation also names
51
+ * a key and a recipient, and must never pass for a holding.
52
+ */
53
+ export declare function credentialHolderTypes(types: ReadonlyMap<string, RecordTypeDefinition>): ReadonlyMap<string, RecordTypeDefinition>;
54
+ /**
55
+ * The responder's current version of a key, for `VisibilityContext.currentKeyVersion`: the
56
+ * stored key record of that id, when it is live and belongs to `owner`.
57
+ */
58
+ export declare function credentialKeyVersionReader(store: RecordStoreAdapter, methods?: MethodRegistry): VisibilityContext['currentKeyVersion'];
59
+ /**
60
+ * The authorizer a responder runs on a holder proof, for `VisibilityContext.authorizeProof`:
61
+ * the wrapping authorizer apply runs, reading `store`, without the receiver binding — a proof
62
+ * is the requester's wrapping, never addressed to the responder. Anything but a wrapping, or
63
+ * a record whose op does not verify, is refused.
64
+ */
65
+ export declare function credentialProofAuthorizer(deps: CredentialRecordTypeDeps, store: RecordStoreAdapter): VisibilityContext['authorizeProof'];