@atproto/jwk 0.1.0 → 0.1.2

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/src/jwt.ts CHANGED
@@ -24,150 +24,154 @@ export const isUnsignedJwt = (data: unknown): data is UnsignedJwt =>
24
24
  /**
25
25
  * @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4}
26
26
  */
27
- export const jwtHeaderSchema = z.object({
28
- /** "alg" (Algorithm) Header Parameter */
29
- alg: z.string(),
30
- /** "jku" (JWK Set URL) Header Parameter */
31
- jku: z.string().url().optional(),
32
- /** "jwk" (JSON Web Key) Header Parameter */
33
- jwk: z
34
- .object({
35
- kty: z.string(),
36
- crv: z.string().optional(),
37
- x: z.string().optional(),
38
- y: z.string().optional(),
39
- e: z.string().optional(),
40
- n: z.string().optional(),
41
- })
42
- .optional(),
43
- /** "kid" (Key ID) Header Parameter */
44
- kid: z.string().optional(),
45
- /** "x5u" (X.509 URL) Header Parameter */
46
- x5u: z.string().optional(),
47
- /** "x5c" (X.509 Certificate Chain) Header Parameter */
48
- x5c: z.array(z.string()).optional(),
49
- /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */
50
- x5t: z.string().optional(),
51
- /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */
52
- 'x5t#S256': z.string().optional(),
53
- /** "typ" (Type) Header Parameter */
54
- typ: z.string().optional(),
55
- /** "cty" (Content Type) Header Parameter */
56
- cty: z.string().optional(),
57
- /** "crit" (Critical) Header Parameter */
58
- crit: z.array(z.string()).optional(),
59
- })
27
+ export const jwtHeaderSchema = z
28
+ .object({
29
+ /** "alg" (Algorithm) Header Parameter */
30
+ alg: z.string(),
31
+ /** "jku" (JWK Set URL) Header Parameter */
32
+ jku: z.string().url().optional(),
33
+ /** "jwk" (JSON Web Key) Header Parameter */
34
+ jwk: z
35
+ .object({
36
+ kty: z.string(),
37
+ crv: z.string().optional(),
38
+ x: z.string().optional(),
39
+ y: z.string().optional(),
40
+ e: z.string().optional(),
41
+ n: z.string().optional(),
42
+ })
43
+ .optional(),
44
+ /** "kid" (Key ID) Header Parameter */
45
+ kid: z.string().optional(),
46
+ /** "x5u" (X.509 URL) Header Parameter */
47
+ x5u: z.string().optional(),
48
+ /** "x5c" (X.509 Certificate Chain) Header Parameter */
49
+ x5c: z.array(z.string()).optional(),
50
+ /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */
51
+ x5t: z.string().optional(),
52
+ /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */
53
+ 'x5t#S256': z.string().optional(),
54
+ /** "typ" (Type) Header Parameter */
55
+ typ: z.string().optional(),
56
+ /** "cty" (Content Type) Header Parameter */
57
+ cty: z.string().optional(),
58
+ /** "crit" (Critical) Header Parameter */
59
+ crit: z.array(z.string()).optional(),
60
+ })
61
+ .passthrough()
60
62
 
61
63
  export type JwtHeader = z.infer<typeof jwtHeaderSchema>
62
64
 
63
65
  // https://www.iana.org/assignments/jwt/jwt.xhtml
