@shelbyb/daywalker-sdk 0.6.0 → 0.8.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/config.d.ts CHANGED
@@ -12,7 +12,16 @@ export interface ResolvedConfig {
12
12
  domain: string | null;
13
13
  domainSource: string | null;
14
14
  daywalkerPublicKey: string | PublicJwk | PublicJwks | null;
15
+ /** "auto": offline verification keys come from `${authUrl}/.well-known/jwks.json` (env DAYWALKER_JWKS). */
16
+ jwks: "auto" | null;
15
17
  leaseIssuer: string | null;
18
+ /** Ephemeral signing keys, when enabled: their lifetime and this process's key slot (null = random). */
19
+ ephemeralKey: {
20
+ ttlSeconds: number;
21
+ instanceId: string | null;
22
+ } | null;
23
+ /** Origin clients use to reach this service, for DPoP `htu` checks (env DAYWALKER_PUBLIC_ORIGIN). */
24
+ publicOrigin: string | null;
16
25
  authUrl: string;
17
26
  cacheTtlMs: number;
18
27
  negativeCacheTtlMs: number;
@@ -22,6 +31,8 @@ export interface ResolvedConfig {
22
31
  timeoutMs: number;
23
32
  problems: string[];
24
33
  }
34
+ /** Default lifetime of an ephemeral signing key (6 hours); it is replaced when a quarter of that is left. */
35
+ export declare const DEFAULT_EPHEMERAL_KEY_TTL_SECONDS = 21600;
25
36
  /** Default per-attempt timeout for requests to Daywalker Auth. */
26
37
  export declare const DEFAULT_TIMEOUT_MS = 5000;
27
38
  /**
@@ -31,7 +42,7 @@ export declare const DEFAULT_TIMEOUT_MS = 5000;
31
42
  export declare function publicHostname(value: string | undefined | null): string | null;
32
43
  /**
33
44
  * Throws unless `value` is an https: URL (http: is allowed only for localhost / 127.0.0.1 / [::1]),
34
- * so API keys, SDK keys and write keys never travel in cleartext.
45
+ * so API keys, access tokens, SDK keys and write keys never travel in cleartext.
35
46
  */
36
47
  export declare function assertSecureUrl(name: string, value: string): void;
37
48
  /**
package/dist/config.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** Default lifetime of an ephemeral signing key (6 hours); it is replaced when a quarter of that is left. */
2
+ export const DEFAULT_EPHEMERAL_KEY_TTL_SECONDS = 21_600;
1
3
  /** Default per-attempt timeout for requests to Daywalker Auth. */
2
4
  export const DEFAULT_TIMEOUT_MS = 5_000;
3
5
  const URL_ENV_VARS = [
@@ -50,7 +52,7 @@ export function publicHostname(value) {
50
52
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
51
53
  /**
52
54
  * Throws unless `value` is an https: URL (http: is allowed only for localhost / 127.0.0.1 / [::1]),
53
- * so API keys, SDK keys and write keys never travel in cleartext.
55
+ * so API keys, access tokens, SDK keys and write keys never travel in cleartext.
54
56
  */
55
57
  export function assertSecureUrl(name, value) {
56
58
  let url;
@@ -60,6 +62,9 @@ export function assertSecureUrl(name, value) {
60
62
  catch {
61
63
  throw new Error(`${name} must be an absolute https:// URL (got "${value}")`);
62
64
  }
65
+ if (url.username || url.password) {
66
+ throw new Error(`${name} must not contain credentials`);
67
+ }
63
68
  if (url.protocol === "https:")
64
69
  return;
65
70
  if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))
@@ -81,7 +86,7 @@ export function resolveConfig(options = {}) {
81
86
  const serviceSlugs = parseServiceSlugs(options.serviceSlugs ?? options.serviceSlug ?? env.DAYWALKER_SERVICE_SLUG);
82
87
  const serviceSlug = serviceSlugs[0] ?? null;
83
88
  if (!apiKey)
84
- problems.push("DAYWALKER_API_KEY is not set");
89
+ problems.push("DAYWALKER_API_KEY is not set: it is the client credential for the OAuth client credentials grant");
85
90
  const signingKey = options.signingKey ?? env.DAYWALKER_SIGNING_KEY ?? null;
86
91
  const previousSigningKey = options.previousSigningKey ?? env.DAYWALKER_SIGNING_KEY_PREVIOUS ?? null;
87
92
  const keyId = (options.keyId ?? env.DAYWALKER_KEY_ID ?? "").trim() || null;
@@ -108,7 +113,31 @@ export function resolveConfig(options = {}) {
108
113
  }
109
114
  }
110
115
  const daywalkerPublicKey = options.daywalkerPublicKey ?? (env.DAYWALKER_PUBLIC_KEY ? env.DAYWALKER_PUBLIC_KEY.trim() : null);
116
+ const rawJwks = String(options.jwks ?? env.DAYWALKER_JWKS ?? "").trim().toLowerCase();
117
+ const jwks = rawJwks === "auto" ? "auto" : null;
118
+ if (rawJwks && rawJwks !== "auto" && rawJwks !== "off")
119
+ problems.push(`DAYWALKER_JWKS must be "auto" or "off" (got "${rawJwks.slice(0, 40)}"); JWKS fetching is off`);
111
120
  const leaseIssuer = (options.leaseIssuer ?? env.DAYWALKER_LEASE_ISSUER ?? "").trim() || null;
121
+ let ephemeralKey = null;
122
+ const ephemeralOption = options.ephemeralKey ?? (/^(1|true|yes)$/i.test(env.DAYWALKER_EPHEMERAL_KEY ?? "") ? true : undefined);
123
+ if (ephemeralOption) {
124
+ const o = typeof ephemeralOption === "object" ? ephemeralOption : {};
125
+ const ttlSeconds = o.ttlSeconds ?? (env.DAYWALKER_KEY_TTL_SECONDS ? Number(env.DAYWALKER_KEY_TTL_SECONDS) : DEFAULT_EPHEMERAL_KEY_TTL_SECONDS);
126
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 3_600 || ttlSeconds > 2_592_000) {
127
+ throw new Error(`ephemeralKey.ttlSeconds must be a whole number of seconds from 3600 to 2592000 (got ${ttlSeconds})`);
128
+ }
129
+ const instanceId = (o.instanceId ?? env.DAYWALKER_INSTANCE_ID ?? "").trim() || null;
130
+ if (instanceId !== null && !/^[A-Za-z0-9_.-]{1,64}$/.test(instanceId)) {
131
+ throw new Error("ephemeralKey.instanceId must be 1-64 of A-Z a-z 0-9 . _ -");
132
+ }
133
+ ephemeralKey = { ttlSeconds, instanceId };
134
+ }
135
+ const rawPublicOrigin = (options.publicOrigin ?? env.DAYWALKER_PUBLIC_ORIGIN ?? "").trim();
136
+ let publicOrigin = null;
137
+ if (rawPublicOrigin) {
138
+ assertSecureUrl("publicOrigin", rawPublicOrigin);
139
+ publicOrigin = new URL(rawPublicOrigin).origin;
140
+ }
112
141
  const authUrl = (options.authUrl ?? env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
113
142
  assertSecureUrl("authUrl", authUrl);
114
143
  const cacheTtlMs = options.cacheTtlMs ?? 60_000;
@@ -134,7 +163,10 @@ export function resolveConfig(options = {}) {
134
163
  domain,
135
164
  domainSource,
136
165
  daywalkerPublicKey,
166
+ jwks,
137
167
  leaseIssuer,
168
+ ephemeralKey,
169
+ publicOrigin,
138
170
  authUrl,
139
171
  cacheTtlMs,
140
172
  negativeCacheTtlMs,
@@ -1,3 +1,4 @@
1
+ import { webcrypto } from "./webcrypto.js";
1
2
  import { bytesToBase64, stringToUtf8 } from "./encoding.js";
2
3
  /** Default upper bound for body payload digest calculation (10MB). */
3
4
  export const DEFAULT_MAX_BODY_SIZE_BYTES = 10 * 1024 * 1024;
@@ -15,7 +16,7 @@ export async function computeContentDigest(body, maxSizeBytes = DEFAULT_MAX_BODY
15
16
  if (data.byteLength > maxSizeBytes) {
16
17
  throw new Error(`Request body size (${data.byteLength} bytes) exceeds maximum allowed limit (${maxSizeBytes} bytes)`);
17
18
  }
18
- const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
19
+ const hashBuffer = await webcrypto.digest("SHA-256", data);
19
20
  const hashBase64 = bytesToBase64(new Uint8Array(hashBuffer));
20
21
  return `sha-256=:${hashBase64}:`;
21
22
  }
@@ -24,7 +25,7 @@ export async function computeContentDigest(body, maxSizeBytes = DEFAULT_MAX_BODY
24
25
  */
25
26
  export async function sha256Base64(data) {
26
27
  const bytes = typeof data === "string" ? stringToUtf8(data) : data;
27
- const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", bytes);
28
+ const hashBuffer = await webcrypto.digest("SHA-256", bytes);
28
29
  return bytesToBase64(new Uint8Array(hashBuffer));
29
30
  }
30
31
  /**
@@ -1,3 +1,4 @@
1
+ import { SignatureReplayCache, type ReplayStore } from "./signatures.js";
1
2
  /**
2
3
  * DPoP (RFC 9449) proof-of-possession. Daywalker binds every access and refresh token to a client key and
3
4
  * requires a fresh proof on each token request, refresh and validate call.
@@ -10,7 +11,10 @@ export interface DpopKey {
10
11
  x: string;
11
12
  };
12
13
  }
13
- /** A new in-memory Ed25519 DPoP key. Tokens are bound to it, so keep it for the life of the session. */
14
+ /**
15
+ * A new in-memory Ed25519 DPoP key. Tokens are bound to it, so keep it for the life of the session. The private
16
+ * key is non-extractable: nothing (including this SDK) can export it, so a bound token can't be moved elsewhere.
17
+ */
14
18
  export declare function generateDpopKey(): Promise<DpopKey>;
15
19
  /**
16
20
  * Builds a DPoP proof JWT for one request. `htu` is the URL without query or fragment. Pass the access token
@@ -21,3 +25,36 @@ export declare function createDpopProof(key: DpopKey, params: {
21
25
  url: string;
22
26
  accessToken?: string;
23
27
  }): Promise<string>;
28
+ /** Longest accepted DPoP proof (bytes). A proof is a small JWT; anything bigger is refused unparsed. */
29
+ export declare const MAX_DPOP_PROOF_LENGTH = 4096;
30
+ /** How old (or how far in the future) a proof's `iat` may be. */
31
+ export declare const DPOP_MAX_AGE_SECONDS = 60;
32
+ /** Proof `jti`s seen recently, per process. Pass a shared `ReplayStore` when several instances verify. */
33
+ export declare const globalDpopReplayCache: SignatureReplayCache;
34
+ export interface VerifyDpopProofParams {
35
+ /** The `DPoP` request header. */
36
+ proof: string;
37
+ /** The request method. */
38
+ method: string;
39
+ /** The URL the client called (its scheme, host and path must match the proof's `htu`). */
40
+ url: string;
41
+ /** The access token presented with `Authorization: DPoP`; the proof's `ath` must be its SHA-256. */
42
+ accessToken: string;
43
+ /** The token's `cnf.jkt`: the RFC 7638 thumbprint of the key the token is bound to. */
44
+ expectedJkt: string;
45
+ currentTime?: number;
46
+ /** At most `DPOP_MAX_AGE_SECONDS`. */
47
+ maxAgeSeconds?: number;
48
+ replayStore?: ReplayStore;
49
+ }
50
+ /**
51
+ * Verifies the DPoP proof that must accompany a DPoP-bound (`cnf.jkt`) access token (RFC 9449 §4.3 and
52
+ * §7.1): `typ: dpop+jwt`, EdDSA or ES256 signed by the public key in its `jwk` header (whose thumbprint must
53
+ * equal the token's `cnf.jkt`), `htm` / `htu` matching this request, a recent `iat`, an `ath` equal to the
54
+ * SHA-256 of the access token, and a `jti` never seen before. Without a valid proof a bound token is useless
55
+ * to whoever copied it.
56
+ */
57
+ export declare function verifyDpopProof(params: VerifyDpopProofParams): Promise<{
58
+ valid: boolean;
59
+ error?: string;
60
+ }>;
@@ -1,9 +1,14 @@
1
- import { bytesToBase64Url, stringToUtf8 } from "./encoding.js";
1
+ import { webcrypto } from "./webcrypto.js";
2
+ import { base64UrlToBytesStrict, bytesToBase64Url, stringToUtf8, timingSafeEqualString, utf8ToString } from "./encoding.js";
3
+ import { SignatureReplayCache } from "./signatures.js";
2
4
  import { sha256Base64Url } from "./digest.js";
3
- /** A new in-memory Ed25519 DPoP key. Tokens are bound to it, so keep it for the life of the session. */
5
+ /**
6
+ * A new in-memory Ed25519 DPoP key. Tokens are bound to it, so keep it for the life of the session. The private
7
+ * key is non-extractable: nothing (including this SDK) can export it, so a bound token can't be moved elsewhere.
8
+ */
4
9
  export async function generateDpopKey() {
5
- const pair = (await globalThis.crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]));
6
- const jwk = (await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey));
10
+ const pair = (await webcrypto.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]));
11
+ const jwk = (await webcrypto.exportKey("jwk", pair.publicKey));
7
12
  return { privateKey: pair.privateKey, publicJwk: { kty: "OKP", crv: "Ed25519", x: jwk.x } };
8
13
  }
