@shelbyb/daywalker-sdk 0.5.0 → 0.7.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/dist/cli.js CHANGED
@@ -3,27 +3,35 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createDaywalker } from "./client.js";
5
5
  import { parseServiceSlugs, publicHostname } from "./config.js";
6
- import { generateSigningKey, loadSigningKey, registerPublicKey, redactSecrets } from "./crypto/index.js";
6
+ import { generateSigningKey, keySlotFor, listSigningKeys, loadSigningKey, registerPublicKey, redactSecrets, revokeSigningKey } from "./crypto/index.js";
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[,slug…]> [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--force] [--skip-register] [--json|--export] [--print-key]
10
+ daywalker init [--service <slug[,slug…]>] [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--force] [--skip-register] [--json|--export] [--print-key]
11
11
  daywalker rotate [--service <slug[,slug…]>] [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--skip-register] [--json|--export] [--print-key]
12
- daywalker check
12
+ daywalker check [--offline]
13
+ daywalker keys [--domain <host>] [--auth-url <url>] [--api-key-stdin] [--json]
14
+ daywalker revoke <keyId> [--domain <host>] [--auth-url <url>] [--api-key-stdin] [--force]
13
15
 
14
16
  init Generates an Ed25519 keypair, registers the public key directly with Daywalker Auth
15
17
  (POST /api/v1/keys/register), and writes DAYWALKER_* settings to .env.local (or outputs JSON with --json).
16
18
  Nothing is written if registration fails. --force replaces an existing key (kept as
17
19
  DAYWALKER_SIGNING_KEY_PREVIOUS for rollback). No public ingress or /.well-known route handlers are required.
18
20
  rotate Generates a new Ed25519 key, registers it with Daywalker Auth, and keeps the old one
