@atproto/jwk 0.5.0 → 0.7.0-next.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/src/keyset.ts CHANGED
@@ -6,37 +6,35 @@ import {
6
6
  JwtCreateError,
7
7
  JwtVerifyError,
8
8
  } from './errors.js'
9
- import { Jwk } from './jwk.js'
10
- import { Jwks, JwksPub } from './jwks.js'
9
+ import { PrivateKeyUsage } from './jwk.js'
10
+ import { 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
- import { Key } from './key.js'
14
+ import { ActivityCheckOptions, Key, KeyMatchOptions } from './key.js'
15
15
  import {
16
- DeepReadonly,
17
16
  Override,
18
- UnReadonly,
19
17
  cachedGetter,
20
18
  isDefined,
21
19
  matchesAny,
22
20
  preferredOrderCmp,
23
21
  } from './util.js'
24
22
 
25
- export type JwtSignHeader = Override<JwtHeader, Pick<KeySearch, 'alg' | 'kid'>>
23
+ export type { ActivityCheckOptions, KeyMatchOptions }
24
+ export type FindKeyOptions = KeyMatchOptions & ActivityCheckOptions
25
+
26
+ export type JwtSignHeader = Override<
27
+ JwtHeader,
28
+ Pick<FindKeyOptions, 'alg' | 'kid'>
29
+ >
26
30
 
27
31
  export type JwtPayloadGetter<P = JwtPayload> = (
28
32
  header: JwtHeader,
29
33
  key: Key,
30
34
  ) => P | PromiseLike<P>
31
35
 
32
- export type KeySearch = {
33
- use?: 'sig' | 'enc'
34
- kid?: string | string[]
35
- alg?: string | string[]
36
- }
37
-
38
- const extractPrivateJwk = (key: Key): Jwk | undefined => key.privateJwk
39
- const extractPublicJwk = (key: Key): Jwk | undefined => key.publicJwk
36
+ const extractPrivateJwk = (key: Key) => key.privateJwk
37
+ const extractPublicJwk = (key: Key) => key.publicJwk
40
38
 
41
39
  export class Keyset<K extends Key = Key> implements Iterable<K> {
42
40
  private readonly keys: readonly K[]
@@ -67,15 +65,15 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
67
65
  ) {
68
66
  const keys: K[] = []
69
67
 
70
- const kids = new Set<string>()
68
+ const keyIds = new Set<string>()
71
69
  for (const key of iterable) {
72
70
  if (!key) continue
73
71
 
74
72
  keys.push(key)
75
73
 
76
74
  if (key.kid) {
77
- if (kids.has(key.kid)) throw new JwkError(`Duplicate key: ${key.kid}`)
78
- else kids.add(key.kid)
75
+ if (keyIds.has(key.kid)) throw new JwkError(`Duplicate key: ${key.kid}`)
76
+ else keyIds.add(key.kid)
79
77
  }
80
78
  }
81
79
 
@@ -101,66 +99,67 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
101
99
  }
102
100
 
103
101
  @cachedGetter
104
- get publicJwks(): DeepReadonly<JwksPub> {
105
- return {
106
- keys: Array.from(this, extractPublicJwk).filter(isDefined),
107
- }
102
+ get publicJwks() {
103
+ return Object.freeze({
104
+ keys: Object.freeze(Array.from(this, extractPublicJwk).filter(isDefined)),
105
+ })
108
106
  }
109
107
 
110
108
  @cachedGetter
111
- get privateJwks(): DeepReadonly<Jwks> {
112
- return {
113
- keys: Array.from(this, extractPrivateJwk).filter(isDefined),
114
- }
109
+ get privateJwks() {
110
+ return Object.freeze({
111
+ keys: Object.freeze(
112
+ Array.from(this, extractPrivateJwk).filter(isDefined),
113
+ ),
114
+ })
115
115
  }
116
116
 
117
117
  has(kid: string): boolean {
118
118
  return this.keys.some((key) => key.kid === kid)
119
119
  }
120
120
 
121
- get(search: KeySearch): K {
122
- for (const key of this.list(search)) {
123
- return key
124
- }
121
+ get(options: FindKeyOptions): K {
122
+ const key = this.find(options)
123
+ if (key) return key
125
124
 
126
125
  throw new JwkError(
127
- `Key not found ${search.kid || search.alg || '<unknown>'}`,
126
+ `Key not found ${options.kid ?? options.alg ?? options.usage ?? '<unknown>'}`,
128
127
  ERR_JWK_NOT_FOUND,
129
128
  )
130
129
  }
131
130
 
132
- *list(search: KeySearch): Generator<K> {
133
- // Optimization: Empty string or empty array will not match any key
134
- if (search.kid?.length === 0) return
135
- if (search.alg?.length === 0) return
136
-
137
- for (const key of this) {
138
- if (search.use && key.use !== search.use) continue
131
+ find(options: FindKeyOptions): K | undefined {
132
+ for (const key of this.list(options)) {
133
+ return key
134
+ }
139
135
 
140
- if (Array.isArray(search.kid)) {
141
- if (!key.kid || !search.kid.includes(key.kid)) continue
142
- } else if (search.kid) {
143
- if (key.kid !== search.kid) continue
144
- }
136
+ return undefined
137
+ }
145
138
 
146
- if (Array.isArray(search.alg)) {
147
- if (!search.alg.some((a) => key.algorithms.includes(a))) continue
148
- } else if (typeof search.alg === 'string') {
149
- if (!key.algorithms.includes(search.alg)) continue
139
+ *list<O extends FindKeyOptions>(options: O) {
140
+ for (const key of this) {
141
+ if (key.isActive(options) && key.matches(options)) {
142
+ yield key
150
143
  }
151
-
152
- yield key
153
144
  }
154
145
  }
155
146
 
156
- findPrivateKey({ kid, alg, use }: KeySearch): { key: Key; alg: string } {
147
+ findPrivateKey({
148
+ kid,
149
+ alg,
150
+ usage,
151
+ ...options
152
+ }: FindKeyOptions & { usage: PrivateKeyUsage }): {
153
+ key: Key
154
+ alg: string
155
+ } {
157
156
  const matchingKeys: Key[] = []
158
157
 
159
- for (const key of this.list({ kid, alg, use })) {
160
- // Not a private key
161
- if (!key.isPrivate) continue
158
+ // Allow the loop bellow to return early when a single "alg" is provided
159
+ if (Array.isArray(alg) && alg.length === 1) alg = alg[0]
162
160
 
163
- // Skip negotiation if a specific "alg" was provided
161
+ for (const key of this.list({ ...options, kid, alg, usage })) {
162
+ // Skip negotiation if a single "alg" was provided
164
163
  if (typeof alg === 'string') return { key, alg }
165
164
 
166
165
  matchingKeys.push(key)
@@ -188,7 +187,7 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
188
187
  }
189
188
 
190
189
  throw new JwkError(
191
- `No private key found for ${kid || alg || use || '<unknown>'}`,
190
+ `No private key found for ${kid || alg || usage}`,
192
191
  ERR_JWK_NOT_FOUND,
193
192
  )
194
193
  }
@@ -205,7 +204,8 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
205
204
  const { key, alg } = this.findPrivateKey({
206
205
  alg: sAlg,
207
206
  kid: sKid,
208
- use: 'sig',
207
+ usage: 'sign',
208
+ allowRevoked: false, // For explicitness (default value is false)
209
209
  })
210
210
  const protectedHeader = { ...header, alg, kid: key.kid }
211
211
 
@@ -221,14 +221,14 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
221
221
 
222
222
  async verifyJwt<C extends string = never>(
223
223
  token: SignedJwt,
224
- options?: VerifyOptions<C>,
224
+ options?: ActivityCheckOptions & VerifyOptions<C>,
225
225
  ): Promise<VerifyResult<C> & { key: K }> {
226
226
  const { header } = unsafeDecodeJwt(token)
227
227
  const { kid, alg } = header
228
228
 
229
229
  const errors: unknown[] = []
230
230
 
231
- for (const key of this.list({ kid, alg })) {
231
+ for (const key of this.list({ ...options, kid, alg, usage: 'verify' })) {
232
232
  try {
233
233
  const result = await key.verifyJwt<C>(token, options)
234
234
  return { ...result, key }
@@ -247,8 +247,8 @@ export class Keyset<K extends Key = Key> implements Iterable<K> {
247
247
  }
248
248
  }
249
249
 
250
- toJSON(): JwksPub {
251
- // Make a copy to prevent mutation of the original keyset
252
- return structuredClone(this.publicJwks) as UnReadonly<JwksPub>
250
+ toJSON() {
251
+ // Make a copy to allow mutation of the result
252
+ return structuredClone(this.publicJwks) as JwksPub
253
253
  }
254
254
  }
package/src/util.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { base64url } from 'multiformats/bases/base64'
2
2
  import { RefinementCtx, ZodIssueCode } from 'zod'
3
3
 
4
- // eslint-disable-next-line @typescript-eslint/ban-types
5
4
  export type Simplify<T> = { [K in keyof T]: T[K] } & {}
6
5
  export type Override<T, V> = Simplify<V & Omit<T, keyof V>>
7
6
 
@@ -13,24 +12,6 @@ export type RequiredKey<T, K extends keyof T = never> = Simplify<
13
12
  }
14
13
  >
15
14
 
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
-
34
15
  export const isDefined = <T>(i: T | undefined): i is T => i !== undefined
35
16
 
36
17
  export const preferredOrderCmp =
@@ -44,6 +25,7 @@ export const preferredOrderCmp =
44
25
  return aIdx - bIdx
45
26
  }
46
27
 
28
+ /* eslint-disable @typescript-eslint/no-unused-vars -- `v` is used at runtime in the returned type guards; v8 false-positive */
47
29
  export function matchesAny<T extends string | number | symbol | boolean>(
48
30
  value: null | undefined | T | readonly T[],
49
31
  ): (v: unknown) => v is T {
@@ -53,6 +35,7 @@ export function matchesAny<T extends string | number | symbol | boolean>(
53
35
  ? (v): v is T => value.includes(v)
54
36
  : (v): v is T => v === value
55
37
  }
38
+ /* eslint-enable @typescript-eslint/no-unused-vars */
56
39
 
57
40
  /**
58
41
  * Decorator to cache the result of a getter on a class instance.
@@ -197,3 +180,9 @@ export const segmentedStringRefinementFactory = <C extends number>(
197
180
  return true
198
181
  }
199
182
  }
183
+
184
+ export function isLastOccurrence<
185
+ T extends number | boolean | string | null | undefined | symbol | bigint,
186
+ >(v: T, i: number, arr: readonly T[]): boolean {
187
+ return arr.indexOf(v, i + 1) === -1
188
+ }
@@ -1 +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.8.2"}
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":"6.0.3"}