@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/LICENSE +21 -0
- package/README.md +209 -2
- package/assets/logo-512.png +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +254 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +376 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +100 -0
- 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 +25 -0
- package/dist/express.js +68 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/middleware.d.ts +20 -0
- package/dist/middleware.js +72 -0
- package/dist/next.d.ts +63 -0
- package/dist/next.js +189 -0
- package/dist/signing.d.ts +3 -0
- package/dist/signing.js +2 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +46 -0
- package/package.json +68 -4
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { base64ToBytes, base64UrlToBytes, bytesToBase64, stringToUtf8, utf8ToString, timingSafeEqualString, } from "./encoding.js";
|
|
2
|
+
import { computeContentDigest } from "./digest.js";
|
|
3
|
+
import { loadPublicKey, loadSigningKey } from "./keys.js";
|
|
4
|
+
export const SIGNATURE_VERSION = "rfc9421";
|
|
5
|
+
export const SIGNATURE_LABEL = "sig1";
|
|
6
|
+
export const SIGNATURE_ALG = "ed25519";
|
|
7
|
+
export const MAX_CLOCK_SKEW_SECONDS = 300;
|
|
8
|
+
export const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z0-9_\-\.]{1,128}$/;
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes HTTP request path per RFC 3986 to prevent path traversal & signature confusion.
|
|
11
|
+
*/
|
|
12
|
+
export function normalizePath(rawPath) {
|
|
13
|
+
if (!rawPath || !rawPath.trim())
|
|
14
|
+
return "/";
|
|
15
|
+
let p = rawPath.trim();
|
|
16
|
+
p = p.replace(/\/+/g, "/");
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(p, "http://localhost");
|
|
19
|
+
return parsed.pathname + parsed.search;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return p.startsWith("/") ? p : `/${p}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates that an identifier (keyId, serviceSlug, etc.) contains only safe alphanumeric/hyphen characters.
|
|
27
|
+
*/
|
|
28
|
+
export function assertSafeIdentifier(name, value) {
|
|
29
|
+
if (!value || typeof value !== "string" || value.length > 128 || !SAFE_IDENTIFIER_PATTERN.test(value)) {
|
|
30
|
+
throw new Error(`Invalid ${name}: must be 1-128 safe characters (alphanumeric, '_', '-', '.')`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* In-memory LRU / sliding cache for nonces / signature replay protection.
|
|
35
|
+
*/
|
|
36
|
+
class SignatureReplayCache {
|
|
37
|
+
seen = new Map();
|
|
38
|
+
ttlMs;
|
|
39
|
+
constructor(ttlSeconds = 360) {
|
|
40
|
+
this.ttlMs = ttlSeconds * 1000;
|
|
41
|
+
}
|
|
42
|
+
checkAndRecord(signatureId) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (this.seen.size > 5000) {
|
|
45
|
+
for (const [k, exp] of this.seen.entries()) {
|
|
46
|
+
if (exp <= now)
|
|
47
|
+
this.seen.delete(k);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const expiresAt = this.seen.get(signatureId);
|
|
51
|
+
if (expiresAt && expiresAt > now) {
|
|
52
|
+
return false; // Replay detected
|
|
53
|
+
}
|
|
54
|
+
this.seen.set(signatureId, now + this.ttlMs);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export const globalReplayCache = new SignatureReplayCache();
|
|
59
|
+
/**
|
|
60
|
+
* Format the RFC 9421 Signature-Input parameters string.
|
|
61
|
+
* Example: `("@method" "@path" "@authority" "content-digest");created=1791561600;keyid="...";alg="ed25519";nonce="..."`
|
|
62
|
+
*/
|
|
63
|
+
export function formatSignatureParams(params) {
|
|
64
|
+
const components = params.components ?? ["@method", "@path", "@authority", "content-digest"];
|
|
65
|
+
const componentsStr = components.map((c) => `"${c}"`).join(" ");
|
|
66
|
+
let out = `(${componentsStr});created=${params.created};keyid="${params.keyid}";alg="${params.alg ?? SIGNATURE_ALG}"`;
|
|
67
|
+
if (params.nonce) {
|
|
68
|
+
out += `;nonce="${params.nonce}"`;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Construct canonical RFC 9421 signature base string.
|
|
74
|
+
*/
|
|
75
|
+
export function createSignatureBase(params) {
|
|
76
|
+
const method = params.method.toUpperCase();
|
|
77
|
+
const path = normalizePath(params.path);
|
|
78
|
+
const authority = params.authority.toLowerCase();
|
|
79
|
+
const lines = [
|
|
80
|
+
`"@method": ${method}`,
|
|
81
|
+
`"@path": ${path}`,
|
|
82
|
+
`"@authority": ${authority}`,
|
|
83
|
+
];
|
|
84
|
+
if (params.contentDigest !== undefined && params.contentDigest !== null) {
|
|
85
|
+
lines.push(`"content-digest": ${params.contentDigest}`);
|
|
86
|
+
}
|
|
87
|
+
if (params.additionalComponents) {
|
|
88
|
+
for (const [k, v] of Object.entries(params.additionalComponents)) {
|
|
89
|
+
lines.push(`"${k.toLowerCase()}": ${v}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push(`"@signature-params": ${params.signatureParams}`);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Sign an outgoing HTTP request according to RFC 9421 + RFC 9530.
|
|
97
|
+
*/
|
|
98
|
+
export async function signRequest(params) {
|
|
99
|
+
const signingKey = await loadSigningKey(params.key);
|
|
100
|
+
const created = params.created ?? Math.floor(Date.now() / 1000);
|
|
101
|
+
const keyid = params.keyid ?? signingKey.publicJwk.kid;
|
|
102
|
+
const alg = params.alg ?? SIGNATURE_ALG;
|
|
103
|
+
if (keyid) {
|
|
104
|
+
assertSafeIdentifier("keyid", keyid);
|
|
105
|
+
}
|
|
106
|
+
const hasBody = params.body !== undefined && params.body !== null;
|
|
107
|
+
const contentDigest = hasBody ? await computeContentDigest(params.body) : null;
|
|
108
|
+
const components = ["@method", "@path", "@authority"];
|
|
109
|
+
if (contentDigest !== null) {
|
|
110
|
+
components.push("content-digest");
|
|
111
|
+
}
|
|
112
|
+
const signatureParams = formatSignatureParams({
|
|
113
|
+
components,
|
|
114
|
+
created,
|
|
115
|
+
keyid,
|
|
116
|
+
alg,
|
|
117
|
+
nonce: params.nonce,
|
|
118
|
+
});
|
|
119
|
+
const signatureBase = createSignatureBase({
|
|
120
|
+
method: params.method,
|
|
121
|
+
path: params.path,
|
|
122
|
+
authority: params.authority,
|
|
123
|
+
contentDigest,
|
|
124
|
+
signatureParams,
|
|
125
|
+
});
|
|
126
|
+
const signatureBuffer = await globalThis.crypto.subtle.sign("Ed25519", signingKey.privateKey, stringToUtf8(signatureBase));
|
|
127
|
+
const signatureBase64 = bytesToBase64(new Uint8Array(signatureBuffer));
|
|
128
|
+
const headers = {
|
|
129
|
+
"Signature-Input": `${SIGNATURE_LABEL}=${signatureParams}`,
|
|
130
|
+
Signature: `${SIGNATURE_LABEL}=:${signatureBase64}:`,
|
|
131
|
+
};
|
|
132
|
+
if (contentDigest !== null) {
|
|
133
|
+
headers["Content-Digest"] = contentDigest;
|
|
134
|
+
}
|
|
135
|
+
if (params.apiKey) {
|
|
136
|
+
headers["Authorization"] = `Bearer ${params.apiKey}`;
|
|
137
|
+
}
|
|
138
|
+
return headers;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verify an incoming RFC 9421 HTTP Message Signature.
|
|
142
|
+
*/
|
|
143
|
+
export async function verifyRequestSignature(params) {
|
|
144
|
+
const getHeader = (name) => {
|
|
145
|
+
if (params.headers instanceof Headers) {
|
|
146
|
+
return params.headers.get(name) ?? undefined;
|
|
147
|
+
}
|
|
148
|
+
const lower = name.toLowerCase();
|
|
149
|
+
for (const [k, v] of Object.entries(params.headers)) {
|
|
150
|
+
if (k.toLowerCase() === lower && v !== undefined)
|
|
151
|
+
return v;
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
};
|
|
155
|
+
const sigInputHeader = getHeader("Signature-Input");
|
|
156
|
+
const signatureHeader = getHeader("Signature");
|
|
157
|
+
const contentDigestHeader = getHeader("Content-Digest");
|
|
158
|
+
if (!sigInputHeader || !signatureHeader) {
|
|
159
|
+
return { valid: false, error: "Missing Signature-Input or Signature header" };
|
|
160
|
+
}
|
|
161
|
+
const sigInputMatch = sigInputHeader.match(new RegExp(`(?:^|,\\s*)${SIGNATURE_LABEL}=([^,]+)`));
|
|
162
|
+
if (!sigInputMatch) {
|
|
163
|
+
return { valid: false, error: `Signature-Input header does not contain ${SIGNATURE_LABEL}` };
|
|
164
|
+
}
|
|
165
|
+
const signatureParams = sigInputMatch[1].trim();
|
|
166
|
+
// Validate parameters and check for component duplicates
|
|
167
|
+
const coveredComponentsMatch = signatureParams.match(/^\(([^)]*)\)/);
|
|
168
|
+
if (!coveredComponentsMatch) {
|
|
169
|
+
return { valid: false, error: "Signature-Input missing covered components list" };
|
|
170
|
+
}
|
|
171
|
+
const rawComponents = coveredComponentsMatch[1].trim().split(/\s+/).filter(Boolean);
|
|
172
|
+
const seenComponents = new Set();
|
|
173
|
+
for (const c of rawComponents) {
|
|
174
|
+
if (seenComponents.has(c)) {
|
|
175
|
+
return { valid: false, error: `Duplicate covered component in Signature-Input: ${c}` };
|
|
176
|
+
}
|
|
177
|
+
seenComponents.add(c);
|
|
178
|
+
}
|
|
179
|
+
const sigMatch = signatureHeader.match(new RegExp(`(?:^|,\\s*)${SIGNATURE_LABEL}=:([A-Za-z0-9+/=_-]+):`));
|
|
180
|
+
if (!sigMatch) {
|
|
181
|
+
return { valid: false, error: `Signature header does not contain valid ${SIGNATURE_LABEL} byte sequence` };
|
|
182
|
+
}
|
|
183
|
+
const signatureBase64 = sigMatch[1].trim();
|
|
184
|
+
const createdMatch = signatureParams.match(/created=(\d+)/);
|
|
185
|
+
if (!createdMatch) {
|
|
186
|
+
return { valid: false, error: "Signature-Input missing created timestamp" };
|
|
187
|
+
}
|
|
188
|
+
const created = parseInt(createdMatch[1], 10);
|
|
189
|
+
const now = params.currentTime ?? Math.floor(Date.now() / 1000);
|
|
190
|
+
const maxSkew = params.maxClockSkewSeconds ?? MAX_CLOCK_SKEW_SECONDS;
|
|
191
|
+
if (Math.abs(now - created) > maxSkew) {
|
|
192
|
+
return { valid: false, error: `Signature timestamp expired or clock skew too large (created=${created}, now=${now})` };
|
|
193
|
+
}
|
|
194
|
+
if (params.replayCheck !== false) {
|
|
195
|
+
const replayKey = `${signatureBase64}:${created}`;
|
|
196
|
+
if (!globalReplayCache.checkAndRecord(replayKey)) {
|
|
197
|
+
return { valid: false, error: "Replay detected: request signature was already presented" };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const keyidMatch = signatureParams.match(/keyid="([^"]+)"/);
|
|
201
|
+
const keyid = keyidMatch ? keyidMatch[1] : undefined;
|
|
202
|
+
if (keyid) {
|
|
203
|
+
try {
|
|
204
|
+
assertSafeIdentifier("keyid", keyid);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return { valid: false, error: e.message };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const algMatch = signatureParams.match(/alg="([^"]+)"/);
|
|
211
|
+
const alg = algMatch ? algMatch[1] : undefined;
|
|
212
|
+
if (alg && alg.toLowerCase() !== "ed25519" && alg.toLowerCase() !== "eddsa") {
|
|
213
|
+
return { valid: false, error: `Unsupported signature algorithm: ${alg}` };
|
|
214
|
+
}
|
|
215
|
+
if (signatureParams.includes('"content-digest"')) {
|
|
216
|
+
if (!contentDigestHeader) {
|
|
217
|
+
return { valid: false, error: "Signature covers content-digest but Content-Digest header is missing" };
|
|
218
|
+
}
|
|
219
|
+
const computedDigest = await computeContentDigest(params.body ?? "");
|
|
220
|
+
if (!computedDigest || !timingSafeEqualString(contentDigestHeader.trim(), computedDigest)) {
|
|
221
|
+
return { valid: false, error: `Content-Digest mismatch: expected ${computedDigest}, got ${contentDigestHeader}` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const signatureBase = createSignatureBase({
|
|
225
|
+
method: params.method,
|
|
226
|
+
path: params.path,
|
|
227
|
+
authority: params.authority,
|
|
228
|
+
contentDigest: signatureParams.includes('"content-digest"') ? contentDigestHeader?.trim() : null,
|
|
229
|
+
signatureParams,
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
const { publicKey } = await loadPublicKey(params.publicKey);
|
|
233
|
+
const signatureBytes = signatureBase64.includes("-") || signatureBase64.includes("_")
|
|
234
|
+
? base64UrlToBytes(signatureBase64)
|
|
235
|
+
: base64ToBytes(signatureBase64);
|
|
236
|
+
const isValid = await globalThis.crypto.subtle.verify("Ed25519", publicKey, signatureBytes, stringToUtf8(signatureBase));
|
|
237
|
+
if (!isValid) {
|
|
238
|
+
return { valid: false, error: "Cryptographic signature verification failed" };
|
|
239
|
+
}
|
|
240
|
+
return { valid: true, keyid };
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return { valid: false, error: `Verification error: ${err.message}` };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Zero-egress offline verification for asymmetric JWT token leases.
|
|
248
|
+
*/
|
|
249
|
+
export async function verifyJwtLease(params) {
|
|
250
|
+
try {
|
|
251
|
+
const parts = params.token.trim().split(".");
|
|
252
|
+
if (parts.length !== 3) {
|
|
253
|
+
return { valid: false, error: "Token is not a valid 3-part JWT" };
|
|
254
|
+
}
|
|
255
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
256
|
+
const headerJson = utf8ToString(base64UrlToBytes(headerB64));
|
|
257
|
+
const payloadJson = utf8ToString(base64UrlToBytes(payloadB64));
|
|
258
|
+
const header = JSON.parse(headerJson);
|
|
259
|
+
const payload = JSON.parse(payloadJson);
|
|
260
|
+
if (header.alg !== "EdDSA" && header.alg !== "ed25519") {
|
|
261
|
+
return { valid: false, error: `Unsupported JWT alg: ${header.alg}` };
|
|
262
|
+
}
|
|
263
|
+
const { publicKey } = await loadPublicKey(params.publicKey);
|
|
264
|
+
const signedData = stringToUtf8(`${headerB64}.${payloadB64}`);
|
|
265
|
+
const signatureBytes = base64UrlToBytes(signatureB64);
|
|
266
|
+
const isValid = await globalThis.crypto.subtle.verify("Ed25519", publicKey, signatureBytes, signedData);
|
|
267
|
+
if (!isValid) {
|
|
268
|
+
return { valid: false, error: "JWT signature verification failed" };
|
|
269
|
+
}
|
|
270
|
+
const now = params.currentTime ?? Math.floor(Date.now() / 1000);
|
|
271
|
+
if (payload.exp && typeof payload.exp === "number" && payload.exp < now) {
|
|
272
|
+
return { valid: false, error: "JWT token lease has expired" };
|
|
273
|
+
}
|
|
274
|
+
if (payload.nbf && typeof payload.nbf === "number" && payload.nbf > now + 60) {
|
|
275
|
+
return { valid: false, error: "JWT token lease not active yet" };
|
|
276
|
+
}
|
|
277
|
+
if (params.expectedServiceSlug) {
|
|
278
|
+
const tokenSlug = (payload.serviceSlug || payload.aud || "").toLowerCase();
|
|
279
|
+
if (tokenSlug && tokenSlug !== params.expectedServiceSlug.toLowerCase()) {
|
|
280
|
+
return {
|
|
281
|
+
valid: false,
|
|
282
|
+
error: `JWT audience mismatch: expected ${params.expectedServiceSlug}, got ${tokenSlug}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { valid: true, payload };
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return { valid: false, error: `Failed to verify JWT lease: ${err.message}` };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Register public key directly with Daywalker Auth via POST /api/v1/keys/register (Push Architecture).
|
|
294
|
+
*/
|
|
295
|
+
export async function registerPublicKey(params) {
|
|
296
|
+
const authUrl = (params.authUrl ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
297
|
+
const registerUrl = `${authUrl}/api/v1/keys/register`;
|
|
298
|
+
const doFetch = params.fetchFn ?? globalThis.fetch;
|
|
299
|
+
try {
|
|
300
|
+
const res = await doFetch(registerUrl, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
"Content-Type": "application/json",
|
|
304
|
+
Accept: "application/json",
|
|
305
|
+
Authorization: `Bearer ${params.apiKey}`,
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
keyId: params.publicJwk.kid,
|
|
309
|
+
domain: params.domain ?? undefined,
|
|
310
|
+
serviceSlug: params.serviceSlug ?? undefined,
|
|
311
|
+
publicKey: params.publicJwk,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
if (res.ok) {
|
|
315
|
+
return { ok: true, status: res.status };
|
|
316
|
+
}
|
|
317
|
+
const data = (await res.json().catch(() => ({})));
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
status: res.status,
|
|
321
|
+
error: data.message || data.error || `HTTP ${res.status}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
status: 0,
|
|
328
|
+
error: err.message,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Daywalker } from "./client.js";
|
|
2
|
+
import type { TokenPayload } from "./types.js";
|
|
3
|
+
interface Req {
|
|
4
|
+
method?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
8
|
+
get?: (name: string) => string | undefined;
|
|
9
|
+
auth?: TokenPayload;
|
|
10
|
+
}
|
|
11
|
+
interface Res {
|
|
12
|
+
setHeader(name: string, value: string): unknown;
|
|
13
|
+
status(code: number): Res;
|
|
14
|
+
json(body: unknown): unknown;
|
|
15
|
+
statusCode?: number;
|
|
16
|
+
end(body?: string): unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Express / Connect / Node http middleware that validates incoming Daywalker tokens.
|
|
20
|
+
*/
|
|
21
|
+
export declare function daywalkerAuth(options?: {
|
|
22
|
+
client?: Daywalker;
|
|
23
|
+
}): (req: Req, res: Res, next?: (err?: unknown) => void) => Promise<void>;
|
|
24
|
+
export declare function daywalkerKeys(client?: Daywalker): (req: Req, res: Res, next?: () => void) => Promise<void>;
|
|
25
|
+
export {};
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { daywalker } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Express / Connect / Node http middleware that validates incoming Daywalker tokens.
|
|
4
|
+
*/
|
|
5
|
+
export function daywalkerAuth(options = {}) {
|
|
6
|
+
const client = options.client ?? daywalker;
|
|
7
|
+
return async (req, res, next) => {
|
|
8
|
+
const getHeader = (name) => {
|
|
9
|
+
if (typeof req.get === "function")
|
|
10
|
+
return req.get(name);
|
|
11
|
+
if (req.headers) {
|
|
12
|
+
const val = req.headers[name.toLowerCase()] ?? req.headers[name];
|
|
13
|
+
return Array.isArray(val) ? val[0] : val;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
17
|
+
const authHeader = getHeader("authorization");
|
|
18
|
+
const apiTokenHeader = getHeader("x-api-token");
|
|
19
|
+
let token = null;
|
|
20
|
+
if (authHeader && authHeader.toLowerCase().startsWith("bearer ")) {
|
|
21
|
+
token = authHeader.slice(7).trim();
|
|
22
|
+
}
|
|
23
|
+
else if (apiTokenHeader) {
|
|
24
|
+
token = apiTokenHeader.trim();
|
|
25
|
+
}
|
|
26
|
+
if (!token) {
|
|
27
|
+
if (typeof res.status === "function" && typeof res.json === "function") {
|
|
28
|
+
res.status(401).json({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
res.statusCode = 401;
|
|
32
|
+
res.setHeader("Content-Type", "application/json");
|
|
33
|
+
res.end(JSON.stringify({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const result = await client.validateToken(token);
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
const status = result.status === 429 ? 429 : 401;
|
|
39
|
+
if (result.retryAfter) {
|
|
40
|
+
res.setHeader("Retry-After", String(result.retryAfter));
|
|
41
|
+
}
|
|
42
|
+
if (typeof res.status === "function" && typeof res.json === "function") {
|
|
43
|
+
res.status(status).json({ error: result.error, message: result.message, retryAfter: result.retryAfter });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
res.statusCode = status;
|
|
47
|
+
res.setHeader("Content-Type", "application/json");
|
|
48
|
+
res.end(JSON.stringify({ error: result.error, message: result.message, retryAfter: result.retryAfter }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
req.auth = result.data;
|
|
52
|
+
next?.();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function daywalkerKeys(client = daywalker) {
|
|
56
|
+
return async (req, res, next) => {
|
|
57
|
+
const path = req.path ?? (req.url ?? "").split("?")[0];
|
|
58
|
+
if ((req.method ?? "GET") === "GET" && path === "/.well-known/daywalker-keys.json") {
|
|
59
|
+
const keysDoc = await client.keys();
|
|
60
|
+
res.statusCode = 200;
|
|
61
|
+
res.setHeader("Content-Type", "application/json");
|
|
62
|
+
res.setHeader("Cache-Control", "public, max-age=300");
|
|
63
|
+
res.end(JSON.stringify(keysDoc));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
next?.();
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Daywalker, createDaywalker, daywalker, } from "./client.js";
|
|
2
|
+
export { DaywalkerError, DaywalkerAuthError, DaywalkerRateLimitError, } from "./types.js";
|
|
3
|
+
export type { DaywalkerConfig, RateLimitWindow, TokenPayload, ValidateErrorCode, ValidateResult, RetryOptions, SelfCheckResult, PublicJwk, SigningKey, } from "./types.js";
|
|
4
|
+
export { resolveConfig, publicHostname, } from "./config.js";
|
|
5
|
+
export type { ResolvedConfig, } from "./config.js";
|
|
6
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
|
7
|
+
export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, } from "./crypto/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Daywalker, createDaywalker, daywalker, } from "./client.js";
|
|
2
|
+
export { DaywalkerError, DaywalkerAuthError, DaywalkerRateLimitError, } from "./types.js";
|
|
3
|
+
export { resolveConfig, publicHostname, } from "./config.js";
|
|
4
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Daywalker } from "./client.js";
|
|
2
|
+
export interface DaywalkerMiddlewareOptions {
|
|
3
|
+
client?: Daywalker;
|
|
4
|
+
serviceSlug?: string;
|
|
5
|
+
headerName?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Standard Web / Hono compatible middleware.
|
|
9
|
+
* Works with Hono, standard Web Request/Response routers, and middleware pipelines.
|
|
10
|
+
*
|
|
11
|
+
* Example in Hono:
|
|
12
|
+
* import { Hono } from "hono";
|
|
13
|
+
* import { daywalkerAuth } from "@shelbyb/daywalker-sdk/middleware";
|
|
14
|
+
*
|
|
15
|
+
* const app = new Hono();
|
|
16
|
+
* app.use("/api/*", daywalkerAuth());
|
|
17
|
+
* app.get("/api/me", (c) => c.json({ auth: c.get("auth") }));
|
|
18
|
+
*/
|
|
19
|
+
export declare function daywalkerAuth(options?: DaywalkerMiddlewareOptions): (c: any, next: () => Promise<Response | void | undefined>) => Promise<Response | void>;
|
|
20
|
+
export { daywalkerAuth as daywalkerMiddleware };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { daywalker } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Standard Web / Hono compatible middleware.
|
|
4
|
+
* Works with Hono, standard Web Request/Response routers, and middleware pipelines.
|
|
5
|
+
*
|
|
6
|
+
* Example in Hono:
|
|
7
|
+
* import { Hono } from "hono";
|
|
8
|
+
* import { daywalkerAuth } from "@shelbyb/daywalker-sdk/middleware";
|
|
9
|
+
*
|
|
10
|
+
* const app = new Hono();
|
|
11
|
+
* app.use("/api/*", daywalkerAuth());
|
|
12
|
+
* app.get("/api/me", (c) => c.json({ auth: c.get("auth") }));
|
|
13
|
+
*/
|
|
14
|
+
export function daywalkerAuth(options = {}) {
|
|
15
|
+
const client = options.client ?? daywalker;
|
|
16
|
+
const serviceSlug = options.serviceSlug;
|
|
17
|
+
return async function daywalkerMiddleware(c, next) {
|
|
18
|
+
let authHeader;
|
|
19
|
+
let apiTokenHeader;
|
|
20
|
+
if (typeof c?.req?.header === "function") {
|
|
21
|
+
authHeader = c.req.header("authorization");
|
|
22
|
+
apiTokenHeader = c.req.header("x-api-token");
|
|
23
|
+
}
|
|
24
|
+
else if (c?.req?.headers instanceof Headers || c?.headers instanceof Headers) {
|
|
25
|
+
const headers = (c.req?.headers ?? c.headers);
|
|
26
|
+
authHeader = headers.get("authorization") ?? undefined;
|
|
27
|
+
apiTokenHeader = headers.get("x-api-token") ?? undefined;
|
|
28
|
+
}
|
|
29
|
+
else if (typeof c?.req?.headers === "object" || typeof c?.headers === "object") {
|
|
30
|
+
const headers = c.req?.headers ?? c.headers;
|
|
31
|
+
authHeader = headers["authorization"] || headers["Authorization"];
|
|
32
|
+
apiTokenHeader = headers["x-api-token"] || headers["X-Api-Token"];
|
|
33
|
+
}
|
|
34
|
+
let token = null;
|
|
35
|
+
if (authHeader && authHeader.toLowerCase().startsWith("bearer ")) {
|
|
36
|
+
token = authHeader.slice(7).trim();
|
|
37
|
+
}
|
|
38
|
+
else if (apiTokenHeader) {
|
|
39
|
+
token = apiTokenHeader.trim();
|
|
40
|
+
}
|
|
41
|
+
if (!token) {
|
|
42
|
+
if (typeof c?.json === "function") {
|
|
43
|
+
return c.json({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" }, 401);
|
|
44
|
+
}
|
|
45
|
+
return Response.json({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" }, { status: 401 });
|
|
46
|
+
}
|
|
47
|
+
const result = await client.validateToken(token, { serviceSlug });
|
|
48
|
+
if (!result.ok) {
|
|
49
|
+
const status = result.status === 429 ? 429 : 401;
|
|
50
|
+
const body = {
|
|
51
|
+
error: result.error,
|
|
52
|
+
message: result.message,
|
|
53
|
+
retryAfter: result.retryAfter,
|
|
54
|
+
};
|
|
55
|
+
const headers = result.retryAfter ? { "Retry-After": String(result.retryAfter) } : undefined;
|
|
56
|
+
if (typeof c?.json === "function") {
|
|
57
|
+
return c.json(body, status, headers);
|
|
58
|
+
}
|
|
59
|
+
return Response.json(body, { status, headers });
|
|
60
|
+
}
|
|
61
|
+
// Attach to context if available
|
|
62
|
+
if (typeof c?.set === "function") {
|
|
63
|
+
c.set("auth", result.data);
|
|
64
|
+
c.set("daywalkerAuth", result.data);
|
|
65
|
+
}
|
|
66
|
+
else if (c) {
|
|
67
|
+
c.auth = result.data;
|
|
68
|
+
}
|
|
69
|
+
return next();
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export { daywalkerAuth as daywalkerMiddleware };
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Daywalker } from "./client.js";
|
|
2
|
+
import type { TokenPayload } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Real Server Action IDs are hex hashes. Scanners probing for CVE-2025-55182 (React2Shell)
|
|
5
|
+
* send junk like `Next-Action: x`, which Next.js rejects with an internal error.
|
|
6
|
+
* Dropping them up front protects application logs and server resources.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SERVER_ACTION_ID_PATTERN: RegExp;
|
|
9
|
+
export declare const DEFAULT_BYPASS_PATHS: string[];
|
|
10
|
+
export interface NextMiddlewareOptions {
|
|
11
|
+
client?: Daywalker;
|
|
12
|
+
serviceSlug?: string;
|
|
13
|
+
bypassPaths?: string[];
|
|
14
|
+
errorPath?: string;
|
|
15
|
+
homePath?: string;
|
|
16
|
+
protectApiOnly?: boolean;
|
|
17
|
+
validateServerActions?: boolean;
|
|
18
|
+
allowedHosts?: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sanitizes open redirect path parameter (`from`).
|
|
22
|
+
* Prevents protocol-relative URLs (`//evil.com`), backslashes (`/\evil.com`), and control characters.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeRedirectPath(path: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Hardened Next.js Middleware for root `middleware.ts`.
|
|
27
|
+
*
|
|
28
|
+
* Implements:
|
|
29
|
+
* 1. Server Action probe defense (CVE-2025-55182 scanner mitigation)
|
|
30
|
+
* 2. Static asset and health check bypass
|
|
31
|
+
* 3. Self-healing service error page routing with `from` back-redirect
|
|
32
|
+
* 4. Distinct handling for API routes (JSON + CORS) vs UI pages (redirect)
|
|
33
|
+
*
|
|
34
|
+
* Example:
|
|
35
|
+
* // middleware.ts
|
|
36
|
+
* import { createDaywalkerMiddleware } from "@shelbyb/daywalker-sdk/next";
|
|
37
|
+
*
|
|
38
|
+
* export const middleware = createDaywalkerMiddleware();
|
|
39
|
+
*
|
|
40
|
+
* export const config = {
|
|
41
|
+
* matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
42
|
+
* };
|
|
43
|
+
*/
|
|
44
|
+
export declare function createDaywalkerMiddleware(options?: NextMiddlewareOptions): (request: Request) => Promise<Response>;
|
|
45
|
+
/**
|
|
46
|
+
* Next.js App Router route handler wrapper with Daywalker token validation.
|
|
47
|
+
* Extracts `Authorization: Bearer <token>` or `x-api-token`, validates via client,
|
|
48
|
+
* returns 401/429 JSON response on error, or passes validated `auth` payload in context.
|
|
49
|
+
*
|
|
50
|
+
* Example:
|
|
51
|
+
* // app/api/protected/route.ts
|
|
52
|
+
* import { withDaywalkerAuth } from "@shelbyb/daywalker-sdk/next";
|
|
53
|
+
*
|
|
54
|
+
* export const POST = withDaywalkerAuth(async (req, { auth }) => {
|
|
55
|
+
* return Response.json({ message: "Hello " + auth.serviceSlug, auth });
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
export declare function withDaywalkerAuth<Context extends Record<string, unknown> = Record<string, unknown>>(handler: (req: Request, context: Context & {
|
|
59
|
+
auth: TokenPayload;
|
|
60
|
+
}) => Promise<Response>, clientOrOptions?: Daywalker | {
|
|
61
|
+
client?: Daywalker;
|
|
62
|
+
serviceSlug?: string;
|
|
63
|
+
}): (req: Request, context?: Context) => Promise<Response>;
|