@kokuin/token 0.1.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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @kokuin/token
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ npm install @kokuin/token
7
+ ```
package/lib/cache.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { type DIDDoc } from './peer4.js';
2
+ /**
3
+ * Resolves a DID string to a DID document.
4
+ * Implementations may be sync or async, returning undefined when the DID is unknown.
5
+ */
6
+ export type DIDResolver = (did: string) => DIDDoc | undefined | Promise<DIDDoc | undefined>;
7
+ /**
8
+ * Content-addressed store for did:peer:4 documents, keyed by the short-form DID.
9
+ * `set` MUST verify that the short form matches the hash of the canonical doc before storing.
10
+ */
11
+ export type DIDCache = {
12
+ get(shortForm: string): DIDDoc | undefined | Promise<DIDDoc | undefined>;
13
+ set(shortForm: string, doc: DIDDoc): void | Promise<void>;
14
+ };
15
+ export type CreateInMemoryDIDCacheOptions = {
16
+ /** Maximum number of cached entries. Oldest evicted first. Default 10_000. */
17
+ maxEntries?: number;
18
+ };
19
+ /**
20
+ * Build an in-memory DID cache. The returned cache verifies short-form/doc binding on every set
21
+ * and evicts least-recently-used entries when maxEntries is exceeded.
22
+ */
23
+ export declare function createInMemoryDIDCache(options?: CreateInMemoryDIDCacheOptions): DIDCache;
package/lib/cache.js ADDED
@@ -0,0 +1,41 @@
1
+ import { encodePeer4, isPeer4 } from './peer4.js';
2
+ const DEFAULT_MAX_ENTRIES = 10_000;
3
+ /**
4
+ * Build an in-memory DID cache. The returned cache verifies short-form/doc binding on every set
5
+ * and evicts least-recently-used entries when maxEntries is exceeded.
6
+ */ export function createInMemoryDIDCache(options = {}) {
7
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
8
+ if (maxEntries < 1) {
9
+ throw new Error('DIDCache: maxEntries must be at least 1');
10
+ }
11
+ const docs = new Map();
12
+ function touch(shortForm, doc) {
13
+ docs.delete(shortForm);
14
+ docs.set(shortForm, doc);
15
+ }
16
+ return {
17
+ get (shortForm) {
18
+ const doc = docs.get(shortForm);
19
+ if (doc != null) {
20
+ touch(shortForm, doc);
21
+ }
22
+ return doc;
23
+ },
24
+ set (shortForm, doc) {
25
+ if (!isPeer4(shortForm)) {
26
+ return Promise.reject(new Error('DIDCache: short form must be a did:peer:4 identifier'));
27
+ }
28
+ const expected = encodePeer4(doc).shortForm;
29
+ if (expected !== shortForm) {
30
+ return Promise.reject(new Error('DIDCache: short form/doc hash mismatch'));
31
+ }
32
+ touch(shortForm, doc);
33
+ while(docs.size > maxEntries){
34
+ const oldest = docs.keys().next().value;
35
+ if (oldest == null) break;
36
+ docs.delete(oldest);
37
+ }
38
+ return Promise.resolve();
39
+ }
40
+ };
41
+ }
package/lib/did.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { DIDResolver } from './cache.js';
2
+ import type { DIDDoc } from './peer4.js';
3
+ import type { SignatureAlgorithm } from './schemas.js';
4
+ /** @internal */
5
+ export declare const CODECS: Record<SignatureAlgorithm, Uint8Array>;
6
+ /** @internal */
7
+ export declare function getAlgorithmAndPublicKey(bytes: Uint8Array): [SignatureAlgorithm, Uint8Array] | null;
8
+ /** @internal */
9
+ export declare function getDID(codec: Uint8Array, publicKey: Uint8Array): string;
10
+ /** @internal */
11
+ export declare function getSignatureInfo(did: string): [SignatureAlgorithm, Uint8Array];
12
+ export type ResolveIssuerHeader = {
13
+ kid?: string;
14
+ };
15
+ export type ResolveIssuerWithDocResult = {
16
+ alg: SignatureAlgorithm;
17
+ publicKey: Uint8Array;
18
+ /** Present when iss was a peer:4 long form or resolver returned a doc — caller may use to populate a cache. */
19
+ peer4Doc?: {
20
+ shortForm: string;
21
+ doc: DIDDoc;
22
+ };
23
+ };
24
+ /**
25
+ * Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
26
+ * plus the decoded peer:4 doc when one was obtained inline or via the resolver.
27
+ * Callers writing to a DID cache should write `peer4Doc` only after signature verification.
28
+ */
29
+ export declare function resolveIssuerWithDoc(iss: string, header?: ResolveIssuerHeader, resolver?: DIDResolver): Promise<ResolveIssuerWithDocResult>;
30
+ /**
31
+ * Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
32
+ */
33
+ export declare function resolveIssuer(iss: string, header?: ResolveIssuerHeader, resolver?: DIDResolver): Promise<[SignatureAlgorithm, Uint8Array]>;
34
+ /**
35
+ * Fold a DID to its canonical form for equality comparison.
36
+ * For did:peer:4, returns the short form regardless of whether input is long or short.
37
+ * All other DID methods pass through unchanged.
38
+ */
39
+ export declare function normalizeDID(did: string): string;
package/lib/did.js ADDED
@@ -0,0 +1,146 @@
1
+ import { base58 } from '@scure/base';
2
+ import { decodeMultibase } from './multibase.js';
3
+ import { decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4 } from './peer4.js';
4
+ /** @internal */ export const CODECS = {
5
+ ES256: new Uint8Array([
6
+ 128,
7
+ 36
8
+ ]),
9
+ EdDSA: new Uint8Array([
10
+ 0xed,
11
+ 0x01
12
+ ])
13
+ };
14
+ const EXPECTED_KEY_SIZES = {
15
+ EdDSA: 32,
16
+ ES256: 33
17
+ };
18
+ const PREFIX = 'did:key:z';
19
+ function isCodecMatch(codec, bytes) {
20
+ if (bytes.length < codec.length) return false;
21
+ for(let i = 0; i < codec.length; i++){
22
+ if (bytes[i] !== codec[i]) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
28
+ /** @internal */ export function getAlgorithmAndPublicKey(bytes) {
29
+ for (const [alg, codec] of Object.entries(CODECS)){
30
+ if (isCodecMatch(codec, bytes)) {
31
+ return [
32
+ alg,
33
+ bytes.slice(codec.length)
34
+ ];
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ /** @internal */ export function getDID(codec, publicKey) {
40
+ const bytes = new Uint8Array(codec.length + publicKey.length);
41
+ codec.forEach((v, i)=>{
42
+ bytes[i] = v;
43
+ });
44
+ bytes.set(publicKey, codec.length);
45
+ return PREFIX + base58.encode(bytes);
46
+ }
47
+ /** @internal */ export function getSignatureInfo(did) {
48
+ if (!did.startsWith(PREFIX)) {
49
+ throw new Error('Invalid DID format');
50
+ }
51
+ const bytes = base58.decode(did.slice(PREFIX.length));
52
+ const info = getAlgorithmAndPublicKey(bytes);
53
+ if (info == null) {
54
+ throw new Error('Unsupported DID signature codec');
55
+ }
56
+ const [alg, publicKey] = info;
57
+ const expectedSize = EXPECTED_KEY_SIZES[alg];
58
+ if (expectedSize != null && publicKey.length !== expectedSize) {
59
+ throw new Error('Invalid public key size');
60
+ }
61
+ return info;
62
+ }
63
+ /**
64
+ * Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
65
+ * plus the decoded peer:4 doc when one was obtained inline or via the resolver.
66
+ * Callers writing to a DID cache should write `peer4Doc` only after signature verification.
67
+ */ export async function resolveIssuerWithDoc(iss, header = {}, resolver) {
68
+ if (isPeer4(iss)) {
69
+ const shortForm = getPeer4ShortForm(iss);
70
+ if (iss !== shortForm) {
71
+ const { doc } = decodePeer4(iss);
72
+ const [alg, publicKey] = resolveKidOrAuth(doc, header.kid);
73
+ return {
74
+ alg,
75
+ publicKey,
76
+ peer4Doc: {
77
+ shortForm,
78
+ doc
79
+ }
80
+ };
81
+ }
82
+ if (resolver == null) {
83
+ throw new Error(`Unknown DID: ${shortForm}`);
84
+ }
85
+ const doc = await resolver(shortForm);
86
+ if (doc == null) {
87
+ throw new Error(`Unknown DID: ${shortForm}`);
88
+ }
89
+ const expected = encodePeer4(doc).shortForm;
90
+ if (expected !== shortForm) {
91
+ throw new Error('DIDResolver: short form/doc hash mismatch');
92
+ }
93
+ const [alg, publicKey] = resolveKidOrAuth(doc, header.kid);
94
+ return {
95
+ alg,
96
+ publicKey,
97
+ peer4Doc: {
98
+ shortForm,
99
+ doc
100
+ }
101
+ };
102
+ }
103
+ const [alg, publicKey] = getSignatureInfo(iss);
104
+ return {
105
+ alg,
106
+ publicKey
107
+ };
108
+ }
109
+ /**
110
+ * Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
111
+ */ export async function resolveIssuer(iss, header = {}, resolver) {
112
+ const { alg, publicKey } = await resolveIssuerWithDoc(iss, header, resolver);
113
+ return [
114
+ alg,
115
+ publicKey
116
+ ];
117
+ }
118
+ function resolveKidOrAuth(doc, kid) {
119
+ if (kid == null) {
120
+ const auth = doc.authentication;
121
+ if (auth == null || auth.length === 0) {
122
+ throw new Error('resolveIssuer: did:peer:4 token missing kid and doc has no authentication entries');
123
+ }
124
+ return resolveKidFromDoc(doc, auth[0]);
125
+ }
126
+ return resolveKidFromDoc(doc, kid);
127
+ }
128
+ function resolveKidFromDoc(doc, kid) {
129
+ const method = doc.verificationMethod.find((m)=>m.id === kid);
130
+ if (method == null) {
131
+ throw new Error(`KidNotFound: ${kid}`);
132
+ }
133
+ const bytes = decodeMultibase(method.publicKeyMultibase);
134
+ const info = getAlgorithmAndPublicKey(bytes);
135
+ if (info == null) {
136
+ throw new Error('Unsupported verification method codec');
137
+ }
138
+ return info;
139
+ }
140
+ /**
141
+ * Fold a DID to its canonical form for equality comparison.
142
+ * For did:peer:4, returns the short form regardless of whether input is long or short.
143
+ * All other DID methods pass through unchanged.
144
+ */ export function normalizeDID(did) {
145
+ return isPeer4(did) ? getPeer4ShortForm(did) : did;
146
+ }
@@ -0,0 +1,119 @@
1
+ import { type DIDDoc } from './peer4.js';
2
+ import type { SignedToken } from './types.js';
3
+ export type Identity = {
4
+ readonly id: string;
5
+ };
6
+ export type SignTokenOptions = {
7
+ /** Extra header fields merged into the signed JWS header. */
8
+ header?: Record<string, unknown>;
9
+ /** Pick a non-primary signing key by fragment (e.g. "#key-1"). */
10
+ kid?: string;
11
+ /**
12
+ * Override the first-per-aud long-form policy for did:peer:4 identities.
13
+ * - true: always use long form (no-op for did:key, where longForm === id).
14
+ * - false: always use short form.
15
+ * - undefined (default): use long form on first token to a given payload.aud, short form thereafter.
16
+ */
17
+ embedLongForm?: boolean;
18
+ };
19
+ export type SigningIdentity = Identity & {
20
+ publicKey: Uint8Array;
21
+ signToken: <Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions) => Promise<SignedToken<Payload>>;
22
+ };
23
+ export type DecryptingIdentity = Identity & {
24
+ decrypt(jwe: string): Promise<Uint8Array>;
25
+ agreeKey(ephemeralPublicKey: Uint8Array): Promise<Uint8Array>;
26
+ };
27
+ export type FullIdentity = SigningIdentity & DecryptingIdentity;
28
+ export type OwnIdentity = FullIdentity & {
29
+ privateKey: Uint8Array;
30
+ };
31
+ export type IdentityProvider<T extends SigningIdentity = SigningIdentity> = {
32
+ provideIdentity(keyID: string): Promise<T>;
33
+ };
34
+ export declare function isSigningIdentity(identity: Identity): identity is SigningIdentity;
35
+ export declare function isDecryptingIdentity(identity: Identity): identity is DecryptingIdentity;
36
+ export declare function isFullIdentity(identity: Identity): identity is FullIdentity;
37
+ export declare function isOwnIdentity(identity: Identity): identity is OwnIdentity;
38
+ /**
39
+ * Create a signing identity from an Ed25519 private key.
40
+ */
41
+ export declare function createSigningIdentity(privateKey: Uint8Array): SigningIdentity;
42
+ /**
43
+ * Create a decrypting identity from an Ed25519 private key.
44
+ * Uses X25519 key derivation for ECDH key agreement.
45
+ */
46
+ export declare function createDecryptingIdentity(privateKey: Uint8Array): DecryptingIdentity;
47
+ /**
48
+ * Create a full identity (signing + decrypting) from an Ed25519 private key.
49
+ */
50
+ export declare function createFullIdentity(privateKey: Uint8Array): FullIdentity;
51
+ /**
52
+ * Generate a random identity with a new Ed25519 private key.
53
+ */
54
+ export declare function randomIdentity(): OwnIdentity;
55
+ export type KeyPurpose = 'sig' | 'kem';
56
+ export type KeyAlg = 'EdDSA' | 'X25519';
57
+ export type IdentityKeySpec = {
58
+ purpose: KeyPurpose;
59
+ alg: KeyAlg;
60
+ /**
61
+ * Optional caller-supplied private key bytes.
62
+ *
63
+ * - When provided, the key pair is derived deterministically from these bytes.
64
+ * For a single-key spec on `did:peer:4`, this means the resulting short form
65
+ * is also deterministic — useful for reproducible identities (tests, seed-based
66
+ * recovery, restoring an identity from a stored secret).
67
+ * - When omitted, a fresh random key is generated via the algorithm's
68
+ * `randomSecretKey()` utility (Ed25519 or X25519).
69
+ *
70
+ * The caller owns the lifecycle of the supplied bytes.
71
+ */
72
+ privateKey?: Uint8Array;
73
+ };
74
+ export type CreateIdentityInput = {
75
+ keys: Array<IdentityKeySpec>;
76
+ didMethod?: 'key' | 'peer:4';
77
+ };
78
+ export type ResolvedKey = {
79
+ fragment: string;
80
+ alg: KeyAlg;
81
+ purpose: KeyPurpose;
82
+ privateKey: Uint8Array;
83
+ publicKey: Uint8Array;
84
+ };
85
+ export type MultiKeyIdentity = {
86
+ id: string;
87
+ longForm: string;
88
+ doc: DIDDoc;
89
+ keys: Array<ResolvedKey>;
90
+ publicKey: Uint8Array;
91
+ privateKey: Uint8Array;
92
+ signToken<Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions): Promise<SignedToken<Payload>>;
93
+ decrypt(jwe: string): Promise<Uint8Array>;
94
+ agreeKey(ephemeralPublicKey: Uint8Array, kid?: string): Promise<Uint8Array>;
95
+ };
96
+ /**
97
+ * Build a multi-key identity from one or more key specs.
98
+ *
99
+ * DID method selection:
100
+ * - Single classical (`EdDSA` / `X25519`) signing key with no override → `did:key`.
101
+ * - Anything else (multiple keys, KEM-only, any non-classical algorithm) → `did:peer:4`.
102
+ * - Caller may force a method via `input.didMethod`. Invalid combinations throw
103
+ * `IdentityError.InvalidMethod` (e.g. forcing `did:key` with multiple keys).
104
+ *
105
+ * Key resolution:
106
+ * - Each `IdentityKeySpec.privateKey` is honored when present (deterministic), otherwise
107
+ * a fresh random key is generated. See {@link IdentityKeySpec.privateKey}.
108
+ * - At least one signing key (`purpose: 'sig'`) is required; the first becomes the
109
+ * identity's primary signing key and seeds `publicKey` / `privateKey` on the result.
110
+ *
111
+ * For `did:peer:4`, the returned `id` is the short form (hash of the doc) and is
112
+ * deterministic with respect to the resolved keys — providing the same `privateKey`
113
+ * bytes across runs yields the same short form. `longForm` carries the full doc and
114
+ * is what peers exchange for first contact (see `signToken({ embedLongForm })`).
115
+ *
116
+ * @throws `Error` when `input.keys` is empty or no `sig` key is provided.
117
+ * @throws `IdentityError.InvalidMethod` when `didMethod` cannot satisfy the key set.
118
+ */
119
+ export declare function createIdentity(input: CreateIdentityInput): Promise<MultiKeyIdentity>;