@shelbyb/daywalker-sdk 0.3.0 → 0.4.1

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.
Files changed (54) hide show
  1. package/README.md +64 -23
  2. package/dist/analytics/index.d.ts +16 -2
  3. package/dist/analytics/index.js +79 -14
  4. package/dist/cli.js +133 -31
  5. package/dist/client.d.ts +29 -2
  6. package/dist/client.js +314 -62
  7. package/dist/config.d.ts +8 -2
  8. package/dist/config.js +14 -0
  9. package/dist/crypto/encoding.d.ts +5 -0
  10. package/dist/crypto/encoding.js +14 -0
  11. package/dist/crypto/index.d.ts +3 -3
  12. package/dist/crypto/index.js +2 -2
  13. package/dist/crypto/keys.d.ts +3 -3
  14. package/dist/crypto/keys.js +28 -7
  15. package/dist/crypto/signatures.d.ts +42 -12
  16. package/dist/crypto/signatures.js +236 -79
  17. package/dist/express.d.ts +1 -0
  18. package/dist/express.js +8 -6
  19. package/dist/flags/client.d.ts +32 -4
  20. package/dist/flags/client.js +74 -12
  21. package/dist/flags/common/events.d.ts +9 -0
  22. package/dist/flags/common/events.js +51 -6
  23. package/dist/flags/common/index.d.ts +3 -3
  24. package/dist/flags/common/index.js +3 -3
  25. package/dist/flags/common/sse.d.ts +24 -3
  26. package/dist/flags/common/sse.js +85 -15
  27. package/dist/flags/common/version.d.ts +2 -0
  28. package/dist/flags/common/version.js +2 -0
  29. package/dist/flags/engine/index.d.ts +1 -1
  30. package/dist/flags/engine/index.js +1 -1
  31. package/dist/flags/engine/operators.d.ts +20 -3
  32. package/dist/flags/engine/operators.js +38 -32
  33. package/dist/flags/node.d.ts +26 -14
  34. package/dist/flags/node.js +51 -7
  35. package/dist/flags/react.d.ts +6 -4
  36. package/dist/flags/react.js +47 -17
  37. package/dist/index.d.ts +7 -6
  38. package/dist/index.js +4 -4
  39. package/dist/middleware.d.ts +18 -0
  40. package/dist/middleware.js +28 -7
  41. package/dist/next.d.ts +52 -8
  42. package/dist/next.js +151 -37
  43. package/dist/openfeature/server.d.ts +3 -0
  44. package/dist/openfeature/server.js +14 -2
  45. package/dist/openfeature/shared.d.ts +1 -1
  46. package/dist/openfeature/shared.js +22 -2
  47. package/dist/openfeature/web.js +9 -3
  48. package/dist/types.d.ts +56 -4
  49. package/dist/types.js +6 -0
  50. package/docs/analytics.md +1 -1
  51. package/docs/flags-node.md +1 -1
  52. package/docs/flags.md +2 -1
  53. package/package.json +32 -14
  54. package/assets/logo-512.png +0 -0
