@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/next.d.ts
CHANGED
|
@@ -1,10 +1,63 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Daywalker } from "./client.js";
|
|
2
|
+
import type { TokenPayload } from "./types.js";
|
|
2
3
|
/**
|
|
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";
|
|
4
53
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
54
|
+
* export const POST = withDaywalkerAuth(async (req, { auth }) => {
|
|
55
|
+
* return Response.json({ message: "Hello " + auth.serviceSlug, auth });
|
|
56
|
+
* });
|
|
7
57
|
*/
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
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>;
|
package/dist/next.js
CHANGED
|
@@ -1,14 +1,189 @@
|
|
|
1
|
-
import { daywalker } from "./client.js";
|
|
1
|
+
import { daywalker, Daywalker } from "./client.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Real Server Action IDs are hex hashes. Scanners probing for CVE-2025-55182 (React2Shell)
|
|
4
|
+
* send junk like `Next-Action: x`, which Next.js rejects with an internal error.
|
|
5
|
+
* Dropping them up front protects application logs and server resources.
|
|
6
|
+
*/
|
|
7
|
+
export const SERVER_ACTION_ID_PATTERN = /^[0-9a-f]{40,}$/i;
|
|
8
|
+
export const DEFAULT_BYPASS_PATHS = [
|
|
9
|
+
"/api/health",
|
|
10
|
+
"/healthz",
|
|
11
|
+
"/favicon.ico",
|
|
12
|
+
"/robots.txt",
|
|
13
|
+
"/sitemap.xml",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Sanitizes open redirect path parameter (`from`).
|
|
17
|
+
* Prevents protocol-relative URLs (`//evil.com`), backslashes (`/\evil.com`), and control characters.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeRedirectPath(path) {
|
|
20
|
+
if (!path || typeof path !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
const clean = path.trim().replace(/[\r\n\0]/g, "");
|
|
23
|
+
if (!clean.startsWith("/") || clean.startsWith("//") || clean.startsWith("/\\")) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return clean;
|
|
27
|
+
}
|
|
28
|
+
function extractToken(req) {
|
|
29
|
+
const authHeader = req.headers.get("authorization");
|
|
30
|
+
if (authHeader && authHeader.toLowerCase().startsWith("bearer ")) {
|
|
31
|
+
return authHeader.slice(7).trim();
|
|
32
|
+
}
|
|
33
|
+
const apiTokenHeader = req.headers.get("x-api-token");
|
|
34
|
+
if (apiTokenHeader) {
|
|
35
|
+
return apiTokenHeader.trim();
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hardened Next.js Middleware for root `middleware.ts`.
|
|
41
|
+
*
|
|
42
|
+
* Implements:
|
|
43
|
+
* 1. Server Action probe defense (CVE-2025-55182 scanner mitigation)
|
|
44
|
+
* 2. Static asset and health check bypass
|
|
45
|
+
* 3. Self-healing service error page routing with `from` back-redirect
|
|
46
|
+
* 4. Distinct handling for API routes (JSON + CORS) vs UI pages (redirect)
|
|
47
|
+
*
|
|
48
|
+
* Example:
|
|
49
|
+
* // middleware.ts
|
|
50
|
+
* import { createDaywalkerMiddleware } from "@shelbyb/daywalker-sdk/next";
|
|
51
|
+
*
|
|
52
|
+
* export const middleware = createDaywalkerMiddleware();
|
|
53
|
+
*
|
|
54
|
+
* export const config = {
|
|
55
|
+
* matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
56
|
+
* };
|
|
57
|
+
*/
|
|
58
|
+
export function createDaywalkerMiddleware(options = {}) {
|
|
59
|
+
const client = options.client ?? daywalker;
|
|
60
|
+
const bypassPaths = options.bypassPaths ?? DEFAULT_BYPASS_PATHS;
|
|
61
|
+
const errorPath = options.errorPath ?? "/service-error";
|
|
62
|
+
const homePath = options.homePath ?? "/";
|
|
63
|
+
const validateServerActions = options.validateServerActions ?? true;
|
|
64
|
+
return async function middleware(request) {
|
|
65
|
+
const url = new URL(request.url);
|
|
66
|
+
const pathname = url.pathname;
|
|
67
|
+
const search = url.search;
|
|
68
|
+
// Host header validation if allowedHosts configured
|
|
69
|
+
if (options.allowedHosts && options.allowedHosts.length > 0) {
|
|
70
|
+
const host = request.headers.get("host")?.toLowerCase().replace(/:\d+$/, "");
|
|
71
|
+
if (host && !options.allowedHosts.some((h) => h.toLowerCase() === host)) {
|
|
72
|
+
return Response.json({ error: "Forbidden", message: "Invalid host header." }, { status: 403 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 0. Reject malformed Server Action requests before routing or auth checks
|
|
76
|
+
if (validateServerActions) {
|
|
77
|
+
const actionId = request.headers.get("next-action");
|
|
78
|
+
if (actionId !== null && (actionId.length > 256 || !SERVER_ACTION_ID_PATTERN.test(actionId.trim()))) {
|
|
79
|
+
return Response.json({ error: "Bad Request", message: "Invalid Server Action request." }, { status: 400 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 1. Bypass static files and health check probes
|
|
83
|
+
if (bypassPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`)) ||
|
|
84
|
+
pathname.startsWith("/_next") ||
|
|
85
|
+
(pathname.includes(".") && !pathname.endsWith(".json"))) {
|
|
86
|
+
return globalThis.NextResponse?.next
|
|
87
|
+
? globalThis.NextResponse.next()
|
|
88
|
+
: new Response(null, { headers: { "x-middleware-next": "1" } });
|
|
89
|
+
}
|
|
90
|
+
// 2. Validate token (from request header, or service's configured DAYWALKER_API_KEY)
|
|
91
|
+
const incomingToken = extractToken(request);
|
|
92
|
+
const authResult = await client.validateToken(incomingToken ? { token: incomingToken, serviceSlug: options.serviceSlug } : { serviceSlug: options.serviceSlug });
|
|
93
|
+
// 3. Handle service error page route
|
|
94
|
+
if (pathname === errorPath) {
|
|
95
|
+
if (authResult.ok) {
|
|
96
|
+
// If the service token is now valid, heal/redirect back to home or referer
|
|
97
|
+
const redirectUrl = new URL(homePath, request.url);
|
|
98
|
+
return globalThis.NextResponse?.redirect
|
|
99
|
+
? globalThis.NextResponse.redirect(redirectUrl)
|
|
100
|
+
: Response.redirect(redirectUrl.toString(), 307);
|
|
101
|
+
}
|
|
102
|
+
// Allow rendering the error page
|
|
103
|
+
return globalThis.NextResponse?.next
|
|
104
|
+
? globalThis.NextResponse.next()
|
|
105
|
+
: new Response(null, { headers: { "x-middleware-next": "1" } });
|
|
106
|
+
}
|
|
107
|
+
// 4. Handle invalid token for all other routes
|
|
108
|
+
if (!authResult.ok) {
|
|
109
|
+
// API routes: return JSON error response
|
|
110
|
+
if (pathname.startsWith("/api/")) {
|
|
111
|
+
return Response.json({
|
|
112
|
+
error: authResult.error || "Unauthorized",
|
|
113
|
+
message: authResult.message || "Service authorization check failed with Daywalker Auth.",
|
|
114
|
+
retryAfter: authResult.retryAfter,
|
|
115
|
+
}, {
|
|
116
|
+
status: authResult.status || 401,
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"Access-Control-Allow-Origin": "*",
|
|
120
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
121
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Api-Token",
|
|
122
|
+
...(authResult.retryAfter ? { "Retry-After": String(authResult.retryAfter) } : {}),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// If user only wanted API protection, allow web routes through
|
|
127
|
+
if (options.protectApiOnly) {
|
|
128
|
+
return globalThis.NextResponse?.next
|
|
129
|
+
? globalThis.NextResponse.next()
|
|
130
|
+
: new Response(null, { headers: { "x-middleware-next": "1" } });
|
|
131
|
+
}
|
|
132
|
+
// Web UI routes: redirect to diagnostic service error screen
|
|
133
|
+
const redirectUrl = new URL(errorPath, request.url);
|
|
134
|
+
redirectUrl.searchParams.set("error", authResult.error || "Unauthorized");
|
|
135
|
+
redirectUrl.searchParams.set("message", authResult.message || "Service authorization check failed.");
|
|
136
|
+
redirectUrl.searchParams.set("status", String(authResult.status || 401));
|
|
137
|
+
if (pathname !== "/" || search) {
|
|
138
|
+
const rawTarget = `${pathname}${search}`;
|
|
139
|
+
const sanitized = sanitizeRedirectPath(rawTarget);
|
|
140
|
+
if (sanitized) {
|
|
141
|
+
redirectUrl.searchParams.set("from", sanitized);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return globalThis.NextResponse?.redirect
|
|
145
|
+
? globalThis.NextResponse.redirect(redirectUrl)
|
|
146
|
+
: Response.redirect(redirectUrl.toString(), 307);
|
|
147
|
+
}
|
|
148
|
+
return globalThis.NextResponse?.next
|
|
149
|
+
? globalThis.NextResponse.next()
|
|
150
|
+
: new Response(null, { headers: { "x-middleware-next": "1" } });
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Next.js App Router route handler wrapper with Daywalker token validation.
|
|
155
|
+
* Extracts `Authorization: Bearer <token>` or `x-api-token`, validates via client,
|
|
156
|
+
* returns 401/429 JSON response on error, or passes validated `auth` payload in context.
|
|
157
|
+
*
|
|
158
|
+
* Example:
|
|
159
|
+
* // app/api/protected/route.ts
|
|
160
|
+
* import { withDaywalkerAuth } from "@shelbyb/daywalker-sdk/next";
|
|
4
161
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
162
|
+
* export const POST = withDaywalkerAuth(async (req, { auth }) => {
|
|
163
|
+
* return Response.json({ message: "Hello " + auth.serviceSlug, auth });
|
|
164
|
+
* });
|
|
7
165
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
166
|
+
export function withDaywalkerAuth(handler, clientOrOptions) {
|
|
167
|
+
const client = clientOrOptions instanceof Daywalker
|
|
168
|
+
? clientOrOptions
|
|
169
|
+
: clientOrOptions?.client ?? daywalker;
|
|
170
|
+
const serviceSlug = !(clientOrOptions instanceof Daywalker) ? clientOrOptions?.serviceSlug : undefined;
|
|
171
|
+
return async function daywalkerAuthHandler(req, context) {
|
|
172
|
+
const token = extractToken(req);
|
|
173
|
+
if (!token) {
|
|
174
|
+
return Response.json({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" }, { status: 401 });
|
|
175
|
+
}
|
|
176
|
+
const result = await client.validateToken(token, { serviceSlug });
|
|
177
|
+
if (!result.ok) {
|
|
178
|
+
return Response.json({ error: result.error, message: result.message, retryAfter: result.retryAfter }, {
|
|
179
|
+
status: result.status === 429 ? 429 : 401,
|
|
180
|
+
headers: result.retryAfter ? { "Retry-After": String(result.retryAfter) } : undefined,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const mergedContext = {
|
|
184
|
+
...(context ?? {}),
|
|
185
|
+
auth: result.data,
|
|
186
|
+
};
|
|
187
|
+
return handler(req, mergedContext);
|
|
12
188
|
};
|
|
13
189
|
}
|
|
14
|
-
export const GET = () => keysRouteHandler()();
|
package/dist/signing.d.ts
CHANGED
|
@@ -1,53 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Domain-signed requests to Daywalker Auth.
|
|
4
|
-
*
|
|
5
|
-
* Each request is signed with an Ed25519 key whose public half the calling service serves at
|
|
6
|
-
* https://<domain>/.well-known/daywalker-keys.json. Daywalker fetches that key over HTTPS, so
|
|
7
|
-
* a valid signature proves the request comes from whoever controls the domain, wherever the
|
|
8
|
-
* service is hosted. This must match Daywalker's verifier exactly (src/lib/request-signature.ts
|
|
9
|
-
* in daywalker-service-auth).
|
|
10
|
-
*/
|
|
11
|
-
export declare const SIGNATURE_VERSION = "daywalker-v1";
|
|
1
|
+
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./crypto/index.js";
|
|
12
2
|
export declare const KEYS_PATH = "/.well-known/daywalker-keys.json";
|
|
13
|
-
export
|
|
14
|
-
kty: "OKP";
|
|
15
|
-
crv: "Ed25519";
|
|
16
|
-
x: string;
|
|
17
|
-
kid: string;
|
|
18
|
-
use: "sig";
|
|
19
|
-
alg: "EdDSA";
|
|
20
|
-
}
|
|
21
|
-
export interface SigningKey {
|
|
22
|
-
privateKey: KeyObject;
|
|
23
|
-
publicJwk: PublicJwk;
|
|
24
|
-
}
|
|
25
|
-
/** Generate a new key; `privateKey` is base64 PKCS#8 DER, the format DAYWALKER_SIGNING_KEY expects. */
|
|
26
|
-
export declare function generateSigningKey(): {
|
|
27
|
-
privateKey: string;
|
|
28
|
-
publicJwk: PublicJwk;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Load a private key from env: PKCS#8 PEM (literal "\n" escapes are fine) or base64 PKCS#8 DER.
|
|
32
|
-
* The key id is the RFC 7638 JWK thumbprint, so it's stable without extra configuration.
|
|
33
|
-
*/
|
|
34
|
-
export declare function loadSigningKey(raw: string): SigningKey;
|
|
35
|
-
export declare function canonicalPayload(p: {
|
|
36
|
-
method: string;
|
|
37
|
-
path: string;
|
|
38
|
-
domain: string;
|
|
39
|
-
timestamp: string;
|
|
40
|
-
nonce: string;
|
|
41
|
-
body: string;
|
|
42
|
-
}): string;
|
|
43
|
-
/** The X-Daywalker-* headers for one request. Send exactly `body` as the request body. */
|
|
44
|
-
export declare function signRequest(params: {
|
|
45
|
-
method: string;
|
|
46
|
-
/** URL path only, e.g. /api/v1/tokens/validate */
|
|
47
|
-
path: string;
|
|
48
|
-
domain: string;
|
|
49
|
-
body: string;
|
|
50
|
-
key: SigningKey;
|
|
51
|
-
timestamp?: number;
|
|
52
|
-
nonce?: string;
|
|
53
|
-
}): Record<string, string>;
|
|
3
|
+
export type { PublicJwk, SigningKey, SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, } from "./types.js";
|
package/dist/signing.js
CHANGED
|
@@ -1,51 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Domain-signed requests to Daywalker Auth.
|
|
4
|
-
*
|
|
5
|
-
* Each request is signed with an Ed25519 key whose public half the calling service serves at
|
|
6
|
-
* https://<domain>/.well-known/daywalker-keys.json. Daywalker fetches that key over HTTPS, so
|
|
7
|
-
* a valid signature proves the request comes from whoever controls the domain, wherever the
|
|
8
|
-
* service is hosted. This must match Daywalker's verifier exactly (src/lib/request-signature.ts
|
|
9
|
-
* in daywalker-service-auth).
|
|
10
|
-
*/
|
|
11
|
-
export const SIGNATURE_VERSION = "daywalker-v1";
|
|
1
|
+
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, bytesToHex, hexToBytes, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./crypto/index.js";
|
|
12
2
|
export const KEYS_PATH = "/.well-known/daywalker-keys.json";
|
|
13
|
-
/** Generate a new key; `privateKey` is base64 PKCS#8 DER, the format DAYWALKER_SIGNING_KEY expects. */
|
|
14
|
-
export function generateSigningKey() {
|
|
15
|
-
const { privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
16
|
-
const der = privateKey.export({ format: "der", type: "pkcs8" }).toString("base64");
|
|
17
|
-
return { privateKey: der, publicJwk: loadSigningKey(der).publicJwk };
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Load a private key from env: PKCS#8 PEM (literal "\n" escapes are fine) or base64 PKCS#8 DER.
|
|
21
|
-
* The key id is the RFC 7638 JWK thumbprint, so it's stable without extra configuration.
|
|
22
|
-
*/
|
|
23
|
-
export function loadSigningKey(raw) {
|
|
24
|
-
const value = raw.trim();
|
|
25
|
-
const privateKey = value.includes("-----BEGIN")
|
|
26
|
-
? crypto.createPrivateKey(value.replace(/\\n/g, "\n"))
|
|
27
|
-
: crypto.createPrivateKey({ key: Buffer.from(value, "base64"), format: "der", type: "pkcs8" });
|
|
28
|
-
if (privateKey.asymmetricKeyType !== "ed25519") {
|
|
29
|
-
throw new Error(`DAYWALKER_SIGNING_KEY must be an Ed25519 key (got ${privateKey.asymmetricKeyType})`);
|
|
30
|
-
}
|
|
31
|
-
const { x } = crypto.createPublicKey(privateKey).export({ format: "jwk" });
|
|
32
|
-
const kid = crypto.createHash("sha256").update(JSON.stringify({ crv: "Ed25519", kty: "OKP", x })).digest("base64url");
|
|
33
|
-
return { privateKey, publicJwk: { kty: "OKP", crv: "Ed25519", x, kid, use: "sig", alg: "EdDSA" } };
|
|
34
|
-
}
|
|
35
|
-
export function canonicalPayload(p) {
|
|
36
|
-
const bodySha256 = crypto.createHash("sha256").update(p.body, "utf8").digest("hex");
|
|
37
|
-
return [SIGNATURE_VERSION, p.method.toUpperCase(), p.path, p.domain.toLowerCase(), p.timestamp, p.nonce, bodySha256].join("\n");
|
|
38
|
-
}
|
|
39
|
-
/** The X-Daywalker-* headers for one request. Send exactly `body` as the request body. */
|
|
40
|
-
export function signRequest(params) {
|
|
41
|
-
const timestamp = String(params.timestamp ?? Math.floor(Date.now() / 1000));
|
|
42
|
-
const nonce = params.nonce ?? crypto.randomBytes(24).toString("base64url");
|
|
43
|
-
const payload = canonicalPayload({ ...params, timestamp, nonce });
|
|
44
|
-
return {
|
|
45
|
-
"X-Daywalker-Domain": params.domain.toLowerCase(),
|
|
46
|
-
"X-Daywalker-Key-Id": params.key.publicJwk.kid,
|
|
47
|
-
"X-Daywalker-Timestamp": timestamp,
|
|
48
|
-
"X-Daywalker-Nonce": nonce,
|
|
49
|
-
"X-Daywalker-Signature": crypto.sign(null, Buffer.from(payload), params.key.privateKey).toString("base64url"),
|
|
50
|
-
};
|
|
51
|
-
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface RateLimitWindow {
|
|
2
|
+
limit: number;
|
|
3
|
+
remaining: number;
|
|
4
|
+
reset: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TokenPayload {
|
|
7
|
+
serviceSlug: string;
|
|
8
|
+
tenantId: string;
|
|
9
|
+
rateLimit: RateLimitWindow;
|
|
10
|
+
rateLimitHourly?: RateLimitWindow;
|
|
11
|
+
tokenId?: string;
|
|
12
|
+
tokenName?: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
stage?: string;
|
|
15
|
+
offlineVerified?: boolean;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export type ValidateErrorCode = "EXPIRED" | "REVOKED" | "INVALID_TOKEN" | "RATE_LIMITED" | "NETWORK_ERROR" | "CONFIGURATION_ERROR" | string;
|
|
19
|
+
export type ValidateResult = {
|
|
20
|
+
ok: true;
|
|
21
|
+
data: TokenPayload;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
error: ValidateErrorCode;
|
|
25
|
+
status: number;
|
|
26
|
+
message: string;
|
|
27
|
+
retryAfter?: number;
|
|
28
|
+
};
|
|
29
|
+
export interface PublicJwk {
|
|
30
|
+
kty: "OKP";
|
|
31
|
+
crv: "Ed25519";
|
|
32
|
+
x: string;
|
|
33
|
+
kid: string;
|
|
34
|
+
use?: "sig";
|
|
35
|
+
alg?: "EdDSA" | "ed25519";
|
|
36
|
+
}
|
|
37
|
+
export interface SigningKey {
|
|
38
|
+
privateKey: CryptoKey;
|
|
39
|
+
publicKey: CryptoKey;
|
|
40
|
+
publicJwk: PublicJwk;
|
|
41
|
+
privateKeyPkcs8Base64?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RetryOptions {
|
|
44
|
+
maxRetries?: number;
|
|
45
|
+
initialDelayMs?: number;
|
|
46
|
+
maxDelayMs?: number;
|
|
47
|
+
backoffFactor?: number;
|
|
48
|
+
jitter?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface DaywalkerConfig {
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
signingKey?: string | SigningKey;
|
|
53
|
+
previousSigningKey?: string | SigningKey;
|
|
54
|
+
keyId?: string;
|
|
55
|
+
serviceSlug?: string;
|
|
56
|
+
domain?: string;
|
|
57
|
+
daywalkerPublicKey?: string | PublicJwk;
|
|
58
|
+
authUrl?: string;
|
|
59
|
+
cacheTtlMs?: number;
|
|
60
|
+
maxRetries?: number;
|
|
61
|
+
retry?: RetryOptions;
|
|
62
|
+
fetch?: typeof fetch;
|
|
63
|
+
env?: Record<string, string | undefined>;
|
|
64
|
+
}
|
|
65
|
+
export interface SelfCheckResult {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
serviceSlug: string | null;
|
|
68
|
+
domain: string | null;
|
|
69
|
+
signing: boolean;
|
|
70
|
+
keyId: string | null;
|
|
71
|
+
offlineVerificationReady: boolean;
|
|
72
|
+
problems: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface SignedHeaders {
|
|
75
|
+
"Content-Digest"?: string;
|
|
76
|
+
"Signature-Input": string;
|
|
77
|
+
Signature: string;
|
|
78
|
+
Authorization?: string;
|
|
79
|
+
[key: string]: string | undefined;
|
|
80
|
+
}
|
|
81
|
+
export interface SignRequestParams {
|
|
82
|
+
method: string;
|
|
83
|
+
path: string;
|
|
84
|
+
authority: string;
|
|
85
|
+
body?: string | Uint8Array | null;
|
|
86
|
+
key: SigningKey | string;
|
|
87
|
+
created?: number;
|
|
88
|
+
keyid?: string;
|
|
89
|
+
alg?: string;
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
nonce?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface VerifySignatureParams {
|
|
94
|
+
method: string;
|
|
95
|
+
path: string;
|
|
96
|
+
authority: string;
|
|
97
|
+
headers: Record<string, string | undefined> | Headers;
|
|
98
|
+
body?: string | Uint8Array | null;
|
|
99
|
+
publicKey: PublicJwk | CryptoKey | string;
|
|
100
|
+
currentTime?: number;
|
|
101
|
+
maxClockSkewSeconds?: number;
|
|
102
|
+
replayCheck?: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface VerifySignatureResult {
|
|
105
|
+
valid: boolean;
|
|
106
|
+
keyid?: string;
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
export declare class DaywalkerError extends Error {
|
|
110
|
+
readonly status?: number;
|
|
111
|
+
readonly code?: string;
|
|
112
|
+
constructor(message: string, status?: number, code?: string);
|
|
113
|
+
toJSON(): Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
export declare class DaywalkerAuthError extends DaywalkerError {
|
|
116
|
+
constructor(message: string, status?: number, code?: ValidateErrorCode);
|
|
117
|
+
}
|
|
118
|
+
export declare class DaywalkerRateLimitError extends DaywalkerError {
|
|
119
|
+
readonly retryAfter?: number;
|
|
120
|
+
constructor(message: string, retryAfter?: number);
|
|
121
|
+
toJSON(): Record<string, unknown>;
|
|
122
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { redactSecrets } from "./crypto/encoding.js";
|
|
2
|
+
export class DaywalkerError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
constructor(message, status, code) {
|
|
6
|
+
const cleanMessage = redactSecrets(message);
|
|
7
|
+
super(cleanMessage);
|
|
8
|
+
this.name = "DaywalkerError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
12
|
+
}
|
|
13
|
+
toJSON() {
|
|
14
|
+
return {
|
|
15
|
+
name: this.name,
|
|
16
|
+
message: redactSecrets(this.message),
|
|
17
|
+
status: this.status,
|
|
18
|
+
code: this.code,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
22
|
+
return this.toJSON();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class DaywalkerAuthError extends DaywalkerError {
|
|
26
|
+
constructor(message, status = 401, code = "INVALID_TOKEN") {
|
|
27
|
+
super(message, status, code);
|
|
28
|
+
this.name = "DaywalkerAuthError";
|
|
29
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class DaywalkerRateLimitError extends DaywalkerError {
|
|
33
|
+
retryAfter;
|
|
34
|
+
constructor(message, retryAfter) {
|
|
35
|
+
super(message, 429, "RATE_LIMITED");
|
|
36
|
+
this.name = "DaywalkerRateLimitError";
|
|
37
|
+
this.retryAfter = retryAfter;
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
}
|
|
40
|
+
toJSON() {
|
|
41
|
+
return {
|
|
42
|
+
...super.toJSON(),
|
|
43
|
+
retryAfter: this.retryAfter,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shelbyb/daywalker-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Validate Daywalker API tokens and make domain-signed calls to Daywalker Auth from any service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,16 +14,21 @@
|
|
|
14
14
|
"types": "./dist/next.d.ts",
|
|
15
15
|
"import": "./dist/next.js"
|
|
16
16
|
},
|
|
17
|
+
"./middleware": {
|
|
18
|
+
"types": "./dist/middleware.d.ts",
|
|
19
|
+
"import": "./dist/middleware.js"
|
|
20
|
+
},
|
|
17
21
|
"./express": {
|
|
18
22
|
"types": "./dist/express.d.ts",
|
|
19
23
|
"import": "./dist/express.js"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"bin": {
|
|
23
|
-
"daywalker": "
|
|
27
|
+
"daywalker": "dist/cli.js"
|
|
24
28
|
},
|
|
25
29
|
"files": [
|
|
26
30
|
"dist",
|
|
31
|
+
"assets",
|
|
27
32
|
"README.md",
|
|
28
33
|
"LICENSE"
|
|
29
34
|
],
|
|
@@ -33,7 +38,8 @@
|
|
|
33
38
|
"scripts": {
|
|
34
39
|
"build": "tsc -p tsconfig.json",
|
|
35
40
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
-
"
|
|
41
|
+
"test": "npm run build && node --import tsx --test test/*.test.ts",
|
|
42
|
+
"prepublishOnly": "npm test"
|
|
37
43
|
},
|
|
38
44
|
"publishConfig": {
|
|
39
45
|
"access": "public",
|
|
@@ -42,13 +48,13 @@
|
|
|
42
48
|
},
|
|
43
49
|
"repository": {
|
|
44
50
|
"type": "git",
|
|
45
|
-
"url": "git+https://github.com/Shelbyb/daywalker-
|
|
46
|
-
"directory": "packages/sdk"
|
|
51
|
+
"url": "git+https://github.com/Shelbyb/daywalker-sdk.git"
|
|
47
52
|
},
|
|
48
53
|
"license": "MIT",
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@types/node": "^22.13.5",
|
|
51
|
-
"typescript": "^5.9.3"
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"tsx": "^4.23.15"
|
|
52
58
|
},
|
|
53
59
|
"keywords": [
|
|
54
60
|
"daywalker",
|
|
@@ -57,5 +63,8 @@
|
|
|
57
63
|
"ed25519",
|
|
58
64
|
"request-signing"
|
|
59
65
|
],
|
|
60
|
-
"homepage": "https://auth.daywalker.dev/docs"
|
|
66
|
+
"homepage": "https://auth.daywalker.dev/docs",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/Shelbyb/daywalker-sdk/issues"
|
|
69
|
+
}
|
|
61
70
|
}
|