19
- in .env.local as DAYWALKER_SIGNING_KEY_PREVIOUS during deployment transition.
21
+ in .env.local as DAYWALKER_SIGNING_KEY_PREVIOUS during deployment transition. The old key stays
22
+ registered until you revoke it (\`daywalker revoke <keyId>\`) once the new key is deployed everywhere.
20
23
  Nothing is written, and the exit code is non-zero, unless registration succeeds.
21
- check Validates configuration and confirms Daywalker Auth connection and signature readiness.
24
+ check Validates configuration and signature readiness, then runs the OAuth client credentials grant
25
+ against Daywalker Auth to confirm the credentials work and cover the configured service(s).
26
+ keys Lists the signing keys registered for the domain (status, slot, expiry, last use).
27
+ revoke Revokes a signing key now; Daywalker also ends the OAuth sessions it started. Refuses the key in
28
+ DAYWALKER_KEY_ID (the one this service signs with) unless --force.
22
29
 
23
30
  Options:
24
31
  --api-key-stdin Read the Daywalker API key from stdin (e.g. \`op read ... | daywalker init --api-key-stdin\`).
25
32
  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.
33
+ (--api-key <key> is refused: it would expose the key in shell history and process listings.)
34
+ --offline check only: skip the live OAuth client credentials check against Daywalker Auth.
27
35
  --skip-register Do not register the public key (alias: --no-register); register it yourself before deploying.
28
36
  --json, --export Output configuration as JSON for piping into Vercel, DigitalOcean, Fly.io, AWS SSM, etc.
29
37
  The JSON includes the private key (DAYWALKER_SIGNING_KEY); nothing is written to .env.local.
@@ -59,8 +67,6 @@ async function readStdin() {
59
67
  return Buffer.concat(chunks).toString("utf8").trim();
60
68
  }
61
69
  async function resolveApiKey(opts, existing) {
62
- if (typeof opts["api-key"] === "string")
63
- return opts["api-key"].trim() || null;
64
70
  if (opts["api-key-stdin"]) {
65
71
  const fromStdin = await readStdin();
66
72
  if (!fromStdin)
@@ -69,6 +75,13 @@ async function resolveApiKey(opts, existing) {
69
75
  }
70
76
  return String(existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
71
77
  }
78
+ /** Why a key API call failed, with what to do when the domain is full. */
79
+ function keyApiFailure(what, result) {
80
+ const hint = result.code === "KEY_LIMIT_REACHED"
81
+ ? "\n The domain has 20 live keys: list them with `daywalker keys` and revoke unused ones with `daywalker revoke <keyId>`."
82
+ : "";
83
+ return `${what} failed (${result.status || "network"}${result.code ? ` ${result.code}` : ""}): ${result.error}${hint}`;
84
+ }
72
85
  function skipRegister(opts) {
73
86
  return Boolean(opts["skip-register"] || opts["no-register"]);
74
87
  }
@@ -181,8 +194,9 @@ async function init(opts) {
181
194
  const envFile = path.join(dir, ".env.local");
182
195
  const existing = readEnvFile(envFile);
183
196
  const slugs = parseServiceSlugs(String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? ""));
184
- if (slugs.length === 0 || !slugs.every((s) => /^[a-z0-9_-]+$/.test(s))) {
185
- fail("--service must be this service's Daywalker slug (e.g. --service sonalyze), or a comma-separated list when one API key covers several (--service gigpulse,devpulse)");
197
+ // Optional: requests never name a service. Only needed to verify access tokens offline (audience check).
198
+ if (!slugs.every((s) => /^[a-z0-9_-]+$/.test(s))) {
199
+ fail("--service must be Daywalker service slug(s) (e.g. --service sonalyze, or gigpulse,devpulse)");
186
200
  }
187
201
  const slug = slugs.join(",");
188
202
  const rawDomain = opts.domain ? String(opts.domain) : existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? "";
@@ -210,12 +224,13 @@ async function init(opts) {
210
224
  if (apiKey && activeKey && !skipRegister(opts)) {
211
225
  if (!isJson)
212
226
  console.log(`• Registering public key (${activeKey.publicJwk.kid}) with Daywalker Auth...`);
227
+ // Each key gets its own slot, so registering it never retires a key a running deployment still uses.
213
228
  const regResult = await registerPublicKey({
214
229
  authUrl,
215
230
  apiKey,
216
- serviceSlug: slug,
217
231
  domain,
218
232
  publicJwk: activeKey.publicJwk,
233
+ instanceId: keySlotFor(activeKey.publicJwk.kid),
219
234
  });
220
235
  if (regResult.ok) {
221
236
  registered = true;
@@ -223,7 +238,7 @@ async function init(opts) {
223
238
  console.log(`✔ Public key registered with Daywalker Auth (Push Architecture)`);
224
239
  }
225
240
  else {
226
- fail(`Key registration failed (${regResult.status}): ${regResult.error}\n` +
241
+ fail(`${keyApiFailure("Key registration", regResult)}\n` +
227
242
  ` Nothing was written. Fix the problem and re-run, or pass --skip-register to write the key without registering it.`);
228
243
  }
229
244
  }
@@ -232,7 +247,7 @@ async function init(opts) {
232
247
  }
233
248
  const signingKeyStr = key?.privateKeyPkcs8Base64 ?? existing.get("DAYWALKER_SIGNING_KEY") ?? "";
234
249
  const envValues = {
235
- DAYWALKER_SERVICE_SLUG: slug,
250
+ ...(slug ? { DAYWALKER_SERVICE_SLUG: slug } : {}),
236
251
  DAYWALKER_KEY_ID: kid,
237
252
  DAYWALKER_AUTH_URL: authUrl,
238
253
  ...(domain ? { DAYWALKER_DOMAIN: domain } : {}),
@@ -247,15 +262,19 @@ async function init(opts) {
247
262
  }
248
263
  writeEnvFile(envFile, envValues, { DAYWALKER_SIGNING_KEY_PREVIOUS: PREVIOUS_KEY_COMMENT });
249
264
  ensureGitignored(dir, ".env.local");
250
- console.log(`✔ Daywalker configured for service "${slug}"${domain ? ` (${domain})` : ""}`);
265
+ console.log(`✔ Daywalker configured${slug ? ` (audience "${slug}")` : ""}${domain ? ` (${domain})` : ""}`);
251
266
  console.log(` • RFC 9421 / RFC 9530 HTTP Message Signing active`);
252
- console.log(` • .env.local: DAYWALKER_SERVICE_SLUG, DAYWALKER_KEY_ID, DAYWALKER_AUTH_URL${domain ? ", DAYWALKER_DOMAIN" : ""}${apiKey ? ", DAYWALKER_API_KEY" : ""}${signingKeyStr ? ", DAYWALKER_SIGNING_KEY" : ""}`);
267
+ console.log(` • .env.local: ${slug ? "DAYWALKER_SERVICE_SLUG, " : ""}DAYWALKER_KEY_ID, DAYWALKER_AUTH_URL${domain ? ", DAYWALKER_DOMAIN" : ""}${apiKey ? ", DAYWALKER_API_KEY" : ""}${signingKeyStr ? ", DAYWALKER_SIGNING_KEY" : ""}`);
253
268
  if (kid)
254
269
  console.log(` • Key ID (RFC 7638 thumbprint): ${kid}`);
255
270
  if (registered)
256
271
  console.log(` • Zero-ingress push architecture: public key registered directly`);
257
- if (previousKey)
258
- console.log(` • Previous key kept as DAYWALKER_SIGNING_KEY_PREVIOUS for rollback; remove it once the new key is deployed.`);
272
+ if (previousKey) {
273
+ const previousKid = (await loadSigningKey(previousKey).catch(() => null))?.publicJwk.kid;
274
+ console.log(` • Previous key kept as DAYWALKER_SIGNING_KEY_PREVIOUS for rollback; it stays registered.`);
275
+ if (previousKid)
276
+ console.log(` Once the new key is deployed everywhere: npx daywalker revoke ${previousKid}`);
277
+ }
259
278
  if (opts["print-key"] && signingKeyStr)
260
279
  console.log(`\nDAYWALKER_SIGNING_KEY=${signingKeyStr}`);
261
280
  }
@@ -275,20 +294,20 @@ async function rotate(opts) {
275
294
  // The new key only becomes active once Daywalker Auth knows it, so registration must succeed
276
295
  // before anything is written (unless the caller explicitly registers it some other way).
277
296
  if (!skipRegister(opts)) {
278
- if (!apiKey || !slug) {
279
- fail("rotate needs DAYWALKER_API_KEY and DAYWALKER_SERVICE_SLUG to register the new key (or pass --skip-register to write it unregistered)");
297
+ if (!apiKey) {
298
+ fail("rotate needs DAYWALKER_API_KEY to register the new key (or pass --skip-register to write it unregistered)");
280
299
  }
281
300
  if (!isJson)
282
301
  console.log(`• Registering new public key (${key.publicJwk.kid}) with Daywalker Auth...`);
283
302
  const regResult = await registerPublicKey({
284
303
  authUrl,
285
304
  apiKey,
286
- serviceSlug: slug,
287
305
  domain: rotateDomain,
288
306
  publicJwk: key.publicJwk,
307
+ instanceId: keySlotFor(key.publicJwk.kid),
289
308
  });
290
309
  if (!regResult.ok) {
291
- fail(`Key registration failed (${regResult.status}): ${regResult.error}\n Nothing was written; the current key is still active.`);
310
+ fail(`${keyApiFailure("Key registration", regResult)}\n Nothing was written; the current key is still active.`);
292
311
  }
293
312
  if (!isJson)
294
313
  console.log(`✔ New public key registered with Daywalker Auth`);
@@ -306,14 +325,27 @@ async function rotate(opts) {
306
325
  }
307
326
  writeEnvFile(envFile, envValues, { DAYWALKER_SIGNING_KEY_PREVIOUS: PREVIOUS_KEY_COMMENT });
308
327
  console.log(`✔ New key ${key.publicJwk.kid} written; previous key preserved as DAYWALKER_SIGNING_KEY_PREVIOUS.`);
309
- console.log(` After deployment rollout is complete, you may remove DAYWALKER_SIGNING_KEY_PREVIOUS.`);
328
+ const previousKid = (await loadSigningKey(current).catch(() => null))?.publicJwk.kid;
329
+ console.log(` The previous key stays registered, so running deployments keep working.`);
330
+ if (previousKid)
331
+ console.log(` Once the new key is deployed everywhere: npx daywalker revoke ${previousKid}, then remove DAYWALKER_SIGNING_KEY_PREVIOUS.`);
310
332
  if (opts["print-key"])
311
333
  console.log(`\nDAYWALKER_SIGNING_KEY=${key.privateKeyPkcs8Base64}`);
312
334
  }
313
- async function check() {
335
+ async function check(opts) {
314
336
  const env = { ...process.env, ...Object.fromEntries(readEnvFile(path.join(process.cwd(), ".env.local"))) };
315
337
  const client = createDaywalker({ env });
316
338
  const result = await client.selfCheck();
339
+ if (result.ok && !opts.offline) {
340
+ const live = await client.validateServiceCredentials();
341
+ if (!live.ok) {
342
+ result.ok = false;
343
+ result.problems.push(`OAuth client credentials check failed (${live.status} ${live.error}): ${live.message}`);
344
+ }
345
+ else {
346
+ console.log(`✔ OAuth client credentials grant succeeded (services: ${(live.data.services ?? []).join(", ")})`);
347
+ }
348
+ }
317
349
  if (result.ok) {
318
350
  console.log(`✔ Daywalker configuration looks good`);
319
351
  if (result.serviceSlugs.length > 1)
@@ -335,11 +367,58 @@ async function check() {
335
367
  console.error(redactSecrets(` - ${p}`));
336
368
  process.exit(1);
337
369
  }
370
+ /** Domain, API key and auth URL for the key commands, from flags, .env.local or the environment. */
371
+ async function keyCommandContext(opts) {
372
+ const existing = readEnvFile(path.join(process.cwd(), ".env.local"));
373
+ const apiKey = await resolveApiKey(opts, existing);
374
+ if (!apiKey)
375
+ fail("Set DAYWALKER_API_KEY (or pipe it to --api-key-stdin)");
376
+ const rawDomain = String(opts.domain ?? existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? "");
377
+ const domain = publicHostname(rawDomain);
378
+ const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
379
+ const currentKid = existing.get("DAYWALKER_KEY_ID") ?? process.env.DAYWALKER_KEY_ID ?? null;
380
+ const previous = existing.get("DAYWALKER_SIGNING_KEY_PREVIOUS") ?? process.env.DAYWALKER_SIGNING_KEY_PREVIOUS;
381
+ const previousKid = previous ? ((await loadSigningKey(previous).catch(() => null))?.publicJwk.kid ?? null) : null;
382
+ return { apiKey, domain, rawDomain, authUrl, currentKid, previousKid };
383
+ }
384
+ async function keys(opts) {
385
+ const ctx = await keyCommandContext(opts);
386
+ if (!ctx.domain)
387
+ fail(`--domain (${ctx.rawDomain || "not set"}) must be a public hostname (or set DAYWALKER_DOMAIN)`);
388
+ const result = await listSigningKeys({ authUrl: ctx.authUrl, apiKey: ctx.apiKey, domain: ctx.domain });
389
+ if (!result.ok)
390
+ fail(keyApiFailure("Listing keys", result));
391
+ const list = result.keys ?? [];
392
+ if (opts.json) {
393
+ console.log(JSON.stringify(list, null, 2));
394
+ return;
395
+ }
396
+ const when = (t) => (t === null ? "-" : new Date(t * 1000).toISOString().replace(/\.\d{3}Z$/, "Z"));
397
+ console.log(`Signing keys for ${ctx.domain} (${list.filter((k) => k.status !== "REVOKED").length} live of 20 allowed):`);
398
+ for (const k of list) {
399
+ const tag = k.keyId === ctx.currentKid ? " (current)" : k.keyId === ctx.previousKid ? " (previous)" : "";
400
+ console.log(` ${k.keyId}${tag} ${k.status} slot=${k.instanceId ?? "-"} expires=${when(k.expiresAt)} lastUsed=${when(k.lastUsedAt)}`);
401
+ }
402
+ }
403
+ async function revoke(keyId, opts) {
404
+ if (!keyId)
405
+ fail("Usage: daywalker revoke <keyId>");
406
+ const ctx = await keyCommandContext(opts);
407
+ if (keyId === ctx.currentKid && !opts.force) {
408
+ fail(`${keyId} is DAYWALKER_KEY_ID, the key this service signs with: revoking it stops this service from authenticating. Pass --force if that is what you want.`);
409
+ }
410
+ const result = await revokeSigningKey({ authUrl: ctx.authUrl, apiKey: ctx.apiKey, keyId });
411
+ if (!result.ok)
412
+ fail(keyApiFailure(`Revoking ${keyId}`, result));
413
+ console.log(`✔ Revoked signing key ${keyId}; Daywalker ended the OAuth sessions it started.`);
414
+ if (keyId === ctx.previousKid)
415
+ console.log(" You can now remove DAYWALKER_SIGNING_KEY_PREVIOUS from .env.local.");
416
+ }
338
417
  async function main() {
339
418
  const [cmd, ...rest] = process.argv.slice(2);
340
419
  const parsedArgs = args(rest);
341
- if (typeof parsedArgs["api-key"] === "string") {
342
- console.warn("! --api-key exposes the key in shell history and process listings; prefer DAYWALKER_API_KEY in the environment or .env.local.");
420
+ if (parsedArgs["api-key"] !== undefined) {
421
+ fail("--api-key is not supported: it exposes the key in shell history and process listings. Use DAYWALKER_API_KEY in the environment or .env.local, or pipe it to --api-key-stdin.");
343
422
  }
344
423
  if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
345
424
  console.log(HELP);
@@ -352,7 +431,13 @@ async function main() {
352
431
  await rotate(parsedArgs);
353
432
  }
354
433
  else if (cmd === "check") {
355
- await check();
434
+ await check(parsedArgs);
435
+ }
436
+ else if (cmd === "keys") {
437
+ await keys(parsedArgs);
438
+ }
439
+ else if (cmd === "revoke") {
440
+ await revoke(rest[0] && !rest[0].startsWith("--") ? rest[0] : undefined, parsedArgs);
356
441
  }
357
442
  else {
358
443
  fail(`Unknown command "${cmd}". Run \`daywalker --help\` for usage.`);
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ResolvedConfig } from "./config.js";
2
- import { type DaywalkerConfig, type IssueLeaseOptions, type LeaseResponse, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
2
+ import { type DaywalkerConfig, type ValidateTokenOptions, type TokenSet, type GetTokensOptions, 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
5
  /** Longest 429 `Retry-After` waited out by default (`retry.maxRetryAfterMs`); longer ones return the 429. */
@@ -17,18 +17,45 @@ export type DaywalkerFetchInit = Omit<RequestInit, "body"> & {
17
17
  /** Sent as `Idempotency-Key` (unless already set) and lets non-idempotent requests retry network errors. */
18
18
  idempotencyKey?: string;
19
19
  };
20
+ /**
21
+ * Longest a successful validation is cached (5 minutes), whatever `cacheTtlMs` says, so a revoked token
22
+ * can't stay accepted for long.
23
+ */
24
+ export declare const MAX_CACHE_TTL_MS = 300000;
25
+ /** Longest incoming token accepted (characters); longer input is refused without calling Daywalker. */
26
+ export declare const MAX_TOKEN_LENGTH = 8192;
27
+ /** How long a fetched JWKS (`jwks: "auto"`) is used before it is refetched. */
28
+ export declare const JWKS_CACHE_TTL_MS = 600000;
29
+ /**
30
+ * Least time between two JWKS fetches, whatever triggers them (an unknown `kid`, an expired set, a failed
31
+ * fetch), so a flood of tokens naming made-up `kid`s costs at most one request to Daywalker per interval.
32
+ */
33
+ export declare const JWKS_REFETCH_COOLDOWN_MS = 30000;
34
+ /** What signs this client's requests, and where it came from. */
35
+ type SignerSource = "signer" | "signingKey" | "ephemeral" | "keyStore";
20
36
  export declare class Daywalker {
21
- private readonly _config;
22
- private readonly _rawConfig;
23
- private _signingKeyPromise?;
24
- private _previousSigningKeyPromise?;
25
- private readonly _cache;
26
- private readonly _inflight;
37
+ #private;
27
38
  constructor(config?: DaywalkerConfig);
28
- get config(): ResolvedConfig;
39
+ /** The resolved configuration, frozen, with the API key and private keys shown as "[configured]". */
40
+ get config(): Readonly<ResolvedConfig>;
29
41
  toJSON(): Record<string, unknown>;
30
- private getLoadedSigningKey;
31
- private getLoadedPreviousSigningKey;
42
+ /**
43
+ * Shuts the client down: revokes its ephemeral signing key (which also ends the OAuth sessions it started)
44
+ * and forgets its tokens. Afterwards every call that needs Daywalker fails with CLIENT_CLOSED. Safe to call
45
+ * twice. Keys from `signingKey`, `keyStore` or `signer` are long-lived and are left alone.
46
+ */
47
+ close(): Promise<{
48
+ revoked: string[];
49
+ failed: string[];
50
+ }>;
51
+ /**
52
+ * Resolves the signing key now instead of on the first request: loads it, or provisions and registers it
53
+ * (see `keyStore`). Call it at startup to fail fast. Returns where the key came from and its id.
54
+ */
55
+ provisionSigningKey(): Promise<{
56
+ source: SignerSource;
57
+ keyId: string;
58
+ } | null>;
32
59
  /**
33
60
  * Signs and executes a request to Daywalker Auth (same origin as `authUrl` only).
34
61
  * - Redirects are never followed: a 3xx throws a DaywalkerError (code UNEXPECTED_REDIRECT), so the API key,
@@ -38,57 +65,63 @@ export declare class Daywalker {
38
65
  * - 429s are retried with exponential backoff, honoring `Retry-After` up to `retry.maxRetryAfterMs`. Network
39
66
  * errors and timeouts are retried only for GET / HEAD / OPTIONS, with an `idempotencyKey`, or when
40
67
  * `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.
68
+ * - An explicit `Authorization` header is sent as given; otherwise `DPoP <access token>` from the OAuth
69
+ * client credentials flow (`accessToken()`) is added, with a DPoP proof bound to it. The API key is never
70
+ * sent here: it only authenticates the client credentials grant. An answer of 401 ACCESS_TOKEN_EXPIRED
71
+ * refreshes the token and retries once. The signature covers method, path, query, authority and
72
+ * Content-Digest, never Authorization.
43
73
  */
44
74
  fetch(path: string, init?: DaywalkerFetchInit): Promise<Response>;
45
- private _cacheGet;
46
- private _cacheSet;
47
75
  /**
48
- * Validates an incoming bearer token with memory caching and discriminated union return.
49
- * Supports zero-egress offline verification for JWT token leases when Daywalker's public key is configured.
50
- * A missing token is always rejected (401); it never falls back to this service's own API key —
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.
76
+ * Validates an incoming token with memory caching and a discriminated union result.
77
+ * - Access tokens (JWTs) are verified offline when Daywalker's public key is configured. A DPoP-bound
78
+ * token (`cnf.jkt`) is only accepted with a valid DPoP proof (`options.dpop`; the adapters pass it).
79
+ * - Other tokens are validated online with `POST /api/v1/tokens/validate`, authenticated with this
80
+ * service's own DPoP-bound access token. The answer must name the expected service.
81
+ * - A missing, oversized or malformed token is rejected (401) without calling Daywalker. A token is never
82
+ * replaced by this service's own credentials: use `validateServiceCredentials()` for that.
83
+ * - Online validation fails closed: only a 2xx JSON answer with `valid: true` and a sane shape is accepted.
84
+ * Outages (network errors, timeouts, 5xx, malformed answers) return status 503 with error NETWORK_ERROR or
85
+ * SERVICE_UNAVAILABLE and are never cached. Each call gets its own copy of the (cached) result.
55
86
  */
56
- validateToken(tokenOrOptions?: string | {
87
+ validateToken(tokenOrOptions?: string | (ValidateTokenOptions & {
57
88
  token?: string;
58
- serviceSlug?: string;
59
- forceRefresh?: boolean;
60
- offlineOnly?: boolean;
61
- }, options?: {
62
- serviceSlug?: string;
63
- forceRefresh?: boolean;
64
- offlineOnly?: boolean;
65
- }): Promise<ValidateResult>;
66
- private _validateOnline;
89
+ }), options?: ValidateTokenOptions): Promise<ValidateResult>;
67
90
  /**
68
- * Service health check: validates this service's own configured API key (DAYWALKER_API_KEY).
69
- * Never use this to authenticate an incoming request.
91
+ * Service health check: runs this service's OAuth client credentials flow (`accessToken()`) and checks
92
+ * that the granted services include `serviceSlug` (default: the configured one; with none configured, at
93
+ * least one service must be granted). Never use this to authenticate an incoming request. `forceRefresh`
94
+ * starts a new client credentials grant.
70
95
  */
71
- validateServiceKey(options?: {
96
+ validateServiceCredentials(options?: {
72
97
  serviceSlug?: string;
73
98
  forceRefresh?: boolean;
74
99
  }): Promise<ValidateResult>;
75
100
  /**
76
101
  * Throws DaywalkerAuthError or DaywalkerRateLimitError if token is invalid or rate limited.
77
102
  */
78
- requireValidToken(tokenOrOptions?: string | {
103
+ requireValidToken(tokenOrOptions?: string | (ValidateTokenOptions & {
79
104
  token?: string;
80
- serviceSlug?: string;
81
- offlineOnly?: boolean;
82
- }, options?: {
83
- serviceSlug?: string;
84
- offlineOnly?: boolean;
85
- }): Promise<TokenPayload>;
105
+ }), options?: ValidateTokenOptions): Promise<TokenPayload>;
106
+ /**
107
+ * Client credentials grant (RFC 6749 §4.4): the signed request (API key + registered Ed25519 key) is
108
+ * exchanged for a short-lived access token JWT and a rotating refresh token. By default the grant is
109
+ * narrowed to the configured service slug(s) (least privilege); with none configured it covers every
110
+ * service the credentials do. The tokens are remembered for `accessToken()`. The result is frozen.
111
+ */
112
+ getTokens(options?: GetTokensOptions): Promise<TokenSet>;
113
+ /**
114
+ * Refresh token rotation: trades the refresh token for a NEW access token and a NEW refresh token; the
115
+ * old refresh token stops working. Presenting an old one again revokes the whole session, so refreshes
116
+ * are serialized per client and the new refresh token must replace the stored one.
117
+ */
118
+ refreshTokens(refreshToken?: string): Promise<TokenSet>;
86
119
  /**
87
- * Exchanges an RFC 9421 signed request for a short-lived EdDSA JWT lease (POST /api/v1/leases/issue).
88
- * The lease lists every service slug the API key covers (`services`, also the JWT's `services` and `aud`
89
- * claims), so one set of credentials serves all of them. Pass `{ services: [...] }` to narrow it.
120
+ * A valid access token, refreshing (or re-authenticating with the client credentials once the session
121
+ * ended) when it is within 30 seconds of expiry. Concurrent callers share one request, so the single-use
122
+ * refresh token is never presented twice.
90
123
  */
91
- issueLease(options?: number | IssueLeaseOptions): Promise<LeaseResponse>;
124
+ accessToken(): Promise<string>;
92
125
  /**
93
126
  * Fetches Daywalker's public JWKS (GET /.well-known/jwks.json) for offline lease verification.
94
127
  * Uses the configured `fetch`, sends no credentials, never follows redirects and times out after `timeoutMs`.
@@ -104,7 +137,8 @@ export declare class Daywalker {
104
137
  keys: PublicJwk[];
105
138
  }>;
106
139
  /**
107
- * Check configuration and verify signature readiness.
140
+ * Check configuration and verify signature readiness. Never provisions or registers a key: with a
141
+ * `keyStore` it only reads what is stored and reports what first use would need.
108
142
  */
109
143
  selfCheck(): Promise<SelfCheckResult>;
110
144
  }
@@ -113,3 +147,4 @@ export declare class Daywalker {
113
147
  */
114
148
  export declare function createDaywalker(config?: DaywalkerConfig): Daywalker;
115
149
  export declare const daywalker: Daywalker & (() => Daywalker);
150
+ export {};