@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/README.md +191 -31
- package/dist/cli.js +111 -26
- package/dist/client.d.ts +81 -46
- package/dist/client.js +960 -187
- package/dist/config.d.ts +12 -1
- package/dist/config.js +34 -4
- package/dist/cookies.d.ts +37 -0
- package/dist/cookies.js +58 -0
- package/dist/crypto/digest.js +3 -2
- package/dist/crypto/dpop.d.ts +60 -0
- package/dist/crypto/dpop.js +146 -0
- package/dist/crypto/encoding.js +2 -0
- package/dist/crypto/index.d.ts +6 -2
- package/dist/crypto/index.js +4 -2
- package/dist/crypto/key-api.d.ts +69 -0
- package/dist/crypto/key-api.js +217 -0
- package/dist/crypto/keys.d.ts +5 -0
- package/dist/crypto/keys.js +25 -15
- package/dist/crypto/signatures.d.ts +15 -26
- package/dist/crypto/signatures.js +47 -86
- package/dist/crypto/webcrypto.d.ts +38 -0
- package/dist/crypto/webcrypto.js +25 -0
- package/dist/express.d.ts +2 -1
- package/dist/express.js +12 -32
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/keystore/file.d.ts +14 -0
- package/dist/keystore/file.js +77 -0
- package/dist/keystore/index.d.ts +18 -0
- package/dist/keystore/index.js +71 -0
- package/dist/middleware.d.ts +27 -3
- package/dist/middleware.js +75 -22
- package/dist/next.d.ts +5 -3
- package/dist/next.js +38 -42
- package/dist/types.d.ts +125 -20
- package/package.json +11 -1
- package/dist/signing.d.ts +0 -3
- package/dist/signing.js +0 -2
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
|
|
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>
|
|
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
|
-
|
|
185
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
279
|
-
fail("rotate needs DAYWALKER_API_KEY
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
342
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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 `
|
|
42
|
-
*
|
|
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
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
|
|
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:
|
|
69
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
-
|
|
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 {};
|