@devorash/core 0.1.0

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/dist/constants/api-url.gen.d.ts +4 -0
  4. package/dist/constants/api-url.gen.d.ts.map +1 -0
  5. package/dist/constants/api-url.gen.example.d.ts +7 -0
  6. package/dist/constants/api-url.gen.example.d.ts.map +1 -0
  7. package/dist/constants/api-url.gen.example.js +7 -0
  8. package/dist/constants/api-url.gen.example.js.map +1 -0
  9. package/dist/constants/api-url.gen.js +4 -0
  10. package/dist/constants/api-url.gen.js.map +1 -0
  11. package/dist/constants/index.d.ts +203 -0
  12. package/dist/constants/index.d.ts.map +1 -0
  13. package/dist/constants/index.js +236 -0
  14. package/dist/constants/index.js.map +1 -0
  15. package/dist/constants/sessionLifecycle.d.ts +22 -0
  16. package/dist/constants/sessionLifecycle.d.ts.map +1 -0
  17. package/dist/constants/sessionLifecycle.js +21 -0
  18. package/dist/constants/sessionLifecycle.js.map +1 -0
  19. package/dist/index.d.ts +15 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +72 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/security/index.d.ts +118 -0
  24. package/dist/security/index.d.ts.map +1 -0
  25. package/dist/security/index.js +345 -0
  26. package/dist/security/index.js.map +1 -0
  27. package/dist/security/request-body.d.ts +16 -0
  28. package/dist/security/request-body.d.ts.map +1 -0
  29. package/dist/security/request-body.js +84 -0
  30. package/dist/security/request-body.js.map +1 -0
  31. package/dist/security/signing.d.ts +127 -0
  32. package/dist/security/signing.d.ts.map +1 -0
  33. package/dist/security/signing.js +297 -0
  34. package/dist/security/signing.js.map +1 -0
  35. package/dist/types/index.d.ts +487 -0
  36. package/dist/types/index.d.ts.map +1 -0
  37. package/dist/types/index.js +19 -0
  38. package/dist/types/index.js.map +1 -0
  39. package/dist/utils/index.d.ts +211 -0
  40. package/dist/utils/index.d.ts.map +1 -0
  41. package/dist/utils/index.js +600 -0
  42. package/dist/utils/index.js.map +1 -0
  43. package/package.json +53 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Security utilities for Devora SDKs
