@kokuin/token 0.5.0 → 0.6.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,5 +1,5 @@
1
1
  import type { DIDResolver } from './cache.js';
2
- import { type MethodRegistry } from './method.js';
2
+ import { type MethodRegistry, type ResolvedSigningKey } from './method.js';
3
3
  import type { DIDDoc } from './peer4.js';
4
4
  import type { SignatureAlgorithm } from './schemas.js';
5
5
  import type { DIDString } from './types.js';
@@ -142,3 +142,13 @@ export declare function normalizeDID(did: string): string;
142
142
  * A sender MUST use the published key: the derived one is a different key and will not decrypt.
143
143
  */
144
144
  export declare function getAgreementKey(doc: DIDDoc): Uint8Array | null;
145
+ /**
146
+ * The signing keys a DID carries in its identifier, or `null` when it carries none. `did:key` is its
147
+ * key; a `did:peer:4` long form embeds the document, so its `authentication` keys (signing is what
148
+ * a pin or a membership is for) are readable from the string. Anything network-backed -- a
149
+ * short-form `did:peer:4`, `did:kokuin:` -- answers `null` rather than looking up.
150
+ *
151
+ * Throws on an identifier that claims to carry a key and does not; the caller treats that as no
152
+ * match.
153
+ */
154
+ export declare function signingKeysOf(did: string): Array<ResolvedSigningKey> | null;
package/lib/did.js CHANGED
@@ -282,11 +282,11 @@ function hasKeyPrefix(did) {
282
282
  }