package/dist/cli.js CHANGED
@@ -7,20 +7,27 @@ import { generateSigningKey, loadSigningKey, registerPublicKey, redactSecrets }
7
7
  const HELP = `daywalker: connect a service to Daywalker Auth (RFC 9421 / Push Architecture)
8
8
 
9
9
  Usage:
10
- daywalker init --service <slug> [--api-key <key>] [--domain <host>] [--auth-url <url>] [--json|--export] [--print-key]
11
- daywalker rotate [--service <slug>] [--api-key <key>] [--auth-url <url>] [--json|--export] [--print-key]
10
+ daywalker init --service <slug> [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--force] [--skip-register] [--json|--export] [--print-key]
11
+ daywalker rotate [--service <slug>] [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--skip-register] [--json|--export] [--print-key]
12
12
  daywalker check
13
13
 
14
14
  init Generates an Ed25519 keypair, registers the public key directly with Daywalker Auth
15
15
  (POST /api/v1/keys/register), and writes DAYWALKER_* settings to .env.local (or outputs JSON with --json).
16
- No public ingress or /.well-known route handlers are required.
16
+ Nothing is written if registration fails. --force replaces an existing key (kept as
17
+ DAYWALKER_SIGNING_KEY_PREVIOUS for rollback). No public ingress or /.well-known route handlers are required.
17
18
  rotate Generates a new Ed25519 key, registers it with Daywalker Auth, and keeps the old one
18
19
  in .env.local as DAYWALKER_SIGNING_KEY_PREVIOUS during deployment transition.
20
+ Nothing is written, and the exit code is non-zero, unless registration succeeds.
19
21
  check Validates configuration and confirms Daywalker Auth connection and signature readiness.
20
22
 
21
23
  Options:
24
+ --api-key-stdin Read the Daywalker API key from stdin (e.g. \`op read ... | daywalker init --api-key-stdin\`).
25
+ The key is otherwise taken from DAYWALKER_API_KEY in the environment or .env.local.
26
+ --api-key <key> also works but exposes the key in shell history and process listings.
27
+ --skip-register Do not register the public key (alias: --no-register); register it yourself before deploying.
22
28
  --json, --export Output configuration as JSON for piping into Vercel, DigitalOcean, Fly.io, AWS SSM, etc.
23
- --print-key Print the private key to stdout (disabled by default for security).
29
+ The JSON includes the private key (DAYWALKER_SIGNING_KEY); nothing is written to .env.local.
30
+ --print-key Print the private key to stdout after writing .env.local (disabled by default for security).
24
31
  `;
