@kokuin/token 0.4.0 → 0.5.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/lib/did.d.ts CHANGED
@@ -1,15 +1,86 @@
1
1
  import type { DIDResolver } from './cache.js';
2
+ import { type MethodRegistry } from './method.js';
2
3
  import type { DIDDoc } from './peer4.js';
3
4
  import type { SignatureAlgorithm } from './schemas.js';
4
5
  import type { DIDString } from './types.js';
5
- /** @internal */
6
+ /**
7
+ * Multicodec prefixes per signature algorithm, following the `did:key` convention. Supported, not
8
+ * internal: `@kokuin/capability` builds the `cnf.kid` confirmation claim from this and
9
+ * {@link getAlgorithmAndPublicKey}, and `cnf.kid` is a published wire format third parties produce
10
+ * and read.
11
+ */
6
12
  export declare const CODECS: Record<SignatureAlgorithm, Uint8Array>;
7
- /** @internal */
13
+ /**
14
+ * Split multicodec-prefixed key bytes into their algorithm and the raw public key, or `null` when
15
+ * the prefix names no algorithm this package knows.
16
+ *
17
+ * The decoder half of the `cnf.kid` wire format — see {@link CODECS}.
18
+ */
8
19
  export declare function getAlgorithmAndPublicKey(bytes: Uint8Array): [SignatureAlgorithm, Uint8Array] | null;
9
20
  /** @internal */
10
21
  export declare function getDID(codec: Uint8Array, publicKey: Uint8Array): DIDString;
11
- /** @internal */
22
+ /**
23
+ * The signature algorithm and raw public key a `did:key` identifier carries. Throws when the string
24
+ * is not a `did:key`, its encoded form is implausibly long, or the codec names no supported
25
+ * algorithm.
26
+ *
27
+ * Supported, not internal: `@kumiai/mls` checks an MLS credential's key against the key its DID
28
+ * names, a comparison only this function makes correctly — the alternative is a second decoder in
29
+ * another repo whose failure mode is accepting a credential for the wrong key. Any other method
30
+ * resolves through a `DIDMethodResolver`; this is the self-contained case.
31
+ */
12
32
  export declare function getSignatureInfo(did: string): [SignatureAlgorithm, Uint8Array];
33
+ /**
34
+ * The issuer could not be resolved to a usable signing key: no method, resolver, or registry entry
35
+ * turned `iss` into one — including a resolver that has no answer, throws, or answers with something
36
+ * unusable (an oversized document, or one not hashing to the DID asked for).
37
+ *
38
+ * Distinct from every other verification failure on purpose. An invalid signature or a bad `kid`
39
+ * means the issuer *was* resolved and the token is bad (positive evidence); this means nothing was
40
+ * learned either way, and a caller treating "could not check" as "checked and fine" fails open.
41
+ * `@kokuin/capability`'s revocation checker turns on this distinction, so it must be a type, not a
42
+ * message — text matching is how such a check regresses silently.
43
+ */
44
+ export declare class UnresolvableIssuerError extends Error {
45
+ /**
46
+ * Identifies the error by value, not identity: within this package `instanceof` is exact, but a
47
+ * duplicated `@kokuin/token` in a consumer's tree would make a cross-copy `instanceof` false (which
48
+ * fails closed, so this is hardening). Consumers should use `isUnresolvableIssuerError`.
49
+ */
50
+ static get brand(): string;
51
+ /** The brand, readable from an instance — what `isUnresolvableIssuerError` matches on. */
52
+ get brand(): string;
53
+ constructor(message: string, options?: ErrorOptions);
54
+ }
55
+ /**
56
+ * Whether a thrown value means the issuer could not be resolved.
57
+ *
58
+ * Prefer this to `instanceof UnresolvableIssuerError` across a package boundary: it matches on the
59
+ * brand, so it holds even if the thrower and the checker resolved different copies of this package.
60
+ */
61
+ export declare function isUnresolvableIssuerError(value: unknown): value is UnresolvableIssuerError;
62
+ /**
63
+ * A DID method resolved the issuer, and the token then named a key that issuer does not have. The
64
+ * counterpart of {@link UnresolvableIssuerError}: a `DIDMethodResolver` needs a way to say "resolved,
65
+ * and the token is bad" too, else `resolveIssuerWithDoc` wraps everything a method throws as
66
+ * unresolvable and an unauthenticated `kid` naming a real DID and an invented key reads as "could not
67
+ * check" rather than "checked, and bad". Throw it only for that; not knowing the DID at all is
68
+ * {@link UnresolvableIssuerError}.
69
+ */
70
+ export declare class IssuerKeyNotFoundError extends Error {
71
+ /** @see UnresolvableIssuerError.brand — same reasoning, across the same package boundary. */
72
+ static get brand(): string;
73
+ /** The brand, readable from an instance — what `isIssuerKeyNotFoundError` matches on. */
74
+ get brand(): string;
75
+ constructor(message: string, options?: ErrorOptions);
76
+ }
77
+ /**
78
+ * Whether a thrown value means the issuer resolved but does not have the key the token named.
79
+ *
80
+ * Prefer this to `instanceof IssuerKeyNotFoundError` across a package boundary, for the same
81
+ * reason as {@link isUnresolvableIssuerError}.
82
+ */
83
+ export declare function isIssuerKeyNotFoundError(value: unknown): value is IssuerKeyNotFoundError;
13
84
  export type ResolveIssuerHeader = {
14
85
  kid?: string;
15
86
  };
