@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.
package/dist/client.js ADDED
@@ -0,0 +1,376 @@
1
+ import { resolveConfig } from "./config.js";
2
+ import { loadSigningKey, signRequest, verifyJwtLease, maskSecret, redactSecrets, } from "./crypto/index.js";
3
+ import { DaywalkerAuthError, DaywalkerRateLimitError, } from "./types.js";
4
+ function parseRetryAfter(headerValue) {
5
+ if (!headerValue)
6
+ return undefined;
7
+ const seconds = parseInt(headerValue.trim(), 10);
8
+ if (!Number.isNaN(seconds) && seconds >= 0) {
9
+ return seconds;
10
+ }
11
+ const date = Date.parse(headerValue);
12
+ if (!Number.isNaN(date)) {
13
+ const diff = Math.ceil((date - Date.now()) / 1000);
14
+ return diff > 0 ? diff : 0;
15
+ }
16
+ return undefined;
17
+ }
18
+ export class Daywalker {
19
+ _config;
20
+ _rawConfig;
21
+ _signingKeyPromise;
22
+ _cache = new Map();
23
+ constructor(config = {}) {
24
+ this._rawConfig = config;
25
+ this._config = resolveConfig(config);
26
+ }
27
+ get config() {
28
+ return this._config;
29
+ }
30
+ toJSON() {
31
+ return {
32
+ serviceSlug: this._config.serviceSlug,
33
+ domain: this._config.domain,
34
+ authUrl: this._config.authUrl,
35
+ keyId: this._config.keyId,
36
+ apiKey: maskSecret(this._config.apiKey),
37
+ signingKey: this._config.signingKey ? "[configured]" : null,
38
+ daywalkerPublicKey: this._config.daywalkerPublicKey ? "[configured]" : null,
39
+ };
40
+ }
41
+ // Node.js custom inspect masking
42
+ [Symbol.for("nodejs.util.inspect.custom")]() {
43
+ return this.toJSON();
44
+ }
45
+ async getLoadedSigningKey() {
46
+ if (!this._config.signingKey)
47
+ return null;
48
+ if (typeof this._config.signingKey !== "string" && "privateKey" in this._config.signingKey) {
49
+ return this._config.signingKey;
50
+ }
51
+ if (!this._signingKeyPromise) {
52
+ this._signingKeyPromise = loadSigningKey(this._config.signingKey);
53
+ }
54
+ return this._signingKeyPromise;
55
+ }
56
+ /**
57
+ * Signs and executes requests with automatic retry and exponential backoff on 429 responses.
58
+ */
59
+ async fetch(path, init) {
60
+ const authUrl = this._config.authUrl;
61
+ const url = new URL(path, authUrl);
62
+ const method = (init?.method ?? (init?.body !== undefined ? "POST" : "GET")).toUpperCase();
63
+ const doFetch = this._rawConfig.fetch ?? globalThis.fetch;
64
+ const retryOpts = {
65
+ maxRetries: init?.retry?.maxRetries ?? this._config.maxRetries ?? 3,
66
+ initialDelayMs: init?.retry?.initialDelayMs ?? 500,
67
+ maxDelayMs: init?.retry?.maxDelayMs ?? 30_000,
68
+ backoffFactor: init?.retry?.backoffFactor ?? 2,
69
+ jitter: init?.retry?.jitter ?? true,
70
+ ...init?.retry,
71
+ };
72
+ const rawBody = init?.body;
73
+ let bodyString = null;
74
+ if (rawBody !== undefined && rawBody !== null) {
75
+ if (typeof rawBody === "string") {
76
+ bodyString = rawBody;
77
+ }
78
+ else if (rawBody instanceof Uint8Array) {
79
+ bodyString = rawBody;
80
+ }
81
+ else if (typeof rawBody === "object") {
82
+ bodyString = JSON.stringify(rawBody);
83
+ }
84
+ }
85
+ const signingKey = await this.getLoadedSigningKey();
86
+ let attempt = 0;
87
+ while (true) {
88
+ const headers = new Headers(init?.headers);
89
+ if (rawBody !== undefined && !headers.has("content-type") && typeof rawBody === "object" && !(rawBody instanceof Uint8Array)) {
90
+ headers.set("content-type", "application/json");
91
+ }
92
+ if (!headers.has("accept")) {
93
+ headers.set("accept", "application/json");
94
+ }
95
+ if (this._config.apiKey && !headers.has("authorization")) {
96
+ headers.set("authorization", `Bearer ${this._config.apiKey}`);
97
+ }
98
+ if (signingKey) {
99
+ const signedHeaders = await signRequest({
100
+ method,
101
+ path: url.pathname,
102
+ authority: url.host,
103
+ body: bodyString,
104
+ key: signingKey,
105
+ keyid: this._config.keyId ?? undefined,
106
+ apiKey: this._config.apiKey ?? undefined,
107
+ });
108
+ for (const [k, v] of Object.entries(signedHeaders)) {
109
+ if (v !== undefined) {
110
+ headers.set(k, v);
111
+ }
112
+ }
113
+ }
114
+ const reqInit = {
115
+ ...init,
116
+ method,
117
+ headers,
118
+ body: bodyString ?? undefined,
119
+ };
120
+ let res;
121
+ try {
122
+ res = await doFetch(url.toString(), reqInit);
123
+ }
124
+ catch (err) {
125
+ if (attempt < (retryOpts.maxRetries ?? 3)) {
126
+ attempt++;
127
+ const delay = (retryOpts.initialDelayMs ?? 500) * Math.pow(retryOpts.backoffFactor ?? 2, attempt - 1);
128
+ await new Promise((r) => setTimeout(r, delay));
129
+ continue;
130
+ }
131
+ throw err;
132
+ }
133
+ if (res.status === 429 && attempt < (retryOpts.maxRetries ?? 3)) {
134
+ attempt++;
135
+ const retryAfterSec = parseRetryAfter(res.headers.get("retry-after"));
136
+ let delayMs = (retryOpts.initialDelayMs ?? 500) * Math.pow(retryOpts.backoffFactor ?? 2, attempt - 1);
137
+ if (retryOpts.jitter) {
138
+ delayMs = delayMs * (0.8 + Math.random() * 0.4);
139
+ }
140
+ if (retryAfterSec !== undefined) {
141
+ delayMs = Math.max(delayMs, retryAfterSec * 1000);
142
+ }
143
+ delayMs = Math.min(delayMs, retryOpts.maxDelayMs ?? 30_000);
144
+ await new Promise((r) => setTimeout(r, delayMs));
145
+ continue;
146
+ }
147
+ return res;
148
+ }
149
+ }
150
+ /**
151
+ * Validates incoming bearer tokens with memory caching and discriminated union return.
152
+ * Supports zero-egress offline verification for JWT token leases when Daywalker's public key is configured.
153
+ */
154
+ async validateToken(tokenOrOptions, options) {
155
+ let token;
156
+ let serviceSlug;
157
+ let forceRefresh = false;
158
+ let offlineOnly = false;
159
+ if (typeof tokenOrOptions === "string") {
160
+ token = tokenOrOptions;
161
+ serviceSlug = (options?.serviceSlug ?? this._config.serviceSlug ?? "").toLowerCase();
162
+ forceRefresh = options?.forceRefresh ?? false;
163
+ offlineOnly = options?.offlineOnly ?? false;
164
+ }
165
+ else {
166
+ token = tokenOrOptions?.token ?? this._config.apiKey ?? undefined;
167
+ serviceSlug = (tokenOrOptions?.serviceSlug ?? this._config.serviceSlug ?? "").toLowerCase();
168
+ forceRefresh = tokenOrOptions?.forceRefresh ?? false;
169
+ offlineOnly = tokenOrOptions?.offlineOnly ?? false;
170
+ }
171
+ if (!token) {
172
+ return {
173
+ ok: false,
174
+ error: "CONFIGURATION_ERROR",
175
+ status: 400,
176
+ message: "Missing token to validate",
177
+ };
178
+ }
179
+ const cacheKey = `${serviceSlug || "default"}:${token}`;
180
+ const cached = this._cache.get(cacheKey);
181
+ if (!forceRefresh && cached && cached.expiresAt > Date.now()) {
182
+ return cached.result;
183
+ }
184
+ // Check for offline JWT lease verification
185
+ if (this._config.daywalkerPublicKey && token.split(".").length === 3) {
186
+ const jwtResult = await verifyJwtLease({
187
+ token,
188
+ publicKey: this._config.daywalkerPublicKey,
189
+ expectedServiceSlug: serviceSlug || undefined,
190
+ });
191
+ if (jwtResult.valid && jwtResult.payload) {
192
+ const payload = jwtResult.payload;
193
+ const result = {
194
+ ok: true,
195
+ data: {
196
+ serviceSlug: serviceSlug || (payload.serviceSlug || payload.aud || ""),
197
+ tenantId: (payload.tenantId || payload.sub || ""),
198
+ rateLimit: payload.rateLimit ?? {
199
+ limit: 1000,
200
+ remaining: 999,
201
+ reset: Math.floor(Date.now() / 1000) + 60,
202
+ },
203
+ rateLimitHourly: payload.rateLimitHourly,
204
+ tokenId: (payload.tokenId || payload.sub),
205
+ tokenName: payload.tokenName,
206
+ userId: payload.userId,
207
+ stage: payload.stage,
208
+ offlineVerified: true,
209
+ ...payload,
210
+ },
211
+ };
212
+ if (this._config.cacheTtlMs > 0) {
213
+ this._cache.set(cacheKey, { result, expiresAt: Date.now() + this._config.cacheTtlMs });
214
+ }
215
+ return result;
216
+ }
217
+ else if (offlineOnly) {
218
+ return {
219
+ ok: false,
220
+ error: "INVALID_TOKEN",
221
+ status: 401,
222
+ message: jwtResult.error ?? "Offline token verification failed",
223
+ };
224
+ }
225
+ }
226
+ else if (offlineOnly) {
227
+ return {
228
+ ok: false,
229
+ error: "CONFIGURATION_ERROR",
230
+ status: 500,
231
+ message: "Offline verification requires DAYWALKER_PUBLIC_KEY and a valid JWT token lease",
232
+ };
233
+ }
234
+ // Online verification
235
+ try {
236
+ const validateBody = { token };
237
+ if (serviceSlug) {
238
+ validateBody.serviceSlug = serviceSlug;
239
+ }
240
+ const res = await this.fetch("/api/v1/tokens/validate", {
241
+ method: "POST",
242
+ body: validateBody,
243
+ });
244
+ const json = (await res.json().catch(() => ({})));
245
+ if (res.status === 200 && json.valid !== false) {
246
+ const payloadData = (typeof json.data === "object" && json.data ? json.data : json);
247
+ const data = {
248
+ serviceSlug: json.serviceSlug || serviceSlug,
249
+ tenantId: (payloadData.tenantId || payloadData.userId || json.tenantId || json.userId || ""),
250
+ rateLimit: (payloadData.rateLimit || json.rateLimit || { limit: 0, remaining: 0, reset: 0 }),
251
+ rateLimitHourly: (payloadData.rateLimitHourly || json.rateLimitHourly),
252
+ tokenId: (payloadData.tokenId || json.tokenId),
253
+ tokenName: (payloadData.tokenName || json.tokenName),
254
+ userId: (payloadData.userId || json.userId),
255
+ stage: (payloadData.stage || json.stage),
256
+ ...payloadData,
257
+ };
258
+ const result = {
259
+ ok: true,
260
+ data,
261
+ };
262
+ if (this._config.cacheTtlMs > 0) {
263
+ this._cache.set(cacheKey, { result, expiresAt: Date.now() + this._config.cacheTtlMs });
264
+ }
265
+ return result;
266
+ }
267
+ if (res.status === 429) {
268
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after")) ?? json.retryAfter;
269
+ return {
270
+ ok: false,
271
+ error: "RATE_LIMITED",
272
+ status: 429,
273
+ message: redactSecrets(json.message || "Rate limit exceeded"),
274
+ retryAfter,
275
+ };
276
+ }
277
+ let error = "INVALID_TOKEN";
278
+ const errMsg = (json.message || json.error || "");
279
+ if (json.error === "EXPIRED" || /expired/i.test(errMsg)) {
280
+ error = "EXPIRED";
281
+ }
282
+ else if (json.error === "REVOKED" || /revoked/i.test(errMsg)) {
283
+ error = "REVOKED";
284
+ }
285
+ return {
286
+ ok: false,
287
+ error,
288
+ status: res.status,
289
+ message: redactSecrets(errMsg || `Validation failed with HTTP ${res.status}`),
290
+ };
291
+ }
292
+ catch (err) {
293
+ return {
294
+ ok: false,
295
+ error: "NETWORK_ERROR",
296
+ status: 503,
297
+ message: redactSecrets(`Could not reach Daywalker Auth: ${err.message}`),
298
+ };
299
+ }
300
+ }
301
+ /**
302
+ * Throws DaywalkerAuthError or DaywalkerRateLimitError if token is invalid or rate limited.
303
+ */
304
+ async requireValidToken(tokenOrOptions, options) {
305
+ const result = await this.validateToken(tokenOrOptions, options);
306
+ if (!result.ok) {
307
+ if (result.status === 429) {
308
+ throw new DaywalkerRateLimitError(result.message, result.retryAfter);
309
+ }
310
+ throw new DaywalkerAuthError(result.message, result.status, result.error);
311
+ }
312
+ return result.data;
313
+ }
314
+ /**
315
+ * The public JWK(s) configured for this service.
316
+ */
317
+ async keys() {
318
+ const key = await this.getLoadedSigningKey();
319
+ return { keys: key ? [key.publicJwk] : [] };
320
+ }
321
+ /**
322
+ * Check configuration and verify signature readiness.
323
+ */
324
+ async selfCheck() {
325
+ const problems = [...this._config.problems];
326
+ const signingKey = await this.getLoadedSigningKey().catch((err) => {
327
+ problems.push(`Signing key load error: ${err.message}`);
328
+ return null;
329
+ });
330
+ if (!signingKey) {
331
+ problems.push("DAYWALKER_SIGNING_KEY is not set: requests will be sent without RFC 9421 signature");
332
+ }
333
+ const keyId = signingKey?.publicJwk.kid ?? null;
334
+ const offlineVerificationReady = Boolean(this._config.daywalkerPublicKey);
335
+ return {
336
+ ok: problems.length === 0,
337
+ serviceSlug: this._config.serviceSlug,
338
+ domain: this._config.domain,
339
+ signing: Boolean(signingKey),
340
+ keyId,
341
+ offlineVerificationReady,
342
+ problems,
343
+ };
344
+ }
345
+ }
346
+ /**
347
+ * Factory function for creating instantiable Daywalker client instances.
348
+ */
349
+ export function createDaywalker(config) {
350
+ return new Daywalker(config);
351
+ }
352
+ // Retain lazy default client instance for plug-and-play simplicity,
353
+ // callable as `daywalker()` or usable directly as `daywalker.fetch(...)`.
354
+ let defaultInstance = null;
355
+ function getDefaultInstance() {
356
+ if (!defaultInstance) {
357
+ defaultInstance = new Daywalker();
358
+ }
359
+ return defaultInstance;
360
+ }
361
+ export const daywalker = new Proxy((() => getDefaultInstance()), {
362
+ get(_target, prop) {
363
+ const instance = getDefaultInstance();
364
+ const val = instance[prop];
365
+ if (typeof val === "function") {
366
+ return val.bind(instance);
367
+ }
368
+ return val;
369
+ },
370
+ apply(_target, _thisArg, args) {
371
+ if (args.length > 0) {
372
+ return new Daywalker(args[0]);
373
+ }
374
+ return getDefaultInstance();
375
+ },
376
+ });
@@ -0,0 +1,21 @@
1
+ import type { DaywalkerConfig, PublicJwk, SigningKey } from "./types.js";
2
+ export interface ResolvedConfig {
3
+ apiKey: string | null;
4
+ signingKey: string | SigningKey | null;
5
+ previousSigningKey: string | SigningKey | null;
6
+ keyId: string | null;
7
+ serviceSlug: string | null;
8
+ domain: string | null;
9
+ domainSource: string | null;
10
+ daywalkerPublicKey: string | PublicJwk | null;
11
+ authUrl: string;
12
+ cacheTtlMs: number;
13
+ maxRetries: number;
14
+ problems: string[];
15
+ }
16
+ /**
17
+ * A public hostname from a URL or bare host, or null for anything that can't be fetched over
18
+ * the internet (localhost, 0.0.0.0, IP addresses, single labels).
19
+ */
20
+ export declare function publicHostname(value: string | undefined | null): string | null;
21
+ export declare function resolveConfig(options?: DaywalkerConfig): ResolvedConfig;
package/dist/config.js ADDED
@@ -0,0 +1,100 @@
1
+ const URL_ENV_VARS = [
2
+ "APP_URL",
3
+ "NEXTAUTH_URL",
4
+ "NEXT_PUBLIC_APP_URL",
5
+ "BETTER_AUTH_URL",
6
+ "PUBLIC_URL",
7
+ "SITE_URL",
8
+ "URL",
9
+ "VERCEL_PROJECT_PRODUCTION_URL",
10
+ "RENDER_EXTERNAL_HOSTNAME",
11
+ "RAILWAY_PUBLIC_DOMAIN",
12
+ "VERCEL_URL",
13
+ ];
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
+ }
26
+ /**
27
+ * A public hostname from a URL or bare host, or null for anything that can't be fetched over
28
+ * the internet (localhost, 0.0.0.0, IP addresses, single labels).
29
+ */
30
+ export function publicHostname(value) {
31
+ if (!value || !value.trim())
32
+ return null;
33
+ let host = value.trim().toLowerCase();
34
+ if (host.length > 253)
35
+ return null;
36
+ try {
37
+ host = new URL(/^[a-z][a-z0-9+.-]*:\/\//.test(host) ? host : `https://${host}`).hostname;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ host = host.replace(/\.$/, "");
43
+ if (host.length > 253 || isIpAddress(host.replace(/^\[|\]$/g, "")) || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
44
+ return null;
45
+ }
46
+ return DOMAIN_PATTERN.test(host) ? host : null;
47
+ }
48
+ export function resolveConfig(options = {}) {
49
+ const env = options.env ?? (typeof process !== "undefined" && process.env ? process.env : {});
50
+ const problems = [];
51
+ const apiKey = (options.apiKey ?? env.DAYWALKER_API_KEY ?? "").trim() || null;
52
+ const serviceSlug = (options.serviceSlug ?? env.DAYWALKER_SERVICE_SLUG ?? "").trim().toLowerCase() || null;
53
+ if (!apiKey)
54
+ problems.push("DAYWALKER_API_KEY is not set");
55
+ if (!serviceSlug)
56
+ problems.push("DAYWALKER_SERVICE_SLUG is not set");
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;
60
+ let domain = null;
61
+ let domainSource = null;
62
+ const explicit = options.domain ?? env.DAYWALKER_DOMAIN ?? env.DAYWALKER_SIGNING_DOMAIN;
63
+ if (explicit) {
64
+ domain = publicHostname(explicit);
65
+ domainSource = options.domain ? "options.domain" : env.DAYWALKER_DOMAIN ? "DAYWALKER_DOMAIN" : "DAYWALKER_SIGNING_DOMAIN";
66
+ if (!domain)
67
+ problems.push(`${domainSource} (${explicit}) is not a public domain name`);
68
+ }
69
+ else {
70
+ for (const name of URL_ENV_VARS) {
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
+ }
80
+ }
81
+ }
82
+ const daywalkerPublicKey = options.daywalkerPublicKey ?? (env.DAYWALKER_PUBLIC_KEY ? env.DAYWALKER_PUBLIC_KEY.trim() : null);
83
+ const authUrl = (options.authUrl ?? env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
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
+ };
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;