@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 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
+ ```
@@ -0,0 +1,3 @@
1
+ export { Keyset } from './keyset.js';
2
+ export type { KeySearchOptions } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { Keyset } from './keyset.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
@@ -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":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAIvD;;;GAGG;AACH,MAAM,oBAAoB,GAAG;IAC5B,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;CACE,CAAC;AAEX;;GAEG;AACH,MAAM,OAAO,MAAM;IACD,IAAI,CAAuC;IACpD,WAAW,CAA6C;IAEhE;;;;;OAKG;IACH,YAAY,IAAiC,EAAE;QAC9C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QACzD,CAAC;QAED,2BAA2B;QAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAAA,CACrC;IAED,mCAAmC;IACnC,IAAI,IAAI,GAAW;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAAA,CACxB;IAED;;;OAGG;IACH,IAAI,UAAU,GAAmC;QAChD,IAAI,CAAC,WAAW,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC,WAAW,CAAC;IAAA,CACxB;IAED;;;;;OAKG;IACH,IAAI,CAAC,OAA0B,EAAyC;QACvE,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,OAAO,GAAG,CAAC;QACZ,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAED;;;;;;OAMG;IACH,GAAG,CAAC,OAA0B,EAA6B;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,OAAO,EAAE,GAAG,IAAI,OAAO,EAAE,GAAG,IAAI,KAAK,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,GAAG,CAAC;IAAA,CACX;IAED;;;;OAIG;IACH,CAAC,IAAI,CAAC,OAA0B,EAAwC;QACvE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAE9E,oCAAoC;QACpC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,GAA4C,CAAC,CAAC;YAC1F,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,GAA4C,CAAC,CAAC;YAC1F,OAAO,IAAI,GAAG,IAAI,CAAC;QAAA,CACnB,CAAC,CAAC;QAEH,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBACpC,SAAS;YACV,CAAC;YACD,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC5C,SAAS;YACV,CAAC;YACD,MAAM,GAAG,CAAC;QACX,CAAC;IAAA,CACD;IAED;;;;;;OAMG;IACH,cAAc,CAAC,UAA8B,EAAmD;QAC/F,+DAA+D;QAC/D,MAAM,IAAI,GAAG,UAAU,IAAI,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;IAAA,CAC7B;IAED,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAwC;QACxD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;IAAA,CACpC;CACD"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
package/lib/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Keyset } from './keyset.js';
2
+ export type { KeySearchOptions } from './types.js';
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
@@ -0,0 +1,7 @@
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
+ }
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
+ }