@@ -22,16 +93,40 @@ export type ResolveIssuerWithDocResult = {
22
93
  doc: DIDDoc;
23
94
  };
24
95
  };
96
+ /**
97
+ * Whether to resolve the issuer's *current* signing key or one it signed with in the past. `false`
98
+ * (default) asks {@link DIDMethodResolver.resolve} — the safe question, right for a live signer.
99
+ * `true` asks {@link DIDMethodResolver.resolveHistoric}, an explicit statement that the artefact was
100
+ * issued in the past and must survive key rotation. A resolver with no `resolveHistoric` **refuses**
101
+ * rather than falling back to `resolve`, which would be the permissive scan by another name.
102
+ */
103
+ export type ResolveIssuerMode = {
104
+ historic?: boolean;
105
+ };
106
+ /** Params for {@link resolveIssuer} / {@link resolveIssuerWithDoc}. */
107
+ export type ResolveIssuerParams = {
108
+ iss: string;
109
+ header?: ResolveIssuerHeader;
110
+ resolver?: DIDResolver;
111
+ methods?: MethodRegistry;
112
+ /** See {@link ResolveIssuerMode.historic}. */
113
+ historic?: boolean;
114
+ };
25
115
  /**
26
116
  * Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
27
117
  * plus the decoded peer:4 doc when one was obtained inline or via the resolver.
28
118
  * Callers writing to a DID cache should write `peer4Doc` only after signature verification.
119
+ *
120
+ * The `historic` param selects which question is asked of a `DIDMethodResolver` — see
121
+ * {@link ResolveIssuerMode}. It reaches only the method-registry branch: `did:key` carries its key
122
+ * in the identifier and a `did:peer:4` document is fixed by its own hash, so neither has a past key
123
+ * set distinct from its present one.
29
124
  */
30
- export declare function resolveIssuerWithDoc(iss: string, header?: ResolveIssuerHeader, resolver?: DIDResolver): Promise<ResolveIssuerWithDocResult>;
125
+ export declare function resolveIssuerWithDoc({ iss, header, resolver, methods, historic, }: ResolveIssuerParams): Promise<ResolveIssuerWithDocResult>;
31
126
  /**
32
127
  * Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
33
128
  */
