@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/client.d.ts
CHANGED
|
@@ -1,78 +1,60 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type PublicJwk } from "./
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error?: string;
|
|
21
|
-
message?: string;
|
|
22
|
-
}
|
|
23
|
-
export interface ValidationResult {
|
|
24
|
-
valid: boolean;
|
|
25
|
-
/** HTTP status from Daywalker, or 503 when it couldn't be reached */
|
|
26
|
-
status: number;
|
|
27
|
-
data: TokenValidation;
|
|
28
|
-
/** Seconds to wait before retrying (429 responses) */
|
|
29
|
-
retryAfter?: number;
|
|
30
|
-
/** Whether the request carried a domain signature */
|
|
31
|
-
signed: boolean;
|
|
32
|
-
}
|
|
33
|
-
export interface SelfCheckResult {
|
|
34
|
-
ok: boolean;
|
|
35
|
-
domain: string | null;
|
|
36
|
-
keysUrl: string | null;
|
|
37
|
-
signing: boolean;
|
|
38
|
-
problems: string[];
|
|
39
|
-
}
|
|
40
|
-
export declare class DaywalkerError extends Error {
|
|
41
|
-
readonly status: number;
|
|
42
|
-
readonly result?: ValidationResult | undefined;
|
|
43
|
-
constructor(message: string, status: number, result?: ValidationResult | undefined);
|
|
44
|
-
}
|
|
45
|
-
export interface Daywalker {
|
|
46
|
-
readonly config: ResolvedConfig;
|
|
1
|
+
import { type ResolvedConfig } from "./config.js";
|
|
2
|
+
import { type DaywalkerConfig, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
|
|
3
|
+
export type DaywalkerFetchInit = Omit<RequestInit, "body"> & {
|
|
4
|
+
body?: BodyInit | object | null;
|
|
5
|
+
retry?: RetryOptions;
|
|
6
|
+
};
|
|
7
|
+
export declare class Daywalker {
|
|
8
|
+
private readonly _config;
|
|
9
|
+
private readonly _rawConfig;
|
|
10
|
+
private _signingKeyPromise?;
|
|
11
|
+
private readonly _cache;
|
|
12
|
+
constructor(config?: DaywalkerConfig);
|
|
13
|
+
get config(): ResolvedConfig;
|
|
14
|
+
toJSON(): Record<string, unknown>;
|
|
15
|
+
private getLoadedSigningKey;
|
|
16
|
+
/**
|
|
17
|
+
* Signs and executes requests with automatic retry and exponential backoff on 429 responses.
|
|
18
|
+
*/
|
|
19
|
+
fetch(path: string, init?: DaywalkerFetchInit): Promise<Response>;
|
|
47
20
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
21
|
+
* Validates incoming bearer tokens with memory caching and discriminated union return.
|
|
22
|
+
* Supports zero-egress offline verification for JWT token leases when Daywalker's public key is configured.
|
|
50
23
|
*/
|
|
51
|
-
validateToken(
|
|
24
|
+
validateToken(tokenOrOptions?: string | {
|
|
52
25
|
token?: string;
|
|
53
26
|
serviceSlug?: string;
|
|
54
27
|
forceRefresh?: boolean;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
offlineOnly?: boolean;
|
|
29
|
+
}, options?: {
|
|
30
|
+
serviceSlug?: string;
|
|
31
|
+
forceRefresh?: boolean;
|
|
32
|
+
offlineOnly?: boolean;
|
|
33
|
+
}): Promise<ValidateResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Throws DaywalkerAuthError or DaywalkerRateLimitError if token is invalid or rate limited.
|
|
36
|
+
*/
|
|
37
|
+
requireValidToken(tokenOrOptions?: string | {
|
|
58
38
|
token?: string;
|
|
59
39
|
serviceSlug?: string;
|
|
60
|
-
|
|
40
|
+
offlineOnly?: boolean;
|
|
41
|
+
}, options?: {
|
|
42
|
+
serviceSlug?: string;
|
|
43
|
+
offlineOnly?: boolean;
|
|
44
|
+
}): Promise<TokenPayload>;
|
|
61
45
|
/**
|
|
62
|
-
*
|
|
63
|
-
* when a signing key and domain are configured. Pass the body as a string, or as an object
|
|
64
|
-
* to send JSON.
|
|
46
|
+
* The public JWK(s) configured for this service.
|
|
65
47
|
*/
|
|
66
|
-
|
|
67
|
-
body?: string | object;
|
|
68
|
-
}): Promise<Response>;
|
|
69
|
-
/** The JWKS document to serve at /.well-known/daywalker-keys.json */
|
|
70
|
-
keys(): {
|
|
48
|
+
keys(): Promise<{
|
|
71
49
|
keys: PublicJwk[];
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Check configuration and verify signature readiness.
|
|
53
|
+
*/
|
|
74
54
|
selfCheck(): Promise<SelfCheckResult>;
|
|
75
55
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Factory function for creating instantiable Daywalker client instances.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createDaywalker(config?: DaywalkerConfig): Daywalker;
|
|
60
|
+
export declare const daywalker: Daywalker & (() => Daywalker);
|
package/dist/client.js
CHANGED
|
@@ -1,143 +1,376 @@
|
|
|
1
1
|
import { resolveConfig } from "./config.js";
|
|
2
|
-
import { signRequest,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.name = "DaywalkerError";
|
|
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;
|
|
11
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;
|
|
12
17
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
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 = {
|
|
43
115
|
...init,
|
|
44
116
|
method,
|
|
45
117
|
headers,
|
|
46
|
-
body:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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;
|
|
52
148
|
}
|
|
53
149
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
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()) {
|
|
64
182
|
return cached.result;
|
|
65
|
-
|
|
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
|
|
66
235
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
};
|
|
71
291
|
}
|
|
72
292
|
catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
error: "NETWORK_ERROR",
|
|
76
296
|
status: 503,
|
|
77
|
-
|
|
78
|
-
data: {
|
|
79
|
-
valid: false,
|
|
80
|
-
error: timedOut ? "Request Timeout" : "Service Unavailable",
|
|
81
|
-
message: timedOut ? "Daywalker Auth did not respond in time" : `Could not reach Daywalker Auth: ${err.message}`,
|
|
82
|
-
},
|
|
297
|
+
message: redactSecrets(`Could not reach Daywalker Auth: ${err.message}`),
|
|
83
298
|
};
|
|
84
299
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
91
311
|
}
|
|
92
|
-
return result;
|
|
312
|
+
return result.data;
|
|
93
313
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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] : [] };
|
|
99
320
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
else {
|
|
112
|
-
const doc = (await res.json().catch(() => null));
|
|
113
|
-
if (!doc?.keys?.some((k) => k.kid === config.signingKey.publicJwk.kid)) {
|
|
114
|
-
problems.push(`${keysUrl} doesn't publish this service's current key (kid ${config.signingKey.publicJwk.kid})`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
problems.push(`Could not fetch ${keysUrl}: ${err.message}`);
|
|
120
|
-
}
|
|
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");
|
|
121
332
|
}
|
|
122
|
-
|
|
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
|
+
};
|
|
123
344
|
}
|
|
124
|
-
return {
|
|
125
|
-
config,
|
|
126
|
-
validateToken,
|
|
127
|
-
async requireValidToken(opts = {}) {
|
|
128
|
-
const result = await validateToken(opts);
|
|
129
|
-
if (!result.valid)
|
|
130
|
-
throw new DaywalkerError(result.data.message ?? "Token validation failed", result.status, result);
|
|
131
|
-
return result;
|
|
132
|
-
},
|
|
133
|
-
fetch: signedFetch,
|
|
134
|
-
keys,
|
|
135
|
-
selfCheck,
|
|
136
|
-
};
|
|
137
345
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return
|
|
346
|
+
/**
|
|
347
|
+
* Factory function for creating instantiable Daywalker client instances.
|
|
348
|
+
*/
|
|
349
|
+
export function createDaywalker(config) {
|
|
350
|
+
return new Daywalker(config);
|
|
143
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
|
+
});
|