9
14
  const b64u = (value) => bytesToBase64Url(stringToUtf8(JSON.stringify(value)));
@@ -15,7 +20,7 @@ export async function createDpopProof(key, params) {
15
20
  const u = new URL(params.url);
16
21
  const header = { typ: "dpop+jwt", alg: "EdDSA", jwk: key.publicJwk };
17
22
  const claims = {
18
- jti: globalThis.crypto.randomUUID(),
23
+ jti: webcrypto.randomUUID(),
19
24
  htm: params.method.toUpperCase(),
20
25
  htu: `${u.origin}${u.pathname}`,
21
26
  iat: Math.floor(Date.now() / 1000),
@@ -23,6 +28,119 @@ export async function createDpopProof(key, params) {
23
28
  if (params.accessToken)
24
29
  claims.ath = await sha256Base64Url(params.accessToken);
25
30
  const signingInput = `${b64u(header)}.${b64u(claims)}`;
26
- const sig = new Uint8Array(await globalThis.crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, stringToUtf8(signingInput)));
31
+ const sig = new Uint8Array(await webcrypto.sign({ name: "Ed25519" }, key.privateKey, stringToUtf8(signingInput)));
27
32
  return `${signingInput}.${bytesToBase64Url(sig)}`;
28
33
  }
34
+ /** Longest accepted DPoP proof (bytes). A proof is a small JWT; anything bigger is refused unparsed. */
35
+ export const MAX_DPOP_PROOF_LENGTH = 4096;
36
+ /** How old (or how far in the future) a proof's `iat` may be. */
37
+ export const DPOP_MAX_AGE_SECONDS = 60;
38
+ /** Proof `jti`s seen recently, per process. Pass a shared `ReplayStore` when several instances verify. */
39
+ export const globalDpopReplayCache = new SignatureReplayCache();
40
+ /** Normalized `htu`: scheme, host and path, no query or fragment (RFC 9449 §4.3). */
41
+ function htuOf(value) {
42
+ try {
43
+ const u = new URL(value);
44
+ if (u.protocol !== "https:" && u.protocol !== "http:")
45
+ return null;
46
+ return `${u.protocol}//${u.host}${u.pathname}`;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ const B64URL = /^[A-Za-z0-9_-]+$/;
53
+ /** RFC 7638 thumbprint of an Ed25519 or P-256 public JWK, or null for any other (or a private) key. */
54
+ async function proofKey(jwk) {
55
+ if (!jwk || typeof jwk !== "object" || Array.isArray(jwk))
56
+ return null;
57
+ const k = jwk;
58
+ if ("d" in k || "p" in k || "q" in k)
59
+ return null; // never a private key
60
+ if (k.kty === "OKP" && k.crv === "Ed25519" && typeof k.x === "string" && B64URL.test(k.x)) {
61
+ const key = await webcrypto.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: k.x }, { name: "Ed25519" }, false, ["verify"]);
62
+ return { key, jkt: await sha256Base64Url(JSON.stringify({ crv: "Ed25519", kty: "OKP", x: k.x })), alg: "EdDSA" };
63
+ }
64
+ if (k.kty === "EC" && k.crv === "P-256" && typeof k.x === "string" && typeof k.y === "string" && B64URL.test(k.x) && B64URL.test(k.y)) {
65
+ const key = await webcrypto.importKey("jwk", { kty: "EC", crv: "P-256", x: k.x, y: k.y }, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
66
+ return { key, jkt: await sha256Base64Url(JSON.stringify({ crv: "P-256", kty: "EC", x: k.x, y: k.y })), alg: "ES256" };
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Verifies the DPoP proof that must accompany a DPoP-bound (`cnf.jkt`) access token (RFC 9449 §4.3 and
72
+ * §7.1): `typ: dpop+jwt`, EdDSA or ES256 signed by the public key in its `jwk` header (whose thumbprint must
73
+ * equal the token's `cnf.jkt`), `htm` / `htu` matching this request, a recent `iat`, an `ath` equal to the
74
+ * SHA-256 of the access token, and a `jti` never seen before. Without a valid proof a bound token is useless
75
+ * to whoever copied it.
76
+ */
77
+ export async function verifyDpopProof(params) {
78
+ try {
79
+ const { proof } = params;
80
+ if (typeof proof !== "string" || proof.length === 0 || proof.length > MAX_DPOP_PROOF_LENGTH) {
81
+ return { valid: false, error: "Missing or oversized DPoP proof" };
82
+ }
83
+ const parts = proof.split(".");
84
+ if (parts.length !== 3)
85
+ return { valid: false, error: "DPoP proof is not a JWT" };
86
+ let header;
87
+ let claims;
88
+ let signature;
89
+ try {
90
+ header = JSON.parse(utf8ToString(base64UrlToBytesStrict(parts[0])));
91
+ claims = JSON.parse(utf8ToString(base64UrlToBytesStrict(parts[1])));
92
+ signature = base64UrlToBytesStrict(parts[2]);
93
+ }
94
+ catch {
95
+ return { valid: false, error: "DPoP proof segments must be canonical base64url JSON" };
96
+ }
97
+ if (!header || typeof header !== "object" || Array.isArray(header) || !claims || typeof claims !== "object" || Array.isArray(claims)) {
98
+ return { valid: false, error: "DPoP proof header and claims must be JSON objects" };
99
+ }
100
+ if (header.typ !== "dpop+jwt")
101
+ return { valid: false, error: "DPoP proof typ must be dpop+jwt" };
102
+ if (header.crit !== undefined)
103
+ return { valid: false, error: "Unsupported DPoP crit header" };
104
+ const key = await proofKey(header.jwk);
105
+ if (!key)
106
+ return { valid: false, error: "DPoP proof jwk must be an Ed25519 or P-256 public key" };
107
+ if (header.alg !== key.alg)
108
+ return { valid: false, error: `DPoP proof alg must be ${key.alg} for this key` };
109
+ const signed = stringToUtf8(`${parts[0]}.${parts[1]}`);
110
+ const ok = key.alg === "EdDSA"
111
+ ? await webcrypto.verify({ name: "Ed25519" }, key.key, signature, signed)
112
+ : await webcrypto.verify({ name: "ECDSA", hash: "SHA-256" }, key.key, signature, signed);
113
+ if (!ok)
114
+ return { valid: false, error: "DPoP proof signature verification failed" };
115
+ if (typeof params.expectedJkt !== "string" || !timingSafeEqualString(key.jkt, params.expectedJkt)) {
116
+ return { valid: false, error: "DPoP proof key does not match the token's cnf.jkt" };
117
+ }
118
+ if (typeof claims.htm !== "string" || claims.htm !== params.method.toUpperCase()) {
119
+ return { valid: false, error: "DPoP proof htm does not match the request method" };
120
+ }
121
+ const expectedHtu = htuOf(params.url);
122
+ if (!expectedHtu || typeof claims.htu !== "string" || htuOf(claims.htu) !== expectedHtu) {
123
+ return { valid: false, error: "DPoP proof htu does not match the request URL" };
124
+ }
125
+ const now = params.currentTime ?? Math.floor(Date.now() / 1000);
126
+ const maxAge = Math.max(1, Math.min(params.maxAgeSeconds ?? DPOP_MAX_AGE_SECONDS, DPOP_MAX_AGE_SECONDS));
127
+ if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat) || Math.abs(now - claims.iat) > maxAge) {
128
+ return { valid: false, error: "DPoP proof iat is missing or outside the accepted window" };
129
+ }
130
+ const ath = await sha256Base64Url(params.accessToken);
131
+ if (typeof claims.ath !== "string" || !timingSafeEqualString(claims.ath, ath)) {
132
+ return { valid: false, error: "DPoP proof ath does not match the access token" };
133
+ }
134
+ if (typeof claims.jti !== "string" || claims.jti.length === 0 || claims.jti.length > 256) {
135
+ return { valid: false, error: "DPoP proof has no usable jti" };
136
+ }
137
+ const store = params.replayStore ?? globalDpopReplayCache;
138
+ if (!(await store.record(`dpop:${key.jkt}:${claims.jti}`, 2 * maxAge * 1000))) {
139
+ return { valid: false, error: "DPoP proof replayed (jti already used)" };
140
+ }
141
+ return { valid: true };
142
+ }
143
+ catch (err) {
144
+ return { valid: false, error: `DPoP proof verification error: ${err.message}` };
145
+ }
146
+ }
@@ -70,6 +70,8 @@ export function bytesToHex(bytes) {
70
70
  }
71
71
  export function hexToBytes(hex) {
72
72
  const clean = hex.trim().replace(/^0x/i, "");
73
+ if (clean.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(clean))
74
+ throw new Error("Invalid hex string");
73
75
  const bytes = new Uint8Array(clean.length / 2);
74
76
  for (let i = 0; i < clean.length; i += 2) {
75
77
  bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
@@ -1,7 +1,9 @@
1
1
  export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
2
2
  export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
3
- export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
4
- export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, } from "./signatures.js";
3
+ export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, importSigningKey, } from "./keys.js";
4
+ export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, SERVICE_SLUG_PATTERN, API_KEY_PATTERN, } from "./signatures.js";
5
+ export { registerPublicKey, listSigningKeys, revokeSigningKey, keySlotFor, randomInstanceId, INSTANCE_ID_PATTERN, MIN_KEY_TTL_SECONDS, MAX_KEY_TTL_SECONDS, } from "./key-api.js";
6
+ export type { SigningKeyRecord, KeyApiResult } from "./key-api.js";
5
7
  export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, ReplayStore, } from "./signatures.js";
6
- export { generateDpopKey, createDpopProof } from "./dpop.js";
7
- export type { DpopKey } from "./dpop.js";
8
+ export { generateDpopKey, createDpopProof, verifyDpopProof, globalDpopReplayCache, MAX_DPOP_PROOF_LENGTH, DPOP_MAX_AGE_SECONDS } from "./dpop.js";
9
+ export type { DpopKey, VerifyDpopProofParams } from "./dpop.js";
@@ -1,5 +1,6 @@
1
1
  export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
2
2
  export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
3
- export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
4
- export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, } from "./signatures.js";
5
- export { generateDpopKey, createDpopProof } from "./dpop.js";
3
+ export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, importSigningKey, } from "./keys.js";
4
+ export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, SERVICE_SLUG_PATTERN, API_KEY_PATTERN, } from "./signatures.js";
5
+ export { registerPublicKey, listSigningKeys, revokeSigningKey, keySlotFor, randomInstanceId, INSTANCE_ID_PATTERN, MIN_KEY_TTL_SECONDS, MAX_KEY_TTL_SECONDS, } from "./key-api.js";
6
+ export { generateDpopKey, createDpopProof, verifyDpopProof, globalDpopReplayCache, MAX_DPOP_PROOF_LENGTH, DPOP_MAX_AGE_SECONDS } from "./dpop.js";
@@ -0,0 +1,69 @@
1
+ import type { PublicJwk } from "../types.js";
2
+ /** Daywalker's `instanceId` format: one key slot per (domain, instanceId). */
3
+ export declare const INSTANCE_ID_PATTERN: RegExp;
4
+ /** Key lifetime bounds Daywalker accepts for `ttlSeconds` (1 hour to 30 days). */
5
+ export declare const MIN_KEY_TTL_SECONDS = 3600;
6
+ export declare const MAX_KEY_TTL_SECONDS = 2592000;
7
+ /**
8
+ * The instance slot the SDK registers a key in: one slot per key (`dw-<keyId>`), so registering a key never
9
+ * demotes another one (Daywalker moves the previous key of the same slot to ROTATING, then revokes it).
10
+ */
11
+ export declare function keySlotFor(keyId: string): string;
12
+ /** A signing key as Daywalker reports it. */
13
+ export interface SigningKeyRecord {
14
+ id?: string;
15
+ keyId: string;
16
+ domain: string;
17
+ status: "ACTIVE" | "ROTATING" | "REVOKED";
18
+ instanceId: string | null;
19
+ /** Unix seconds, or null when the key doesn't expire. */
20
+ expiresAt: number | null;
21
+ createdAt: number | null;
22
+ lastUsedAt: number | null;
23
+ }
24
+ export interface KeyApiResult {
25
+ ok: boolean;
26
+ status: number;
27
+ /** Daywalker's error code (`KEY_LIMIT_REACHED`, `KEY_ID_CONFLICT`, `domain_not_allowed`, …). */
28
+ code?: string;
29
+ error?: string;
30
+ }
31
+ interface KeyApiBase {
32
+ authUrl?: string;
33
+ apiKey: string;
34
+ fetchFn?: typeof fetch;
35
+ }
36
+ /**
37
+ * Register public key directly with Daywalker Auth via POST /api/v1/keys/register (Push Architecture).
38
+ * Re-registering the same key is idempotent (200). `instanceId` puts the key in a slot: registering a new key
39
+ * in an occupied slot moves the old one to ROTATING for 10 minutes, then revokes it; without `instanceId`
40
+ * the same happens to every other slot-less key of the domain, so the SDK always passes one (`keySlotFor`).
41
+ * `ttlSeconds` (1 hour to 30 days) or `expiresAt` make the key expire. Errors carry Daywalker's `code`
42
+ * (`KEY_LIMIT_REACHED`: 20 live keys per domain; `KEY_ID_CONFLICT`; `domain_not_allowed`).
43
+ */
44
+ export declare function registerPublicKey(params: KeyApiBase & {
45
+ serviceSlug?: string;
46
+ domain?: string | null;
47
+ publicJwk: PublicJwk;
48
+ instanceId?: string;
49
+ ttlSeconds?: number;
50
+ expiresAt?: Date;
51
+ }): Promise<KeyApiResult & {
52
+ key?: SigningKeyRecord;
53
+ }>;
54
+ /** Lists the signing keys registered for `domain` (GET /api/v1/keys), newest first. */
55
+ export declare function listSigningKeys(params: KeyApiBase & {
56
+ domain: string;
57
+ }): Promise<KeyApiResult & {
58
+ keys?: SigningKeyRecord[];
59
+ }>;
60
+ /**
61
+ * Revokes a signing key now (DELETE /api/v1/keys/{keyId}). Daywalker also ends every OAuth session that was
62
+ * authenticated with it. Revoking a key that is already revoked succeeds; an unknown key is 404 `key_not_found`.
63
+ */
64
+ export declare function revokeSigningKey(params: KeyApiBase & {
65
+ keyId: string;
66
+ }): Promise<KeyApiResult>;
67
+ /** A random instance slot for a process (`dw-i-<random>`), for keys that live only as long as it does. */
68
+ export declare function randomInstanceId(): string;
69
+ export {};