@shelbyb/daywalker-sdk 0.1.0 → 0.1.2
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 +164 -44
- package/assets/logo-512.png +0 -0
- package/dist/cli.js +162 -140
- package/dist/client.d.ts +48 -66
- package/dist/client.js +354 -121
- package/dist/config.d.ts +9 -34
- package/dist/config.js +45 -32
- package/dist/crypto/digest.d.ts +17 -0
- package/dist/crypto/digest.js +36 -0
- package/dist/crypto/encoding.d.ts +29 -0
- package/dist/crypto/encoding.js +117 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +4 -0
- package/dist/crypto/keys.d.ts +25 -0
- package/dist/crypto/keys.js +236 -0
- package/dist/crypto/signatures.d.ts +83 -0
- package/dist/crypto/signatures.js +331 -0
- package/dist/express.d.ts +12 -5
- package/dist/express.js +56 -8
- package/dist/index.d.ts +7 -6
- package/dist/index.js +4 -3
- package/dist/middleware.d.ts +20 -0
- package/dist/middleware.js +72 -0
- package/dist/next.d.ts +60 -7
- package/dist/next.js +184 -9
- package/dist/signing.d.ts +2 -52
- package/dist/signing.js +1 -50
- package/dist/types.d.ts +122 -0
- package/dist/types.js +46 -0
- package/package.json +16 -7
package/dist/config.d.ts
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
* Everything a service needs, read from its environment:
|
|
4
|
-
*
|
|
5
|
-
* DAYWALKER_API_KEY the service's Daywalker API token (srv_live_…) required
|
|
6
|
-
* DAYWALKER_SERVICE_SLUG the service it validates against (e.g. "sonalyze") required
|
|
7
|
-
* DAYWALKER_SIGNING_KEY Ed25519 private key from `daywalker init` required for allowlisted tokens
|
|
8
|
-
* DAYWALKER_SIGNING_KEY_PREVIOUS old key, still published during a rotation optional
|
|
9
|
-
* DAYWALKER_DOMAIN public hostname that serves /.well-known/daywalker-keys.json
|
|
10
|
-
* (defaults to the hostname of APP_URL, NEXTAUTH_URL, …)
|
|
11
|
-
* DAYWALKER_AUTH_URL defaults to https://auth.daywalker.dev
|
|
12
|
-
*/
|
|
13
|
-
export interface DaywalkerOptions {
|
|
14
|
-
apiKey?: string;
|
|
15
|
-
serviceSlug?: string;
|
|
16
|
-
/** Private key (PEM or base64 DER) or an already loaded key. */
|
|
17
|
-
signingKey?: string | SigningKey | null;
|
|
18
|
-
previousSigningKey?: string | SigningKey | null;
|
|
19
|
-
domain?: string;
|
|
20
|
-
authUrl?: string;
|
|
21
|
-
/** Override for tests or custom runtimes. */
|
|
22
|
-
fetch?: typeof fetch;
|
|
23
|
-
/** Request timeout in ms (default 5000). */
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
/** How long a successful validation is cached, in ms (default 60000, 0 disables). */
|
|
26
|
-
cacheTtlMs?: number;
|
|
27
|
-
env?: Record<string, string | undefined>;
|
|
28
|
-
}
|
|
1
|
+
import type { DaywalkerConfig, PublicJwk, SigningKey } from "./types.js";
|
|
29
2
|
export interface ResolvedConfig {
|
|
30
3
|
apiKey: string | null;
|
|
4
|
+
signingKey: string | SigningKey | null;
|
|
5
|
+
previousSigningKey: string | SigningKey | null;
|
|
6
|
+
keyId: string | null;
|
|
31
7
|
serviceSlug: string | null;
|
|
32
|
-
signingKey: SigningKey | null;
|
|
33
|
-
previousSigningKey: SigningKey | null;
|
|
34
8
|
domain: string | null;
|
|
35
|
-
/** Where the domain came from, for diagnostics. */
|
|
36
9
|
domainSource: string | null;
|
|
10
|
+
daywalkerPublicKey: string | PublicJwk | null;
|
|
37
11
|
authUrl: string;
|
|
12
|
+
cacheTtlMs: number;
|
|
13
|
+
maxRetries: number;
|
|
38
14
|
problems: string[];
|
|
39
15
|
}
|
|
40
16
|
/**
|
|
41
17
|
* A public hostname from a URL or bare host, or null for anything that can't be fetched over
|
|
42
|
-
* the internet (localhost, 0.0.0.0, IP addresses, single labels).
|
|
43
|
-
* bind address (http://0.0.0.0:3000) as the request origin, which must never be used.
|
|
18
|
+
* the internet (localhost, 0.0.0.0, IP addresses, single labels).
|
|
44
19
|
*/
|
|
45
20
|
export declare function publicHostname(value: string | undefined | null): string | null;
|
|
46
|
-
export declare function resolveConfig(options?:
|
|
21
|
+
export declare function resolveConfig(options?: DaywalkerConfig): ResolvedConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import net from "node:net";
|
|
2
|
-
import { loadSigningKey } from "./signing.js";
|
|
3
|
-
// Platform variables that commonly hold the app's public URL or hostname, in priority order
|
|
4
1
|
const URL_ENV_VARS = [
|
|
5
2
|
"APP_URL",
|
|
6
3
|
"NEXTAUTH_URL",
|
|
@@ -8,22 +5,34 @@ const URL_ENV_VARS = [
|
|
|
8
5
|
"BETTER_AUTH_URL",
|
|
9
6
|
"PUBLIC_URL",
|
|
10
7
|
"SITE_URL",
|
|
11
|
-
"URL",
|
|
8
|
+
"URL",
|
|
12
9
|
"VERCEL_PROJECT_PRODUCTION_URL",
|
|
13
10
|
"RENDER_EXTERNAL_HOSTNAME",
|
|
14
11
|
"RAILWAY_PUBLIC_DOMAIN",
|
|
15
12
|
"VERCEL_URL",
|
|
16
13
|
];
|
|
17
14
|
const DOMAIN_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
|
|
15
|
+
function isIpAddress(host) {
|
|
16
|
+
if (host.length > 253)
|
|
17
|
+
return false;
|
|
18
|
+
// IPv4
|
|
19
|
+
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(host))
|
|
20
|
+
return true;
|
|
21
|
+
// IPv6
|
|
22
|
+
if (host.includes(":"))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
18
26
|
/**
|
|
19
27
|
* A public hostname from a URL or bare host, or null for anything that can't be fetched over
|
|
20
|
-
* the internet (localhost, 0.0.0.0, IP addresses, single labels).
|
|
21
|
-
* bind address (http://0.0.0.0:3000) as the request origin, which must never be used.
|
|
28
|
+
* the internet (localhost, 0.0.0.0, IP addresses, single labels).
|
|
22
29
|
*/
|
|
23
30
|
export function publicHostname(value) {
|
|
24
31
|
if (!value || !value.trim())
|
|
25
32
|
return null;
|
|
26
33
|
let host = value.trim().toLowerCase();
|
|
34
|
+
if (host.length > 253)
|
|
35
|
+
return null;
|
|
27
36
|
try {
|
|
28
37
|
host = new URL(/^[a-z][a-z0-9+.-]*:\/\//.test(host) ? host : `https://${host}`).hostname;
|
|
29
38
|
}
|
|
@@ -31,26 +40,13 @@ export function publicHostname(value) {
|
|
|
31
40
|
return null;
|
|
32
41
|
}
|
|
33
42
|
host = host.replace(/\.$/, "");
|
|
34
|
-
if (
|
|
43
|
+
if (host.length > 253 || isIpAddress(host.replace(/^\[|\]$/g, "")) || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
|
|
35
44
|
return null;
|
|
36
45
|
}
|
|
37
46
|
return DOMAIN_PATTERN.test(host) ? host : null;
|
|
38
47
|
}
|
|
39
|
-
function keyFrom(value, name, problems) {
|
|
40
|
-
if (!value)
|
|
41
|
-
return null;
|
|
42
|
-
if (typeof value !== "string")
|
|
43
|
-
return value;
|
|
44
|
-
try {
|
|
45
|
-
return loadSigningKey(value);
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
problems.push(`${name} could not be loaded: ${err.message}`);
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
48
|
export function resolveConfig(options = {}) {
|
|
53
|
-
const env = options.env ?? process.env;
|
|
49
|
+
const env = options.env ?? (typeof process !== "undefined" && process.env ? process.env : {});
|
|
54
50
|
const problems = [];
|
|
55
51
|
const apiKey = (options.apiKey ?? env.DAYWALKER_API_KEY ?? "").trim() || null;
|
|
56
52
|
const serviceSlug = (options.serviceSlug ?? env.DAYWALKER_SERVICE_SLUG ?? "").trim().toLowerCase() || null;
|
|
@@ -58,8 +54,9 @@ export function resolveConfig(options = {}) {
|
|
|
58
54
|
problems.push("DAYWALKER_API_KEY is not set");
|
|
59
55
|
if (!serviceSlug)
|
|
60
56
|
problems.push("DAYWALKER_SERVICE_SLUG is not set");
|
|
61
|
-
const signingKey =
|
|
62
|
-
const previousSigningKey =
|
|
57
|
+
const signingKey = options.signingKey ?? env.DAYWALKER_SIGNING_KEY ?? null;
|
|
58
|
+
const previousSigningKey = options.previousSigningKey ?? env.DAYWALKER_SIGNING_KEY_PREVIOUS ?? null;
|
|
59
|
+
const keyId = (options.keyId ?? env.DAYWALKER_KEY_ID ?? "").trim() || null;
|
|
63
60
|
let domain = null;
|
|
64
61
|
let domainSource = null;
|
|
65
62
|
const explicit = options.domain ?? env.DAYWALKER_DOMAIN ?? env.DAYWALKER_SIGNING_DOMAIN;
|
|
@@ -71,17 +68,33 @@ export function resolveConfig(options = {}) {
|
|
|
71
68
|
}
|
|
72
69
|
else {
|
|
73
70
|
for (const name of URL_ENV_VARS) {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
const val = env[name];
|
|
72
|
+
if (val) {
|
|
73
|
+
const host = publicHostname(val);
|
|
74
|
+
if (host) {
|
|
75
|
+
domain = host;
|
|
76
|
+
domainSource = name;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
problems.push("No public domain found: set DAYWALKER_DOMAIN (requests will be sent unsigned)");
|
|
84
|
-
}
|
|
82
|
+
const daywalkerPublicKey = options.daywalkerPublicKey ?? (env.DAYWALKER_PUBLIC_KEY ? env.DAYWALKER_PUBLIC_KEY.trim() : null);
|
|
85
83
|
const authUrl = (options.authUrl ?? env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
86
|
-
|
|
84
|
+
const cacheTtlMs = options.cacheTtlMs ?? 60_000;
|
|
85
|
+
const maxRetries = options.maxRetries ?? (options.retry?.maxRetries ?? 3);
|
|
86
|
+
return {
|
|
87
|
+
apiKey,
|
|
88
|
+
signingKey,
|
|
89
|
+
previousSigningKey,
|
|
90
|
+
keyId,
|
|
91
|
+
serviceSlug,
|
|
92
|
+
domain,
|
|
93
|
+
domainSource,
|
|
94
|
+
daywalkerPublicKey,
|
|
95
|
+
authUrl,
|
|
96
|
+
cacheTtlMs,
|
|
97
|
+
maxRetries,
|
|
98
|
+
problems,
|
|
99
|
+
};
|
|
87
100
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Default upper bound for body payload digest calculation (10MB). */
|
|
2
|
+
export declare const DEFAULT_MAX_BODY_SIZE_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* RFC 9530 Digest Fields computation.
|
|
5
|
+
* Hashes payload with SHA-256 via WebCrypto `crypto.subtle.digest`.
|
|
6
|
+
* Formats as `sha-256=:<base64-digest>:` (RFC 9651 structured field byte sequence).
|
|
7
|
+
* Enforces body size limit to mitigate memory exhaustion DoS.
|
|
8
|
+
*/
|
|
9
|
+
export declare function computeContentDigest(body: Uint8Array | string | null | undefined, maxSizeBytes?: number): Promise<string | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Compute SHA-256 hash returned as base64 string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sha256Base64(data: Uint8Array | string): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Compute SHA-256 hash returned as base64url string (useful for JWK thumbprints).
|
|
16
|
+
*/
|
|
17
|
+
export declare function sha256Base64Url(data: Uint8Array | string): Promise<string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bytesToBase64, stringToUtf8 } from "./encoding.js";
|
|
2
|
+
/** Default upper bound for body payload digest calculation (10MB). */
|
|
3
|
+
export const DEFAULT_MAX_BODY_SIZE_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
/**
|
|
5
|
+
* RFC 9530 Digest Fields computation.
|
|
6
|
+
* Hashes payload with SHA-256 via WebCrypto `crypto.subtle.digest`.
|
|
7
|
+
* Formats as `sha-256=:<base64-digest>:` (RFC 9651 structured field byte sequence).
|
|
8
|
+
* Enforces body size limit to mitigate memory exhaustion DoS.
|
|
9
|
+
*/
|
|
10
|
+
export async function computeContentDigest(body, maxSizeBytes = DEFAULT_MAX_BODY_SIZE_BYTES) {
|
|
11
|
+
if (body === null || body === undefined) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const data = typeof body === "string" ? stringToUtf8(body) : body;
|
|
15
|
+
if (data.byteLength > maxSizeBytes) {
|
|
16
|
+
throw new Error(`Request body size (${data.byteLength} bytes) exceeds maximum allowed limit (${maxSizeBytes} bytes)`);
|
|
17
|
+
}
|
|
18
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
19
|
+
const hashBase64 = bytesToBase64(new Uint8Array(hashBuffer));
|
|
20
|
+
return `sha-256=:${hashBase64}:`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute SHA-256 hash returned as base64 string.
|
|
24
|
+
*/
|
|
25
|
+
export async function sha256Base64(data) {
|
|
26
|
+
const bytes = typeof data === "string" ? stringToUtf8(data) : data;
|
|
27
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
28
|
+
return bytesToBase64(new Uint8Array(hashBuffer));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compute SHA-256 hash returned as base64url string (useful for JWK thumbprints).
|
|
32
|
+
*/
|
|
33
|
+
export async function sha256Base64Url(data) {
|
|
34
|
+
const base64 = await sha256Base64(data);
|
|
35
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency Web standard encoding utilities.
|
|
3
|
+
* Runs in Node.js 18+, Bun, Deno, Cloudflare Workers, and browser/Edge runtimes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function stringToUtf8(str: string): Uint8Array;
|
|
6
|
+
export declare function utf8ToString(bytes: Uint8Array): string;
|
|
7
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
8
|
+
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
9
|
+
export declare function bytesToBase64Url(bytes: Uint8Array): string;
|
|
10
|
+
export declare function base64UrlToBytes(b64url: string): Uint8Array;
|
|
11
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
12
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Constant-time byte comparison to mitigate microarchitectural timing attacks.
|
|
15
|
+
*/
|
|
16
|
+
export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Constant-time string comparison helper.
|
|
19
|
+
*/
|
|
20
|
+
export declare function timingSafeEqualString(a: string, b: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Masks a secret string for safe display/logging (e.g. `srv_...cdef` or `[none]`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function maskSecret(val: string | null | undefined): string;
|
|
25
|
+
/**
|
|
26
|
+
* Redacts secrets, tokens, keys, and authorization headers from strings.
|
|
27
|
+
* Used across SDK error handlers and logs to prevent sensitive credential exposure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function redactSecrets(input: string | null | undefined): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency Web standard encoding utilities.
|
|
3
|
+
* Runs in Node.js 18+, Bun, Deno, Cloudflare Workers, and browser/Edge runtimes.
|
|
4
|
+
*/
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
export function stringToUtf8(str) {
|
|
8
|
+
return encoder.encode(str);
|
|
9
|
+
}
|
|
10
|
+
export function utf8ToString(bytes) {
|
|
11
|
+
return decoder.decode(bytes);
|
|
12
|
+
}
|
|
13
|
+
export function bytesToBase64(bytes) {
|
|
14
|
+
if (typeof Buffer !== "undefined") {
|
|
15
|
+
return Buffer.from(bytes).toString("base64");
|
|
16
|
+
}
|
|
17
|
+
let binary = "";
|
|
18
|
+
const len = bytes.byteLength;
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
binary += String.fromCharCode(bytes[i]);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
}
|
|
24
|
+
export function base64ToBytes(b64) {
|
|
25
|
+
const clean = b64.trim().replace(/\s+/g, "");
|
|
26
|
+
if (typeof Buffer !== "undefined") {
|
|
27
|
+
const buf = Buffer.from(clean, "base64");
|
|
28
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
29
|
+
}
|
|
30
|
+
const binary = atob(clean);
|
|
31
|
+
const bytes = new Uint8Array(binary.length);
|
|
32
|
+
for (let i = 0; i < binary.length; i++) {
|
|
33
|
+
bytes[i] = binary.charCodeAt(i);
|
|
34
|
+
}
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
export function bytesToBase64Url(bytes) {
|
|
38
|
+
return bytesToBase64(bytes)
|
|
39
|
+
.replace(/\+/g, "-")
|
|
40
|
+
.replace(/\//g, "_")
|
|
41
|
+
.replace(/=+$/, "");
|
|
42
|
+
}
|
|
43
|
+
export function base64UrlToBytes(b64url) {
|
|
44
|
+
let b64 = b64url.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
45
|
+
while (b64.length % 4 !== 0) {
|
|
46
|
+
b64 += "=";
|
|
47
|
+
}
|
|
48
|
+
return base64ToBytes(b64);
|
|
49
|
+
}
|
|
50
|
+
export function bytesToHex(bytes) {
|
|
51
|
+
let hex = "";
|
|
52
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
53
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
54
|
+
}
|
|
55
|
+
return hex;
|
|
56
|
+
}
|
|
57
|
+
export function hexToBytes(hex) {
|
|
58
|
+
const clean = hex.trim().replace(/^0x/i, "");
|
|
59
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
60
|
+
for (let i = 0; i < clean.length; i += 2) {
|
|
61
|
+
bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
|
|
62
|
+
}
|
|
63
|
+
return bytes;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Constant-time byte comparison to mitigate microarchitectural timing attacks.
|
|
67
|
+
*/
|
|
68
|
+
export function timingSafeEqual(a, b) {
|
|
69
|
+
if (a.byteLength !== b.byteLength)
|
|
70
|
+
return false;
|
|
71
|
+
let diff = 0;
|
|
72
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
73
|
+
diff |= a[i] ^ b[i];
|
|
74
|
+
}
|
|
75
|
+
return diff === 0;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Constant-time string comparison helper.
|
|
79
|
+
*/
|
|
80
|
+
export function timingSafeEqualString(a, b) {
|
|
81
|
+
return timingSafeEqual(stringToUtf8(a), stringToUtf8(b));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Masks a secret string for safe display/logging (e.g. `srv_...cdef` or `[none]`).
|
|
85
|
+
*/
|
|
86
|
+
export function maskSecret(val) {
|
|
87
|
+
if (!val || typeof val !== "string")
|
|
88
|
+
return "[none]";
|
|
89
|
+
if (val.length <= 8)
|
|
90
|
+
return "***";
|
|
91
|
+
return `${val.slice(0, 4)}...${val.slice(-4)}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Redacts secrets, tokens, keys, and authorization headers from strings.
|
|
95
|
+
* Used across SDK error handlers and logs to prevent sensitive credential exposure.
|
|
96
|
+
*/
|
|
97
|
+
export function redactSecrets(input) {
|
|
98
|
+
if (!input || typeof input !== "string")
|
|
99
|
+
return "";
|
|
100
|
+
let out = input;
|
|
101
|
+
// Bearer tokens & Authorization headers
|
|
102
|
+
out = out.replace(/(Bearer\s+)[A-Za-z0-9_\-\.]+/gi, "$1[REDACTED]");
|
|
103
|
+
// API keys (e.g. srv_live_..., srv_test_..., dw_...)
|
|
104
|
+
out = out.replace(/\b(srv_(?:live|test)_[a-zA-Z0-9_\-]+)\b/g, (match) => {
|
|
105
|
+
if (match.length <= 12)
|
|
106
|
+
return "[REDACTED_KEY]";
|
|
107
|
+
return `${match.slice(0, 8)}...[REDACTED]`;
|
|
108
|
+
});
|
|
109
|
+
out = out.replace(/\b(dw_[a-zA-Z0-9_\-]+)\b/g, "[REDACTED_KEY]");
|
|
110
|
+
// Standard JWTs (header.payload.signature)
|
|
111
|
+
out = out.replace(/\beyJ[A-Za-z0-9_\-]{8,}\.eyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\b/g, "[REDACTED_JWT]");
|
|
112
|
+
// PEM Private keys
|
|
113
|
+
out = out.replace(/-----BEGIN [A-Z\s]+ PRIVATE KEY-----[\s\S]*?-----END [A-Z\s]+ PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]");
|
|
114
|
+
// Query parameters in URLs like ?apiKey=... &token=...
|
|
115
|
+
out = out.replace(/([?&](?:apiKey|api_key|token|signingKey|signing_key|secret)=)[^&\s]+/gi, "$1[REDACTED]");
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
|
|
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, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
|
|
5
|
+
export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, } from "./signatures.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
|
|
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, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PublicJwk, SigningKey } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the RFC 7638 JWK thumbprint for an Ed25519 public key.
|
|
4
|
+
*/
|
|
5
|
+
export declare function calculateJwkThumbprint(jwk: {
|
|
6
|
+
crv: string;
|
|
7
|
+
kty: string;
|
|
8
|
+
x: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Generate a new Ed25519 keypair using WebCrypto.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateSigningKey(): Promise<SigningKey>;
|
|
14
|
+
/**
|
|
15
|
+
* Load and import an Ed25519 private key from various string representations
|
|
16
|
+
* (PKCS#8 Base64 DER, PKCS#8 PEM, Base64url raw seed, or JSON JWK).
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadSigningKey(raw: string | SigningKey): Promise<SigningKey>;
|
|
19
|
+
/**
|
|
20
|
+
* Load an Ed25519 public key from JWK, PEM, Base64 SPKI, or Base64url raw public key.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadPublicKey(raw: string | PublicJwk | CryptoKey): Promise<{
|
|
23
|
+
publicKey: CryptoKey;
|
|
24
|
+
publicJwk: PublicJwk;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { base64ToBytes, base64UrlToBytes, bytesToBase64, bytesToBase64Url, } from "./encoding.js";
|
|
2
|
+
import { sha256Base64Url } from "./digest.js";
|
|
3
|
+
// PKCS#8 prefix for Ed25519 private keys (16 bytes)
|
|
4
|
+
// 30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20
|
|
5
|
+
const ED25519_PKCS8_PREFIX = new Uint8Array([
|
|
6
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
|
7
|
+
]);
|
|
8
|
+
// SPKI prefix for Ed25519 public keys (12 bytes)
|
|
9
|
+
// 30 2a 30 05 06 03 2b 65 70 03 21 00
|
|
10
|
+
const ED25519_SPKI_PREFIX = new Uint8Array([
|
|
11
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* Calculate the RFC 7638 JWK thumbprint for an Ed25519 public key.
|
|
15
|
+
*/
|
|
16
|
+
export async function calculateJwkThumbprint(jwk) {
|
|
17
|
+
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });
|
|
18
|
+
return sha256Base64Url(canonical);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a new Ed25519 keypair using WebCrypto.
|
|
22
|
+
*/
|
|
23
|
+
export async function generateSigningKey() {
|
|
24
|
+
const keyPair = (await globalThis.crypto.subtle.generateKey("Ed25519", true, [
|
|
25
|
+
"sign",
|
|
26
|
+
"verify",
|
|
27
|
+
]));
|
|
28
|
+
const jwk = (await globalThis.crypto.subtle.exportKey("jwk", keyPair.privateKey));
|
|
29
|
+
const pkcs8Buffer = await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
|
30
|
+
const privateKeyPkcs8Base64 = bytesToBase64(new Uint8Array(pkcs8Buffer));
|
|
31
|
+
const kid = await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: jwk.x });
|
|
32
|
+
const publicJwk = {
|
|
33
|
+
kty: "OKP",
|
|
34
|
+
crv: "Ed25519",
|
|
35
|
+
x: jwk.x,
|
|
36
|
+
kid,
|
|
37
|
+
use: "sig",
|
|
38
|
+
alg: "EdDSA",
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
privateKey: keyPair.privateKey,
|
|
42
|
+
publicKey: keyPair.publicKey,
|
|
43
|
+
publicJwk,
|
|
44
|
+
privateKeyPkcs8Base64,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load and import an Ed25519 private key from various string representations
|
|
49
|
+
* (PKCS#8 Base64 DER, PKCS#8 PEM, Base64url raw seed, or JSON JWK).
|
|
50
|
+
*/
|
|
51
|
+
export async function loadSigningKey(raw) {
|
|
52
|
+
if (typeof raw !== "string") {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
// If raw string is JSON JWK
|
|
57
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(trimmed);
|
|
60
|
+
if (parsed.kty === "OKP" && parsed.crv === "Ed25519" && parsed.d) {
|
|
61
|
+
const privateKey = await globalThis.crypto.subtle.importKey("jwk", parsed, "Ed25519", true, ["sign"]);
|
|
62
|
+
const jwk = (await globalThis.crypto.subtle.exportKey("jwk", privateKey));
|
|
63
|
+
const kid = parsed.kid || (await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: jwk.x }));
|
|
64
|
+
const publicJwk = {
|
|
65
|
+
kty: "OKP",
|
|
66
|
+
crv: "Ed25519",
|
|
67
|
+
x: jwk.x,
|
|
68
|
+
kid,
|
|
69
|
+
use: "sig",
|
|
70
|
+
alg: "EdDSA",
|
|
71
|
+
};
|
|
72
|
+
const publicKey = await globalThis.crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, "Ed25519", true, ["verify"]);
|
|
73
|
+
const pkcs8Buffer = await globalThis.crypto.subtle.exportKey("pkcs8", privateKey);
|
|
74
|
+
return {
|
|
75
|
+
privateKey,
|
|
76
|
+
publicKey,
|
|
77
|
+
publicJwk,
|
|
78
|
+
privateKeyPkcs8Base64: bytesToBase64(new Uint8Array(pkcs8Buffer)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Continue to binary/PEM parsing
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Handle PEM or Base64 / Base64url
|
|
87
|
+
let cleaned = trimmed
|
|
88
|
+
.replace(/-----BEGIN [A-Z0-9_-]+ PRIVATE KEY-----/gi, "")
|
|
89
|
+
.replace(/-----END [A-Z0-9_-]+ PRIVATE KEY-----/gi, "")
|
|
90
|
+
.replace(/\\n/g, "")
|
|
91
|
+
.replace(/\s+/g, "");
|
|
92
|
+
let keyBytes;
|
|
93
|
+
try {
|
|
94
|
+
keyBytes = cleaned.includes("-") || cleaned.includes("_")
|
|
95
|
+
? base64UrlToBytes(cleaned)
|
|
96
|
+
: base64ToBytes(cleaned);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error("Invalid base64 encoding in signing key");
|
|
100
|
+
}
|
|
101
|
+
// If 32 bytes raw seed, wrap into standard PKCS#8 DER
|
|
102
|
+
if (keyBytes.length === 32) {
|
|
103
|
+
const pkcs8 = new Uint8Array(ED25519_PKCS8_PREFIX.length + 32);
|
|
104
|
+
pkcs8.set(ED25519_PKCS8_PREFIX, 0);
|
|
105
|
+
pkcs8.set(keyBytes, ED25519_PKCS8_PREFIX.length);
|
|
106
|
+
keyBytes = pkcs8;
|
|
107
|
+
}
|
|
108
|
+
let privateKey;
|
|
109
|
+
try {
|
|
110
|
+
privateKey = await globalThis.crypto.subtle.importKey("pkcs8", keyBytes, "Ed25519", true, ["sign"]);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new Error(`Failed to import Ed25519 private key: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
const jwk = (await globalThis.crypto.subtle.exportKey("jwk", privateKey));
|
|
116
|
+
const kid = await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: jwk.x });
|
|
117
|
+
const publicJwk = {
|
|
118
|
+
kty: "OKP",
|
|
119
|
+
crv: "Ed25519",
|
|
120
|
+
x: jwk.x,
|
|
121
|
+
kid,
|
|
122
|
+
use: "sig",
|
|
123
|
+
alg: "EdDSA",
|
|
124
|
+
};
|
|
125
|
+
const publicKey = await globalThis.crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, "Ed25519", true, ["verify"]);
|
|
126
|
+
return {
|
|
127
|
+
privateKey,
|
|
128
|
+
publicKey,
|
|
129
|
+
publicJwk,
|
|
130
|
+
privateKeyPkcs8Base64: bytesToBase64(keyBytes),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Load an Ed25519 public key from JWK, PEM, Base64 SPKI, or Base64url raw public key.
|
|
135
|
+
*/
|
|
136
|
+
export async function loadPublicKey(raw) {
|
|
137
|
+
if (typeof raw === "object" && "type" in raw && raw.type === "public") {
|
|
138
|
+
const exported = (await globalThis.crypto.subtle.exportKey("jwk", raw));
|
|
139
|
+
const kid = exported.kid || (await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: exported.x }));
|
|
140
|
+
return {
|
|
141
|
+
publicKey: raw,
|
|
142
|
+
publicJwk: {
|
|
143
|
+
kty: "OKP",
|
|
144
|
+
crv: "Ed25519",
|
|
145
|
+
x: exported.x,
|
|
146
|
+
kid,
|
|
147
|
+
use: "sig",
|
|
148
|
+
alg: "EdDSA",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (typeof raw === "object" && "kty" in raw && raw.kty === "OKP" && raw.crv === "Ed25519") {
|
|
153
|
+
const kid = raw.kid || (await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: raw.x }));
|
|
154
|
+
const publicKey = await globalThis.crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: raw.x }, "Ed25519", true, ["verify"]);
|
|
155
|
+
return {
|
|
156
|
+
publicKey,
|
|
157
|
+
publicJwk: {
|
|
158
|
+
kty: "OKP",
|
|
159
|
+
crv: "Ed25519",
|
|
160
|
+
x: raw.x,
|
|
161
|
+
kid,
|
|
162
|
+
use: "sig",
|
|
163
|
+
alg: "EdDSA",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (typeof raw !== "string") {
|
|
168
|
+
throw new Error("Invalid public key format");
|
|
169
|
+
}
|
|
170
|
+
const trimmed = raw.trim();
|
|
171
|
+
// Try parsing as JSON JWK or JWKS
|
|
172
|
+
if (trimmed.startsWith("{")) {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(trimmed);
|
|
175
|
+
const jwkCandidate = parsed.keys ? parsed.keys[0] : parsed;
|
|
176
|
+
if (jwkCandidate && jwkCandidate.kty === "OKP" && jwkCandidate.crv === "Ed25519") {
|
|
177
|
+
return loadPublicKey(jwkCandidate);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Continue
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Handle PEM / Base64 / Base64url
|
|
185
|
+
let cleaned = trimmed
|
|
186
|
+
.replace(/-----BEGIN [A-Z0-9_-]+ PUBLIC KEY-----/gi, "")
|
|
187
|
+
.replace(/-----END [A-Z0-9_-]+ PUBLIC KEY-----/gi, "")
|
|
188
|
+
.replace(/\\n/g, "")
|
|
189
|
+
.replace(/\s+/g, "");
|
|
190
|
+
let keyBytes;
|
|
191
|
+
try {
|
|
192
|
+
keyBytes = cleaned.includes("-") || cleaned.includes("_")
|
|
193
|
+
? base64UrlToBytes(cleaned)
|
|
194
|
+
: base64ToBytes(cleaned);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new Error("Invalid base64 encoding in public key");
|
|
198
|
+
}
|
|
199
|
+
// If 32 bytes raw public key (x coord), import as JWK directly
|
|
200
|
+
if (keyBytes.length === 32) {
|
|
201
|
+
const x = bytesToBase64Url(keyBytes);
|
|
202
|
+
const kid = await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x });
|
|
203
|
+
const publicKey = await globalThis.crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x }, "Ed25519", true, ["verify"]);
|
|
204
|
+
return {
|
|
205
|
+
publicKey,
|
|
206
|
+
publicJwk: {
|
|
207
|
+
kty: "OKP",
|
|
208
|
+
crv: "Ed25519",
|
|
209
|
+
x,
|
|
210
|
+
kid,
|
|
211
|
+
use: "sig",
|
|
212
|
+
alg: "EdDSA",
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Try SPKI format
|
|
217
|
+
try {
|
|
218
|
+
const publicKey = await globalThis.crypto.subtle.importKey("spki", keyBytes, "Ed25519", true, ["verify"]);
|
|
219
|
+
const jwk = (await globalThis.crypto.subtle.exportKey("jwk", publicKey));
|
|
220
|
+
const kid = await calculateJwkThumbprint({ crv: "Ed25519", kty: "OKP", x: jwk.x });
|
|
221
|
+
return {
|
|
222
|
+
publicKey,
|
|
223
|
+
publicJwk: {
|
|
224
|
+
kty: "OKP",
|
|
225
|
+
crv: "Ed25519",
|
|
226
|
+
x: jwk.x,
|
|
227
|
+
kid,
|
|
228
|
+
use: "sig",
|
|
229
|
+
alg: "EdDSA",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
throw new Error(`Failed to import public key: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|