@sdxc/jwt 0.0.0-pre.1
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/LICENSE.md +21 -0
- package/README.md +450 -0
- package/dist/algorithm.d.ts +25 -0
- package/dist/algorithm.js +25 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/jwk.d.ts +176 -0
- package/dist/jwk.js +210 -0
- package/dist/jwt.d.ts +217 -0
- package/dist/jwt.js +322 -0
- package/dist/key-storage.d.ts +59 -0
- package/dist/key-storage.js +12 -0
- package/dist/lib/parser.d.ts +136 -0
- package/dist/lib/parser.js +190 -0
- package/package.json +24 -0
package/dist/jwk.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key material for the JWTs this monorepo issues and verifies.
|
|
3
|
+
*
|
|
4
|
+
* Covers the whole life of a signing key: generating a pair, serializing it so it
|
|
5
|
+
* survives in a bucket or a database row, importing it back into `CryptoKey` objects,
|
|
6
|
+
* publishing the public half as a JWKS, and turning someone else's JWKS — local or
|
|
7
|
+
* remote — into the resolver a token is verified through.
|
|
8
|
+
*
|
|
9
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
10
|
+
* @copyright Sergio Xalambrí 2026
|
|
11
|
+
*/
|
|
12
|
+
import * as jose from "jose";
|
|
13
|
+
import type { KeyStorage } from "./key-storage.js";
|
|
14
|
+
import * as SignatureAlgorithm from "./algorithm.js";
|
|
15
|
+
/**
|
|
16
|
+
* Everything about the keys a token is signed with or verified against.
|
|
17
|
+
*
|
|
18
|
+
* Modeled as a namespace rather than loose exports because the names only make
|
|
19
|
+
* sense qualified: `JWK.KeyPair`, `JWK.generateKeyPair`, `JWK.toJSON`.
|
|
20
|
+
*/
|
|
21
|
+
export declare namespace JWK {
|
|
22
|
+
/**
|
|
23
|
+
* ES256 is issued here; RS256 verifies tokens from upstream identity
|
|
24
|
+
* providers; EdDSA derives its nonce from the key and message, keeping the
|
|
25
|
+
* ECDSA nonce-reuse leak that exposes a private key out of reach.
|
|
26
|
+
*/
|
|
27
|
+
const Algorithm: typeof SignatureAlgorithm;
|
|
28
|
+
/** One of the supported signature algorithms. */
|
|
29
|
+
type Algorithm = (typeof SignatureAlgorithm)[keyof typeof SignatureAlgorithm];
|
|
30
|
+
/** A key pair imported into a usable form, with the public half in JWK format. */
|
|
31
|
+
interface KeyPair {
|
|
32
|
+
/** Identifier published as the `kid`, and written into every token header. */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Algorithm the pair was generated for. */
|
|
35
|
+
alg: Algorithm;
|
|
36
|
+
/** Public half, used to verify. */
|
|
37
|
+
public: jose.CryptoKey;
|
|
38
|
+
/** Private half, used to sign. */
|
|
39
|
+
private: jose.CryptoKey;
|
|
40
|
+
/** When the pair was generated, used to prefer the newest key. */
|
|
41
|
+
created: Date;
|
|
42
|
+
/** When the pair stopped being usable for signing, if it has. */
|
|
43
|
+
expired?: Date;
|
|
44
|
+
/** Public half in JWK form, carrying `kid` and `use`, ready to publish. */
|
|
45
|
+
jwk: jose.JWK;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A key pair in the form it is stored and transported in.
|
|
49
|
+
*
|
|
50
|
+
* PEM strings and a millisecond timestamp, so the whole pair survives
|
|
51
|
+
* `JSON.stringify` into a bucket or a text column with nothing lost.
|
|
52
|
+
*/
|
|
53
|
+
interface SerializedKeyPair {
|
|
54
|
+
/** Identifier that becomes the `kid`. */
|
|
55
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
56
|
+
/** Algorithm the pair was generated for. */
|
|
57
|
+
alg: Algorithm;
|
|
58
|
+
/** Public key in SPKI PEM form. */
|
|
59
|
+
publicKey: string;
|
|
60
|
+
/** Private key in PKCS#8 PEM form. */
|
|
61
|
+
privateKey: string;
|
|
62
|
+
/** Generation time in milliseconds since the epoch. */
|
|
63
|
+
created: number;
|
|
64
|
+
/** Expiry, when the pair has been retired from signing. */
|
|
65
|
+
expired?: Date;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The half of a key pair `JWT.sign` needs.
|
|
69
|
+
*
|
|
70
|
+
* A structural subset of `KeyPair`, so a full pair is accepted wherever this is
|
|
71
|
+
* expected, and a caller holding only a private key already has enough to sign.
|
|
72
|
+
*/
|
|
73
|
+
interface SigningKey {
|
|
74
|
+
/** Identifier written into the token header as `kid`. */
|
|
75
|
+
id: string;
|
|
76
|
+
/** Algorithm the key was generated for, matched against the requested one. */
|
|
77
|
+
alg: string;
|
|
78
|
+
/** The private half. */
|
|
79
|
+
private: jose.CryptoKey;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The half of a key pair `JWT.verify` needs when the keys are already in hand.
|
|
83
|
+
*
|
|
84
|
+
* A single `jwk` field carries both the key material and the `kid` a token's
|
|
85
|
+
* header is matched against — everything selecting a key from a set takes.
|
|
86
|
+
*/
|
|
87
|
+
interface VerificationKey {
|
|
88
|
+
jwk: jose.JWK;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Answers with the key a given token's header calls for.
|
|
92
|
+
*
|
|
93
|
+
* What `importLocal` and `importRemote` hand back: the key set is only consulted
|
|
94
|
+
* once there is a token to consult it about.
|
|
95
|
+
*/
|
|
96
|
+
type KeyResolver = jose.JWTVerifyGetKey;
|
|
97
|
+
/** Where `JWT.verify` can find a token's key: the keys themselves, or a resolver. */
|
|
98
|
+
type VerificationKeys = KeyResolver | VerificationKey[];
|
|
99
|
+
/** Request headers, timeouts, and cache windows a remote key set can be given. */
|
|
100
|
+
type RemoteOptions = jose.RemoteJWKSetOptions;
|
|
101
|
+
/**
|
|
102
|
+
* Generates a new key pair in serialized form.
|
|
103
|
+
*
|
|
104
|
+
* Generated extractable, so the pair survives being written to storage and
|
|
105
|
+
* every isolate can import it to verify tokens any other isolate signed.
|
|
106
|
+
*
|
|
107
|
+
* @param alg - Algorithm to generate for.
|
|
108
|
+
* @returns The pair as PEM strings, ready to store.
|
|
109
|
+
* @example
|
|
110
|
+
* let serialized = await JWK.generateKeyPair(JWK.Algorithm.ES256);
|
|
111
|
+
* await db.create(table, { id: serialized.id, private_key: serialized.privateKey });
|
|
112
|
+
*/
|
|
113
|
+
function generateKeyPair(alg: Algorithm): Promise<SerializedKeyPair>;
|
|
114
|
+
/**
|
|
115
|
+
* Imports a stored key pair back into usable `CryptoKey` objects.
|
|
116
|
+
*
|
|
117
|
+
* The public half imports extractable, ready to re-export as a JWK; the
|
|
118
|
+
* private half imports non-extractable, keeping it confined to the runtime.
|
|
119
|
+
*
|
|
120
|
+
* @param value - A pair previously produced by `generateKeyPair`.
|
|
121
|
+
* @returns The pair with both halves imported and the public JWK attached.
|
|
122
|
+
* @example
|
|
123
|
+
* let keyPair = await JWK.importKeyPair({ id, alg, publicKey, privateKey, created });
|
|
124
|
+
*/
|
|
125
|
+
function importKeyPair(value: SerializedKeyPair): Promise<KeyPair>;
|
|
126
|
+
/**
|
|
127
|
+
* Loads the signing keys out of storage, generating one on first use.
|
|
128
|
+
*
|
|
129
|
+
* Every stored key comes back newest first; finding none usable mints an ES256
|
|
130
|
+
* key and re-reads storage, so every isolate converges on the same result.
|
|
131
|
+
*
|
|
132
|
+
* @param storage - Where key files live.
|
|
133
|
+
* @returns The usable key pairs, newest first.
|
|
134
|
+
* @example
|
|
135
|
+
* let keys = await JWK.signingKeys(storage);
|
|
136
|
+
* let jwks = JWK.toJSON(keys);
|
|
137
|
+
*/
|
|
138
|
+
function signingKeys(storage: KeyStorage): Promise<KeyPair[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Resolves a token's key by matching its header's `kid`, key type, curve,
|
|
141
|
+
* algorithm, and use against the set; anything but exactly one match is an
|
|
142
|
+
* error. Kept async so callers await it the same way as `importRemote`.
|
|
143
|
+
*
|
|
144
|
+
* @param jwks - The key set, as served by a JWKS endpoint.
|
|
145
|
+
* @returns A resolver that answers with the key a given token names.
|
|
146
|
+
* @example
|
|
147
|
+
* let keys = await JWK.importLocal(jwks);
|
|
148
|
+
* let token = await IdToken.verify(raw, keys, { issuer, audience, algorithms });
|
|
149
|
+
*/
|
|
150
|
+
function importLocal(jwks: jose.JSONWebKeySet): Promise<KeyResolver>;
|
|
151
|
+
/**
|
|
152
|
+
* Points a resolver at a JWKS endpoint, fetched on first use and re-fetched
|
|
153
|
+
* at most once per cooldown window when a token names an unseen `kid` —
|
|
154
|
+
* carrying a relying party across a key rotation between deploys.
|
|
155
|
+
*
|
|
156
|
+
* @param url - The JWKS endpoint.
|
|
157
|
+
* @param options - Request headers, timeouts, and cache windows.
|
|
158
|
+
* @returns A resolver that answers with the key a given token names.
|
|
159
|
+
* @example
|
|
160
|
+
* let keys = JWK.importRemote(new URL(jwksUrl));
|
|
161
|
+
*/
|
|
162
|
+
function importRemote(url: URL, options?: RemoteOptions): Promise<KeyResolver>;
|
|
163
|
+
/**
|
|
164
|
+
* Renders key pairs as the JSON a `/.well-known/jwks.json` endpoint serves.
|
|
165
|
+
*
|
|
166
|
+
* Each entry keeps only the public parameters its key type publishes here,
|
|
167
|
+
* plus `kid` and `alg`, keeping unvetted private material out of the set.
|
|
168
|
+
*
|
|
169
|
+
* @param keys - The key pairs to publish.
|
|
170
|
+
* @returns The JWKS document.
|
|
171
|
+
* @throws When a pair's key type has no published shape on record.
|
|
172
|
+
* @example
|
|
173
|
+
* return Response.json(JWK.toJSON(await JWK.signingKeys(storage)));
|
|
174
|
+
*/
|
|
175
|
+
function toJSON(keys: KeyPair[]): jose.JSONWebKeySet;
|
|
176
|
+
}
|
package/dist/jwk.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key material for the JWTs this monorepo issues and verifies.
|
|
3
|
+
*
|
|
4
|
+
* Covers the whole life of a signing key: generating a pair, serializing it so it
|
|
5
|
+
* survives in a bucket or a database row, importing it back into `CryptoKey` objects,
|
|
6
|
+
* publishing the public half as a JWKS, and turning someone else's JWKS — local or
|
|
7
|
+
* remote — into the resolver a token is verified through.
|
|
8
|
+
*
|
|
9
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
10
|
+
* @copyright Sergio Xalambrí 2026
|
|
11
|
+
*/
|
|
12
|
+
import * as jose from "jose";
|
|
13
|
+
import * as SignatureAlgorithm from "./algorithm.js";
|
|
14
|
+
/**
|
|
15
|
+
* Prefix every stored signing key is written under.
|
|
16
|
+
*
|
|
17
|
+
* Listing by prefix is the only index this package has over the bucket, so the
|
|
18
|
+
* prefix is what separates key files from anything else sharing the bucket.
|
|
19
|
+
*/
|
|
20
|
+
const SIGNING_KEY_PREFIX = "signing:key";
|
|
21
|
+
/**
|
|
22
|
+
* Page size used while walking the stored keys, well under the 1000-item cap
|
|
23
|
+
* an object store typically enforces, so a page is rarely followed by a second.
|
|
24
|
+
*/
|
|
25
|
+
const SCAN_PAGE_SIZE = 100;
|
|
26
|
+
/**
|
|
27
|
+
* The public parameters a JWKS entry carries, one key type at a time.
|
|
28
|
+
* Rebuilding an entry only from the fields listed here keeps private components
|
|
29
|
+
* — `d`, and an RSA key's `p`, `q`, `dp`, `dq`, `qi` — out of a published set.
|
|
30
|
+
*/
|
|
31
|
+
const PUBLISHED_JWK_FIELDS = {
|
|
32
|
+
EC: ({ crv, x, y }) => ({ crv, x, y }),
|
|
33
|
+
OKP: ({ crv, x }) => ({ crv, x }),
|
|
34
|
+
RSA: ({ e, n }) => ({ e, n }),
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Everything about the keys a token is signed with or verified against.
|
|
38
|
+
*
|
|
39
|
+
* Modeled as a namespace rather than loose exports because the names only make
|
|
40
|
+
* sense qualified: `JWK.KeyPair`, `JWK.generateKeyPair`, `JWK.toJSON`.
|
|
41
|
+
*/
|
|
42
|
+
export var JWK;
|
|
43
|
+
(function (JWK) {
|
|
44
|
+
/**
|
|
45
|
+
* ES256 is issued here; RS256 verifies tokens from upstream identity
|
|
46
|
+
* providers; EdDSA derives its nonce from the key and message, keeping the
|
|
47
|
+
* ECDSA nonce-reuse leak that exposes a private key out of reach.
|
|
48
|
+
*/
|
|
49
|
+
JWK.Algorithm = SignatureAlgorithm;
|
|
50
|
+
/**
|
|
51
|
+
* Generates a new key pair in serialized form.
|
|
52
|
+
*
|
|
53
|
+
* Generated extractable, so the pair survives being written to storage and
|
|
54
|
+
* every isolate can import it to verify tokens any other isolate signed.
|
|
55
|
+
*
|
|
56
|
+
* @param alg - Algorithm to generate for.
|
|
57
|
+
* @returns The pair as PEM strings, ready to store.
|
|
58
|
+
* @example
|
|
59
|
+
* let serialized = await JWK.generateKeyPair(JWK.Algorithm.ES256);
|
|
60
|
+
* await db.create(table, { id: serialized.id, private_key: serialized.privateKey });
|
|
61
|
+
*/
|
|
62
|
+
async function generateKeyPair(alg) {
|
|
63
|
+
let key = await jose.generateKeyPair(alg, { extractable: true });
|
|
64
|
+
return {
|
|
65
|
+
id: crypto.randomUUID(),
|
|
66
|
+
publicKey: await jose.exportSPKI(key.publicKey),
|
|
67
|
+
privateKey: await jose.exportPKCS8(key.privateKey),
|
|
68
|
+
created: Date.now(),
|
|
69
|
+
alg,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
JWK.generateKeyPair = generateKeyPair;
|
|
73
|
+
/**
|
|
74
|
+
* Imports a stored key pair back into usable `CryptoKey` objects.
|
|
75
|
+
*
|
|
76
|
+
* The public half imports extractable, ready to re-export as a JWK; the
|
|
77
|
+
* private half imports non-extractable, keeping it confined to the runtime.
|
|
78
|
+
*
|
|
79
|
+
* @param value - A pair previously produced by `generateKeyPair`.
|
|
80
|
+
* @returns The pair with both halves imported and the public JWK attached.
|
|
81
|
+
* @example
|
|
82
|
+
* let keyPair = await JWK.importKeyPair({ id, alg, publicKey, privateKey, created });
|
|
83
|
+
*/
|
|
84
|
+
async function importKeyPair(value) {
|
|
85
|
+
let publicKey = await jose.importSPKI(value.publicKey, value.alg, { extractable: true });
|
|
86
|
+
let privateKey = await jose.importPKCS8(value.privateKey, value.alg);
|
|
87
|
+
let jwk = await jose.exportJWK(publicKey);
|
|
88
|
+
jwk.kid = value.id;
|
|
89
|
+
jwk.use = "sig";
|
|
90
|
+
return {
|
|
91
|
+
id: value.id,
|
|
92
|
+
alg: value.alg,
|
|
93
|
+
created: new Date(value.created),
|
|
94
|
+
expired: value.expired,
|
|
95
|
+
public: publicKey,
|
|
96
|
+
private: privateKey,
|
|
97
|
+
jwk,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
JWK.importKeyPair = importKeyPair;
|
|
101
|
+
/**
|
|
102
|
+
* Loads the signing keys out of storage, generating one on first use.
|
|
103
|
+
*
|
|
104
|
+
* Every stored key comes back newest first; finding none usable mints an ES256
|
|
105
|
+
* key and re-reads storage, so every isolate converges on the same result.
|
|
106
|
+
*
|
|
107
|
+
* @param storage - Where key files live.
|
|
108
|
+
* @returns The usable key pairs, newest first.
|
|
109
|
+
* @example
|
|
110
|
+
* let keys = await JWK.signingKeys(storage);
|
|
111
|
+
* let jwks = JWK.toJSON(keys);
|
|
112
|
+
*/
|
|
113
|
+
async function signingKeys(storage) {
|
|
114
|
+
let results = [];
|
|
115
|
+
for await (let entry of scan(storage, SIGNING_KEY_PREFIX)) {
|
|
116
|
+
let file = await storage.get(entry.key);
|
|
117
|
+
if (!file)
|
|
118
|
+
continue;
|
|
119
|
+
results.push(await importKeyPair(JSON.parse(await file.text())));
|
|
120
|
+
}
|
|
121
|
+
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
122
|
+
if (results.some((item) => !item.expired))
|
|
123
|
+
return results;
|
|
124
|
+
await storeKeyPair(storage, SIGNING_KEY_PREFIX, await generateKeyPair(JWK.Algorithm.ES256));
|
|
125
|
+
return await signingKeys(storage);
|
|
126
|
+
}
|
|
127
|
+
JWK.signingKeys = signingKeys;
|
|
128
|
+
/**
|
|
129
|
+
* Resolves a token's key by matching its header's `kid`, key type, curve,
|
|
130
|
+
* algorithm, and use against the set; anything but exactly one match is an
|
|
131
|
+
* error. Kept async so callers await it the same way as `importRemote`.
|
|
132
|
+
*
|
|
133
|
+
* @param jwks - The key set, as served by a JWKS endpoint.
|
|
134
|
+
* @returns A resolver that answers with the key a given token names.
|
|
135
|
+
* @example
|
|
136
|
+
* let keys = await JWK.importLocal(jwks);
|
|
137
|
+
* let token = await IdToken.verify(raw, keys, { issuer, audience, algorithms });
|
|
138
|
+
*/
|
|
139
|
+
// biome-ignore lint/suspicious/useAwait: symmetry with `importRemote`, see above.
|
|
140
|
+
async function importLocal(jwks) {
|
|
141
|
+
return jose.createLocalJWKSet(jwks);
|
|
142
|
+
}
|
|
143
|
+
JWK.importLocal = importLocal;
|
|
144
|
+
/**
|
|
145
|
+
* Points a resolver at a JWKS endpoint, fetched on first use and re-fetched
|
|
146
|
+
* at most once per cooldown window when a token names an unseen `kid` —
|
|
147
|
+
* carrying a relying party across a key rotation between deploys.
|
|
148
|
+
*
|
|
149
|
+
* @param url - The JWKS endpoint.
|
|
150
|
+
* @param options - Request headers, timeouts, and cache windows.
|
|
151
|
+
* @returns A resolver that answers with the key a given token names.
|
|
152
|
+
* @example
|
|
153
|
+
* let keys = JWK.importRemote(new URL(jwksUrl));
|
|
154
|
+
*/
|
|
155
|
+
// biome-ignore lint/suspicious/useAwait: async for the reason given on `importLocal`.
|
|
156
|
+
async function importRemote(url, options) {
|
|
157
|
+
return jose.createRemoteJWKSet(url, options);
|
|
158
|
+
}
|
|
159
|
+
JWK.importRemote = importRemote;
|
|
160
|
+
/**
|
|
161
|
+
* Renders key pairs as the JSON a `/.well-known/jwks.json` endpoint serves.
|
|
162
|
+
*
|
|
163
|
+
* Each entry keeps only the public parameters its key type publishes here,
|
|
164
|
+
* plus `kid` and `alg`, keeping unvetted private material out of the set.
|
|
165
|
+
*
|
|
166
|
+
* @param keys - The key pairs to publish.
|
|
167
|
+
* @returns The JWKS document.
|
|
168
|
+
* @throws When a pair's key type has no published shape on record.
|
|
169
|
+
* @example
|
|
170
|
+
* return Response.json(JWK.toJSON(await JWK.signingKeys(storage)));
|
|
171
|
+
*/
|
|
172
|
+
function toJSON(keys) {
|
|
173
|
+
return {
|
|
174
|
+
keys: keys.map(({ alg, id, jwk }) => {
|
|
175
|
+
let publish = jwk.kty ? PUBLISHED_JWK_FIELDS[jwk.kty] : undefined;
|
|
176
|
+
if (!publish)
|
|
177
|
+
throw new Error(`Cannot publish a key of type ${String(jwk.kty)}`);
|
|
178
|
+
return { ...publish(jwk), kty: jwk.kty, kid: id, alg };
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
JWK.toJSON = toJSON;
|
|
183
|
+
/**
|
|
184
|
+
* Walks every stored key under a prefix, page by page.
|
|
185
|
+
*
|
|
186
|
+
* Every page's entries are yielded, including the first, so the earliest key
|
|
187
|
+
* file stays included, and any returned cursor is followed to the next page.
|
|
188
|
+
*
|
|
189
|
+
* @param storage - Where key files live.
|
|
190
|
+
* @param prefix - Key prefix to walk.
|
|
191
|
+
* @yields Every listed entry, in the order the store returns them.
|
|
192
|
+
*/
|
|
193
|
+
async function* scan(storage, prefix) {
|
|
194
|
+
let cursor;
|
|
195
|
+
do {
|
|
196
|
+
let result = await storage.list({ prefix, cursor, limit: SCAN_PAGE_SIZE });
|
|
197
|
+
yield* result.files;
|
|
198
|
+
cursor = result.cursor;
|
|
199
|
+
} while (cursor);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* @param storage - Where key files live.
|
|
203
|
+
* @param prefix - Key prefix to write under.
|
|
204
|
+
* @param serialized - The pair to store.
|
|
205
|
+
*/
|
|
206
|
+
async function storeKeyPair(storage, prefix, serialized) {
|
|
207
|
+
let file = new File([JSON.stringify(serialized)], "jwks.json", { type: "application/json" });
|
|
208
|
+
await storage.set(`${prefix}:${serialized.id}`, file);
|
|
209
|
+
}
|
|
210
|
+
})(JWK || (JWK = {}));
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JWT payload as an object with typed accessors, meant to be subclassed.
|
|
3
|
+
*
|
|
4
|
+
* A decoded token is a bag of `unknown`, and reading claims straight off it spreads
|
|
5
|
+
* the same string literals and the same type checks across every call site. `JWT`
|
|
6
|
+
* gives each kind of token a class instead: the registered claims arrive as getters,
|
|
7
|
+
* an application adds its own on top through `this.parser`, and signing, verifying,
|
|
8
|
+
* and decoding all produce an instance of that class.
|
|
9
|
+
*
|
|
10
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
11
|
+
* @copyright Sergio Xalambrí 2026
|
|
12
|
+
*/
|
|
13
|
+
import type { DurationString } from "@sdxc/duration";
|
|
14
|
+
import * as jose from "jose";
|
|
15
|
+
import type { JWK } from "./jwk.js";
|
|
16
|
+
import { ObjectParser } from "./lib/parser.js";
|
|
17
|
+
/**
|
|
18
|
+
* A JWT payload, with the registered claims exposed as typed accessors.
|
|
19
|
+
*
|
|
20
|
+
* Subclass it to add accessors for claims beyond the registered set; an
|
|
21
|
+
* instance proxies to `payload`, so an accessor-less claim still reads by name.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* class IdToken extends JWT {
|
|
25
|
+
* override get subject() {
|
|
26
|
+
* return this.parser.string("sub"); // required here, so `string` not `string | null`
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* get email() {
|
|
30
|
+
* return this.parser.string("email");
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export declare class JWT implements jose.JWTPayload {
|
|
35
|
+
/** The raw claim set, readable whole for callers that want the payload itself. */
|
|
36
|
+
readonly payload: JWT.Payload;
|
|
37
|
+
/** Type-checked reads over `payload`, for the accessors a subclass defines. */
|
|
38
|
+
protected parser: ObjectParser;
|
|
39
|
+
/**
|
|
40
|
+
* Any claim, by name, for the ones no accessor covers.
|
|
41
|
+
*
|
|
42
|
+
* Required to satisfy `jose.JWTPayload`, and it is what the proxy below fills in.
|
|
43
|
+
*/
|
|
44
|
+
[propName: string]: unknown;
|
|
45
|
+
/**
|
|
46
|
+
* Wraps a claim set in a proxy: a claim with no accessor still reads through
|
|
47
|
+
* to the payload, and assigning an unknown property lands there too, so a
|
|
48
|
+
* missing claim answers `null` and callers can test for it directly.
|
|
49
|
+
*
|
|
50
|
+
* @param payload - The claims, defaulting to an empty set so a token can be built
|
|
51
|
+
* up through the setters.
|
|
52
|
+
* @example
|
|
53
|
+
* let jwt = new JWT({ sub: "user-123" });
|
|
54
|
+
* jwt.subject; // "user-123"
|
|
55
|
+
*/
|
|
56
|
+
constructor(payload?: JWT.PayloadInput);
|
|
57
|
+
/**
|
|
58
|
+
* The `aud` claim: who the token is for.
|
|
59
|
+
*
|
|
60
|
+
* @returns One audience, several, or `null` when the claim is absent or malformed.
|
|
61
|
+
*/
|
|
62
|
+
get audience(): string | string[] | null;
|
|
63
|
+
/** @param value - The audience to set, or `null` to drop the claim. */
|
|
64
|
+
set audience(value: string | string[] | null);
|
|
65
|
+
/**
|
|
66
|
+
* The `exp` claim, as stored.
|
|
67
|
+
*
|
|
68
|
+
* @returns The raw claim value, or `null` when the token does not expire.
|
|
69
|
+
*/
|
|
70
|
+
get expirationTime(): number | null;
|
|
71
|
+
/** @param value - The expiration to set, or `null` to drop the claim. */
|
|
72
|
+
set expirationTime(value: number | null);
|
|
73
|
+
/**
|
|
74
|
+
* How many seconds are left before the token expires.
|
|
75
|
+
*
|
|
76
|
+
* Counted in the seconds RFC 7519 stores `exp` in, so the number is a length of
|
|
77
|
+
* time and goes negative once the token is past its expiry.
|
|
78
|
+
*
|
|
79
|
+
* @returns The seconds until `exp`, or `null` when the claim is absent.
|
|
80
|
+
*/
|
|
81
|
+
get expiresIn(): number | null;
|
|
82
|
+
/**
|
|
83
|
+
* The expiration as a `Date`.
|
|
84
|
+
*
|
|
85
|
+
* @returns The expiry, or `null` when the token does not expire.
|
|
86
|
+
*/
|
|
87
|
+
get expiresAt(): Date | null;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the token is past its expiry, by this class's reading of `exp`.
|
|
90
|
+
*
|
|
91
|
+
* A convenience for display and for cache decisions. `JWT.verify` is what enforces
|
|
92
|
+
* `exp`, against the units the RFC defines.
|
|
93
|
+
*
|
|
94
|
+
* @returns `false` for a token with no `exp`, since nothing says it has expired.
|
|
95
|
+
*/
|
|
96
|
+
get expired(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The `iat` claim: when the token was issued.
|
|
99
|
+
*
|
|
100
|
+
* @returns The issuance time, or `null` when the claim is absent.
|
|
101
|
+
*/
|
|
102
|
+
get issuedAt(): Date | null;
|
|
103
|
+
/** @param value - The issuance time to set, or `null` to drop the claim. */
|
|
104
|
+
set issuedAt(value: Date | null);
|
|
105
|
+
/**
|
|
106
|
+
* The `iss` claim: who issued the token.
|
|
107
|
+
*
|
|
108
|
+
* @returns The issuer, or `null` when the claim is absent.
|
|
109
|
+
*/
|
|
110
|
+
get issuer(): string | null;
|
|
111
|
+
/** @param value - The issuer to set, or `null` to drop the claim. */
|
|
112
|
+
set issuer(value: string | null);
|
|
113
|
+
/**
|
|
114
|
+
* The `jti` claim: the token's unique identifier.
|
|
115
|
+
*
|
|
116
|
+
* @returns The identifier, or `null` when the claim is absent.
|
|
117
|
+
*/
|
|
118
|
+
get id(): string | null;
|
|
119
|
+
/** @param value - The identifier to set, or `null` to drop the claim. */
|
|
120
|
+
set id(value: string | null);
|
|
121
|
+
/**
|
|
122
|
+
* The `nbf` claim: when the token starts being valid.
|
|
123
|
+
*
|
|
124
|
+
* @returns The start of validity, or `null` when the claim is absent.
|
|
125
|
+
*/
|
|
126
|
+
get notBefore(): Date | null;
|
|
127
|
+
/** @param value - The start of validity to set, or `null` to drop the claim. */
|
|
128
|
+
set notBefore(value: Date | null);
|
|
129
|
+
/**
|
|
130
|
+
* The `sub` claim: who the token is about.
|
|
131
|
+
*
|
|
132
|
+
* @returns The subject, or `null` when the claim is absent.
|
|
133
|
+
*/
|
|
134
|
+
get subject(): string | null;
|
|
135
|
+
/** @param value - The subject to set, or `null` to drop the claim. */
|
|
136
|
+
set subject(value: string | null);
|
|
137
|
+
/**
|
|
138
|
+
* Signs this token.
|
|
139
|
+
*
|
|
140
|
+
* @param algorithm - Algorithm to sign with; also selects the key.
|
|
141
|
+
* @param jwks - Candidate keys, newest first.
|
|
142
|
+
* @returns The compact-serialized token.
|
|
143
|
+
* @example
|
|
144
|
+
* let signed = await token.sign(JWK.Algorithm.ES256, await JWK.signingKeys(storage));
|
|
145
|
+
*/
|
|
146
|
+
sign(algorithm: JWK.Algorithm, jwks: JWK.SigningKey[]): Promise<string>;
|
|
147
|
+
/**
|
|
148
|
+
* The claims a subclass exposes, as a plain object.
|
|
149
|
+
*
|
|
150
|
+
* Reads the getters declared directly on the instance's own prototype, so what
|
|
151
|
+
* comes out is the subclass's view of the token: the claims it models.
|
|
152
|
+
*
|
|
153
|
+
* @returns Each own getter's name mapped to its value.
|
|
154
|
+
*/
|
|
155
|
+
toJSON(): Record<string, unknown>;
|
|
156
|
+
/**
|
|
157
|
+
* Signs a token with the first key matching the algorithm.
|
|
158
|
+
*
|
|
159
|
+
* The keys arrive newest first, so the newest signs while older ones stay
|
|
160
|
+
* published to verify what they already signed, keyed by the `kid` header.
|
|
161
|
+
*
|
|
162
|
+
* @param jwt - The token to sign.
|
|
163
|
+
* @param algorithm - Algorithm to sign with; also selects the key.
|
|
164
|
+
* @param jwks - Candidate keys, newest first.
|
|
165
|
+
* @returns The compact-serialized token.
|
|
166
|
+
* @throws When no key in the set was generated for that algorithm.
|
|
167
|
+
*/
|
|
168
|
+
static sign(jwt: JWT, algorithm: JWK.Algorithm, jwks: JWK.SigningKey[]): Promise<string>;
|
|
169
|
+
/**
|
|
170
|
+
* Verifies a token and returns it as an instance of the class this was called on.
|
|
171
|
+
*
|
|
172
|
+
* The key is picked by the token's `kid`, letting a retired key keep verifying
|
|
173
|
+
* what it already signed through a rotation; `algorithms` pins accepted ones.
|
|
174
|
+
*
|
|
175
|
+
* @param token - The compact-serialized token.
|
|
176
|
+
* @param jwks - The keys themselves, or a resolver from `JWK.importLocal` /
|
|
177
|
+
* `JWK.importRemote`. Keys given directly are wrapped into a local key set so
|
|
178
|
+
* they go through the same per-token `kid` selection a resolver would apply.
|
|
179
|
+
* @param options - Expected issuer, audience, algorithms, clock tolerance, and so on.
|
|
180
|
+
* @returns The verified token.
|
|
181
|
+
* @throws When the set offers no key for the token, or when a check on it fails.
|
|
182
|
+
* @example
|
|
183
|
+
* let idToken = await IdToken.verify(raw, keys, {
|
|
184
|
+
* issuer,
|
|
185
|
+
* audience: clientId,
|
|
186
|
+
* algorithms: [JWK.Algorithm.ES256],
|
|
187
|
+
* });
|
|
188
|
+
*/
|
|
189
|
+
static verify<M extends JWT>(this: new (payload: JWT.Payload) => M, token: string, jwks: JWK.VerificationKeys, options?: JWT.VerifyOptions): Promise<M>;
|
|
190
|
+
/**
|
|
191
|
+
* Reads a token's claims as the token presents them, prior to verification.
|
|
192
|
+
*
|
|
193
|
+
* Use it to decide how to verify a token — reading `iss` to pick a JWKS, for
|
|
194
|
+
* instance — and let `verify` be what every decision about the request rests on.
|
|
195
|
+
*
|
|
196
|
+
* @param token - The compact-serialized token.
|
|
197
|
+
* @returns The decoded token, as an instance of the class this was called on.
|
|
198
|
+
*/
|
|
199
|
+
static decode<M extends JWT>(this: new (payload: JWT.Payload) => M, token: string): M;
|
|
200
|
+
}
|
|
201
|
+
export declare namespace JWT {
|
|
202
|
+
/** A JWT claim set, with every time claim as the seconds since the epoch. */
|
|
203
|
+
type Payload = jose.JWTPayload;
|
|
204
|
+
/**
|
|
205
|
+
* A claim set as it may be written, where a time claim can be a length of time.
|
|
206
|
+
*
|
|
207
|
+
* `exp: "1h"` is an hour from the moment the token is built; a number stays
|
|
208
|
+
* seconds since the epoch. Either way, `payload` holds the resolved seconds.
|
|
209
|
+
*/
|
|
210
|
+
type PayloadInput = Omit<Payload, "exp" | "iat" | "nbf"> & {
|
|
211
|
+
exp?: number | DurationString;
|
|
212
|
+
iat?: number | DurationString;
|
|
213
|
+
nbf?: number | DurationString;
|
|
214
|
+
};
|
|
215
|
+
/** The checks `JWT.verify` can be asked to run beyond the signature. */
|
|
216
|
+
type VerifyOptions = jose.JWTVerifyOptions;
|
|
217
|
+
}
|