@shelbyb/daywalker-sdk 0.1.3 → 0.1.4
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 +3 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +31 -3
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,10 +210,12 @@ Inbound tokens may be supplied as `x-api-token: srv_live_...` or `Authorization:
|
|
|
210
210
|
|
|
211
211
|
Each outbound request signed by `@shelbyb/daywalker-sdk` includes:
|
|
212
212
|
- `Content-Digest: sha-256=:<base64-encoded-hash>:`
|
|
213
|
-
- `Signature-Input: sig1=("@method" "@path" "@authority" "content-digest");created=<unix_seconds>;keyid="<kid>";alg="ed25519"`
|
|
213
|
+
- `Signature-Input: sig1=("@method" "@path" "@authority" "content-digest");created=<unix_seconds>;keyid="<kid>";alg="ed25519";nonce="<random>"`
|
|
214
214
|
- `Signature: sig1=:<base64-encoded-signature>:`
|
|
215
215
|
- `Authorization: Bearer <DAYWALKER_API_KEY>`
|
|
216
216
|
|
|
217
|
+
A fresh random `nonce` is generated for every attempt (including retries), so identical requests in the same second still produce unique signatures and never trip replay protection. `validateToken()` also shares one in-flight request between concurrent callers for the same token, and briefly caches failures (`negativeCacheTtlMs`, default 3000ms; 429s and network errors are never cached).
|
|
218
|
+
|
|
217
219
|
The exact signature base and test cases are verified against [`test-vectors/signing-rfc9421.json`](./test-vectors/signing-rfc9421.json).
|
|
218
220
|
|
|
219
221
|
---
|
package/dist/client.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class Daywalker {
|
|
|
9
9
|
private readonly _rawConfig;
|
|
10
10
|
private _signingKeyPromise?;
|
|
11
11
|
private readonly _cache;
|
|
12
|
+
private readonly _inflight;
|
|
12
13
|
constructor(config?: DaywalkerConfig);
|
|
13
14
|
get config(): ResolvedConfig;
|
|
14
15
|
toJSON(): Record<string, unknown>;
|
|
@@ -31,6 +32,7 @@ export declare class Daywalker {
|
|
|
31
32
|
forceRefresh?: boolean;
|
|
32
33
|
offlineOnly?: boolean;
|
|
33
34
|
}): Promise<ValidateResult>;
|
|
35
|
+
private _validateOnline;
|
|
34
36
|
/**
|
|
35
37
|
* Throws DaywalkerAuthError or DaywalkerRateLimitError if token is invalid or rate limited.
|
|
36
38
|
*/
|
package/dist/client.js
CHANGED
|
@@ -15,11 +15,20 @@ function parseRetryAfter(headerValue) {
|
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
|
+
function generateNonce() {
|
|
19
|
+
const bytes = new Uint8Array(16);
|
|
20
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
21
|
+
let bin = "";
|
|
22
|
+
for (const b of bytes)
|
|
23
|
+
bin += String.fromCharCode(b);
|
|
24
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
+
}
|
|
18
26
|
export class Daywalker {
|
|
19
27
|
_config;
|
|
20
28
|
_rawConfig;
|
|
21
29
|
_signingKeyPromise;
|
|
22
30
|
_cache = new Map();
|
|
31
|
+
_inflight = new Map();
|
|
23
32
|
constructor(config = {}) {
|
|
24
33
|
this._rawConfig = config;
|
|
25
34
|
this._config = resolveConfig(config);
|
|
@@ -104,6 +113,7 @@ export class Daywalker {
|
|
|
104
113
|
key: signingKey,
|
|
105
114
|
keyid: this._config.keyId ?? undefined,
|
|
106
115
|
apiKey: this._config.apiKey ?? undefined,
|
|
116
|
+
nonce: generateNonce(),
|
|
107
117
|
});
|
|
108
118
|
for (const [k, v] of Object.entries(signedHeaders)) {
|
|
109
119
|
if (v !== undefined) {
|
|
@@ -231,7 +241,25 @@ export class Daywalker {
|
|
|
231
241
|
message: "Offline verification requires DAYWALKER_PUBLIC_KEY and a valid JWT token lease",
|
|
232
242
|
};
|
|
233
243
|
}
|
|
234
|
-
// Online verification
|
|
244
|
+
// Online verification, deduplicated across concurrent callers
|
|
245
|
+
const inflightKey = `${cacheKey}`;
|
|
246
|
+
const pending = this._inflight.get(inflightKey);
|
|
247
|
+
if (pending)
|
|
248
|
+
return pending;
|
|
249
|
+
const run = this._validateOnline(token, serviceSlug, cacheKey).finally(() => {
|
|
250
|
+
this._inflight.delete(inflightKey);
|
|
251
|
+
});
|
|
252
|
+
this._inflight.set(inflightKey, run);
|
|
253
|
+
return run;
|
|
254
|
+
}
|
|
255
|
+
async _validateOnline(token, serviceSlug, cacheKey) {
|
|
256
|
+
const negTtl = this._config.negativeCacheTtlMs;
|
|
257
|
+
const fail = (result) => {
|
|
258
|
+
if (negTtl > 0 && result.error !== "RATE_LIMITED" && result.error !== "NETWORK_ERROR") {
|
|
259
|
+
this._cache.set(cacheKey, { result, expiresAt: Date.now() + negTtl });
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
};
|
|
235
263
|
try {
|
|
236
264
|
const validateBody = { token };
|
|
237
265
|
if (serviceSlug) {
|
|
@@ -282,12 +310,12 @@ export class Daywalker {
|
|
|
282
310
|
else if (json.error === "REVOKED" || /revoked/i.test(errMsg)) {
|
|
283
311
|
error = "REVOKED";
|
|
284
312
|
}
|
|
285
|
-
return {
|
|
313
|
+
return fail({
|
|
286
314
|
ok: false,
|
|
287
315
|
error,
|
|
288
316
|
status: res.status,
|
|
289
317
|
message: redactSecrets(errMsg || `Validation failed with HTTP ${res.status}`),
|
|
290
|
-
};
|
|
318
|
+
});
|
|
291
319
|
}
|
|
292
320
|
catch (err) {
|
|
293
321
|
return {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -82,6 +82,7 @@ export function resolveConfig(options = {}) {
|
|
|
82
82
|
const daywalkerPublicKey = options.daywalkerPublicKey ?? (env.DAYWALKER_PUBLIC_KEY ? env.DAYWALKER_PUBLIC_KEY.trim() : null);
|
|
83
83
|
const authUrl = (options.authUrl ?? env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
84
84
|
const cacheTtlMs = options.cacheTtlMs ?? 60_000;
|
|
85
|
+
const negativeCacheTtlMs = options.negativeCacheTtlMs ?? 3_000;
|
|
85
86
|
const maxRetries = options.maxRetries ?? (options.retry?.maxRetries ?? 3);
|
|
86
87
|
return {
|
|
87
88
|
apiKey,
|
|
@@ -94,6 +95,7 @@ export function resolveConfig(options = {}) {
|
|
|
94
95
|
daywalkerPublicKey,
|
|
95
96
|
authUrl,
|
|
96
97
|
cacheTtlMs,
|
|
98
|
+
negativeCacheTtlMs,
|
|
97
99
|
maxRetries,
|
|
98
100
|
problems,
|
|
99
101
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface DaywalkerConfig {
|
|
|
57
57
|
daywalkerPublicKey?: string | PublicJwk;
|
|
58
58
|
authUrl?: string;
|
|
59
59
|
cacheTtlMs?: number;
|
|
60
|
+
/** Short cache for failed validations (not 429/network errors). Default 3000ms; 0 disables. */
|
|
61
|
+
negativeCacheTtlMs?: number;
|
|
60
62
|
maxRetries?: number;
|
|
61
63
|
retry?: RetryOptions;
|
|
62
64
|
fetch?: typeof fetch;
|
package/package.json
CHANGED