34
- export declare function resolveIssuer(iss: string, header?: ResolveIssuerHeader, resolver?: DIDResolver): Promise<[SignatureAlgorithm, Uint8Array]>;
129
+ export declare function resolveIssuer({ iss, header, resolver, methods, historic, }: ResolveIssuerParams): Promise<[SignatureAlgorithm, Uint8Array]>;
35
130
  /**
36
131
  * Fold a DID to its canonical form for equality comparison.
37
132
  * For did:peer:4, returns the short form regardless of whether input is long or short.
package/lib/did.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { base58 } from '@scure/base';
2
+ import { findMethodResolver } from './method.js';
2
3
  import { decodeMultibase } from './multibase.js';
3
4
  import { assertDocWithinMaxSize, decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4 } from './peer4.js';
4
- /** @internal */ export const CODECS = {
5
+ /**
6
+ * Multicodec prefixes per signature algorithm, following the `did:key` convention. Supported, not
7
+ * internal: `@kokuin/capability` builds the `cnf.kid` confirmation claim from this and
8
+ * {@link getAlgorithmAndPublicKey}, and `cnf.kid` is a published wire format third parties produce
9
+ * and read.
10
+ */ export const CODECS = {
5
11
  ES256: new Uint8Array([
6
12
  128,
7
13
  36
@@ -28,7 +34,12 @@ function isCodecMatch(codec, bytes) {
28
34
  }
29
35
  return true;
30
36
  }
31
- /** @internal */ export function getAlgorithmAndPublicKey(bytes) {
37
+ /**
38
+ * Split multicodec-prefixed key bytes into their algorithm and the raw public key, or `null` when
39
+ * the prefix names no algorithm this package knows.
40
+ *
41
+ * The decoder half of the `cnf.kid` wire format — see {@link CODECS}.
42
+ */ export function getAlgorithmAndPublicKey(bytes) {
32
43
  for (const [alg, codec] of Object.entries(CODECS)){
33
44
  if (isCodecMatch(codec, bytes)) {
34
45
  return [
@@ -47,7 +58,16 @@ function isCodecMatch(codec, bytes) {
47
58
  bytes.set(publicKey, codec.length);
48
59
  return `${PREFIX}${base58.encode(bytes)}`;
49
60
  }
50
- /** @internal */ export function getSignatureInfo(did) {
61
+ /**
62
+ * The signature algorithm and raw public key a `did:key` identifier carries. Throws when the string
63
+ * is not a `did:key`, its encoded form is implausibly long, or the codec names no supported
64
+ * algorithm.
65
+ *
66
+ * Supported, not internal: `@kumiai/mls` checks an MLS credential's key against the key its DID
67
+ * names, a comparison only this function makes correctly — the alternative is a second decoder in
68
+ * another repo whose failure mode is accepting a credential for the wrong key. Any other method
69
+ * resolves through a `DIDMethodResolver`; this is the self-contained case.
70
+ */ export function getSignatureInfo(did) {
51
71
  if (!did.startsWith(PREFIX)) {
52
72
  throw new Error('Invalid DID format');
53
73
  }
@@ -67,11 +87,114 @@ function isCodecMatch(codec, bytes) {
67
87
  }
68
88
  return info;
69
89
  }
90
+ const UNRESOLVABLE_ISSUER_BRAND = '@kokuin/token/UnresolvableIssuerError';
91
+ /**
92
+ * The issuer could not be resolved to a usable signing key: no method, resolver, or registry entry
93
+ * turned `iss` into one — including a resolver that has no answer, throws, or answers with something
94
+ * unusable (an oversized document, or one not hashing to the DID asked for).
95
+ *
96
+ * Distinct from every other verification failure on purpose. An invalid signature or a bad `kid`
97
+ * means the issuer *was* resolved and the token is bad (positive evidence); this means nothing was
98
+ * learned either way, and a caller treating "could not check" as "checked and fine" fails open.
99
+ * `@kokuin/capability`'s revocation checker turns on this distinction, so it must be a type, not a
100
+ * message — text matching is how such a check regresses silently.
101
+ */ export class UnresolvableIssuerError extends Error {
102
+ /**
103
+ * Identifies the error by value, not identity: within this package `instanceof` is exact, but a
104
+ * duplicated `@kokuin/token` in a consumer's tree would make a cross-copy `instanceof` false (which
105
+ * fails closed, so this is hardening). Consumers should use `isUnresolvableIssuerError`.
106
+ */ static get brand() {
107
+ return UNRESOLVABLE_ISSUER_BRAND;
108
+ }
109
+ /** The brand, readable from an instance — what `isUnresolvableIssuerError` matches on. */ get brand() {
110
+ return UNRESOLVABLE_ISSUER_BRAND;
111
+ }
112
+ constructor(message, options){
113
+ super(message, options);
114
+ this.name = 'UnresolvableIssuerError';
115
+ }
116
+ }
117
+ /**
118
+ * Whether a thrown value means the issuer could not be resolved.
119
+ *
120
+ * Prefer this to `instanceof UnresolvableIssuerError` across a package boundary: it matches on the
121
+ * brand, so it holds even if the thrower and the checker resolved different copies of this package.
122
+ */ export function isUnresolvableIssuerError(value) {
123
+ return value instanceof Error && value.brand === UNRESOLVABLE_ISSUER_BRAND;
124
+ }
125
+ const ISSUER_KEY_NOT_FOUND_BRAND = '@kokuin/token/IssuerKeyNotFoundError';
126
+ /**
127
+ * A DID method resolved the issuer, and the token then named a key that issuer does not have. The
128
+ * counterpart of {@link UnresolvableIssuerError}: a `DIDMethodResolver` needs a way to say "resolved,
129
+ * and the token is bad" too, else `resolveIssuerWithDoc` wraps everything a method throws as
130
+ * unresolvable and an unauthenticated `kid` naming a real DID and an invented key reads as "could not
131
+ * check" rather than "checked, and bad". Throw it only for that; not knowing the DID at all is
132
+ * {@link UnresolvableIssuerError}.
133
+ */ export class IssuerKeyNotFoundError extends Error {
134
+ /** @see UnresolvableIssuerError.brand — same reasoning, across the same package boundary. */ static get brand() {
135
+ return ISSUER_KEY_NOT_FOUND_BRAND;
136
+ }
137
+ /** The brand, readable from an instance — what `isIssuerKeyNotFoundError` matches on. */ get brand() {
138
+ return ISSUER_KEY_NOT_FOUND_BRAND;
139
+ }
140
+ constructor(message, options){
141
+ super(message, options);
142
+ this.name = 'IssuerKeyNotFoundError';
143
+ }
144
+ }
145
+ /**
146
+ * Whether a thrown value means the issuer resolved but does not have the key the token named.
147
+ *
148
+ * Prefer this to `instanceof IssuerKeyNotFoundError` across a package boundary, for the same
149
+ * reason as {@link isUnresolvableIssuerError}.
150
+ */ export function isIssuerKeyNotFoundError(value) {
151
+ return value instanceof Error && value.brand === ISSUER_KEY_NOT_FOUND_BRAND;
152
+ }
70
153
  /**
71
154
  * Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
72
155
  * plus the decoded peer:4 doc when one was obtained inline or via the resolver.
73
156
  * Callers writing to a DID cache should write `peer4Doc` only after signature verification.
74
- */ export async function resolveIssuerWithDoc(iss, header = {}, resolver) {
157
+ *
158
+ * The `historic` param selects which question is asked of a `DIDMethodResolver` — see
159
+ * {@link ResolveIssuerMode}. It reaches only the method-registry branch: `did:key` carries its key
160
+ * in the identifier and a `did:peer:4` document is fixed by its own hash, so neither has a past key
161
+ * set distinct from its present one.
162
+ */ export async function resolveIssuerWithDoc({ iss, header = {}, resolver, methods, historic = false }) {
163
+ if (methods != null) {
164
+ const methodResolver = findMethodResolver(methods, iss);
165
+ if (methodResolver != null) {
166
+ // The historic question is answered only by `resolveHistoric`; a resolver without one is not
167
+ // asked `resolve` instead, which would substitute a different question. `UnresolvableIssuerError`
168
+ // is right — nothing was learned either way, which a fail-closed caller treats as a denial.
169
+ if (historic && methodResolver.resolveHistoric == null) {
170
+ throw new UnresolvableIssuerError(`DID method ${methodResolver.method} cannot resolve historic keys: ${iss}`);
171
+ }
172
+ // A method resolver throws its own error strings; re-type them so a method-backed resolution
173
+ // failure is indistinguishable from the built-in ones below. Message preserved, original kept
174
+ // as `cause`.
175
+ let resolved;
176
+ try {
177
+ // Called as a method of its own resolver, not detached: an implementation may be a class with
178
+ // private state.
179
+ resolved = historic && methodResolver.resolveHistoric != null ? await methodResolver.resolveHistoric(iss, header) : await methodResolver.resolve(iss, header);
180
+ } catch (cause) {
181
+ // Except the one failure that is not a failure to resolve: "I have this issuer, it has no such
182
+ // key" is what the `kid` branches below report for `did:peer:4` (ordinary errors — see
183
+ // IssuerKeyNotFoundError). Wrapping it would let an unauthenticated header make any issuer read
184
+ // as unresolvable, which callers fail closed on.
185
+ if (isIssuerKeyNotFoundError(cause)) {
186
+ throw cause;
187
+ }
188
+ throw new UnresolvableIssuerError(cause instanceof Error ? cause.message : `Unknown DID: ${iss}`, {
189
+ cause
190
+ });
191
+ }
192
+ return {
193
+ alg: resolved.alg,
194
+ publicKey: resolved.publicKey
195
+ };
196
+ }
197
+ }
75
198
  if (isPeer4(iss)) {
76
199
  const shortForm = getPeer4ShortForm(iss);
77
200
  if (iss !== shortForm) {
@@ -87,16 +210,36 @@ function isCodecMatch(codec, bytes) {
87
210
  };
88
211
  }
89
212
  if (resolver == null) {
90
- throw new Error(`Unknown DID: ${shortForm}`);
213
+ throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`);
214
+ }
215
+ // Throwing is the normal style for a network-backed resolver, so one that throws must be
216
+ // indistinguishable from one that returns nothing — else the fail-closed guarantee holds only for
217
+ // resolvers that signal failure by returning `undefined`.
218
+ let doc;
219
+ try {
220
+ doc = await resolver(shortForm);
221
+ } catch (cause) {
222
+ throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`, {
223
+ cause
224
+ });
91
225
  }
92
- const doc = await resolver(shortForm);
93
226
  if (doc == null) {
94
- throw new Error(`Unknown DID: ${shortForm}`);
227
+ throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`);
228
+ }
229
+ // An unusable answer — oversized, or not hashing to the DID asked for — counts as unresolvable,
230
+ // not an ordinary fault: else a caller failing closed solely on `UnresolvableIssuerError` takes a
231
+ // broken or lying resolver as "not revoked". No availability cost worth weighing — a resolver
232
+ // willing to lie about documents already controls resolution completely.
233
+ try {
234
+ assertDocWithinMaxSize(doc);
235
+ } catch (cause) {
236
+ throw new UnresolvableIssuerError(cause instanceof Error ? cause.message : `Unknown DID: ${shortForm}`, {
237
+ cause
238
+ });
95
239
  }
96
- assertDocWithinMaxSize(doc);
97
240
  const expected = encodePeer4(doc).shortForm;
98
241
  if (expected !== shortForm) {
99
- throw new Error('DIDResolver: short form/doc hash mismatch');
242
+ throw new UnresolvableIssuerError('DIDResolver: short form/doc hash mismatch');
100
243
  }
101
244
  const [alg, publicKey] = resolveKidOrAuth(doc, header.kid);
102
245
  return {
@@ -108,16 +251,30 @@ function isCodecMatch(codec, bytes) {
108
251
  }
109
252
  };
110
253
  }
254
+ // `iss` narrows to `never` here (isPeer4's `value is string` collapses the false case), so route
255
+ // the prefix check through a helper taking an unnarrowed `string`.
256
+ if (!hasKeyPrefix(iss)) {
257
+ throw new UnresolvableIssuerError(`Unknown DID: ${iss}`);
258
+ }
111
259
  const [alg, publicKey] = getSignatureInfo(iss);
112
260
  return {
113
261
  alg,
114
262
  publicKey
115
263
  };
116
264
  }
265
+ function hasKeyPrefix(did) {
266
+ return did.startsWith(PREFIX);
267
+ }
117
268
  /**
118
269
  * Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
119
- */ export async function resolveIssuer(iss, header = {}, resolver) {
120
- const { alg, publicKey } = await resolveIssuerWithDoc(iss, header, resolver);
270
+ */ export async function resolveIssuer({ iss, header = {}, resolver, methods, historic = false }) {
271
+ const { alg, publicKey } = await resolveIssuerWithDoc({
272
+ iss,
273
+ header,
274
+ resolver,
275
+ methods,
276
+ historic
277
+ });
121
278
  return [
122
279
  alg,
123
280
  publicKey
package/lib/identity.d.ts CHANGED
@@ -23,11 +23,10 @@ export type SigningIdentity = Identity & {
23
23
  publicKey: Uint8Array;
24
24
  signToken: <Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions) => Promise<SignedToken<Payload>>;
25
25
  };
26
- export type DecryptingIdentity = Identity & {
27
- decrypt(jwe: string): Promise<Uint8Array>;
26
+ export type KeyAgreementIdentity = Identity & {
28
27
  agreeKey(ephemeralPublicKey: Uint8Array): Promise<Uint8Array>;
29
28
  };
30
- export type FullIdentity = SigningIdentity & DecryptingIdentity;
29
+ export type FullIdentity = SigningIdentity & KeyAgreementIdentity;
31
30
  export type OwnIdentity = FullIdentity & {
32
31
  privateKey: Uint8Array;
33
32
  };
@@ -35,20 +34,40 @@ export type IdentityProvider<T extends SigningIdentity = SigningIdentity> = {
35
34
  provideIdentity(keyID: string): Promise<T>;
36
35
  };
37
36
  export declare function isSigningIdentity(identity: Identity): identity is SigningIdentity;
38
- export declare function isDecryptingIdentity(identity: Identity): identity is DecryptingIdentity;
37
+ export declare function isKeyAgreementIdentity(identity: Identity): identity is KeyAgreementIdentity;
39
38
  export declare function isFullIdentity(identity: Identity): identity is FullIdentity;
40
39
  export declare function isOwnIdentity(identity: Identity): identity is OwnIdentity;
40
+ /**
41
+ * Create a signing identity for an Ed25519 private key under a caller-supplied DID.
42
+ *
43
+ * `createSigningIdentity` derives the DID *from* the key, which only works for `did:key`. A method
44
+ * whose identifier is not a function of the current key — `did:kokuin:`, where the key rotates under
45
+ * a fixed inception digest — must supply its own `id`, so `iss` is the method's DID, not the key's
46
+ * `did:key`. The caller owns the pairing: nothing here checks `id` resolves to `privateKey`'s public
47
+ * key, and a mismatch produces tokens that verify nowhere.
48
+ */
49
+ export declare function createSigningIdentityForDID(id: DIDString, privateKey: Uint8Array): SigningIdentity;
41
50
  /**
42
51
  * Create a signing identity from an Ed25519 private key.
43
52
  */
44
53
  export declare function createSigningIdentity(privateKey: Uint8Array): SigningIdentity;
45
54
  /**
46
- * Create a decrypting identity from an Ed25519 private key.
55
+ * Create a key-agreement identity from an Ed25519 private key.
47
56
  * Uses X25519 key derivation for ECDH key agreement.
48
57
  */
49
- export declare function createDecryptingIdentity(privateKey: Uint8Array): DecryptingIdentity;
58
+ export declare function createKeyAgreementIdentity(privateKey: Uint8Array): KeyAgreementIdentity;
59
+ /**
60
+ * A key-agreement identity for a **raw X25519 private key** under a caller-supplied DID.
61
+ *
62
+ * The counterpart of {@link createSigningIdentityForDID} for the agreement half. Two differences from
63
+ * {@link createKeyAgreementIdentity}: the identifier is supplied (a `did:kokuin:` key rotates under a
64
+ * fixed inception digest, so `id` cannot be a function of the key), and the input is the X25519 scalar
65
+ * itself rather than an Ed25519 key to Montgomery-convert — a controller derives an independent
66
+ * agreement keypair, not the Montgomery form of its signing key.
67
+ */
68
+ export declare function createKeyAgreementIdentityForDID(id: DIDString, x25519PrivateKey: Uint8Array): KeyAgreementIdentity;
50
69
  /**
51
- * Create a full identity (signing + decrypting) from an Ed25519 private key.
70
+ * Create a full identity (signing + key agreement) from an Ed25519 private key.
52
71
  */
53
72
  export declare function createFullIdentity(privateKey: Uint8Array): FullIdentity;
54
73
  /**
@@ -93,7 +112,6 @@ export type MultiKeyIdentity = {
93
112
  publicKey: Uint8Array;
94
113
  privateKey: Uint8Array;
95
114
  signToken<Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions): Promise<SignedToken<Payload>>;
96
- decrypt(jwe: string): Promise<Uint8Array>;
97
115
  agreeKey(ephemeralPublicKey: Uint8Array, kid?: string): Promise<Uint8Array>;
98
116
  };
99
117
  /**
package/lib/identity.js CHANGED
@@ -3,7 +3,6 @@ import { ed25519, x25519 } from '@noble/curves/ed25519.js';
3
3
  import { b64uFromJSON, fromUTF, toB64U } from '@sozai/codec';
4
4
  import { withSpan } from '@sozai/otel';
5
5
  import { CODECS, getDID, normalizeDID } from './did.js';
6
- import { decryptToken } from './jwe.js';
7
6
  import { encodeMultibase } from './multibase.js';
8
7
  import { encodePeer4, isPeer4 } from './peer4.js';
9
8
  import { concatBytes } from './utils.js';
@@ -11,20 +10,29 @@ const tracer = createTracer('token');
11
10
  export function isSigningIdentity(identity) {
12
11
  return 'publicKey' in identity && 'signToken' in identity && typeof identity.signToken === 'function';
13
12
  }
14
- export function isDecryptingIdentity(identity) {
15
- return 'decrypt' in identity && typeof identity.decrypt === 'function' && 'agreeKey' in identity && typeof identity.agreeKey === 'function';
13
+ export function isKeyAgreementIdentity(identity) {
14
+ return 'agreeKey' in identity && typeof identity.agreeKey === 'function';
16
15
  }
17
16
  export function isFullIdentity(identity) {
18
- return isSigningIdentity(identity) && isDecryptingIdentity(identity);
17
+ return isSigningIdentity(identity) && isKeyAgreementIdentity(identity);
19
18
  }
20
19
  export function isOwnIdentity(identity) {
21
20
  return isFullIdentity(identity) && 'privateKey' in identity;
22
21
  }
23
22
  /**
24
- * Create a signing identity from an Ed25519 private key.
25
- */ export function createSigningIdentity(privateKey) {
26
- const publicKey = ed25519.getPublicKey(privateKey);
27
- const id = getDID(CODECS.EdDSA, publicKey);
23
+ * Create a signing identity for an Ed25519 private key under a caller-supplied DID.
24
+ *
25
+ * `createSigningIdentity` derives the DID *from* the key, which only works for `did:key`. A method
26
+ * whose identifier is not a function of the current key — `did:kokuin:`, where the key rotates under
27
+ * a fixed inception digest — must supply its own `id`, so `iss` is the method's DID, not the key's
28
+ * `did:key`. The caller owns the pairing: nothing here checks `id` resolves to `privateKey`'s public
29
+ * key, and a mismatch produces tokens that verify nowhere.
30
+ */ export function createSigningIdentityForDID(id, privateKey) {
31
+ return buildSigningIdentity(id, privateKey, ed25519.getPublicKey(privateKey));
32
+ }
33
+ // Shared body. Takes `publicKey` rather than deriving it, so `createSigningIdentity` — which
34
+ // needs the public key anyway to build the `did:key` — derives it once instead of twice.
35
+ function buildSigningIdentity(id, privateKey, publicKey) {
28
36
  async function signToken(payload, options = {}) {
29
37
  return withSpan(tracer, KokuinSpanNames.TOKEN_SIGN, {
30
38
  attributes: {
@@ -60,36 +68,51 @@ export function isOwnIdentity(identity) {
60
68
  };
61
69
  }
62
70
  /**
63
- * Create a decrypting identity from an Ed25519 private key.
71
+ * Create a signing identity from an Ed25519 private key.
72
+ */ export function createSigningIdentity(privateKey) {
73
+ const publicKey = ed25519.getPublicKey(privateKey);
74
+ return buildSigningIdentity(getDID(CODECS.EdDSA, publicKey), privateKey, publicKey);
75
+ }
76
+ /**
77
+ * Create a key-agreement identity from an Ed25519 private key.
64
78
  * Uses X25519 key derivation for ECDH key agreement.
65
- */ export function createDecryptingIdentity(privateKey) {
79
+ */ export function createKeyAgreementIdentity(privateKey) {
66
80
  const publicKey = ed25519.getPublicKey(privateKey);
67
81
  const id = getDID(CODECS.EdDSA, publicKey);
68
82
  const x25519Private = ed25519.utils.toMontgomerySecret(privateKey);
69
- async function decrypt(jwe) {
70
- return decryptToken({
71
- id,
72
- decrypt,
73
- agreeKey
74
- }, jwe);
75
- }
76
83
  async function agreeKey(ephemeralPublicKey) {
77
84
  return x25519.getSharedSecret(x25519Private, ephemeralPublicKey);
78
85
  }
79
86
  return {
80
87
  id,
81
- decrypt,
82
88
  agreeKey
83
89
  };
84
90
  }
85
91
  /**
86
- * Create a full identity (signing + decrypting) from an Ed25519 private key.
92
+ * A key-agreement identity for a **raw X25519 private key** under a caller-supplied DID.
93
+ *
94
+ * The counterpart of {@link createSigningIdentityForDID} for the agreement half. Two differences from
95
+ * {@link createKeyAgreementIdentity}: the identifier is supplied (a `did:kokuin:` key rotates under a
96
+ * fixed inception digest, so `id` cannot be a function of the key), and the input is the X25519 scalar
97
+ * itself rather than an Ed25519 key to Montgomery-convert — a controller derives an independent
98
+ * agreement keypair, not the Montgomery form of its signing key.
99
+ */ export function createKeyAgreementIdentityForDID(id, x25519PrivateKey) {
100
+ async function agreeKey(ephemeralPublicKey) {
101
+ return x25519.getSharedSecret(x25519PrivateKey, ephemeralPublicKey);
102
+ }
103
+ return {
104
+ id,
105
+ agreeKey
106
+ };
107
+ }
108
+ /**
109
+ * Create a full identity (signing + key agreement) from an Ed25519 private key.
87
110
  */ export function createFullIdentity(privateKey) {
88
111
  const signing = createSigningIdentity(privateKey);
89
- const decrypting = createDecryptingIdentity(privateKey);
112
+ const keyAgreement = createKeyAgreementIdentity(privateKey);
90
113
  return {
91
114
  ...signing,
92
- ...decrypting
115
+ ...keyAgreement
93
116
  };
94
117
  }
95
118
  /**
@@ -217,14 +240,15 @@ function signWith(key, data) {
217
240
  *
218
241
  * A peer:4 identity uses its published `keyAgreement` key. A `did:key` EdDSA identity has no
219
242
  * published agreement key — a sender derives one from its signing key via the birational map —
220
- * so it must derive the matching secret the same way, exactly as `createDecryptingIdentity` does.
243
+ * so it must derive the matching secret the same way, exactly as `createKeyAgreementIdentity` does.
221
244
  */ function pickAgreementSecret(keys, isPeer, kid) {
222
245
  if (kid != null) {
223
246
  // No birational fallback here, unlike the no-kid branch below: a kid names one specific
224
247
  // key, so falling back to a *different*, derived key would silently agree with the wrong
225
248
  // secret. This means a did:key identity's agreeKey(epk, '#key-0') throws where
226
- // agreeKey(epk) succeeds — a known asymmetry, currently unreachable: decryptToken (jwe.ts)
227
- // never passes a kid, and DecryptingIdentity#agreeKey has no kid parameter at all.
249
+ // agreeKey(epk) succeeds — a known asymmetry, currently unreachable: decryptToken
250
+ // (@kokuin/jwe) never passes a kid, and KeyAgreementIdentity#agreeKey has no kid parameter
251
+ // at all.
228
252
  const found = keys.find((key)=>key.fragment === kid);
229
253
  if (found == null) throw new Error(`KidNotFound: ${kid}`);
230
254
  if (found.purpose !== 'kem' || found.alg !== 'X25519') {
@@ -291,14 +315,6 @@ function buildIdentity(id, longForm, doc, keys) {
291
315
  async function agreeKey(ephemeralPublicKey, kid) {
292
316
  return x25519.getSharedSecret(pickAgreementSecret(keys, isPeer, kid), ephemeralPublicKey);
293
317
  }
294
- async function decrypt(jwe) {
295
- pickAgreementSecret(keys, isPeer); // fail fast when this identity cannot agree at all
296
- return decryptToken({
297
- id,
298
- decrypt,
299
- agreeKey
300
- }, jwe);
301
- }
302
318
  return {
303
319
  id,
304
320
  longForm,
@@ -307,7 +323,6 @@ function buildIdentity(id, longForm, doc, keys) {
307
323
  publicKey: primarySig.publicKey,
308
324
  privateKey: primarySig.privateKey,
309
325
  signToken,
310
- decrypt,
311
326
  agreeKey
312
327
  };
313
328
  }
package/lib/index.d.ts CHANGED
@@ -10,14 +10,12 @@
10
10
  * @module token
11
11
  */
12
12
  export { type CreateInMemoryDIDCacheOptions, createInMemoryDIDCache, type DIDCache, type DIDResolver, } from './cache.js';
13
- export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, } from './did.js';
14
- export { type CreateIdentityInput, createDecryptingIdentity, createFullIdentity, createIdentity, createSigningIdentity, type DecryptingIdentity, type FullIdentity, type Identity, type IdentityKeySpec, type IdentityProvider, isDecryptingIdentity, isFullIdentity, isOwnIdentity, isSigningIdentity, type KeyAlg, type KeyPurpose, type MultiKeyIdentity, type OwnIdentity, type ResolvedKey, randomIdentity, type SigningIdentity, type SignTokenOptions, } from './identity.js';
15
- export type { ConcatKDFParams, EncryptOptions, EnvelopeMode, JWEHeader, SharedSecretResult, TokenEncrypter, UnwrapOptions, UnwrappedEnvelope, WrapOptions, } from './jwe.js';
16
- export { concatKDF, createTokenEncrypter, decryptToken, deriveSharedSecret, encryptToken, unwrapEnvelope, wrapEnvelope, } from './jwe.js';
13
+ export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerMode, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError, } from './did.js';
14
+ export { type CreateIdentityInput, createFullIdentity, createIdentity, createKeyAgreementIdentity, createKeyAgreementIdentityForDID, createSigningIdentity, createSigningIdentityForDID, type FullIdentity, type Identity, type IdentityKeySpec, type IdentityProvider, isFullIdentity, isKeyAgreementIdentity, isOwnIdentity, isSigningIdentity, type KeyAgreementIdentity, type KeyAlg, type KeyPurpose, type MultiKeyIdentity, type OwnIdentity, type ResolvedKey, randomIdentity, type SigningIdentity, type SignTokenOptions, } from './identity.js';
17
15
  export type { KeyEntry, KeyStore, MutableKeyEntry } from './keystore.js';
16
+ export { type AgreementAlgorithm, type DIDMethodResolver, findMethodResolver, type MethodRegistry, type ResolvedAgreementKey, type ResolvedSigningKey, } from './method.js';
18
17
  export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash, } from './multibase.js';
19
18
  export { type DecodePeer4Options, type DIDDoc, decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4, type VerificationMethod, validateDIDDoc, } from './peer4.js';
20
- export { createRotationAssertion, type RotationPayload, } from './rotation.js';
21
19
  export { capabilitySchema, type SignatureAlgorithm, type SignedHeader, type SignedPayload, type SupportedHeader, signedHeaderSchema, signedPayloadSchema, supportedHeaderSchema, type UnsignedHeader, unsignedHeaderSchema, validateAlgorithm, validateSignedHeader, validateSignedPayload, validateUnsignedHeader, } from './schemas.js';
22
20
  export { decodePrivateKey, encodePrivateKey, randomPrivateKey, } from './signer.js';
23
21
  export { assertTimeClaimsValid, now, type TimeClaimsPayload, type TimeValidationOptions, } from './time.js';
package/lib/index.js CHANGED
@@ -9,12 +9,11 @@
9
9
  *
10
10
  * @module token
11
11
  */ export { createInMemoryDIDCache } from './cache.js';
12
- export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, normalizeDID, resolveIssuer, resolveIssuerWithDoc } from './did.js';
13
- export { createDecryptingIdentity, createFullIdentity, createIdentity, createSigningIdentity, isDecryptingIdentity, isFullIdentity, isOwnIdentity, isSigningIdentity, randomIdentity } from './identity.js';
14
- export { concatKDF, createTokenEncrypter, decryptToken, deriveSharedSecret, encryptToken, unwrapEnvelope, wrapEnvelope } from './jwe.js';
12
+ export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError } from './did.js';
13
+ export { createFullIdentity, createIdentity, createKeyAgreementIdentity, createKeyAgreementIdentityForDID, createSigningIdentity, createSigningIdentityForDID, isFullIdentity, isKeyAgreementIdentity, isOwnIdentity, isSigningIdentity, randomIdentity } from './identity.js';
14
+ export { findMethodResolver } from './method.js';
15
15
  export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash } from './multibase.js';
16
16
  export { decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4, validateDIDDoc } from './peer4.js';
17
- export { createRotationAssertion } from './rotation.js';
18
17
  export { capabilitySchema, signedHeaderSchema, signedPayloadSchema, supportedHeaderSchema, unsignedHeaderSchema, validateAlgorithm, validateSignedHeader, validateSignedPayload, validateUnsignedHeader } from './schemas.js';
19
18
  export { decodePrivateKey, encodePrivateKey, randomPrivateKey } from './signer.js';
20
19
  export { assertTimeClaimsValid, now } from './time.js';