64
- export const jwtPayloadSchema = z.object({
65
- iss: z.string().optional(),
66
- aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(),
67
- sub: z.string().optional(),
68
- exp: z.number().int().optional(),
69
- nbf: z.number().int().optional(),
70
- iat: z.number().int().optional(),
71
- jti: z.string().optional(),
72
- htm: z.string().optional(),
73
- htu: z.string().optional(),
74
- ath: z.string().optional(),
75
- acr: z.string().optional(),
76
- azp: z.string().optional(),
77
- amr: z.array(z.string()).optional(),
78
- // https://datatracker.ietf.org/doc/html/rfc7800
79
- cnf: z
80
- .object({
81
- kid: z.string().optional(), // Key ID
82
- jwk: jwkPubSchema.optional(), // JWK
83
- jwe: z.string().optional(), // Encrypted key
84
- jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided)
85
-
86
- // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1
87
- jkt: z.string().optional(),
88
-
89
- // https://datatracker.ietf.org/doc/html/rfc8705
90
- 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint
91
-
92
- // https://datatracker.ietf.org/doc/html/rfc9203
93
- osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation
94
- })
95
- .optional(),
96
-
97
- client_id: z.string().optional(),
98
-
99
- scope: z.string().optional(),
100
- nonce: z.string().optional(),
101
-
102
- at_hash: z.string().optional(),
103
- c_hash: z.string().optional(),
104
- s_hash: z.string().optional(),
105
- auth_time: z.number().int().optional(),
106
-
107
- // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
108
-
109
- // OpenID: "profile" scope
110
- name: z.string().optional(),
111
- family_name: z.string().optional(),
112
- given_name: z.string().optional(),
113
- middle_name: z.string().optional(),
114
- nickname: z.string().optional(),
115
- preferred_username: z.string().optional(),
116
- gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values
117
- picture: z.string().url().optional(),
118
- profile: z.string().url().optional(),
119
- website: z.string().url().optional(),
120
- birthdate: z
121
- .string()
122
- .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD
123
- .optional(),
124
- zoneinfo: z
125
- .string()
126
- .regex(/^[A-Za-z0-9_/]+$/)
127
- .optional(),
128
- locale: z
129
- .string()
130
- .regex(/^[a-z]{2}(-[A-Z]{2})?$/)
131
- .optional(),
132
- updated_at: z.number().int().optional(),
133
-
134
- // OpenID: "email" scope
135
- email: z.string().optional(),
136
- email_verified: z.boolean().optional(),
137
-
138
- // OpenID: "phone" scope
139
- phone_number: z.string().optional(),
140
- phone_number_verified: z.boolean().optional(),
141
-
142
- // OpenID: "address" scope
143
- // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
144
- address: z
145
- .object({
146
- formatted: z.string().optional(),
147
- street_address: z.string().optional(),
148
- locality: z.string().optional(),
149
- region: z.string().optional(),
150
- postal_code: z.string().optional(),
151
- country: z.string().optional(),
152
- })
153
- .optional(),
154
-
155
- // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2
156
- authorization_details: z
157
- .array(
158
- z
159
- .object({
160
- type: z.string(),
161
- // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2
162
- locations: z.array(z.string()).optional(),
163
- actions: z.array(z.string()).optional(),
164
- datatypes: z.array(z.string()).optional(),
165
- identifier: z.string().optional(),
166
- privileges: z.array(z.string()).optional(),
167
- })
168
- .passthrough(),
169
- )
170
- .optional(),
171
- })
66
+ export const jwtPayloadSchema = z
67
+ .object({
68
+ iss: z.string().optional(),
69
+ aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(),
70
+ sub: z.string().optional(),
71
+ exp: z.number().int().optional(),
72
+ nbf: z.number().int().optional(),
73
+ iat: z.number().int().optional(),
74
+ jti: z.string().optional(),
75
+ htm: z.string().optional(),
76
+ htu: z.string().optional(),
77
+ ath: z.string().optional(),
78
+ acr: z.string().optional(),
79
+ azp: z.string().optional(),
80
+ amr: z.array(z.string()).optional(),
81
+ // https://datatracker.ietf.org/doc/html/rfc7800
82
+ cnf: z
83
+ .object({
84
+ kid: z.string().optional(), // Key ID
85
+ jwk: jwkPubSchema.optional(), // JWK
86
+ jwe: z.string().optional(), // Encrypted key
87
+ jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided)
88
+
89
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1
90
+ jkt: z.string().optional(),
91
+
92
+ // https://datatracker.ietf.org/doc/html/rfc8705
93
+ 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint
94
+
95
+ // https://datatracker.ietf.org/doc/html/rfc9203
96
+ osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation
97
+ })
98
+ .optional(),
99
+
100
+ client_id: z.string().optional(),
101
+
102
+ scope: z.string().optional(),
103
+ nonce: z.string().optional(),
104
+
105
+ at_hash: z.string().optional(),
106
+ c_hash: z.string().optional(),
107
+ s_hash: z.string().optional(),
108
+ auth_time: z.number().int().optional(),
109
+
110
+ // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
111
+
112
+ // OpenID: "profile" scope
113
+ name: z.string().optional(),
114
+ family_name: z.string().optional(),
115
+ given_name: z.string().optional(),
116
+ middle_name: z.string().optional(),
117
+ nickname: z.string().optional(),
118
+ preferred_username: z.string().optional(),
119
+ gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values
120
+ picture: z.string().url().optional(),
121
+ profile: z.string().url().optional(),
122
+ website: z.string().url().optional(),
123
+ birthdate: z
124
+ .string()
125
+ .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD
126
+ .optional(),
127
+ zoneinfo: z
128
+ .string()
129
+ .regex(/^[A-Za-z0-9_/]+$/)
130
+ .optional(),
131
+ locale: z
132
+ .string()
133
+ .regex(/^[a-z]{2}(-[A-Z]{2})?$/)
134
+ .optional(),
135
+ updated_at: z.number().int().optional(),
136
+
137
+ // OpenID: "email" scope
138
+ email: z.string().optional(),
139
+ email_verified: z.boolean().optional(),
140
+
141
+ // OpenID: "phone" scope
142
+ phone_number: z.string().optional(),
143
+ phone_number_verified: z.boolean().optional(),
144
+
145
+ // OpenID: "address" scope
146
+ // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
147
+ address: z
148
+ .object({
149
+ formatted: z.string().optional(),
150
+ street_address: z.string().optional(),
151
+ locality: z.string().optional(),
152
+ region: z.string().optional(),
153
+ postal_code: z.string().optional(),
154
+ country: z.string().optional(),
155
+ })
156
+ .optional(),
157
+
158
+ // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2
159
+ authorization_details: z
160
+ .array(
161
+ z
162
+ .object({
163
+ type: z.string(),
164
+ // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2
165
+ locations: z.array(z.string()).optional(),
166
+ actions: z.array(z.string()).optional(),
167
+ datatypes: z.array(z.string()).optional(),
168
+ identifier: z.string().optional(),
169
+ privileges: z.array(z.string()).optional(),
170
+ })
171
+ .passthrough(),
172
+ )
173
+ .optional(),
174
+ })
175
+ .passthrough()
172
176
 
