@atproto/jwk 0.4.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/CHANGELOG.md +26 -0
- package/dist/alg.d.ts +2 -2
- package/dist/alg.d.ts.map +1 -1
- package/dist/alg.js +14 -8
- package/dist/alg.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jwk.d.ts +3513 -1310
- package/dist/jwk.d.ts.map +1 -1
- package/dist/jwk.js +148 -48
- package/dist/jwk.js.map +1 -1
- package/dist/jwks.d.ts +209 -1512
- package/dist/jwks.d.ts.map +1 -1
- package/dist/jwks.js +27 -2
- package/dist/jwks.js.map +1 -1
- package/dist/jwt-decode.js.map +1 -1
- package/dist/jwt-verify.js.map +1 -1
- package/dist/jwt.d.ts +3668 -1487
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/key.d.ts +22 -9
- package/dist/key.d.ts.map +1 -1
- package/dist/key.js +101 -18
- package/dist/key.js.map +1 -1
- package/dist/keyset.d.ts +382 -15
- package/dist/keyset.d.ts.map +1 -1
- package/dist/keyset.js +32 -46
- package/dist/keyset.js.map +1 -1
- package/dist/util.d.ts +1 -6
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +4 -0
- package/dist/util.js.map +1 -1
- package/package.json +2 -2
- package/src/alg.ts +22 -10
- package/src/jwk.ts +177 -60
- package/src/jwks.ts +29 -4
- package/src/key.ts +137 -24
- package/src/keyset.ts +60 -60
- package/src/util.ts +6 -18
package/src/key.ts
CHANGED
|
@@ -1,57 +1,92 @@
|
|
|
1
1
|
import { jwkAlgorithms } from './alg.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
Jwk,
|
|
4
|
+
KeyUsage,
|
|
5
|
+
PUBLIC_KEY_USAGE,
|
|
6
|
+
PrivateJwk,
|
|
7
|
+
PublicJwk,
|
|
8
|
+
PublicKeyUsage,
|
|
9
|
+
hasSharedSecretJwk,
|
|
10
|
+
isEncKeyUsage,
|
|
11
|
+
isPrivateJwk,
|
|
12
|
+
isPublicKeyUsage,
|
|
13
|
+
isSigKeyUsage,
|
|
14
|
+
jwkPubSchema,
|
|
15
|
+
jwkSchema,
|
|
16
|
+
} from './jwk.js'
|
|
4
17
|
import { VerifyOptions, VerifyResult } from './jwt-verify.js'
|
|
5
18
|
import { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'
|
|
6
19
|
import { cachedGetter } from './util.js'
|
|
7
20
|
|
|
8
|
-
|
|
21
|
+
export type KeyMatchOptions = {
|
|
22
|
+
usage?: KeyUsage
|
|
23
|
+
kid?: string | string[]
|
|
24
|
+
alg?: string | string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ActivityCheckOptions = {
|
|
28
|
+
allowRevoked?: boolean
|
|
29
|
+
clockTolerance?: number
|
|
30
|
+
currentDate?: Date
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
export abstract class Key<J extends Jwk = Jwk> {
|
|
11
|
-
constructor(
|
|
12
|
-
// A key should always be used either for signing or encryption.
|
|
13
|
-
if (!jwk.use) throw new JwkError('Missing "use" Parameter value')
|
|
14
|
-
}
|
|
34
|
+
constructor(readonly jwk: Readonly<J>) {}
|
|
15
35
|
|
|
36
|
+
@cachedGetter
|
|
16
37
|
get isPrivate(): boolean {
|
|
17
|
-
|
|
18
|
-
if ('d' in jwk && jwk.d !== undefined) return true
|
|
19
|
-
if ('k' in jwk && jwk.k !== undefined) return true
|
|
20
|
-
return false
|
|
38
|
+
return isPrivateJwk(this.jwk)
|
|
21
39
|
}
|
|
22
40
|
|
|
41
|
+
@cachedGetter
|
|
23
42
|
get isSymetric(): boolean {
|
|
24
|
-
|
|
25
|
-
if ('k' in jwk && jwk.k !== undefined) return true
|
|
26
|
-
return false
|
|
43
|
+
return hasSharedSecretJwk(this.jwk)
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
get privateJwk(): Readonly<
|
|
30
|
-
|
|
46
|
+
get privateJwk(): Readonly<PrivateJwk> | undefined {
|
|
47
|
+
if (!this.isPrivate) return undefined
|
|
48
|
+
|
|
49
|
+
return this.jwk as Readonly<PrivateJwk>
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
@cachedGetter
|
|
34
|
-
get publicJwk():
|
|
35
|
-
| Readonly<Exclude<J, { kty: 'oct' }> & { d?: never }>
|
|
36
|
-
| undefined {
|
|
53
|
+
get publicJwk(): Readonly<PublicJwk> | undefined {
|
|
37
54
|
if (this.isSymetric) return undefined
|
|
55
|
+
if (!this.isPrivate) return this.jwk as Readonly<PublicJwk>
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
const validated = jwkPubSchema.safeParse({
|
|
40
58
|
...this.jwk,
|
|
41
59
|
d: undefined,
|
|
42
60
|
k: undefined,
|
|
43
|
-
|
|
61
|
+
use: undefined,
|
|
62
|
+
key_ops: buildPublicKeyOps(this.keyOps) ?? PUBLIC_KEY_USAGE,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// One reason why the parsing might fail is if key_ops is empty. This check
|
|
66
|
+
// also allows to future proof the code (e.g if another type of private key
|
|
67
|
+
// is added that uses a different property than "d" or "k" to store its
|
|
68
|
+
// private value).
|
|
69
|
+
if (!validated.success) return undefined
|
|
70
|
+
|
|
71
|
+
return Object.freeze(validated.data)
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
@cachedGetter
|
|
47
75
|
get bareJwk(): Readonly<Jwk> | undefined {
|
|
48
76
|
if (this.isSymetric) return undefined
|
|
49
77
|
const { kty, crv, e, n, x, y } = this.jwk as any
|
|
50
|
-
return
|
|
78
|
+
return Object.freeze(jwkSchema.parse({ crv, e, kty, n, x, y }))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @note Only defined on public keys
|
|
83
|
+
*/
|
|
84
|
+
get use(): 'sig' | 'enc' | undefined {
|
|
85
|
+
return this.jwk.use
|
|
51
86
|
}
|
|
52
87
|
|
|
53
|
-
get
|
|
54
|
-
return this.jwk.
|
|
88
|
+
get keyOps(): readonly KeyUsage[] | undefined {
|
|
89
|
+
return this.jwk.key_ops
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
/**
|
|
@@ -81,6 +116,67 @@ export abstract class Key<J extends Jwk = Jwk> {
|
|
|
81
116
|
return Object.freeze(Array.from(jwkAlgorithms(this.jwk)))
|
|
82
117
|
}
|
|
83
118
|
|
|
119
|
+
get isRevoked() {
|
|
120
|
+
return this.jwk.revoked != null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isActive(options?: ActivityCheckOptions) {
|
|
124
|
+
if (!options?.allowRevoked && this.isRevoked) return false
|
|
125
|
+
|
|
126
|
+
const tolerance = options?.clockTolerance ?? 0
|
|
127
|
+
if (tolerance !== Infinity) {
|
|
128
|
+
const now = options?.currentDate?.getTime() ?? Date.now()
|
|
129
|
+
const { exp, nbf } = this.jwk
|
|
130
|
+
|
|
131
|
+
if (nbf != null && !(now >= nbf * 1e3 - tolerance)) return false
|
|
132
|
+
if (exp != null && !(now < exp * 1e3 + tolerance)) return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
matches(opts: KeyMatchOptions): boolean {
|
|
139
|
+
if (opts.kid != null) {
|
|
140
|
+
const matchesKid = Array.isArray(opts.kid)
|
|
141
|
+
? this.kid != null && opts.kid.includes(this.kid)
|
|
142
|
+
: this.kid === opts.kid
|
|
143
|
+
if (!matchesKid) return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (opts.alg != null) {
|
|
147
|
+
const matchesAlg = Array.isArray(opts.alg)
|
|
148
|
+
? opts.alg.some((a) => this.algorithms.includes(a))
|
|
149
|
+
: this.algorithms.includes(opts.alg)
|
|
150
|
+
if (!matchesAlg) return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (opts.usage != null) {
|
|
154
|
+
const matchesOps =
|
|
155
|
+
this.keyOps == null ||
|
|
156
|
+
this.keyOps.includes(opts.usage) ||
|
|
157
|
+
// @NOTE Because this.jwk represents the private key (typically used for
|
|
158
|
+
// private operations), the public counterpart operations are allowed.
|
|
159
|
+
(opts.usage === 'verify' && this.keyOps.includes('sign')) ||
|
|
160
|
+
(opts.usage === 'encrypt' && this.keyOps.includes('decrypt')) ||
|
|
161
|
+
(opts.usage === 'wrapKey' && this.keyOps.includes('unwrapKey'))
|
|
162
|
+
if (!matchesOps) return false
|
|
163
|
+
|
|
164
|
+
const matchesUse =
|
|
165
|
+
this.use == null ||
|
|
166
|
+
(this.use === 'sig' && isSigKeyUsage(opts.usage)) ||
|
|
167
|
+
(this.use === 'enc' && isEncKeyUsage(opts.usage))
|
|
168
|
+
if (!matchesUse) return false
|
|
169
|
+
|
|
170
|
+
// @NOTE This is only relevant when "key_ops" and "use" are undefined.
|
|
171
|
+
// This line also ensures that when "opts.usage" is a private key usage
|
|
172
|
+
// (e.g. "sign"), the key is indeed a private key.
|
|
173
|
+
const matchesKeyType = this.isPrivate || isPublicKeyUsage(opts.usage)
|
|
174
|
+
if (!matchesKeyType) return false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
84
180
|
/**
|
|
85
181
|
* Create a signed JWT
|
|
86
182
|
*/
|
|
@@ -96,3 +192,20 @@ export abstract class Key<J extends Jwk = Jwk> {
|
|
|
96
192
|
options?: VerifyOptions<C>,
|
|
97
193
|
): Promise<VerifyResult<C>>
|
|
98
194
|
}
|
|
195
|
+
|
|
196
|
+
function buildPublicKeyOps(
|
|
197
|
+
keyUsages?: readonly KeyUsage[],
|
|
198
|
+
): PublicKeyUsage[] | undefined {
|
|
199
|
+
if (keyUsages == null) return undefined
|
|
200
|
+
|
|
201
|
+
// https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
|
|
202
|
+
// > Duplicate key operation values MUST NOT be present in the array.
|
|
203
|
+
const publicOps = new Set(keyUsages.filter(isPublicKeyUsage))
|
|
204
|
+
|
|
205
|
+
// @NOTE Translating private key usage into public key usage
|
|
206
|
+
if (keyUsages.includes('sign')) publicOps.add('verify')
|
|
207
|
+
if (keyUsages.includes('decrypt')) publicOps.add('encrypt')
|
|
208
|
+
if (keyUsages.includes('unwrapKey')) publicOps.add('wrapKey')
|
|
209
|
+
|
|
210
|
+
return Array.from(publicOps)
|
|
211
|
+
}
|
package/src/keyset.ts
CHANGED
|
@@ -6,37 +6,35 @@ import {
|
|
|
6
6
|
JwtCreateError,
|
|
7
7
|
JwtVerifyError,
|
|
8
8
|
} from './errors.js'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
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 (
|
|
78
|
-
else
|
|
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()
|
|
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()
|
|
112
|
-
return {
|
|
113
|
-
keys:
|
|
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(
|
|
122
|
-
|
|
123
|
-
|
|
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 ${
|
|
126
|
+
`Key not found ${options.kid ?? options.alg ?? options.usage ?? '<unknown>'}`,
|
|
128
127
|
ERR_JWK_NOT_FOUND,
|
|
129
128
|
)
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
} else if (search.kid) {
|
|
143
|
-
if (key.kid !== search.kid) continue
|
|
144
|
-
}
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
145
138
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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({
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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()
|
|
251
|
-
// Make a copy to
|
|
252
|
-
return structuredClone(this.publicJwks) as
|
|
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
|
@@ -13,24 +13,6 @@ export type RequiredKey<T, K extends keyof T = never> = Simplify<
|
|
|
13
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
|
-
|
|
34
16
|
export const isDefined = <T>(i: T | undefined): i is T => i !== undefined
|
|
35
17
|
|
|
36
18
|
export const preferredOrderCmp =
|
|
@@ -197,3 +179,9 @@ export const segmentedStringRefinementFactory = <C extends number>(
|
|
|
197
179
|
return true
|
|
198
180
|
}
|
|
199
181
|
}
|
|
182
|
+
|
|
183
|
+
export function isLastOccurrence<
|
|
184
|
+
T extends number | boolean | string | null | undefined | symbol | bigint,
|
|
185
|
+
>(v: T, i: number, arr: readonly T[]): boolean {
|
|
186
|
+
return arr.indexOf(v, i + 1) === -1
|
|
187
|
+
}
|