@shelbyb/daywalker-sdk 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ import type { PublicJwk, SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult } from "../types.js";
2
+ export declare const SIGNATURE_VERSION = "rfc9421";
3
+ export declare const SIGNATURE_LABEL = "sig1";
4
+ export declare const SIGNATURE_ALG = "ed25519";
5
+ export declare const MAX_CLOCK_SKEW_SECONDS = 300;
6
+ export declare const SAFE_IDENTIFIER_PATTERN: RegExp;
7
+ /**
8
+ * Normalizes HTTP request path per RFC 3986 to prevent path traversal & signature confusion.
9
+ */
10
+ export declare function normalizePath(rawPath: string): string;
11
+ /**
12
+ * Validates that an identifier (keyId, serviceSlug, etc.) contains only safe alphanumeric/hyphen characters.
13
+ */
14
+ export declare function assertSafeIdentifier(name: string, value: string): void;
15
+ /**
16
+ * In-memory LRU / sliding cache for nonces / signature replay protection.
17
+ */
18
+ declare class SignatureReplayCache {
19
+ private seen;
20
+ private readonly ttlMs;
21
+ constructor(ttlSeconds?: number);
22
+ checkAndRecord(signatureId: string): boolean;
23
+ }
24
+ export declare const globalReplayCache: SignatureReplayCache;
25
+ export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, };
26
+ /**
27
+ * Format the RFC 9421 Signature-Input parameters string.
28
+ * Example: `("@method" "@path" "@authority" "content-digest");created=1791561600;keyid="...";alg="ed25519";nonce="..."`
29
+ */
30
+ export declare function formatSignatureParams(params: {
31
+ components?: string[];
32
+ created: number;
33
+ keyid: string;
34
+ alg?: string;
35
+ nonce?: string;
36
+ }): string;
37
+ /**
38
+ * Construct canonical RFC 9421 signature base string.
39
+ */
40
+ export declare function createSignatureBase(params: {
41
+ method: string;
42
+ path: string;
43
+ authority: string;
44
+ contentDigest?: string | null;
45
+ signatureParams: string;
46
+ additionalComponents?: Record<string, string>;
47
+ }): string;
48
+ /**
49
+ * Sign an outgoing HTTP request according to RFC 9421 + RFC 9530.
50
+ */
51
+ export declare function signRequest(params: SignRequestParams): Promise<SignedHeaders>;
52
+ /**
53
+ * Verify an incoming RFC 9421 HTTP Message Signature.
54
+ */
55
+ export declare function verifyRequestSignature(params: VerifySignatureParams): Promise<VerifySignatureResult>;
56
+ /**
57
+ * Zero-egress offline verification for asymmetric JWT token leases.
58
+ */
59
+ export declare function verifyJwtLease(params: {
60
+ token: string;
61
+ publicKey: string | PublicJwk | CryptoKey;
62
+ expectedServiceSlug?: string;
63
+ currentTime?: number;
64
+ }): Promise<{
65
+ valid: boolean;
66
+ payload?: Record<string, unknown>;
67
+ error?: string;
68
+ }>;
69
+ /**
70
+ * Register public key directly with Daywalker Auth via POST /api/v1/keys/register (Push Architecture).
71
+ */
72
+ export declare function registerPublicKey(params: {
73
+ authUrl?: string;
74
+ apiKey: string;
75
+ serviceSlug?: string;
76
+ domain?: string | null;
77
+ publicJwk: PublicJwk;
78
+ fetchFn?: typeof fetch;
79
+ }): Promise<{
80
+ ok: boolean;
81
+ status: number;
82
+ error?: string;
83
+ }>;