3
+ * @module @devorash/core/security
4
+ */
5
+ import { SDK_DEFAULTS, READ_METHODS } from "../constants/index.js";
6
+ // ============================================================================
7
+ // Timestamps
8
+ // ============================================================================
9
+ /**
10
+ * A timestamp tolerance is a whole, non-negative number of seconds. Zero means
11
+ * only the current second is accepted; there is no "falsy means default".
12
+ */
13
+ export function isValidTimestampTolerance(value) {
14
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
15
+ }
16
+ /** Throws for configuration values that {@link isValidTimestampTolerance} rejects. */
17
+ export function assertTimestampTolerance(value, name = "timestampTolerance") {
18
+ if (value !== undefined && !isValidTimestampTolerance(value))
19
+ throw new Error(`Devora SDK: ${name} must be a non-negative integer number of seconds`);
20
+ }
21
+ /**
22
+ * Validate timestamp is within acceptable window
23
+ */
24
+ export function validateTimestamp(requestTimestamp, toleranceSeconds = SDK_DEFAULTS.TIMESTAMP_TOLERANCE) {
25
+ // NaN/Infinity/negative tolerances would make every drift comparison pass.
26
+ if (!isValidTimestampTolerance(toleranceSeconds)) {
27
+ return { valid: false, error: "Invalid timestamp tolerance" };
28
+ }
29
+ if (!Number.isFinite(requestTimestamp) || requestTimestamp <= 0) {
30
+ return {
31
+ valid: false,
32
+ error: "Invalid timestamp",
33
+ };
34
+ }
35
+ const currentTime = Math.floor(Date.now() / 1000);
36
+ const drift = Math.abs(currentTime - requestTimestamp);
37
+ if (drift > toleranceSeconds) {
38
+ return {
39
+ valid: false,
40
+ error: `Request timestamp outside tolerance window (drift: ${drift}s, max: ${toleranceSeconds}s)`,
41
+ };
42
+ }
43
+ return { valid: true };
44
+ }
45
+ // ============================================================================
46
+ // Scope Enforcement Utilities
47
+ // ============================================================================
48
+ /**
49
+ * Check if HTTP method is a write operation
50
+ */
51
+ export function isWriteMethod(method) {
52
+ // Unknown verbs are not safe just because they are absent from our CRUD list.
53
+ return !READ_METHODS.includes(method.toUpperCase());
54
+ }
55
+ /**
56
+ * Path of an origin-form request target (`/path?query`), still percent-encoded.
57
+ *
58
+ * Absolute-form targets (`http://host/path`) are deliberately NOT unwrapped:
59
+ * routers disagree with any regex about where such a host ends (a `?` inside
60
+ * it moves the routed path to `/`), so the result does not start with `/` and
61
+ * {@link isAmbiguousRequestPath} rejects it.
62
+ */
63
+ export function getRawRequestPath(value) {
64
+ return (value ?? "").split(/[?#]/)[0] || "/";
65
+ }
66
+ /** Headers through which common middleware lets a client change the effective method. */
67
+ export const METHOD_OVERRIDE_HEADERS = [
68
+ "x-http-method-override",
69
+ "x-http-method",
70
+ "x-method-override",
71
+ ];
72
+ /** The query parameter method-override middleware reads (`?_method=DELETE`). */
73
+ export const METHOD_OVERRIDE_QUERY_PARAM = "_method";
74
+ /**
75
+ * Every method a request might execute as. The guard judges all of them, so a
76
+ * method-override middleware ordered after the guard cannot turn an allowed
77
+ * POST into a blocked DELETE. `target` (the raw request target) adds any
78
+ * `_method` query parameters. A `_method` field in a request body is invisible
79
+ * to a guard that runs before body parsing; see the guard docs.
80
+ */
81
+ export function getPolicyMethods(method, headers, target) {
82
+ const methods = new Set([method.toUpperCase()]);
83
+ for (const name of METHOD_OVERRIDE_HEADERS) {
84
+ const value = headers?.[name];
85
+ for (const item of Array.isArray(value) ? value : value ? [value] : [])
86
+ for (const part of item.split(","))
87
+ if (part.trim())
88
+ methods.add(part.trim().toUpperCase());
89
+ }
90
+ const queryStart = target?.indexOf("?") ?? -1;
91
+ if (target && queryStart !== -1) {
92
+ const query = target.slice(queryStart + 1).split("#")[0];
93
+ try {
94
+ for (const value of new URLSearchParams(query).getAll(METHOD_OVERRIDE_QUERY_PARAM))
95
+ if (value.trim())
96
+ methods.add(value.trim().toUpperCase());
97
+ }
98
+ catch {
99
+ // An unparseable query overrides nothing; the route guard judges it.
100
+ }
101
+ }
102
+ return [...methods];
103
+ }
104
+ /**
105
+ * Methods a deny rule must cover. Frameworks answer HEAD with the GET handler,
106
+ * so a GET deny rule also denies HEAD.
107
+ */
108
+ export function getDenyMethods(methods) {
109
+ return methods.includes("HEAD") && !methods.includes("GET") ? [...methods, "GET"] : methods;
110
+ }
111
+ /** Join a router mount prefix and a mount-relative origin-form target. */
112
+ export function joinMountedTarget(mountPath, target) {
113
+ if (!mountPath || mountPath === "/")
114
+ return target;
115
+ // A mounted router sees `/` for both `/mount` and `/mount/`; both dispatch
116
+ // to the router's root handler, so report the mount path itself.
117
+ if (target === "/" || target.startsWith("/?") || target.startsWith("/#"))
118
+ return mountPath + target.slice(1);
119
+ return mountPath + target;
120
+ }
121
+ // A plain space (`%20` once decoded) is ordinary route-parameter content and
122
+ // does not move a segment boundary; control characters and backslashes do.
123
+ function hasPathControl(value) {
124
+ return Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 || char.charCodeAt(0) === 92);
125
+ }
126
+ /** Reject paths whose segment boundaries differ between common routers. */
127
+ export function isAmbiguousRequestPath(value) {
128
+ const raw = getRawRequestPath(value);
129
+ if (raw.length > 8192 || !raw.startsWith("/") || hasPathControl(raw))
130
+ return true;
131
+ try {
132
+ const decoded = decodeURIComponent(raw);
133
+ return (
134
+ // A still-encoded sequence after one decode means double encoding; a
135
+ // bare `%` (from a literal `%25`) is ordinary parameter content.
136
+ /%[0-9a-f]{2}/i.test(decoded) ||
137
+ // Routers may treat semicolons as query/matrix delimiters. A deny
138
+ // rule must not compare a different path from the selected handler.
139
+ decoded.includes(";") ||
140
+ hasPathControl(decoded) ||
141
+ /%(?:2f|5c)/i.test(raw) ||
142
+ decoded.includes("//") ||
143
+ decoded.split("/").some((part) => part === "." || part === ".."));
144
+ }
145
+ catch {
146
+ return true;
147
+ }
148
+ }
149
+ /** Decode one segment at a time, preserving case and separators. */
150
+ export function normalizeRequestPath(value) {
151
+ const path = getRawRequestPath(value);
152
+ try {
153
+ return decodeURIComponent(path);
154
+ }
155
+ catch {
156
+ return path;
157
+ }
158
+ }
159
+ /** Linear-space wildcard matching within one path segment. */
160
+ function segmentMatches(pattern, value) {
161
+ let previous = new Uint8Array(value.length + 1);
162
+ previous[0] = 1;
163
+ for (const token of pattern) {
164
+ const current = new Uint8Array(value.length + 1);
165
+ if (token === "*")
166
+ current[0] = previous[0];
167
+ for (let j = 1; j <= value.length; j++) {
168
+ current[j] =
169
+ token === "*"
170
+ ? previous[j] || current[j - 1]
171
+ ? 1
172
+ : 0
173
+ : previous[j - 1] && token === value[j - 1]
174
+ ? 1
175
+ : 0;
176
+ }
177
+ previous = current;
178
+ }
179
+ return previous[value.length] === 1;
180
+ }
181
+ /**
182
+ * Bounded iterative glob matching: * stays within a segment; ** spans zero or
183
+ * more whole segments. Allow rules preserve case/trailing slashes. Deny rules
184
+ * opt into conservative case-insensitive matching for non-strict routers.
185
+ */
186
+ export function matchEndpointPattern(pattern, path, options = {}) {
187
+ if (pattern.length > 500 || path.length > 8192)
188
+ return false;
189
+ let patternPath = pattern.startsWith("/") ? pattern : `/${pattern}`;
190
+ let requestPath = path.startsWith("/") ? path : `/${path}`;
191
+ if (options.caseSensitive === false) {
192
+ patternPath = patternPath.toLowerCase();
193
+ requestPath = requestPath.toLowerCase();
194
+ }
195
+ if (options.ignoreTrailingSlash) {
196
+ patternPath = patternPath.replace(/\/+$/, "") || "/";
197
+ requestPath = requestPath.replace(/\/+$/, "") || "/";
198
+ }
199
+ const patterns = patternPath.slice(1).split("/");
200
+ const parts = requestPath.slice(1).split("/");
201
+ let previous = new Uint8Array(parts.length + 1);
202
+ previous[0] = 1;
203
+ for (const token of patterns) {
204
+ const current = new Uint8Array(parts.length + 1);
205
+ if (token === "**")
206
+ current[0] = previous[0];
207
+ for (let j = 1; j <= parts.length; j++) {
208
+ current[j] =
209
+ token === "**"
210
+ ? previous[j] || current[j - 1]
211
+ ? 1
212
+ : 0
213
+ : previous[j - 1] && segmentMatches(token, parts[j - 1])
214
+ ? 1
215
+ : 0;
216
+ }
217
+ previous = current;
218
+ }
219
+ return previous[parts.length] === 1;
220
+ }
221
+ /**
222
+ * Create a blocked response for scope violations
223
+ */
224
+ export function createBlockedResponse(method, url) {
225
+ return new Response(JSON.stringify({
226
+ error: "Write operations disabled in read-only mode",
227
+ method,
228
+ url,
229
+ code: "SCOPE_VIOLATION",
230
+ }), {
231
+ status: 403,
232
+ headers: { "Content-Type": "application/json" },
233
+ });
234
+ }
235
+ // ============================================================================
236
+ // Token Utilities
237
+ // ============================================================================
238
+ /**
239
+ * Generate a cryptographically secure random string for tokens/IDs.
240
+ *
241
+ * @throws {Error} If no secure random source is available.
242
+ * This is intentional - we must never fall back to Math.random()
243
+ * for security-sensitive operations like token generation.
244
+ */
245
+ export function generateRandomString(length = 32) {
246
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
247
+ let result = "";
248
+ // Check for Web Crypto API (browser and modern Node.js)
249
+ if (typeof crypto === "undefined" || !crypto.getRandomValues) {
250
+ // NO FALLBACK - throw error instead of using insecure Math.random()
251
+ throw new Error("Devora SDK: Secure random number generator not available. " +
252
+ "This environment does not support Web Crypto API. " +
253
+ "For Node.js < 19, ensure you're using the crypto module.");
254
+ }
255
+ // Use rejection sampling to avoid modulo bias
256
+ const maxValidValue = 256 - (256 % chars.length); // 248 for 62 chars
257
+ for (let i = 0; i < length; i++) {
258
+ let randomValue;
259
+ do {
260
+ const randomBytes = new Uint8Array(1);
261
+ crypto.getRandomValues(randomBytes);
262
+ // Non-null assertion is safe: we just created a 1-byte array
263
+ randomValue = randomBytes[0];
264
+ } while (randomValue >= maxValidValue);
265
+ result += chars[randomValue % chars.length];
266
+ }
267
+ return result;
268
+ }
269
+ /**
270
+ * Convert ArrayBuffer to hex string
271
+ */
272
+ export function arrayBufferToHex(buffer) {
273
+ const bytes = new Uint8Array(buffer);
274
+ return Array.from(bytes)
275
+ .map((b) => b.toString(16).padStart(2, "0"))
276
+ .join("");
277
+ }
278
+ /**
279
+ * Convert hex string to ArrayBuffer
280
+ */
281
+ export function hexToArrayBuffer(hex) {
282
+ if (hex.length % 2 !== 0) {
283
+ throw new Error(`Invalid hex string: length must be even (got ${hex.length})`);
284
+ }
285
+ const bytes = new Uint8Array(hex.length / 2);
286
+ for (let i = 0; i < bytes.length; i++) {
287
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
288
+ }
289
+ return bytes.buffer;
290
+ }
291
+ // ============================================================================
292
+ // Error Types
293
+ // ============================================================================
294
+ /**
295
+ * Base error class for Devora SDK errors
296
+ */
297
+ export class DevoraSDKError extends Error {
298
+ code;
299
+ statusCode;
300
+ constructor(message, code, statusCode = 400) {
301
+ super(message);
302
+ this.name = "DevoraSDKError";
303
+ this.code = code;
304
+ this.statusCode = statusCode;
305
+ }
306
+ }
307
+ /**
308
+ * Security-related errors
309
+ */
310
+ export class DevoraSecurityError extends DevoraSDKError {
311
+ securityCode;
312
+ constructor(message, securityCode, statusCode = 401) {
313
+ super(message, `SECURITY_${securityCode}`, statusCode);
314
+ this.name = "DevoraSecurityError";
315
+ this.securityCode = securityCode;
316
+ }
317
+ }
318
+ /**
319
+ * Validation errors
320
+ */
321
+ export class DevoraValidationError extends DevoraSDKError {
322
+ constructor(message, code = "VALIDATION_ERROR") {
323
+ super(message, code, 400);
324
+ this.name = "DevoraValidationError";
325
+ }
326
+ }
327
+ /**
328
+ * Configuration errors
329
+ */
330
+ export class DevoraConfigError extends DevoraSDKError {
331
+ constructor(message) {
332
+ super(message, "CONFIG_ERROR", 500);
333
+ this.name = "DevoraConfigError";
334
+ }
335
+ }
336
+ /**
337
+ * Network/connectivity errors
338
+ */
339
+ export class DevoraNetworkError extends DevoraSDKError {
340
+ constructor(message) {
341
+ super(message, "NETWORK_ERROR", 503);
342
+ this.name = "DevoraNetworkError";
343
+ }
344
+ }
345
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /** Failures raised before an oversized or stalled body is fully materialized. */
2
+ export declare class BodyReadError extends Error {
3
+ readonly code: "BODY_TOO_LARGE" | "BODY_TIMEOUT" | "INVALID_BODY";
4
+ readonly status: 400 | 408 | 413;
5
+ constructor(code: "BODY_TOO_LARGE" | "BODY_TIMEOUT" | "INVALID_BODY", status: 400 | 408 | 413, message: string);
6
+ }
7
+ /**
8
+ * Read the exact body bytes with a size bound and a total deadline, cancelling
9
+ * on failure. A single transport chunk may exceed the limit; it is rejected
10
+ * before being retained. Framework/ingress limits must also bound the
11
+ * transport's own buffering.
12
+ */
13
+ export declare function readBoundedBytes(request: Pick<Request, "headers" | "body">, maxBytes: number, timeoutMs?: number): Promise<Uint8Array>;
14
+ /** {@link readBoundedBytes}, decoded as UTF-8 (invalid sequences become U+FFFD). */
15
+ export declare function readBoundedBody(request: Pick<Request, "headers" | "body">, maxBytes: number, timeoutMs?: number): Promise<string>;
16
+ //# sourceMappingURL=request-body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/security/request-body.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,qBAAa,aAAc,SAAQ,KAAK;aAEtB,IAAI,EAAE,gBAAgB,GAAG,cAAc,GAAG,cAAc;aACxD,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG;gBADvB,IAAI,EAAE,gBAAgB,GAAG,cAAc,GAAG,cAAc,EACxD,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EACvC,OAAO,EAAE,MAAM;CAKhB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,EAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAQ,GACf,OAAO,CAAC,UAAU,CAAC,CA8DrB;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CACpC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,EAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAQ,GACf,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -0,0 +1,84 @@
1
+ /** Failures raised before an oversized or stalled body is fully materialized. */
2
+ export class BodyReadError extends Error {
3
+ code;
4
+ status;
5
+ constructor(code, status, message) {
6
+ super(message);
7
+ this.code = code;
8
+ this.status = status;
9
+ this.name = "BodyReadError";
10
+ }
11
+ }
12
+ /**
13
+ * Read the exact body bytes with a size bound and a total deadline, cancelling
14
+ * on failure. A single transport chunk may exceed the limit; it is rejected
15
+ * before being retained. Framework/ingress limits must also bound the
16
+ * transport's own buffering.
17
+ */
18
+ export async function readBoundedBytes(request, maxBytes, timeoutMs = 5_000) {
19
+ if (!Number.isSafeInteger(maxBytes) ||
20
+ maxBytes < 0 ||
21
+ !Number.isFinite(timeoutMs) ||
22
+ timeoutMs <= 0)
23
+ throw new Error("Invalid body budget");
24
+ const declared = request.headers.get("content-length");
25
+ const cancel = () => {
26
+ void request.body?.cancel().catch(() => { });
27
+ };
28
+ if (declared !== null && !/^\d+$/.test(declared)) {
29
+ cancel();
30
+ throw new BodyReadError("INVALID_BODY", 400, "Invalid Content-Length");
31
+ }
32
+ if (declared !== null && Number(declared) > maxBytes) {
33
+ cancel();
34
+ throw new BodyReadError("BODY_TOO_LARGE", 413, "Request body too large");
35
+ }
36
+ if (!request.body)
37
+ return new Uint8Array();
38
+ const reader = request.body.getReader();
39
+ let timer;
40
+ let finished = false;
41
+ try {
42
+ return await Promise.race([
43
+ (async () => {
44
+ const chunks = [];
45
+ let total = 0;
46
+ while (!finished) {
47
+ const { value, done } = await reader.read();
48
+ if (finished)
49
+ break;
50
+ if (done) {
51
+ const bytes = new Uint8Array(total);
52
+ let offset = 0;
53
+ for (const chunk of chunks) {
54
+ bytes.set(chunk, offset);
55
+ offset += chunk.byteLength;
56
+ }
57
+ return bytes;
58
+ }
59
+ total += value.byteLength;
60
+ if (total > maxBytes)
61
+ throw new BodyReadError("BODY_TOO_LARGE", 413, "Request body too large");
62
+ chunks.push(value);
63
+ }
64
+ throw new BodyReadError("BODY_TIMEOUT", 408, "Request body timed out");
65
+ })(),
66
+ new Promise((_, reject) => {
67
+ timer = setTimeout(() => reject(new BodyReadError("BODY_TIMEOUT", 408, "Request body timed out")), timeoutMs);
68
+ }),
69
+ ]);
70
+ }
71
+ finally {
72
+ finished = true;
73
+ if (timer !== undefined)
74
+ clearTimeout(timer);
75
+ // Do not wait for an uncooperative upstream cancellation.
76
+ void reader.cancel().catch(() => { });
77
+ reader.releaseLock();
78
+ }
79
+ }
80
+ /** {@link readBoundedBytes}, decoded as UTF-8 (invalid sequences become U+FFFD). */
81
+ export async function readBoundedBody(request, maxBytes, timeoutMs = 5_000) {
82
+ return new TextDecoder().decode(await readBoundedBytes(request, maxBytes, timeoutMs));
83
+ }
84
+ //# sourceMappingURL=request-body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-body.js","sourceRoot":"","sources":["../../src/security/request-body.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Devora request signing, version 3.
3
+ *
4
+ * The signature covers the exact bytes on the wire: raw path, raw query and a
5
+ * SHA-256 of the raw body, plus the direction the request travels in. Every
6
+ * header has one accepted spelling. The canonical string joins fields with
7
+ * `\n`; each field's grammar excludes `\n`, so the encoding is injective.
8
+ *
9
+ * This module is pure (no hashing): runtimes supply SHA-256 and HMAC.
10
+ * Conformance vectors: packages/sdks/test/signing-v3-vectors.json.
11
+ *
12
+ * @module @devorash/core/security/signing
13
+ */
14
+ export declare const SIGNING: {
15
+ readonly VERSION: "3";
16
+ readonly ALGORITHM: "DEVORA-HMAC-SHA256";
17
+ /** Devora backend -> customer backend (the SDK verifies). */
18
+ readonly DEVORA_TO_CUSTOMER: "devora-to-customer";
19
+ /** Customer backend SDK -> Devora backend (Devora verifies). */
20
+ readonly CUSTOMER_TO_DEVORA: "customer-to-devora";
21
+ /** SHA-256 of an empty body. */
22
+ readonly EMPTY_BODY_SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
23
+ };
24
+ export type SigningDirection = typeof SIGNING.DEVORA_TO_CUSTOMER | typeof SIGNING.CUSTOMER_TO_DEVORA;
25
+ /** Exact header grammars. (?![\s\S]) requires the actual end of input;
26
+ * JavaScript's $ also accepts a position before a final newline. */
27
+ export declare const SIGNED_HEADER_PATTERNS: {
28
+ readonly signatureVersion: RegExp;
29
+ readonly keyId: RegExp;
30
+ readonly orgId: RegExp;
31
+ readonly sentAt: RegExp;
32
+ readonly requestId: RegExp;
33
+ readonly signature: RegExp;
34
+ readonly method: RegExp;
35
+ readonly bodySha256: RegExp;
36
+ /** The full secret string; its ASCII bytes are the HMAC key. */
37
+ readonly secretKey: RegExp;
38
+ };
39
+ export type SignatureErrorCode = "INVALID_SIGNATURE_HEADERS" | "UNSUPPORTED_SIGNATURE_VERSION" | "ORG_MISMATCH" | "TIMESTAMP_EXPIRED" | "INVALID_REQUEST_TARGET" | "UNSUPPORTED_CONTENT_ENCODING" | "INVALID_SIGNATURE" | "REPLAYED_REQUEST" | "REPLAY_STORE_UNAVAILABLE";
40
+ /** Header values as frameworks expose them. */
41
+ export type HeaderMap = Record<string, string | string[] | undefined>;
42
+ export interface SignatureHeaders {
43
+ keyId: string;
44
+ orgId: string;
45
+ sentAt: string;
46
+ requestId: string;
47
+ signature: string;
48
+ }
49
+ export type SignatureHeaderResult = {
50
+ valid: true;
51
+ headers: SignatureHeaders;
52
+ } | {
53
+ valid: false;
54
+ errorCode: SignatureErrorCode;
55
+ error: string;
56
+ };
57
+ export interface CanonicalFields {
58
+ direction: SigningDirection;
59
+ keyId: string;
60
+ orgId: string;
61
+ sentAt: string;
62
+ requestId: string;
63
+ method: string;
64
+ /** Raw, percent-encoded path (mount-relative for devora-to-customer). */
65
+ path: string;
66
+ /** Raw query after the first `?`, exactly as sent; "" when absent. */
67
+ query: string;
68
+ /** Lowercase hex SHA-256 of the exact body bytes. */
69
+ bodySha256: string;
70
+ }
71
+ /** UTF-8 percent-encode every byte outside `A-Z a-z 0-9 - _ . ~`, with uppercase hex. */
72
+ export declare function strictEncode(value: string): string;
73
+ /**
74
+ * Encode one path parameter. `""`, `.` and `..` are refused: URL parsers
75
+ * remove dot segments (even `%2E%2E`), so they cannot be sent as signed.
76
+ */
77
+ export declare function encodePathParam(value: string): string;
78
+ /** Strict-encoded `key=value` pairs, in the given order (the order is signed). */
79
+ export declare function buildStrictQuery(pairs: ReadonlyArray<readonly [string, string]>): string;
80
+ /**
81
+ * A raw request path as signed: absolute, printable ASCII, uppercase escapes,
82
+ * and no empty or dot segments.
83
+ */
84
+ export declare function isValidSignedPath(path: string): boolean;
85
+ /** A raw query as signed: printable ASCII, no fragment, uppercase escapes. */
86
+ export declare function isValidSignedQuery(query: string): boolean;
87
+ /** Build the canonical string. Throws if any field is outside its grammar. */
88
+ export declare function buildCanonicalString(fields: CanonicalFields): string;
89
+ /**
90
+ * The single value of a header, or `null` when it is present more than once
91
+ * (an array, or a comma-joined value that the grammar then rejects).
92
+ */
93
+ export declare function getSingleHeader(headers: HeaderMap, name: string): string | undefined | null;
94
+ /** Parse and validate the signature headers. Never first-wins, never trims. */
95
+ export declare function parseSignatureHeaders(headers: HeaderMap): SignatureHeaderResult;
96
+ /** Accept only an absent or `identity` Content-Encoding (the body digest is over wire bytes). */
97
+ export declare function hasIdentityContentEncoding(headers: HeaderMap): boolean;
98
+ /** Replay-store namespace: version, direction and key. */
99
+ export declare function replayNamespace(direction: SigningDirection, keyId: string): string;
100
+ /**
101
+ * Nonce lifetime covering the whole acceptance window: the floor second of the
102
+ * timestamp check plus one second of clock jitter.
103
+ */
104
+ export declare function replayExpiresAtMs(sentAt: number, nowSeconds: number, toleranceSeconds: number): number;
105
+ /**
106
+ * Parse a verified raw query into a null-prototype map. Repeated keys become
107
+ * arrays in wire order; `__proto__` and `constructor` are ordinary keys.
108
+ */
109
+ export declare function parseVerifiedQuery(query: string): Record<string, string | string[]>;
110
+ export type VerifiedBodyResult = {
111
+ ok: true;
112
+ value: unknown;
113
+ } | {
114
+ ok: false;
115
+ error: string;
116
+ };
117
+ /**
118
+ * Decode a verified body: empty means `undefined`; otherwise it must be JSON
119
+ * (declared `application/json`) and strictly valid UTF-8.
120
+ */
121
+ export declare function parseVerifiedJsonBody(bytes: Uint8Array, contentType: string | undefined | null): VerifiedBodyResult;
122
+ /**
123
+ * Last `count` segments of a raw path, i.e. the path relative to a mount whose
124
+ * route template has `count` segments. `null` when the path is shorter.
125
+ */
126
+ export declare function routeRelativePath(rawPath: string, routeTemplate: string): string | null;
127
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/security/signing.ts"],"names":[],"mappings":"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"}