25
32
  function args(argv) {
26
33
  const out = {};
@@ -42,6 +49,35 @@ function fail(message) {
42
49
  console.error(redactSecrets(`✖ ${message}`));
43
50
  process.exit(1);
44
51
  }
52
+ /** Read all of stdin (for --api-key-stdin), so the key never appears in argv. */
53
+ async function readStdin() {
54
+ if (process.stdin.isTTY)
55
+ fail("--api-key-stdin expects the key on stdin (pipe it in)");
56
+ const chunks = [];
57
+ for await (const chunk of process.stdin)
58
+ chunks.push(Buffer.from(chunk));
59
+ return Buffer.concat(chunks).toString("utf8").trim();
60
+ }
61
+ async function resolveApiKey(opts, existing) {
62
+ if (typeof opts["api-key"] === "string")
63
+ return opts["api-key"].trim() || null;
64
+ if (opts["api-key-stdin"]) {
65
+ const fromStdin = await readStdin();
66
+ if (!fromStdin)
67
+ fail("--api-key-stdin: no API key received on stdin");
68
+ return fromStdin;
69
+ }
70
+ return String(existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
71
+ }
72
+ function skipRegister(opts) {
73
+ return Boolean(opts["skip-register"] || opts["no-register"]);
74
+ }
75
+ /** --json prints the private key by design (for secret stores); say so on stderr so it is never a surprise. */
76
+ function warnJsonIncludesKey(values) {
77
+ if (values.DAYWALKER_SIGNING_KEY) {
78
+ console.warn("! --json output includes the private signing key (DAYWALKER_SIGNING_KEY); pipe it straight into your secret store.");
79
+ }
80
+ }
45
81
  function readEnvFile(file) {
46
82
  const map = new Map();
47
83
  if (!fs.existsSync(file))
@@ -53,8 +89,35 @@ function readEnvFile(file) {
53
89
  }
54
90
  return map;
55
91
  }
56
- function writeEnvFile(file, values) {
57
- const lines = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n") : [];
92
+ /**
93
+ * Values are written as KEY="value". Anything a dotenv parser could interpret (quotes, `$` expansion
94
+ * as done by Next.js / dotenv-expand, backslash escapes, newlines or other control characters) is
95
+ * rejected instead of escaped, since parsers disagree on escaping rules.
96
+ */
97
+ const UNSAFE_ENV_VALUE = /["'`$\\\u0000-\u001f\u007f]/;
98
+ function assertSafeEnvValues(values) {
99
+ for (const [key, value] of Object.entries(values)) {
100
+ if (value !== null && UNSAFE_ENV_VALUE.test(value)) {
101
+ fail(`${key} contains quotes, "$", backslashes, newlines or control characters; refusing to write it to .env.local`);
102
+ }
103
+ }
104
+ }
105
+ function writeEnvFile(file, values, comments = {}) {
106
+ assertSafeEnvValues(values);
107
+ let stat = null;
108
+ try {
109
+ stat = fs.lstatSync(file);
110
+ }
111
+ catch (err) {
112
+ if (err.code !== "ENOENT")
113
+ throw err;
114
+ }
115
+ // Never follow a symlink (it could point anywhere, e.g. a world-readable file); refuse non-files.
116
+ if (stat && stat.isSymbolicLink())
117
+ fail(`${path.basename(file)} is a symlink; refusing to write secrets through it`);
118
+ if (stat && !stat.isFile())
119
+ fail(`${path.basename(file)} is not a regular file`);
120
+ const lines = stat ? fs.readFileSync(file, "utf8").split("\n") : [];
58
121
  const seen = new Set();
59
122
  const out = [];
60
123
  for (const line of lines) {
@@ -73,12 +136,38 @@ function writeEnvFile(file, values) {
73
136
  if (added.length) {
74
137
  if (out.length)
75
138
  out.push("");
76
- out.push("# Daywalker Auth (managed by `daywalker init`)", ...added.map(([k, v]) => `${k}="${v}"`));
139
+ out.push("# Daywalker Auth (managed by `daywalker init`)");
140
+ for (const [k, v] of added) {
141
+ if (comments[k])
142
+ out.push(`# ${comments[k]}`);
143
+ out.push(`${k}="${v}"`);
144
+ }
145
+ }
146
+ // Atomic replace: write a fresh 0600 temp file in the same directory, then rename it over the
147
+ // target, so the secrets never sit in a file with looser permissions and readers never see a
148
+ // half-written file.
149
+ const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
150
+ const fd = fs.openSync(tmp, "wx", 0o600);
151
+ try {
152
+ fs.fchmodSync(fd, 0o600);
153
+ fs.writeFileSync(fd, out.join("\n") + "\n");
154
+ fs.fsyncSync(fd);
155
+ }
156
+ catch (err) {
157
+ fs.closeSync(fd);
158
+ fs.rmSync(tmp, { force: true });
159
+ throw err;
160
+ }
161
+ fs.closeSync(fd);
162
+ try {
163
+ fs.renameSync(tmp, file);
164
+ }
165
+ catch (err) {
166
+ fs.rmSync(tmp, { force: true });
167
+ throw err;
77
168
  }
78
- fs.writeFileSync(file, out.join("\n") + "\n", { mode: 0o600 });
79
- // `mode` only applies when the file is created; tighten an existing file too.
80
- fs.chmodSync(file, 0o600);
81
169
  }
170
+ const PREVIOUS_KEY_COMMENT = "Previous signing key, kept for rollback while the new key rolls out; remove it once deployed.";
82
171
  function ensureGitignored(dir, entry) {
83
172
  const file = path.join(dir, ".gitignore");
84
173
  const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
@@ -104,15 +193,22 @@ async function init(opts) {
104
193
  if (!domain)
105
194
  fail(`--domain (${rawDomain}) is not a valid public hostname`);
106
195
  }
107
- const apiKey = String(opts["api-key"] ?? existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
196
+ const apiKey = await resolveApiKey(opts, existing);
108
197
  const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
109
- const keyExists = Boolean(existing.get("DAYWALKER_SIGNING_KEY")) && !opts.force;
198
+ const existingKey = existing.get("DAYWALKER_SIGNING_KEY") || null;
199
+ const keyExists = Boolean(existingKey) && !opts.force;
110
200
  const key = keyExists ? null : await generateSigningKey();
111
- const activeKey = key ?? (existing.get("DAYWALKER_SIGNING_KEY") ? await loadSigningKey(existing.get("DAYWALKER_SIGNING_KEY")) : null);
201
+ // --force over an existing key: keep the old one so a deployment can roll back to it.
202
+ const previousKey = key && existingKey ? existingKey : null;
203
+ const activeKey = key ?? (existingKey ? await loadSigningKey(existingKey) : null);
112
204
  const kid = activeKey?.publicJwk.kid ?? String(opts["key-id"] ?? "");
113
- // Register public key directly with Daywalker Auth if API key is provided
205
+ // Register public key directly with Daywalker Auth if API key is provided. Replacing a key
206
+ // requires registration: writing an unregistered key would break request signing.
114
207
  let registered = false;
115
- if (apiKey && activeKey && !opts["skip-register"]) {
208
+ if (previousKey && !apiKey && !skipRegister(opts)) {
209
+ fail("Replacing DAYWALKER_SIGNING_KEY requires registering the new key: set DAYWALKER_API_KEY (or use --api-key-stdin), or pass --skip-register to write it anyway");
210
+ }
211
+ if (apiKey && activeKey && !skipRegister(opts)) {
116
212
  if (!isJson)
117
213
  console.log(`• Registering public key (${activeKey.publicJwk.kid}) with Daywalker Auth...`);
118
214
  const regResult = await registerPublicKey({
@@ -128,13 +224,11 @@ async function init(opts) {
128
224
  console.log(`✔ Public key registered with Daywalker Auth (Push Architecture)`);
129
225
  }
130
226
  else {
131
- if (!isJson) {
132
- console.warn(redactSecrets(`! Key registration warning (${regResult.status}): ${regResult.error}`));
133
- console.warn(` You can register the key later by re-running init with your DAYWALKER_API_KEY.`);
134
- }
227
+ fail(`Key registration failed (${regResult.status}): ${regResult.error}\n` +
228
+ ` Nothing was written. Fix the problem and re-run, or pass --skip-register to write the key without registering it.`);
135
229
  }
136
230
  }
137
- else if (!apiKey && !opts["skip-register"] && !isJson) {
231
+ else if (!apiKey && !skipRegister(opts) && !isJson) {
138
232
  console.log(`• Note: Set DAYWALKER_API_KEY in .env.local to automatically register public keys.`);
139
233
  }
140
234
  const signingKeyStr = key?.privateKeyPkcs8Base64 ?? existing.get("DAYWALKER_SIGNING_KEY") ?? "";
@@ -145,12 +239,14 @@ async function init(opts) {
145
239
  ...(domain ? { DAYWALKER_DOMAIN: domain } : {}),
146
240
  ...(apiKey ? { DAYWALKER_API_KEY: apiKey } : {}),
147
241
  ...(signingKeyStr ? { DAYWALKER_SIGNING_KEY: signingKeyStr } : {}),
242
+ ...(previousKey ? { DAYWALKER_SIGNING_KEY_PREVIOUS: previousKey } : {}),
148
243
  };
149
244
  if (isJson) {
245
+ warnJsonIncludesKey(envValues);
150
246
  console.log(JSON.stringify(envValues, null, 2));
151
247
  return;
152
248
  }
153
- writeEnvFile(envFile, envValues);
249
+ writeEnvFile(envFile, envValues, { DAYWALKER_SIGNING_KEY_PREVIOUS: PREVIOUS_KEY_COMMENT });
154
250
  ensureGitignored(dir, ".env.local");
155
251
  console.log(`✔ Daywalker configured for service "${slug}"${domain ? ` (${domain})` : ""}`);
156
252
  console.log(` • RFC 9421 / RFC 9530 HTTP Message Signing active`);
@@ -159,6 +255,8 @@ async function init(opts) {
159
255
  console.log(` • Key ID (RFC 7638 thumbprint): ${kid}`);
160
256
  if (registered)
161
257
  console.log(` • Zero-ingress push architecture: public key registered directly`);
258
+ if (previousKey)
259
+ console.log(` • Previous key kept as DAYWALKER_SIGNING_KEY_PREVIOUS for rollback; remove it once the new key is deployed.`);
162
260
  if (opts["print-key"] && signingKeyStr)
163
261
  console.log(`\nDAYWALKER_SIGNING_KEY=${signingKeyStr}`);
164
262
  }
@@ -171,11 +269,16 @@ async function rotate(opts) {
171
269
  if (!current)
172
270
  fail("No DAYWALKER_SIGNING_KEY found; run `daywalker init` first");
173
271
  const slug = String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? "").trim().toLowerCase();
174
- const apiKey = String(opts["api-key"] ?? existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
272
+ const apiKey = await resolveApiKey(opts, existing);
175
273
  const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
176
274
  const rotateDomain = publicHostname(String(opts.domain ?? existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? ""));
177
275
  const key = await generateSigningKey();
178
- if (apiKey && slug) {
276
+ // The new key only becomes active once Daywalker Auth knows it, so registration must succeed
277
+ // before anything is written (unless the caller explicitly registers it some other way).
278
+ if (!skipRegister(opts)) {
279
+ if (!apiKey || !slug) {
280
+ fail("rotate needs DAYWALKER_API_KEY and DAYWALKER_SERVICE_SLUG to register the new key (or pass --skip-register to write it unregistered)");
281
+ }
179
282
  if (!isJson)
180
283
  console.log(`• Registering new public key (${key.publicJwk.kid}) with Daywalker Auth...`);
181
284
  const regResult = await registerPublicKey({
@@ -185,25 +288,24 @@ async function rotate(opts) {
185
288
  domain: rotateDomain,
186
289
  publicJwk: key.publicJwk,
187
290
  });
188
- if (regResult.ok) {
189
- if (!isJson)
190
- console.log(`✔ New public key registered with Daywalker Auth`);
191
- }
192
- else {
193
- if (!isJson)
194
- console.warn(redactSecrets(`! Key registration warning: ${regResult.error}`));
291
+ if (!regResult.ok) {
292
+ fail(`Key registration failed (${regResult.status}): ${regResult.error}\n Nothing was written; the current key is still active.`);
195
293
  }
294
+ if (!isJson)
295
+ console.log(`✔ New public key registered with Daywalker Auth`);
196
296
  }
197
297
  const envValues = {
198
298
  DAYWALKER_KEY_ID: key.publicJwk.kid,
199
299
  DAYWALKER_SIGNING_KEY: key.privateKeyPkcs8Base64 ?? "",
300
+ // Kept for rollback: services still signing with the old key keep working during rollout.
200
301
  DAYWALKER_SIGNING_KEY_PREVIOUS: current,
201
302
  };
202
303
  if (isJson) {
304
+ warnJsonIncludesKey(envValues);
203
305
  console.log(JSON.stringify(envValues, null, 2));
204
306
  return;
205
307
  }
206
- writeEnvFile(envFile, envValues);
308
+ writeEnvFile(envFile, envValues, { DAYWALKER_SIGNING_KEY_PREVIOUS: PREVIOUS_KEY_COMMENT });
207
309
  console.log(`✔ New key ${key.publicJwk.kid} written; previous key preserved as DAYWALKER_SIGNING_KEY_PREVIOUS.`);
208
310
  console.log(` After deployment rollout is complete, you may remove DAYWALKER_SIGNING_KEY_PREVIOUS.`);
209
311
  if (opts["print-key"])
package/dist/client.d.ts CHANGED
@@ -2,22 +2,44 @@ import { type ResolvedConfig } from "./config.js";
2
2
  import { type DaywalkerConfig, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
3
3
  /** Validation results kept in memory; the least recently used are evicted past this. */
4
4
  export declare const MAX_VALIDATION_CACHE_ENTRIES = 1000;
5
+ /** Longest 429 `Retry-After` waited out by default (`retry.maxRetryAfterMs`); longer ones return the 429. */
6
+ export declare const MAX_RETRY_AFTER_MS = 60000;
5
7
  export type DaywalkerFetchInit = Omit<RequestInit, "body"> & {
8
+ /**
9
+ * Plain objects and arrays are sent as JSON. Strings, Uint8Array / ArrayBuffer(View) and URLSearchParams
10
+ * are sent (and digested) byte for byte, and Blobs are read into memory first when signing. FormData and
11
+ * ReadableStream bodies can't be digested, so they throw when a signing key is configured.
12
+ */
6
13
  body?: BodyInit | object | null;
7
14
  retry?: RetryOptions;
15
+ /** Per-attempt timeout overriding `config.timeoutMs`; 0 disables. */
16
+ timeoutMs?: number;
17
+ /** Sent as `Idempotency-Key` (unless already set) and lets non-idempotent requests retry network errors. */
18
+ idempotencyKey?: string;
8
19
  };
9
20
  export declare class Daywalker {
10
21
  private readonly _config;
11
22
  private readonly _rawConfig;
12
23
  private _signingKeyPromise?;
24
+ private _previousSigningKeyPromise?;
13
25
  private readonly _cache;
14
26
  private readonly _inflight;
15
27
  constructor(config?: DaywalkerConfig);
16
28
  get config(): ResolvedConfig;
17
29
  toJSON(): Record<string, unknown>;
18
30
  private getLoadedSigningKey;
31
+ private getLoadedPreviousSigningKey;
19
32
  /**
20
- * Signs and executes requests with automatic retry and exponential backoff on 429 responses.
33
+ * Signs and executes a request to Daywalker Auth (same origin as `authUrl` only).
34
+ * - Redirects are never followed: a 3xx throws a DaywalkerError (code UNEXPECTED_REDIRECT), so the API key,
35
+ * signature and body never reach another URL and no redirect target's answer is trusted.
36
+ * - Each attempt times out after `timeoutMs` (default 5000ms) of waiting for response headers; the caller's
37
+ * `signal` aborts immediately, including during backoff, and is never retried.
38
+ * - 429s are retried with exponential backoff, honoring `Retry-After` up to `retry.maxRetryAfterMs`. Network
39
+ * errors and timeouts are retried only for GET / HEAD / OPTIONS, with an `idempotencyKey`, or when
40
+ * `retry.retryNetworkErrors` is true. Options merge as defaults < `config.retry` < `init.retry`.
41
+ * - An explicit `Authorization` header is sent as given; otherwise `Bearer <apiKey>` is added. The signature
42
+ * covers method, path, query, authority and Content-Digest, never Authorization.
21
43
  */
22
44
  fetch(path: string, init?: DaywalkerFetchInit): Promise<Response>;
23
45
  private _cacheGet;
@@ -27,6 +49,9 @@ export declare class Daywalker {
27
49
  * Supports zero-egress offline verification for JWT token leases when Daywalker's public key is configured.
28
50
  * A missing token is always rejected (401); it never falls back to this service's own API key —
29
51
  * use `validateServiceKey()` for that.
52
+ * Online validation fails closed: only a 2xx JSON answer with `valid: true` and a sane shape is accepted.
53
+ * Outages (network errors, timeouts, 5xx, malformed answers) return status 503 with error NETWORK_ERROR or
54
+ * SERVICE_UNAVAILABLE and are never cached. Each call gets its own copy of the (cached) result.
30
55
  */
31
56
  validateToken(tokenOrOptions?: string | {
32
57
  token?: string;
@@ -69,12 +94,14 @@ export declare class Daywalker {
69
94
  }>;
70
95
  /**
71
96
  * Fetches Daywalker's public JWKS (GET /.well-known/jwks.json) for offline lease verification.
97
+ * Uses the configured `fetch`, sends no credentials, never follows redirects and times out after `timeoutMs`.
72
98
  */
73
99
  fetchJwks(): Promise<{
74
100
  keys: PublicJwk[];
75
101
  }>;
76
102
  /**
77
- * The public JWK(s) configured for this service.
103
+ * The public JWK(s) configured for this service: the current signing key, then the previous one
104
+ * (`previousSigningKey`) during a rotation, so requests from instances still on the old key keep verifying.
78
105
  */
79
106
  keys(): Promise<{
80
107
  keys: PublicJwk[];