173
177
  export type JwtPayload = z.infer<typeof jwtPayloadSchema>
package/src/key.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { jwkAlgorithms } from './alg.js'
2
2
  import { JwkError } from './errors.js'
3
3
  import { Jwk, jwkSchema } from './jwk.js'
4
- import { VerifyOptions, VerifyPayload, VerifyResult } from './jwt-verify.js'
4
+ import { VerifyOptions, VerifyResult } from './jwt-verify.js'
5
5
  import { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'
6
6
  import { cachedGetter } from './util.js'
7
7
 
8
- export abstract class Key {
9
- constructor(protected readonly jwk: Readonly<Jwk>) {
8
+ const jwkSchemaReadonly = jwkSchema.readonly()
9
+
10
+ export abstract class Key<J extends Jwk = Jwk> {
11
+ constructor(protected readonly jwk: Readonly<J>) {
10
12
  // A key should always be used either for signing or encryption.
11
13
  if (!jwk.use) throw new JwkError('Missing "use" Parameter value')
12
14
  }
@@ -24,25 +26,28 @@ export abstract class Key {
24
26
  return false
25
27
  }
26
28
 
27
- get privateJwk(): Jwk | undefined {
29
+ get privateJwk(): Readonly<J> | undefined {
28
30
  return this.isPrivate ? this.jwk : undefined
29
31
  }
30
32
 
31
33
  @cachedGetter
32
- get publicJwk(): Jwk | undefined {
34
+ get publicJwk():
35
+ | Readonly<Exclude<J, { kty: 'oct' }> & { d?: never }>
36
+ | undefined {
33
37
  if (this.isSymetric) return undefined
34
- if (this.isPrivate) {
35
- const { d: _, ...jwk } = this.jwk as any
36
- return jwk
37
- }
38
- return this.jwk
38
+
39
+ return jwkSchemaReadonly.parse({
40
+ ...this.jwk,
41
+ d: undefined,
42
+ k: undefined,
43
+ }) as Exclude<J, { kty: 'oct' }> & { d?: never }
39
44
  }
40
45
 
41
46
  @cachedGetter
42
- get bareJwk(): Jwk | undefined {
47
+ get bareJwk(): Readonly<Jwk> | undefined {
43
48
  if (this.isSymetric) return undefined
44
49
  const { kty, crv, e, n, x, y } = this.jwk as any
45
- return jwkSchema.parse({ crv, e, kty, n, x, y })
50
+ return jwkSchemaReadonly.parse({ crv, e, kty, n, x, y })
46
51
  }
47
52
 
48
53
  get use() {
@@ -64,7 +69,7 @@ export abstract class Key {
64
69
  }
65
70
 
66
71
  get crv() {
67
- return (this.jwk as { crv: undefined } | Extract<Jwk, { crv: unknown }>).crv
72
+ return (this.jwk as { crv: undefined } | Extract<J, { crv: unknown }>).crv
68
73
  }
69
74
 
70
75
  /**
@@ -73,7 +78,7 @@ export abstract class Key {
73
78
  */
74
79
  @cachedGetter
75
80
  get algorithms(): readonly string[] {
76
- return Array.from(jwkAlgorithms(this.jwk))
81
+ return Object.freeze(Array.from(jwkAlgorithms(this.jwk)))
77
82
  }
78
83
 
79
84
  /**
@@ -86,8 +91,8 @@ export abstract class Key {
86
91
  *
87
92
  * @throws {JwtVerifyError} if the JWT is invalid
88
93
  */
89
- abstract verifyJwt<
90
- P extends VerifyPayload = JwtPayload,
91
- C extends string = string,
92
- >(token: SignedJwt, options?: VerifyOptions<C>): Promise<VerifyResult<P, C>>
94
+ abstract verifyJwt<C extends string = never>(
95
+ token: SignedJwt,
96
+ options?: VerifyOptions<C>,
97
+ ): Promise<VerifyResult<C>>
93
98
  }
package/src/keyset.ts CHANGED
@@ -7,13 +7,15 @@ import {
7
7
  JwtVerifyError,
8
8
  } from './errors.js'
9
9
  import { Jwk } from './jwk.js'
10
- import { Jwks } from './jwks.js'
10
+ import { Jwks, JwksPub } from './jwks.js'
11
11
  import { unsafeDecodeJwt } from './jwt-decode.js'
12
12
  import { VerifyOptions, VerifyResult } from './jwt-verify.js'
13
13
  import { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'
14
14
  import { Key } from './key.js'
15
15
  import {
16
+ DeepReadonly,
16
17
  Override,
18
+ UnReadonly,
17
19
  cachedGetter,
18
20
  isDefined,
19
21
  matchesAny,
@@ -80,6 +82,10 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
80
82
  this.keys = Object.freeze(keys)
81
83
  }
82
84
 
85
+ get size(): number {
86
+ return this.keys.length
87
+ }
88
+
83
89
  @cachedGetter
84
90
  get signAlgorithms(): readonly string[] {
85
91
  const algorithms = new Set<string>()
@@ -95,14 +101,14 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
95
101
  }
96
102
 
97
103
  @cachedGetter
98
- get publicJwks(): Jwks {
104
+ get publicJwks(): DeepReadonly<JwksPub> {
99
105
  return {
100
106
  keys: Array.from(this, extractPublicJwk).filter(isDefined),
101
107
  }
102
108
  }
103
109
 
104
110
  @cachedGetter
105
- get privateJwks(): Jwks {
111
+ get privateJwks(): DeepReadonly<Jwks> {
106
112
  return {
107
113
  keys: Array.from(this, extractPrivateJwk).filter(isDefined),
108
114
  }
@@ -207,13 +213,10 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
207
213
  }
208
214
  }
209
215
 
210
- async verifyJwt<
211
- P extends Record<string, unknown> = JwtPayload,
212
- C extends string = string,
213
- >(
216
+ async verifyJwt<C extends string = never>(
214
217
  token: SignedJwt,
215
218
  options?: VerifyOptions<C>,
216
- ): Promise<VerifyResult<P, C> & { key: K }> {
219
+ ): Promise<VerifyResult<C> & { key: K }> {
217
220
  const { header } = unsafeDecodeJwt(token)
218
221
  const { kid, alg } = header
219
222
 
@@ -221,7 +224,7 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
221
224
 
222
225
  for (const key of this.list({ kid, alg })) {
223
226
  try {
224
- const result = await key.verifyJwt<P, C>(token, options)
227
+ const result = await key.verifyJwt<C>(token, options)
225
228
  return { ...result, key }
226
229
  } catch (err) {
227
230
  errors.push(err)
@@ -237,4 +240,9 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
237
240
  throw JwtVerifyError.from(errors, ERR_JWT_INVALID)
238
241
  }
239
242
  }
243
+
244
+ toJSON(): JwksPub {
245
+ // Make a copy to prevent mutation of the original keyset
246
+ return structuredClone(this.publicJwks) as UnReadonly<JwksPub>
247
+ }
240
248
  }
package/src/util.ts CHANGED
@@ -5,14 +5,32 @@ import { RefinementCtx, ZodIssueCode } from 'zod'
5
5
  export type Simplify<T> = { [K in keyof T]: T[K] } & {}
6
6
  export type Override<T, V> = Simplify<V & Omit<T, keyof V>>
7
7
 
8
- export type RequiredKey<T, K extends string> = Simplify<
9
- string extends K
10
- ? T
11
- : {
12
- [L in K]: Exclude<L extends keyof T ? T[L] : unknown, undefined>
13
- } & Omit<T, K>
8
+ export type RequiredKey<T, K extends keyof T = never> = Simplify<
9
+ T & {
10
+ [L in K]-?: unknown extends T[L]
11
+ ? NonNullable<unknown> | null
12
+ : Exclude<T[L], undefined>
13
+ }
14
14
  >
15
15
 
16
+ // eslint-disable-next-line @typescript-eslint/ban-types
17
+ export type DeepReadonly<T> = T extends Function
18
+ ? T
19
+ : T extends object
20
+ ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
21
+ : T extends readonly (infer U)[]
22
+ ? readonly DeepReadonly<U>[]
23
+ : T
24
+
25
+ // eslint-disable-next-line @typescript-eslint/ban-types
26
+ export type UnReadonly<T> = T extends Function
27
+ ? T
28
+ : T extends object
29
+ ? { -readonly [K in keyof T]: UnReadonly<T[K]> }
30
+ : T extends readonly (infer U)[]
31
+ ? UnReadonly<U>[]
32
+ : T
33
+
16
34
  export const isDefined = <T>(i: T | undefined): i is T => i !== undefined
17
35
 
18
36
  export const preferredOrderCmp =
@@ -0,0 +1 @@
1
+ {"root":["./src/alg.ts","./src/errors.ts","./src/index.ts","./src/jwk.ts","./src/jwks.ts","./src/jwt-decode.ts","./src/jwt-verify.ts","./src/jwt.ts","./src/key.ts","./src/keyset.ts","./src/util.ts"],"version":"5.6.3"}