@atcute/oauth-keyset 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/LICENSE +14 -0
- package/README.md +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/keyset.d.ts +59 -0
- package/dist/keyset.d.ts.map +1 -0
- package/dist/keyset.js +126 -0
- package/dist/keyset.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/lib/index.ts +2 -0
- package/lib/keyset.ts +144 -0
- package/lib/types.ts +7 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mary
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @atcute/oauth-keyset
|
|
2
|
+
|
|
3
|
+
keyset management for AT Protocol OAuth.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @atcute/oauth-keyset
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## usage
|
|
10
|
+
|
|
11
|
+
### generating keys
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { generateClientAssertionKey } from '@atcute/oauth-crypto';
|
|
15
|
+
import { Keyset } from '@atcute/oauth-keyset';
|
|
16
|
+
|
|
17
|
+
// generate a new ES256 key
|
|
18
|
+
const key = await generateClientAssertionKey('my-key-id');
|
|
19
|
+
|
|
20
|
+
// create a keyset with the key
|
|
21
|
+
const keyset = new Keyset([key]);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### using JWKs directly
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import type { ClientAssertionPrivateJwk } from '@atcute/oauth-crypto';
|
|
28
|
+
import { Keyset } from '@atcute/oauth-keyset';
|
|
29
|
+
|
|
30
|
+
// JWKs can be used directly - no import step needed
|
|
31
|
+
const jwk: ClientAssertionPrivateJwk = {
|
|
32
|
+
kty: 'EC',
|
|
33
|
+
crv: 'P-256',
|
|
34
|
+
kid: 'my-key',
|
|
35
|
+
alg: 'ES256',
|
|
36
|
+
// ... private key parameters (x, y, d)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const keyset = new Keyset([jwk]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### importing from PKCS#8
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { importClientAssertionPkcs8 } from '@atcute/oauth-crypto';
|
|
46
|
+
import { Keyset } from '@atcute/oauth-keyset';
|
|
47
|
+
|
|
48
|
+
// import from PKCS#8 PEM - returns a JWK
|
|
49
|
+
const jwk = await importClientAssertionPkcs8(pemString, {
|
|
50
|
+
kid: 'my-key',
|
|
51
|
+
alg: 'ES256',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const keyset = new Keyset([jwk]);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### using the keyset
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// get public JWKS (for serving at jwks_uri)
|
|
61
|
+
const jwks = keyset.publicJwks;
|
|
62
|
+
|
|
63
|
+
// find a key by criteria
|
|
64
|
+
const key = keyset.find({ kid: 'my-key' });
|
|
65
|
+
const key = keyset.find({ alg: 'ES256' });
|
|
66
|
+
|
|
67
|
+
// find a key for signing with server negotiation
|
|
68
|
+
const { key, alg } = keyset.findForSigning(['ES256', 'ES384']);
|
|
69
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/keyset.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ClientAssertionPrivateJwk, PublicJwk } from '@atcute/oauth-crypto';
|
|
2
|
+
import type { KeySearchOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* a collection of private keys for client authentication.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Keyset {
|
|
7
|
+
private readonly keys;
|
|
8
|
+
private _publicJwks;
|
|
9
|
+
/**
|
|
10
|
+
* creates a new keyset from an array of private JWKs.
|
|
11
|
+
*
|
|
12
|
+
* @param keys array of private JWKs (at least one required, each with `kid` and `alg` set)
|
|
13
|
+
* @throws if keyset is empty or contains duplicate key IDs
|
|
14
|
+
*/
|
|
15
|
+
constructor(keys: ClientAssertionPrivateJwk[]);
|
|
16
|
+
/** number of keys in the keyset */
|
|
17
|
+
get size(): number;
|
|
18
|
+
/**
|
|
19
|
+
* public JWKS for serving at client metadata or jwks_uri.
|
|
20
|
+
* derived lazily on first access, then cached.
|
|
21
|
+
*/
|
|
22
|
+
get publicJwks(): {
|
|
23
|
+
keys: readonly PublicJwk[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* finds the first key matching the given criteria.
|
|
27
|
+
*
|
|
28
|
+
* @param options search criteria (kid and/or alg)
|
|
29
|
+
* @returns matching key or undefined
|
|
30
|
+
*/
|
|
31
|
+
find(options?: KeySearchOptions): ClientAssertionPrivateJwk | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* gets a key matching the given criteria.
|
|
34
|
+
*
|
|
35
|
+
* @param options search criteria (kid and/or alg)
|
|
36
|
+
* @returns matching key
|
|
37
|
+
* @throws if no matching key is found
|
|
38
|
+
*/
|
|
39
|
+
get(options?: KeySearchOptions): ClientAssertionPrivateJwk;
|
|
40
|
+
/**
|
|
41
|
+
* iterates over keys matching the given criteria, in preference order.
|
|
42
|
+
*
|
|
43
|
+
* @param options search criteria (kid and/or alg)
|
|
44
|
+
*/
|
|
45
|
+
list(options?: KeySearchOptions): Generator<ClientAssertionPrivateJwk>;
|
|
46
|
+
/**
|
|
47
|
+
* finds a key for signing, negotiating algorithm with server's supported list.
|
|
48
|
+
*
|
|
49
|
+
* @param serverAlgs algorithms supported by the server (from metadata)
|
|
50
|
+
* @returns key and negotiated algorithm
|
|
51
|
+
* @throws if no compatible key is found
|
|
52
|
+
*/
|
|
53
|
+
findForSigning(serverAlgs?: readonly string[]): {
|
|
54
|
+
key: ClientAssertionPrivateJwk;
|
|
55
|
+
alg: string;
|
|
56
|
+
};
|
|
57
|
+
[Symbol.iterator](): Iterator<ClientAssertionPrivateJwk>;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=keyset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyset.d.ts","sourceRoot":"","sources":["../lib/keyset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkBnD;;GAEG;AACH,qBAAa,MAAM;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuC;IAC5D,OAAO,CAAC,WAAW,CAA6C;IAEhE;;;;;OAKG;IACH,YAAY,IAAI,EAAE,yBAAyB,EAAE,EAe5C;IAED,mCAAmC;IACnC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,IAAI,UAAU,IAAI;QAAE,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;KAAE,CAG/C;IAED;;;;;OAKG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,yBAAyB,GAAG,SAAS,CAKtE;IAED;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,yBAAyB,CAOzD;IAED;;;;OAIG;IACF,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC,yBAAyB,CAAC,CAoBtE;IAED;;;;;;OAMG;IACH,cAAc,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG;QAAE,GAAG,EAAE,yBAAyB,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAU9F;IAED,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,yBAAyB,CAAC,CAEvD;CACD"}
|
package/dist/keyset.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { derivePublicJwk } from '@atcute/oauth-crypto';
|
|
2
|
+
/**
|
|
3
|
+
* preferred algorithm order for signing.
|
|
4
|
+
* EC algorithms first (smaller, faster), then PSS, then PKCS#1 v1.5.
|
|
5
|
+
*/
|
|
6
|
+
const PREFERRED_ALGORITHMS = [
|
|
7
|
+
'ES256',
|
|
8
|
+
'ES384',
|
|
9
|
+
'ES512',
|
|
10
|
+
'PS256',
|
|
11
|
+
'PS384',
|
|
12
|
+
'PS512',
|
|
13
|
+
'RS256',
|
|
14
|
+
'RS384',
|
|
15
|
+
'RS512',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* a collection of private keys for client authentication.
|
|
19
|
+
*/
|
|
20
|
+
export class Keyset {
|
|
21
|
+
keys;
|
|
22
|
+
_publicJwks;
|
|
23
|
+
/**
|
|
24
|
+
* creates a new keyset from an array of private JWKs.
|
|
25
|
+
*
|
|
26
|
+
* @param keys array of private JWKs (at least one required, each with `kid` and `alg` set)
|
|
27
|
+
* @throws if keyset is empty or contains duplicate key IDs
|
|
28
|
+
*/
|
|
29
|
+
constructor(keys) {
|
|
30
|
+
if (keys.length === 0) {
|
|
31
|
+
throw new Error(`keyset must contain at least one key`);
|
|
32
|
+
}
|
|
33
|
+
// check for duplicate kids
|
|
34
|
+
const kids = new Set();
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
if (kids.has(key.kid)) {
|
|
37
|
+
throw new Error(`duplicate key ID: ${key.kid}`);
|
|
38
|
+
}
|
|
39
|
+
kids.add(key.kid);
|
|
40
|
+
}
|
|
41
|
+
this.keys = Object.freeze([...keys]);
|
|
42
|
+
}
|
|
43
|
+
/** number of keys in the keyset */
|
|
44
|
+
get size() {
|
|
45
|
+
return this.keys.length;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* public JWKS for serving at client metadata or jwks_uri.
|
|
49
|
+
* derived lazily on first access, then cached.
|
|
50
|
+
*/
|
|
51
|
+
get publicJwks() {
|
|
52
|
+
this._publicJwks ||= { keys: this.keys.map((k) => derivePublicJwk(k, k.kid, k.alg)) };
|
|
53
|
+
return this._publicJwks;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* finds the first key matching the given criteria.
|
|
57
|
+
*
|
|
58
|
+
* @param options search criteria (kid and/or alg)
|
|
59
|
+
* @returns matching key or undefined
|
|
60
|
+
*/
|
|
61
|
+
find(options) {
|
|
62
|
+
for (const key of this.list(options)) {
|
|
63
|
+
return key;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* gets a key matching the given criteria.
|
|
69
|
+
*
|
|
70
|
+
* @param options search criteria (kid and/or alg)
|
|
71
|
+
* @returns matching key
|
|
72
|
+
* @throws if no matching key is found
|
|
73
|
+
*/
|
|
74
|
+
get(options) {
|
|
75
|
+
const key = this.find(options);
|
|
76
|
+
if (!key) {
|
|
77
|
+
const desc = options?.kid ?? options?.alg ?? 'any';
|
|
78
|
+
throw new Error(`no key found matching: ${desc}`);
|
|
79
|
+
}
|
|
80
|
+
return key;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* iterates over keys matching the given criteria, in preference order.
|
|
84
|
+
*
|
|
85
|
+
* @param options search criteria (kid and/or alg)
|
|
86
|
+
*/
|
|
87
|
+
*list(options) {
|
|
88
|
+
const { kid, alg } = options ?? {};
|
|
89
|
+
const algSet = alg == null ? null : new Set(Array.isArray(alg) ? alg : [alg]);
|
|
90
|
+
// sort keys by algorithm preference
|
|
91
|
+
const sorted = [...this.keys].sort((a, b) => {
|
|
92
|
+
const aIdx = PREFERRED_ALGORITHMS.indexOf(a.alg);
|
|
93
|
+
const bIdx = PREFERRED_ALGORITHMS.indexOf(b.alg);
|
|
94
|
+
return aIdx - bIdx;
|
|
95
|
+
});
|
|
96
|
+
for (const key of sorted) {
|
|
97
|
+
if (kid != null && key.kid !== kid) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (algSet != null && !algSet.has(key.alg)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
yield key;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* finds a key for signing, negotiating algorithm with server's supported list.
|
|
108
|
+
*
|
|
109
|
+
* @param serverAlgs algorithms supported by the server (from metadata)
|
|
110
|
+
* @returns key and negotiated algorithm
|
|
111
|
+
* @throws if no compatible key is found
|
|
112
|
+
*/
|
|
113
|
+
findForSigning(serverAlgs) {
|
|
114
|
+
// if server doesn't specify, default to ES256 per atproto spec
|
|
115
|
+
const algs = serverAlgs ?? ['ES256'];
|
|
116
|
+
const key = this.find({ alg: algs });
|
|
117
|
+
if (!key) {
|
|
118
|
+
throw new Error(`no key found compatible with server algorithms: ${algs.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
return { key, alg: key.alg };
|
|
121
|
+
}
|
|
122
|
+
[Symbol.iterator]() {
|
|
123
|
+
return this.keys[Symbol.iterator]();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=keyset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyset.js","sourceRoot":"","sources":["../lib/keyset.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** criteria for finding a key in a keyset */
|
|
2
|
+
export interface KeySearchOptions {
|
|
3
|
+
/** find by specific key ID */
|
|
4
|
+
kid?: string;
|
|
5
|
+
/** find by algorithm (single or array of acceptable algs) */
|
|
6
|
+
alg?: string | readonly string[];
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAChC,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CACjC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
|
package/lib/index.ts
ADDED
package/lib/keyset.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ClientAssertionPrivateJwk, PublicJwk } from '@atcute/oauth-crypto';
|
|
2
|
+
import { derivePublicJwk } from '@atcute/oauth-crypto';
|
|
3
|
+
|
|
4
|
+
import type { KeySearchOptions } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* preferred algorithm order for signing.
|
|
8
|
+
* EC algorithms first (smaller, faster), then PSS, then PKCS#1 v1.5.
|
|
9
|
+
*/
|
|
10
|
+
const PREFERRED_ALGORITHMS = [
|
|
11
|
+
'ES256',
|
|
12
|
+
'ES384',
|
|
13
|
+
'ES512',
|
|
14
|
+
'PS256',
|
|
15
|
+
'PS384',
|
|
16
|
+
'PS512',
|
|
17
|
+
'RS256',
|
|
18
|
+
'RS384',
|
|
19
|
+
'RS512',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* a collection of private keys for client authentication.
|
|
24
|
+
*/
|
|
25
|
+
export class Keyset {
|
|
26
|
+
private readonly keys: readonly ClientAssertionPrivateJwk[];
|
|
27
|
+
private _publicJwks: { keys: readonly PublicJwk[] } | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* creates a new keyset from an array of private JWKs.
|
|
31
|
+
*
|
|
32
|
+
* @param keys array of private JWKs (at least one required, each with `kid` and `alg` set)
|
|
33
|
+
* @throws if keyset is empty or contains duplicate key IDs
|
|
34
|
+
*/
|
|
35
|
+
constructor(keys: ClientAssertionPrivateJwk[]) {
|
|
36
|
+
if (keys.length === 0) {
|
|
37
|
+
throw new Error(`keyset must contain at least one key`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// check for duplicate kids
|
|
41
|
+
const kids = new Set<string>();
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
if (kids.has(key.kid)) {
|
|
44
|
+
throw new Error(`duplicate key ID: ${key.kid}`);
|
|
45
|
+
}
|
|
46
|
+
kids.add(key.kid);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.keys = Object.freeze([...keys]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** number of keys in the keyset */
|
|
53
|
+
get size(): number {
|
|
54
|
+
return this.keys.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* public JWKS for serving at client metadata or jwks_uri.
|
|
59
|
+
* derived lazily on first access, then cached.
|
|
60
|
+
*/
|
|
61
|
+
get publicJwks(): { keys: readonly PublicJwk[] } {
|
|
62
|
+
this._publicJwks ||= { keys: this.keys.map((k) => derivePublicJwk(k, k.kid, k.alg)) };
|
|
63
|
+
return this._publicJwks;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* finds the first key matching the given criteria.
|
|
68
|
+
*
|
|
69
|
+
* @param options search criteria (kid and/or alg)
|
|
70
|
+
* @returns matching key or undefined
|
|
71
|
+
*/
|
|
72
|
+
find(options?: KeySearchOptions): ClientAssertionPrivateJwk | undefined {
|
|
73
|
+
for (const key of this.list(options)) {
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* gets a key matching the given criteria.
|
|
81
|
+
*
|
|
82
|
+
* @param options search criteria (kid and/or alg)
|
|
83
|
+
* @returns matching key
|
|
84
|
+
* @throws if no matching key is found
|
|
85
|
+
*/
|
|
86
|
+
get(options?: KeySearchOptions): ClientAssertionPrivateJwk {
|
|
87
|
+
const key = this.find(options);
|
|
88
|
+
if (!key) {
|
|
89
|
+
const desc = options?.kid ?? options?.alg ?? 'any';
|
|
90
|
+
throw new Error(`no key found matching: ${desc}`);
|
|
91
|
+
}
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* iterates over keys matching the given criteria, in preference order.
|
|
97
|
+
*
|
|
98
|
+
* @param options search criteria (kid and/or alg)
|
|
99
|
+
*/
|
|
100
|
+
*list(options?: KeySearchOptions): Generator<ClientAssertionPrivateJwk> {
|
|
101
|
+
const { kid, alg } = options ?? {};
|
|
102
|
+
const algSet = alg == null ? null : new Set(Array.isArray(alg) ? alg : [alg]);
|
|
103
|
+
|
|
104
|
+
// sort keys by algorithm preference
|
|
105
|
+
const sorted = [...this.keys].sort((a, b) => {
|
|
106
|
+
const aIdx = PREFERRED_ALGORITHMS.indexOf(a.alg as (typeof PREFERRED_ALGORITHMS)[number]);
|
|
107
|
+
const bIdx = PREFERRED_ALGORITHMS.indexOf(b.alg as (typeof PREFERRED_ALGORITHMS)[number]);
|
|
108
|
+
return aIdx - bIdx;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const key of sorted) {
|
|
112
|
+
if (kid != null && key.kid !== kid) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (algSet != null && !algSet.has(key.alg)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
yield key;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* finds a key for signing, negotiating algorithm with server's supported list.
|
|
124
|
+
*
|
|
125
|
+
* @param serverAlgs algorithms supported by the server (from metadata)
|
|
126
|
+
* @returns key and negotiated algorithm
|
|
127
|
+
* @throws if no compatible key is found
|
|
128
|
+
*/
|
|
129
|
+
findForSigning(serverAlgs?: readonly string[]): { key: ClientAssertionPrivateJwk; alg: string } {
|
|
130
|
+
// if server doesn't specify, default to ES256 per atproto spec
|
|
131
|
+
const algs = serverAlgs ?? ['ES256'];
|
|
132
|
+
|
|
133
|
+
const key = this.find({ alg: algs });
|
|
134
|
+
if (!key) {
|
|
135
|
+
throw new Error(`no key found compatible with server algorithms: ${algs.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { key, alg: key.alg };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[Symbol.iterator](): Iterator<ClientAssertionPrivateJwk> {
|
|
142
|
+
return this.keys[Symbol.iterator]();
|
|
143
|
+
}
|
|
144
|
+
}
|
package/lib/types.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atcute/oauth-keyset",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "keyset management for AT Protocol OAuth",
|
|
5
|
+
"license": "0BSD",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/mary-ext/atcute",
|
|
8
|
+
"directory": "packages/oauth/keyset"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"!lib/**/*.bench.ts",
|
|
14
|
+
"!lib/**/*.test.ts"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@atcute/oauth-crypto": "^0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"vitest": "^4.0.16"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsgo --project tsconfig.build.json",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"prepublish": "rm -rf dist; pnpm run build"
|
|
34
|
+
}
|
|
35
|
+
}
|