283
283
  function resolveKidOrAuth(doc, kid) {
284
284
  if (kid == null) {
285
- const auth = doc.authentication;
286
- if (auth == null || auth.length === 0) {
285
+ const firstAuth = doc.authentication?.[0];
286
+ if (firstAuth == null) {
287
287
  throw new Error('resolveIssuer: did:peer:4 token missing kid and doc has no authentication entries');
288
288
  }
289
- return resolveKidFromDoc(doc, auth[0]);
289
+ return resolveKidFromDoc(doc, firstAuth);
290
290
  }
291
291
  // A kid must reference a method the subject authorized for `authentication`. Without this
292
292
  // check a key listed only under `assertionMethod` (or any other relationship) could sign
@@ -365,3 +365,47 @@ function resolveKidFromDoc(doc, kid) {
365
365
  }
366
366
  return null;
367
367
  }
368
+ /** As long as `resolveKidFromDoc` allows: base58 decoding is O(n^2). */ const MAX_KEY_ENCODED = 64;
369
+ /**
370
+ * The signing keys a DID carries in its identifier, or `null` when it carries none. `did:key` is its
371
+ * key; a `did:peer:4` long form embeds the document, so its `authentication` keys (signing is what
372
+ * a pin or a membership is for) are readable from the string. Anything network-backed -- a
373
+ * short-form `did:peer:4`, `did:kokuin:` -- answers `null` rather than looking up.
374
+ *
375
+ * Throws on an identifier that claims to carry a key and does not; the caller treats that as no
376
+ * match.
377
+ */ export function signingKeysOf(did) {
378
+ if (did.startsWith('did:key:')) {
379
+ const [alg, publicKey] = getSignatureInfo(did);
380
+ return [
381
+ {
382
+ alg,
383
+ publicKey
384
+ }
385
+ ];
386
+ }
387
+ if (!isPeer4(did) || did === getPeer4ShortForm(did)) {
388
+ return null;
389
+ }
390
+ const { doc } = decodePeer4(did);
391
+ const keys = [];
392
+ for (const id of doc.authentication ?? []){
393
+ const method = doc.verificationMethod.find((entry)=>entry.id === id);
394
+ if (method == null || method.publicKeyMultibase.length > MAX_KEY_ENCODED) {
395
+ continue;
396
+ }
397
+ let info = null;
398
+ try {
399
+ info = getAlgorithmAndPublicKey(decodeMultibase(method.publicKeyMultibase));
400
+ } catch {
401
+ continue;
402
+ }
403
+ if (info != null) {
404
+ keys.push({
405
+ alg: info[0],
406
+ publicKey: info[1]
407
+ });
408
+ }
409
+ }
410
+ return keys;
411
+ }
package/lib/identity.js CHANGED
@@ -169,19 +169,21 @@ function isClassical(spec) {
169
169
  function chooseMethod(input) {
170
170
  if (input.didMethod != null) {
171
171
  if (input.didMethod === 'key') {
172
- if (input.keys.length !== 1) {
172
+ const [firstKey, ...rest] = input.keys;
173
+ if (firstKey === undefined || rest.length > 0) {
173
174
  throw new Error('IdentityError.InvalidMethod: did:key requires exactly one key');
174
175
  }
175
- if (!isClassical(input.keys[0])) {
176
+ if (!isClassical(firstKey)) {
176
177
  throw new Error('IdentityError.InvalidMethod: did:key requires a classical algorithm');
177
178
  }
178
- if (input.keys[0].purpose !== 'sig') {
179
+ if (firstKey.purpose !== 'sig') {
179
180
  throw new Error('IdentityError.InvalidMethod: did:key requires a signing key');
180
181
  }
181
182
  }
182
183
  return input.didMethod;
183
184
  }
184
- if (input.keys.length === 1 && isClassical(input.keys[0]) && input.keys[0].purpose === 'sig') {
185
+ const [firstKey, ...rest] = input.keys;
186
+ if (firstKey !== undefined && rest.length === 0 && isClassical(firstKey) && firstKey.purpose === 'sig') {
185
187
  return 'key';
186
188
  }
187
189
  return 'peer:4';
@@ -356,6 +358,9 @@ function buildIdentity(id, longForm, doc, keys) {
356
358
  const keys = resolveKeys(input);
357
359
  if (method === 'key') {
358
360
  const [k] = keys;
361
+ if (k === undefined) {
362
+ throw new Error('createIdentity requires at least one key');
363
+ }
359
364
  const id = getDID(CODECS.EdDSA, k.publicKey);
360
365
  const doc = {
361
366
  '@context': [
package/lib/index.d.ts CHANGED
@@ -10,10 +10,10 @@
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, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerMode, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError, } from './did.js';
13
+ export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerMode, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, signingKeysOf, UnresolvableIssuerError, } from './did.js';
14
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';
15
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';
16
+ export { type AgreementAlgorithm, type DIDMethodResolver, findMethodResolver, type MemberQuery, type MethodRegistry, type ResolvedAgreementKey, type ResolvedMember, type ResolvedSigningKey, } from './method.js';
17
17
  export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash, } from './multibase.js';
18
18
  export { type DecodePeer4Options, type DIDDoc, decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4, type VerificationMethod, validateDIDDoc, } from './peer4.js';
19
19
  export { capabilitySchema, type SignatureAlgorithm, type SignedHeader, type SignedPayload, type SupportedHeader, signedHeaderSchema, signedPayloadSchema, supportedHeaderSchema, type UnsignedHeader, unsignedHeaderSchema, validateAlgorithm, validateSignedHeader, validateSignedPayload, validateUnsignedHeader, } from './schemas.js';
@@ -21,5 +21,5 @@ export { decodePrivateKey, encodePrivateKey, randomPrivateKey, } from './signer.
21
21
  export { assertTimeClaimsValid, now, type TimeClaimsPayload, type TimeValidationOptions, } from './time.js';
22
22
  export { createUnsignedToken, isSignedToken, isUnsignedToken, isVerifiedToken, signToken, type VerifyTokenOptions, verifyToken, } from './token.js';
23
23
  export type * from './types.js';
24
- export { concatBytes, stringifyToken } from './utils.js';
24
+ export { concatBytes, decodeSignedToken, stringifyToken } from './utils.js';
25
25
  export { getVerifier, type Verifier, type Verifiers, } from './verifier.js';
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module token
11
11
  */ export { createInMemoryDIDCache } from './cache.js';
12
- export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError } from './did.js';
12
+ export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, resolveIssuer, resolveIssuerWithDoc, signingKeysOf, UnresolvableIssuerError } from './did.js';
13
13
  export { createFullIdentity, createIdentity, createKeyAgreementIdentity, createKeyAgreementIdentityForDID, createSigningIdentity, createSigningIdentityForDID, isFullIdentity, isKeyAgreementIdentity, isOwnIdentity, isSigningIdentity, randomIdentity } from './identity.js';
14
14
  export { findMethodResolver } from './method.js';
15
15
  export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash } from './multibase.js';
@@ -18,5 +18,5 @@ export { capabilitySchema, signedHeaderSchema, signedPayloadSchema, supportedHea
18
18
  export { decodePrivateKey, encodePrivateKey, randomPrivateKey } from './signer.js';
19
19
  export { assertTimeClaimsValid, now } from './time.js';
20
20
  export { createUnsignedToken, isSignedToken, isUnsignedToken, isVerifiedToken, signToken, verifyToken } from './token.js';
21
- export { concatBytes, stringifyToken } from './utils.js';
21
+ export { concatBytes, decodeSignedToken, stringifyToken } from './utils.js';
22
22
  export { getVerifier } from './verifier.js';
package/lib/method.d.ts CHANGED
@@ -5,6 +5,17 @@ export type ResolvedSigningKey = {
5
5
  alg: SignatureAlgorithm;
6
6
  publicKey: Uint8Array;
7
7
  };
8
+ /** The member of a subject's management tier being asked about: its DID, `add` digest and generation. */
9
+ export type MemberQuery = {
10
+ did: string;
11
+ add: string;
12
+ gen: number;
13
+ };
14
+ /** The signing key a subject's log pins for a member, and when that membership lapses (unix seconds). */
15
+ export type ResolvedMember = {
16
+ key: ResolvedSigningKey;
17
+ lease: number;
18
+ };
8
19
  export type AgreementAlgorithm = 'X25519';
9
20
  /** A key agreement key a DID method resolved for a recipient. */
10
21
  export type ResolvedAgreementKey = {
@@ -64,6 +75,17 @@ export type DIDMethodResolver = {
64
75
  * break every implementation for a member most methods have no answer for.
65
76
  */
66
77
  resolveDenySet?(did: string): Promise<ReadonlySet<string>>;
78
+ /**
79
+ * The key and lease the subject's log holds for one **member** of its management tier, answered for
80
+ * **now**. A grant issued by a member rather than by the subject's own key is valid only while that
81
+ * membership is live, so the verifier asks the log rather than trusting the grant's claims.
82
+ *
83
+ * Optional, and its **absence fails closed**: a method that cannot answer refuses every
84
+ * member-issued grant rather than falling back to {@link resolve}, which names the subject's own
85
+ * keys and would answer a different question. A member that is unknown, removed or lapsed must
86
+ * reject, never resolve.
87
+ */
88
+ resolveMember?(subject: string, member: MemberQuery): Promise<ResolvedMember>;
67
89
  };
68
90
  export type MethodRegistry = ReadonlyArray<DIDMethodResolver>;
69
91
  /** Total: a malformed DID yields `undefined` rather than throwing. */
package/lib/token.js CHANGED
@@ -125,12 +125,12 @@ function matchesVerifiableData(token, data) {
125
125
  if (data === `${b64uFromJSON(token.header)}.${b64uFromJSON(token.payload)}`) {
126
126
  return true;
127
127
  }
128
- const parts = data.split('.');
129
- if (parts.length !== 2) {
128
+ const [headerPart, payloadPart, extra] = data.split('.');
129
+ if (headerPart === undefined || payloadPart === undefined || extra !== undefined) {
130
130
  return false;
131
131
  }
132
132
  try {
133
- return canonicalStringify(b64uToJSON(parts[0])) === canonicalStringify(token.header) && canonicalStringify(b64uToJSON(parts[1])) === canonicalStringify(token.payload);
133
+ return canonicalStringify(b64uToJSON(headerPart)) === canonicalStringify(token.header) && canonicalStringify(b64uToJSON(payloadPart)) === canonicalStringify(token.payload);
134
134
  } catch {
135
135
  // invalid base64url or JSON in data
136
136
  return false;
@@ -210,6 +210,9 @@ async function verifyTokenInner(token, options = {}) {
210
210
  throw new Error('Invalid token format: expected 3 parts separated by dots');
211
211
  }
212
212
  const [encodedHeader, encodedPayload, signature] = parts;
213
+ if (encodedHeader === undefined || encodedPayload === undefined) {
214
+ throw new Error('Invalid token format: expected 3 parts separated by dots');
215
+ }
213
216
  const header = b64uToJSON(encodedHeader);
214
217
  if (header.typ !== 'JWT') {
215
218
  throw new Error('Invalid token header type');
package/lib/utils.d.ts CHANGED
@@ -1,6 +1,10 @@
1
- import type { Token } from './types.js';
1
+ import type { SignedToken, Token } from './types.js';
2
2
  export declare function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array;
3
3
  /**
4
4
  * Convert a Token object to its JWT string representation.
5
5
  */
6
6
  export declare function stringifyToken(token: Token): string;
7
+ /**
8
+ * Parse a signed token string without verifying it. `data` keeps the original signed segments.
9
+ */
10
+ export declare function decodeSignedToken<Payload extends Record<string, unknown> = Record<string, unknown>>(token: string): SignedToken<Payload>;
package/lib/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b64uFromJSON } from '@sozai/codec';
1
+ import { b64uFromJSON, b64uToJSON } from '@sozai/codec';
2
2
  export function concatBytes(a, b) {
3
3
  const out = new Uint8Array(a.length + b.length);
4
4
  out.set(a, 0);
@@ -17,3 +17,17 @@ export function concatBytes(a, b) {
17
17
  }
18
18
  return parts.join('.');
19
19
  }
20
+ /**
21
+ * Parse a signed token string without verifying it. `data` keeps the original signed segments.
22
+ */ export function decodeSignedToken(token) {
23
+ const [header, payload, signature, ...rest] = token.split('.');
24
+ if (!header || !payload || !signature || rest.length > 0) {
25
+ throw new Error('Invalid token format: expected 3 parts separated by dots');
26
+ }
27
+ return {
28
+ data: `${header}.${payload}`,
29
+ header: b64uToJSON(header),
30
+ payload: b64uToJSON(payload),
31
+ signature
32
+ };
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokuin/token",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "JWT signing and verification",
5
5
  "keywords": [
6
6
  "json",
@@ -26,17 +26,17 @@
26
26
  "lib/*"
27
27
  ],
28
28
  "dependencies": {
29
- "@noble/curves": "^2.3.0",
30
- "@noble/hashes": "^2.3.0",
31
- "@scure/base": "^2.3.0",
32
- "@sozai/codec": "^0.4.0",
33
- "@sozai/otel": "^0.3.2",
34
- "@sozai/schema": "^0.1.1",
35
- "@kokuin/otel": "^0.3.0"
29
+ "@kokuin/otel": "^0.3.0",
30
+ "@noble/curves": "^2.4.0",
31
+ "@noble/hashes": "^2.4.0",
32
+ "@scure/base": "^2.4.0",
33
+ "@sozai/codec": "^0.4.3",
34
+ "@sozai/otel": "^0.3.4",
35
+ "@sozai/schema": "^0.1.6"
36
36
  },
37
37
  "devDependencies": {
38
- "@opentelemetry/sdk-trace-base": "^2.10.0",
39
- "@opentelemetry/sdk-trace-node": "^2.10.0",
38
+ "@opentelemetry/api": "^1.10.0",
39
+ "@opentelemetry/sdk-trace": "^3.0.0",
40
40
  "uint8arrays": "^6.1.1"
41
41
  },
42
42
  "publishConfig": {
@@ -44,9 +44,9 @@
44
44
  },
45
45
  "scripts": {
46
46
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
47
- "build:clean": "del lib",
48
- "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
49
- "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
47
+ "build:clean": "del-cli lib",
48
+ "build:js": "del-cli 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
49
+ "build:types": "del-cli 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
50
50
  "test": "pnpm run test:types && pnpm run test:unit",
51
51
  "test:types": "tsc --noEmit --skipLibCheck -p tsconfig.test.json",
52
52
  "test:unit": "vitest run"