@rayspec/auth-core 1.5.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.
- package/LICENSE +105 -0
- package/dist/api-key.d.ts +28 -0
- package/dist/api-key.d.ts.map +1 -0
- package/dist/api-key.js +56 -0
- package/dist/api-key.js.map +1 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +11 -0
- package/dist/audit.js.map +1 -0
- package/dist/authz.d.ts +66 -0
- package/dist/authz.d.ts.map +1 -0
- package/dist/authz.js +112 -0
- package/dist/authz.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/deferrals.d.ts +33 -0
- package/dist/deferrals.d.ts.map +1 -0
- package/dist/deferrals.js +38 -0
- package/dist/deferrals.js.map +1 -0
- package/dist/dto.d.ts +337 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +221 -0
- package/dist/dto.js.map +1 -0
- package/dist/email.d.ts +24 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +46 -0
- package/dist/email.js.map +1 -0
- package/dist/errors.d.ts +87 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +104 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/invite.d.ts +18 -0
- package/dist/invite.d.ts.map +1 -0
- package/dist/invite.js +33 -0
- package/dist/invite.js.map +1 -0
- package/dist/password.d.ts +17 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +46 -0
- package/dist/password.js.map +1 -0
- package/dist/rate-limit.d.ts +93 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +156 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +41 -0
- package/dist/session.js.map +1 -0
- package/dist/tokens.d.ts +117 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +150 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +33 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error envelope + the CLOSED ErrorCode enum.
|
|
3
|
+
*
|
|
4
|
+
* Every non-2xx response is `{ error: { code, message, requestId, details? } }` with a code from
|
|
5
|
+
* the closed enum below. A 404 for a cross-tenant/missing resource carries NO existence leak (the
|
|
6
|
+
* message is uniform). The HTTP status for each code is fixed by STATUS_BY_CODE.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
/** The CLOSED set of error codes the API can return. */
|
|
10
|
+
export declare const ErrorCode: z.ZodEnum<{
|
|
11
|
+
VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
12
|
+
UNAUTHENTICATED: "UNAUTHENTICATED";
|
|
13
|
+
FORBIDDEN: "FORBIDDEN";
|
|
14
|
+
NOT_FOUND: "NOT_FOUND";
|
|
15
|
+
CONFLICT: "CONFLICT";
|
|
16
|
+
IDEMPOTENCY_CONFLICT: "IDEMPOTENCY_CONFLICT";
|
|
17
|
+
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
|
|
18
|
+
RATE_LIMITED: "RATE_LIMITED";
|
|
19
|
+
INTERNAL: "INTERNAL";
|
|
20
|
+
UPSTREAM_ERROR: "UPSTREAM_ERROR";
|
|
21
|
+
NOT_IMPLEMENTED: "NOT_IMPLEMENTED";
|
|
22
|
+
GATEWAY_TIMEOUT: "GATEWAY_TIMEOUT";
|
|
23
|
+
}>;
|
|
24
|
+
export type ErrorCode = z.infer<typeof ErrorCode>;
|
|
25
|
+
/** Fixed HTTP status per error code. */
|
|
26
|
+
export declare const STATUS_BY_CODE: Record<ErrorCode, number>;
|
|
27
|
+
/** The wire error envelope. */
|
|
28
|
+
export declare const ErrorEnvelope: z.ZodObject<{
|
|
29
|
+
error: z.ZodObject<{
|
|
30
|
+
code: z.ZodEnum<{
|
|
31
|
+
VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
32
|
+
UNAUTHENTICATED: "UNAUTHENTICATED";
|
|
33
|
+
FORBIDDEN: "FORBIDDEN";
|
|
34
|
+
NOT_FOUND: "NOT_FOUND";
|
|
35
|
+
CONFLICT: "CONFLICT";
|
|
36
|
+
IDEMPOTENCY_CONFLICT: "IDEMPOTENCY_CONFLICT";
|
|
37
|
+
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
|
|
38
|
+
RATE_LIMITED: "RATE_LIMITED";
|
|
39
|
+
INTERNAL: "INTERNAL";
|
|
40
|
+
UPSTREAM_ERROR: "UPSTREAM_ERROR";
|
|
41
|
+
NOT_IMPLEMENTED: "NOT_IMPLEMENTED";
|
|
42
|
+
GATEWAY_TIMEOUT: "GATEWAY_TIMEOUT";
|
|
43
|
+
}>;
|
|
44
|
+
message: z.ZodString;
|
|
45
|
+
requestId: z.ZodString;
|
|
46
|
+
details: z.ZodOptional<z.ZodUnknown>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type ErrorEnvelope = z.infer<typeof ErrorEnvelope>;
|
|
50
|
+
/**
|
|
51
|
+
* A typed application error carrying an ErrorCode. Handlers/services throw this; the app's error
|
|
52
|
+
* handler maps it to the envelope + the fixed status. A generic 401/404 deliberately uses a
|
|
53
|
+
* uniform message so neither credential validity nor cross-tenant existence leaks.
|
|
54
|
+
*/
|
|
55
|
+
export declare class ApiError extends Error {
|
|
56
|
+
readonly code: ErrorCode;
|
|
57
|
+
readonly details?: unknown;
|
|
58
|
+
constructor(code: ErrorCode, message: string, details?: unknown);
|
|
59
|
+
}
|
|
60
|
+
/** Uniform 401 — never reveals whether the user/credential existed. */
|
|
61
|
+
export declare function unauthenticated(): ApiError;
|
|
62
|
+
/** Uniform 404 — never reveals whether the resource exists in another tenant. */
|
|
63
|
+
export declare function notFound(): ApiError;
|
|
64
|
+
/**
|
|
65
|
+
* 403 — authenticated but not permitted. `details` is included in the envelope ONLY when the caller
|
|
66
|
+
* supplies it (an authenticated scope/permission gap names the missing permission — see
|
|
67
|
+
* requirePermission); a 401/404 stays bare (uniform, no info leak) and existing callers that pass no
|
|
68
|
+
* `details` are byte-for-byte unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export declare function forbidden(message?: string, details?: Record<string, unknown>): ApiError;
|
|
71
|
+
/**
|
|
72
|
+
* The codes that may carry `details` out to the client — the ones whose `details` echoes
|
|
73
|
+
* caller-supplied INPUT context and leaks nothing about existence or credentials:
|
|
74
|
+
* - VALIDATION_ERROR — the Zod issue set (a shape/validation echo of the caller's own body);
|
|
75
|
+
* - FORBIDDEN — the named missing permission for an AUTHENTICATED scope/role gap;
|
|
76
|
+
* - RATE_LIMITED — the retry hint (`retryAfterMs`);
|
|
77
|
+
* - GATEWAY_TIMEOUT — the neutral run error class (`errorClass`).
|
|
78
|
+
* Every other code — notably the existence-leak-sensitive UNAUTHENTICATED (401) / NOT_FOUND (404) —
|
|
79
|
+
* MUST NOT emit `details`, so the envelope strips it structurally below regardless of what a caller
|
|
80
|
+
* passes. This makes the "a bare 401/404 leaks nothing" invariant STRUCTURAL at the one chokepoint
|
|
81
|
+
* every response flows through, not a per-call-site convention. It is behavior-preserving today: no
|
|
82
|
+
* code outside this set is ever thrown with a `details` payload.
|
|
83
|
+
*/
|
|
84
|
+
export declare const DETAILS_ALLOWED: ReadonlySet<ErrorCode>;
|
|
85
|
+
/** Build the wire envelope for a code + message + requestId. Strips `details` for non-allowlisted codes. */
|
|
86
|
+
export declare function errorEnvelope(code: ErrorCode, message: string, requestId: string, details?: unknown): ErrorEnvelope;
|
|
87
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wDAAwD;AACxD,eAAO,MAAM,SAAS;;;;;;;;;;;;;EAapB,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAElD,wCAAwC;AACxC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAapD,CAAC;AAEF,+BAA+B;AAC/B,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;GAIG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;gBACf,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAMhE;AAED,uEAAuE;AACvE,wBAAgB,eAAe,IAAI,QAAQ,CAE1C;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAe,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAE7F;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,SAAS,CAKjD,CAAC;AAEH,4GAA4G;AAC5G,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,OAAO,GAChB,aAAa,CAGf"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error envelope + the CLOSED ErrorCode enum.
|
|
3
|
+
*
|
|
4
|
+
* Every non-2xx response is `{ error: { code, message, requestId, details? } }` with a code from
|
|
5
|
+
* the closed enum below. A 404 for a cross-tenant/missing resource carries NO existence leak (the
|
|
6
|
+
* message is uniform). The HTTP status for each code is fixed by STATUS_BY_CODE.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
/** The CLOSED set of error codes the API can return. */
|
|
10
|
+
export const ErrorCode = z.enum([
|
|
11
|
+
'VALIDATION_ERROR', // 400 — Zod / shape validation failed (defaultHook)
|
|
12
|
+
'UNAUTHENTICATED', // 401 — missing/invalid credential (uniform, no enumeration)
|
|
13
|
+
'FORBIDDEN', // 403 — authenticated but not permitted (live authz deny)
|
|
14
|
+
'NOT_FOUND', // 404 — missing OR cross-tenant (no existence leak)
|
|
15
|
+
'CONFLICT', // 409 — uniqueness / state conflict
|
|
16
|
+
'IDEMPOTENCY_CONFLICT', // 409 — same Idempotency-Key, different body
|
|
17
|
+
'PAYLOAD_TOO_LARGE', // 413 — request body exceeds the configured byte cap (rejected pre-side-effect)
|
|
18
|
+
'RATE_LIMITED', // 429 — rate limit / anti-DoS lock
|
|
19
|
+
'INTERNAL', // 500 — unexpected server error
|
|
20
|
+
'UPSTREAM_ERROR', // 502 — an upstream provider 5xx surfaced on a live run
|
|
21
|
+
'NOT_IMPLEMENTED', // 501 — reserved seam (WorkOS SSO stub)
|
|
22
|
+
'GATEWAY_TIMEOUT', // 504 — a held in-request run exceeded its wall-clock timeout
|
|
23
|
+
]);
|
|
24
|
+
/** Fixed HTTP status per error code. */
|
|
25
|
+
export const STATUS_BY_CODE = {
|
|
26
|
+
VALIDATION_ERROR: 400,
|
|
27
|
+
UNAUTHENTICATED: 401,
|
|
28
|
+
FORBIDDEN: 403,
|
|
29
|
+
NOT_FOUND: 404,
|
|
30
|
+
CONFLICT: 409,
|
|
31
|
+
IDEMPOTENCY_CONFLICT: 409,
|
|
32
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
33
|
+
RATE_LIMITED: 429,
|
|
34
|
+
INTERNAL: 500,
|
|
35
|
+
UPSTREAM_ERROR: 502,
|
|
36
|
+
NOT_IMPLEMENTED: 501,
|
|
37
|
+
GATEWAY_TIMEOUT: 504,
|
|
38
|
+
};
|
|
39
|
+
/** The wire error envelope. */
|
|
40
|
+
export const ErrorEnvelope = z.object({
|
|
41
|
+
error: z.object({
|
|
42
|
+
code: ErrorCode,
|
|
43
|
+
message: z.string(),
|
|
44
|
+
requestId: z.string(),
|
|
45
|
+
details: z.unknown().optional(),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* A typed application error carrying an ErrorCode. Handlers/services throw this; the app's error
|
|
50
|
+
* handler maps it to the envelope + the fixed status. A generic 401/404 deliberately uses a
|
|
51
|
+
* uniform message so neither credential validity nor cross-tenant existence leaks.
|
|
52
|
+
*/
|
|
53
|
+
export class ApiError extends Error {
|
|
54
|
+
code;
|
|
55
|
+
details;
|
|
56
|
+
constructor(code, message, details) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = 'ApiError';
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.details = details;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Uniform 401 — never reveals whether the user/credential existed. */
|
|
64
|
+
export function unauthenticated() {
|
|
65
|
+
return new ApiError('UNAUTHENTICATED', 'Authentication failed.');
|
|
66
|
+
}
|
|
67
|
+
/** Uniform 404 — never reveals whether the resource exists in another tenant. */
|
|
68
|
+
export function notFound() {
|
|
69
|
+
return new ApiError('NOT_FOUND', 'Not found.');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 403 — authenticated but not permitted. `details` is included in the envelope ONLY when the caller
|
|
73
|
+
* supplies it (an authenticated scope/permission gap names the missing permission — see
|
|
74
|
+
* requirePermission); a 401/404 stays bare (uniform, no info leak) and existing callers that pass no
|
|
75
|
+
* `details` are byte-for-byte unchanged.
|
|
76
|
+
*/
|
|
77
|
+
export function forbidden(message = 'Forbidden.', details) {
|
|
78
|
+
return new ApiError('FORBIDDEN', message, details);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The codes that may carry `details` out to the client — the ones whose `details` echoes
|
|
82
|
+
* caller-supplied INPUT context and leaks nothing about existence or credentials:
|
|
83
|
+
* - VALIDATION_ERROR — the Zod issue set (a shape/validation echo of the caller's own body);
|
|
84
|
+
* - FORBIDDEN — the named missing permission for an AUTHENTICATED scope/role gap;
|
|
85
|
+
* - RATE_LIMITED — the retry hint (`retryAfterMs`);
|
|
86
|
+
* - GATEWAY_TIMEOUT — the neutral run error class (`errorClass`).
|
|
87
|
+
* Every other code — notably the existence-leak-sensitive UNAUTHENTICATED (401) / NOT_FOUND (404) —
|
|
88
|
+
* MUST NOT emit `details`, so the envelope strips it structurally below regardless of what a caller
|
|
89
|
+
* passes. This makes the "a bare 401/404 leaks nothing" invariant STRUCTURAL at the one chokepoint
|
|
90
|
+
* every response flows through, not a per-call-site convention. It is behavior-preserving today: no
|
|
91
|
+
* code outside this set is ever thrown with a `details` payload.
|
|
92
|
+
*/
|
|
93
|
+
export const DETAILS_ALLOWED = new Set([
|
|
94
|
+
'VALIDATION_ERROR',
|
|
95
|
+
'FORBIDDEN',
|
|
96
|
+
'RATE_LIMITED',
|
|
97
|
+
'GATEWAY_TIMEOUT',
|
|
98
|
+
]);
|
|
99
|
+
/** Build the wire envelope for a code + message + requestId. Strips `details` for non-allowlisted codes. */
|
|
100
|
+
export function errorEnvelope(code, message, requestId, details) {
|
|
101
|
+
const emitDetails = details !== undefined && DETAILS_ALLOWED.has(code);
|
|
102
|
+
return { error: { code, message, requestId, ...(emitDetails ? { details } : {}) } };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './api-key.js';
|
|
2
|
+
export * from './audit.js';
|
|
3
|
+
export * from './authz.js';
|
|
4
|
+
export * from './config.js';
|
|
5
|
+
export * from './deferrals.js';
|
|
6
|
+
export * from './dto.js';
|
|
7
|
+
export * from './email.js';
|
|
8
|
+
export * from './errors.js';
|
|
9
|
+
export * from './invite.js';
|
|
10
|
+
export * from './password.js';
|
|
11
|
+
export * from './rate-limit.js';
|
|
12
|
+
export * from './session.js';
|
|
13
|
+
export * from './tokens.js';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './api-key.js';
|
|
2
|
+
export * from './audit.js';
|
|
3
|
+
export * from './authz.js';
|
|
4
|
+
export * from './config.js';
|
|
5
|
+
export * from './deferrals.js';
|
|
6
|
+
export * from './dto.js';
|
|
7
|
+
export * from './email.js';
|
|
8
|
+
export * from './errors.js';
|
|
9
|
+
export * from './invite.js';
|
|
10
|
+
export * from './password.js';
|
|
11
|
+
export * from './rate-limit.js';
|
|
12
|
+
export * from './session.js';
|
|
13
|
+
export * from './tokens.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC"}
|
package/dist/invite.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MintedInviteToken {
|
|
2
|
+
/** The full plaintext invite token shown to the owner ONCE (conveyed out-of-band). Never stored. */
|
|
3
|
+
token: string;
|
|
4
|
+
/** The HMAC hash to persist (`invites.token_hash`). */
|
|
5
|
+
hash: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* HMAC-SHA256 an invite token with the boot-required pepper (+ the `invite:` domain prefix); returns a
|
|
9
|
+
* lowercase hex digest. Deterministic, so the redeem path recomputes it and looks the invite up by an
|
|
10
|
+
* exact `token_hash` equality on the unique index.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hashInviteToken(token: string, pepper?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Mint a new invite token: 32 bytes (256-bit) of CSPRNG, base64url-encoded (URL-safe). Returns the
|
|
15
|
+
* one-time plaintext token + the HMAC hash to store. The plaintext is shown EXACTLY ONCE at issue.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mintInviteToken(pepper?: string): MintedInviteToken;
|
|
18
|
+
//# sourceMappingURL=invite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite.d.ts","sourceRoot":"","sources":["../src/invite.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,iBAAiB;IAChC,oGAAoG;IACpG,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAA0B,GAAG,MAAM,CAEzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,MAA0B,GAAG,iBAAiB,CAGrF"}
|
package/dist/invite.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invite-token primitive — the opaque out-of-band org-invite token + its HMAC storage hash.
|
|
3
|
+
*
|
|
4
|
+
* An invite token is a high-entropy (256-bit) bearer credential the org owner conveys OUT-OF-BAND to
|
|
5
|
+
* the invitee (core has no outbound mail). Only its HMAC-SHA256-with-pepper hash is persisted (never
|
|
6
|
+
* the plaintext); the redeem path recomputes the HMAC and looks the invite up by `token_hash` (a
|
|
7
|
+
* unique index) — the same "store a hash, resolve a bearer credential by its hash" pattern as the
|
|
8
|
+
* api-key + session paths. HMAC (not argon2id) is sound here: the token is machine-generated
|
|
9
|
+
* high-entropy, so a slow password KDF is unnecessary and the hot redeem path stays fast.
|
|
10
|
+
*
|
|
11
|
+
* The pepper is the shared server HMAC secret (`RAYSPEC_API_KEY_PEPPER`, boot-required). A DOMAIN
|
|
12
|
+
* PREFIX (`invite:`) is folded into the HMAC so an invite-token hash can never collide with an
|
|
13
|
+
* api-key hash even under the same pepper (domain separation).
|
|
14
|
+
*/
|
|
15
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
16
|
+
import { getApiKeyPepper } from './api-key.js';
|
|
17
|
+
/**
|
|
18
|
+
* HMAC-SHA256 an invite token with the boot-required pepper (+ the `invite:` domain prefix); returns a
|
|
19
|
+
* lowercase hex digest. Deterministic, so the redeem path recomputes it and looks the invite up by an
|
|
20
|
+
* exact `token_hash` equality on the unique index.
|
|
21
|
+
*/
|
|
22
|
+
export function hashInviteToken(token, pepper = getApiKeyPepper()) {
|
|
23
|
+
return createHmac('sha256', pepper).update(`invite:${token}`).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mint a new invite token: 32 bytes (256-bit) of CSPRNG, base64url-encoded (URL-safe). Returns the
|
|
27
|
+
* one-time plaintext token + the HMAC hash to store. The plaintext is shown EXACTLY ONCE at issue.
|
|
28
|
+
*/
|
|
29
|
+
export function mintInviteToken(pepper = getApiKeyPepper()) {
|
|
30
|
+
const token = randomBytes(32).toString('base64url'); // 256-bit, URL-safe
|
|
31
|
+
return { token, hash: hashInviteToken(token, pepper) };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=invite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite.js","sourceRoot":"","sources":["../src/invite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAS/C;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,SAAiB,eAAe,EAAE;IAC/E,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB,eAAe,EAAE;IAChE,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,oBAAoB;IACzE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** OWASP-2024 argon2id parameters. Bump these to strengthen; old hashes still verify. */
|
|
2
|
+
export declare const ARGON2ID_PARAMS: {
|
|
3
|
+
readonly type: 2;
|
|
4
|
+
readonly memoryCost: 19456;
|
|
5
|
+
readonly timeCost: 2;
|
|
6
|
+
readonly parallelism: 1;
|
|
7
|
+
};
|
|
8
|
+
/** Hash a plaintext password with argon2id (params embedded in the returned encoded hash). */
|
|
9
|
+
export declare function hashPassword(plaintext: string): Promise<string>;
|
|
10
|
+
/** Verify a plaintext password against an argon2id-encoded hash. Never throws on a mismatch. */
|
|
11
|
+
export declare function verifyPassword(hash: string, plaintext: string): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Upgrade-on-login helper: true when `hash` was produced with weaker params than the current
|
|
14
|
+
* ARGON2ID_PARAMS, so the caller should transparently re-hash the (just-verified) password.
|
|
15
|
+
*/
|
|
16
|
+
export declare function needsRehash(hash: string): boolean;
|
|
17
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AAcA,yFAAyF;AACzF,eAAO,MAAM,eAAe;;;;;CAKlB,CAAC;AAEX,8FAA8F;AAC9F,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED,gGAAgG;AAChG,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOtF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMjD"}
|
package/dist/password.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password hashing primitive — argon2id, isolated behind hashPassword/verifyPassword so no
|
|
3
|
+
* call site touches the argon2 API directly (argon2id for low-entropy
|
|
4
|
+
* passwords). Verified live (doc-first, 2026-06-22) against argon2 0.44.0:
|
|
5
|
+
* - argon2.argon2id === 2; hash(pw, {type, memoryCost, timeCost, parallelism}) returns an
|
|
6
|
+
* encoded string with the params embedded ($argon2id$v=19$m=19456,t=2,p=1$...);
|
|
7
|
+
* - verify(hash, pw) → boolean; needsRehash(hash, params) → true when params increased.
|
|
8
|
+
*
|
|
9
|
+
* Params are OWASP Password Storage Cheat Sheet (2024): argon2id, m=19 MiB, t=2, p=1.
|
|
10
|
+
* Because the params live INSIDE the hash, raising ARGON2ID_PARAMS later lets verify() keep
|
|
11
|
+
* accepting old hashes while needsRehash() flags them for an upgrade-on-login re-hash.
|
|
12
|
+
*/
|
|
13
|
+
import argon2 from 'argon2';
|
|
14
|
+
/** OWASP-2024 argon2id parameters. Bump these to strengthen; old hashes still verify. */
|
|
15
|
+
export const ARGON2ID_PARAMS = {
|
|
16
|
+
type: argon2.argon2id,
|
|
17
|
+
memoryCost: 19456, // 19 MiB, in KiB
|
|
18
|
+
timeCost: 2,
|
|
19
|
+
parallelism: 1,
|
|
20
|
+
};
|
|
21
|
+
/** Hash a plaintext password with argon2id (params embedded in the returned encoded hash). */
|
|
22
|
+
export async function hashPassword(plaintext) {
|
|
23
|
+
return argon2.hash(plaintext, ARGON2ID_PARAMS);
|
|
24
|
+
}
|
|
25
|
+
/** Verify a plaintext password against an argon2id-encoded hash. Never throws on a mismatch. */
|
|
26
|
+
export async function verifyPassword(hash, plaintext) {
|
|
27
|
+
try {
|
|
28
|
+
return await argon2.verify(hash, plaintext);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// A malformed/unrecognized hash is a verification failure, not a crash.
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Upgrade-on-login helper: true when `hash` was produced with weaker params than the current
|
|
37
|
+
* ARGON2ID_PARAMS, so the caller should transparently re-hash the (just-verified) password.
|
|
38
|
+
*/
|
|
39
|
+
export function needsRehash(hash) {
|
|
40
|
+
return argon2.needsRehash(hash, {
|
|
41
|
+
memoryCost: ARGON2ID_PARAMS.memoryCost,
|
|
42
|
+
timeCost: ARGON2ID_PARAMS.timeCost,
|
|
43
|
+
parallelism: ARGON2ID_PARAMS.parallelism,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC,QAAQ;IACrB,UAAU,EAAE,KAAK,EAAE,iBAAiB;IACpC,QAAQ,EAAE,CAAC;IACX,WAAW,EAAE,CAAC;CACN,CAAC;AAEX,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,SAAiB;IAClD,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;AACjD,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,SAAiB;IAClE,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE;QAC9B,UAAU,EAAE,eAAe,CAAC,UAAU;QACtC,QAAQ,EAAE,eAAe,CAAC,QAAQ;QAClC,WAAW,EAAE,eAAe,CAAC,WAAW;KACzC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter: credential-stuffing + argon2id-DoS + the refresh-reuse
|
|
3
|
+
* anti-DoS lock. In-process fixed-window counter NOW, behind a pluggable interface so a
|
|
4
|
+
* Redis/Postgres store can replace it later without touching call sites (deployment topology is
|
|
5
|
+
* not yet pinned — Open decisions).
|
|
6
|
+
*/
|
|
7
|
+
/** The store contract — swap in Redis/Postgres later. */
|
|
8
|
+
export interface RateLimitStore {
|
|
9
|
+
/** Increment the counter for `key`, returning the new count. The window resets after windowMs. */
|
|
10
|
+
hit(key: string, windowMs: number): {
|
|
11
|
+
count: number;
|
|
12
|
+
resetAt: number;
|
|
13
|
+
};
|
|
14
|
+
/** Force a temporary lock for `key` until now+ms (the refresh-reuse anti-DoS lock). */
|
|
15
|
+
lock(key: string, ms: number): void;
|
|
16
|
+
/** True if `key` is currently locked. */
|
|
17
|
+
isLocked(key: string): boolean;
|
|
18
|
+
/** Reset a key (e.g. on a successful login). */
|
|
19
|
+
reset(key: string): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The default hard cap on the number of tracked (window / lock) keys. Bounds the store's memory so a
|
|
23
|
+
* flood of distinct keys (high-cardinality traffic) cannot grow it without limit — the OOM vector a
|
|
24
|
+
* never-pruned per-key Map otherwise carried. 100k keys is far above any single node's legitimate
|
|
25
|
+
* concurrent-source cardinality yet a trivial memory footprint.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEFAULT_MAX_RATE_LIMIT_ENTRIES = 100000;
|
|
28
|
+
/**
|
|
29
|
+
* In-process store. NOT shared across processes — fine for single-node. BOUNDED + SELF-PRUNING: each
|
|
30
|
+
* time a new/expired window (or a lock) is (re)inserted at or above the cap, expired entries are swept
|
|
31
|
+
* and, if still full, the oldest are evicted — so the maps never exceed `maxEntries`. The clock is
|
|
32
|
+
* injectable (defaults to `Date.now`) so the expiry paths are deterministically testable.
|
|
33
|
+
*/
|
|
34
|
+
export declare class InMemoryRateLimitStore implements RateLimitStore {
|
|
35
|
+
private readonly windows;
|
|
36
|
+
private readonly locks;
|
|
37
|
+
private readonly maxEntries;
|
|
38
|
+
private readonly now;
|
|
39
|
+
constructor(maxEntries?: number, now?: () => number);
|
|
40
|
+
hit(key: string, windowMs: number): {
|
|
41
|
+
count: number;
|
|
42
|
+
resetAt: number;
|
|
43
|
+
};
|
|
44
|
+
lock(key: string, ms: number): void;
|
|
45
|
+
isLocked(key: string): boolean;
|
|
46
|
+
reset(key: string): void;
|
|
47
|
+
/** Clear ALL windows + locks (test isolation; not used on the hot path). */
|
|
48
|
+
clearAll(): void;
|
|
49
|
+
/** The number of tracked windows (observability + makes the max-size bound assertable). */
|
|
50
|
+
size(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Bound `map` to `maxEntries`: a no-op until it reaches the cap, then sweep every entry whose
|
|
53
|
+
* `expiryOf(value) <= now` and — if still at/over the cap (all live) — evict the OLDEST-inserted
|
|
54
|
+
* (a Map iterates in insertion order, so the front is the oldest) until it is under the cap. Called
|
|
55
|
+
* only on the (re)insert path, so the steady-state hot path (incrementing a live counter) pays
|
|
56
|
+
* nothing.
|
|
57
|
+
*/
|
|
58
|
+
private prune;
|
|
59
|
+
}
|
|
60
|
+
/** A named limit policy. */
|
|
61
|
+
export interface RateLimitPolicy {
|
|
62
|
+
/** Max hits allowed within the window. */
|
|
63
|
+
max: number;
|
|
64
|
+
/** Window length in ms. */
|
|
65
|
+
windowMs: number;
|
|
66
|
+
}
|
|
67
|
+
/** Default per-route policies (tuneable). Keyed by a logical bucket name. */
|
|
68
|
+
export declare const DEFAULT_POLICIES: Record<string, RateLimitPolicy>;
|
|
69
|
+
/** Duration of the refresh-reuse anti-DoS lock (a stale token cannot be a repeatable DoS). */
|
|
70
|
+
export declare const REUSE_LOCK_MS: number;
|
|
71
|
+
/**
|
|
72
|
+
* The limiter facade used by the HTTP layer. `check(bucket, id)` returns whether the call is
|
|
73
|
+
* allowed; `lockSource`/`isLocked` back the refresh-reuse anti-DoS lock.
|
|
74
|
+
*/
|
|
75
|
+
export declare class RateLimiter {
|
|
76
|
+
private readonly store;
|
|
77
|
+
private readonly policies;
|
|
78
|
+
constructor(store?: RateLimitStore, policies?: Record<string, RateLimitPolicy>);
|
|
79
|
+
/** True if this (bucket,id) is within its rate budget AND not locked. */
|
|
80
|
+
check(bucket: string, id: string): {
|
|
81
|
+
allowed: boolean;
|
|
82
|
+
retryAfterMs: number;
|
|
83
|
+
};
|
|
84
|
+
/** Lock a source bucket (the refresh-reuse anti-DoS per-source lock). */
|
|
85
|
+
lockSource(bucket: string, id: string, ms?: number): void;
|
|
86
|
+
/** True if a source bucket is locked. */
|
|
87
|
+
isLocked(bucket: string, id: string): boolean;
|
|
88
|
+
/** Reset a bucket (e.g. on a successful authentication). */
|
|
89
|
+
reset(bucket: string, id: string): void;
|
|
90
|
+
/** Clear ALL state (test isolation). No-op if the store does not support it. */
|
|
91
|
+
clearAll(): void;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,kGAAkG;IAClG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,uFAAuF;IACvF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,yCAAyC;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,gDAAgD;IAChD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAEtD;;;;;GAKG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyD;IACjF,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,UAAU,GAAE,MAAuC,EAAE,GAAG,GAAE,MAAM,MAAiB;IAK7F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IActE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAMnC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAU9B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKxB,4EAA4E;IAC5E,QAAQ,IAAI,IAAI;IAKhB,2FAA2F;IAC3F,IAAI,IAAI,MAAM;IAId;;;;;;OAMG;IACH,OAAO,CAAC,KAAK;CAWd;AAED,4BAA4B;AAC5B,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAkB5D,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,aAAa,QAAa,CAAC;AAExC;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;gBAE/C,KAAK,GAAE,cAA6C,EAAE,QAAQ,kCAAmB;IAK7F,yEAAyE;IACzE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE;IAW7E,yEAAyE;IACzE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAgB,GAAG,IAAI;IAIhE,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAI7C,4DAA4D;IAC5D,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIvC,gFAAgF;IAChF,QAAQ,IAAI,IAAI;CAGjB"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter: credential-stuffing + argon2id-DoS + the refresh-reuse
|
|
3
|
+
* anti-DoS lock. In-process fixed-window counter NOW, behind a pluggable interface so a
|
|
4
|
+
* Redis/Postgres store can replace it later without touching call sites (deployment topology is
|
|
5
|
+
* not yet pinned — Open decisions).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The default hard cap on the number of tracked (window / lock) keys. Bounds the store's memory so a
|
|
9
|
+
* flood of distinct keys (high-cardinality traffic) cannot grow it without limit — the OOM vector a
|
|
10
|
+
* never-pruned per-key Map otherwise carried. 100k keys is far above any single node's legitimate
|
|
11
|
+
* concurrent-source cardinality yet a trivial memory footprint.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_MAX_RATE_LIMIT_ENTRIES = 100_000;
|
|
14
|
+
/**
|
|
15
|
+
* In-process store. NOT shared across processes — fine for single-node. BOUNDED + SELF-PRUNING: each
|
|
16
|
+
* time a new/expired window (or a lock) is (re)inserted at or above the cap, expired entries are swept
|
|
17
|
+
* and, if still full, the oldest are evicted — so the maps never exceed `maxEntries`. The clock is
|
|
18
|
+
* injectable (defaults to `Date.now`) so the expiry paths are deterministically testable.
|
|
19
|
+
*/
|
|
20
|
+
export class InMemoryRateLimitStore {
|
|
21
|
+
windows = new Map();
|
|
22
|
+
locks = new Map();
|
|
23
|
+
maxEntries;
|
|
24
|
+
now;
|
|
25
|
+
constructor(maxEntries = DEFAULT_MAX_RATE_LIMIT_ENTRIES, now = Date.now) {
|
|
26
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
27
|
+
this.now = now;
|
|
28
|
+
}
|
|
29
|
+
hit(key, windowMs) {
|
|
30
|
+
const now = this.now();
|
|
31
|
+
const cur = this.windows.get(key);
|
|
32
|
+
if (!cur || cur.resetAt <= now) {
|
|
33
|
+
// A NEW or expired key is about to (re)enter the map — the only growth path — so bound it here.
|
|
34
|
+
this.prune(this.windows, now, (v) => v.resetAt);
|
|
35
|
+
const fresh = { count: 1, resetAt: now + windowMs };
|
|
36
|
+
this.windows.set(key, fresh);
|
|
37
|
+
return fresh;
|
|
38
|
+
}
|
|
39
|
+
cur.count += 1;
|
|
40
|
+
return cur;
|
|
41
|
+
}
|
|
42
|
+
lock(key, ms) {
|
|
43
|
+
const now = this.now();
|
|
44
|
+
this.prune(this.locks, now, (until) => until);
|
|
45
|
+
this.locks.set(key, now + ms);
|
|
46
|
+
}
|
|
47
|
+
isLocked(key) {
|
|
48
|
+
const until = this.locks.get(key);
|
|
49
|
+
if (until === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
if (until <= this.now()) {
|
|
52
|
+
this.locks.delete(key);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
reset(key) {
|
|
58
|
+
this.windows.delete(key);
|
|
59
|
+
this.locks.delete(key);
|
|
60
|
+
}
|
|
61
|
+
/** Clear ALL windows + locks (test isolation; not used on the hot path). */
|
|
62
|
+
clearAll() {
|
|
63
|
+
this.windows.clear();
|
|
64
|
+
this.locks.clear();
|
|
65
|
+
}
|
|
66
|
+
/** The number of tracked windows (observability + makes the max-size bound assertable). */
|
|
67
|
+
size() {
|
|
68
|
+
return this.windows.size;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Bound `map` to `maxEntries`: a no-op until it reaches the cap, then sweep every entry whose
|
|
72
|
+
* `expiryOf(value) <= now` and — if still at/over the cap (all live) — evict the OLDEST-inserted
|
|
73
|
+
* (a Map iterates in insertion order, so the front is the oldest) until it is under the cap. Called
|
|
74
|
+
* only on the (re)insert path, so the steady-state hot path (incrementing a live counter) pays
|
|
75
|
+
* nothing.
|
|
76
|
+
*/
|
|
77
|
+
prune(map, now, expiryOf) {
|
|
78
|
+
if (map.size < this.maxEntries)
|
|
79
|
+
return;
|
|
80
|
+
for (const [k, v] of map) {
|
|
81
|
+
if (expiryOf(v) <= now)
|
|
82
|
+
map.delete(k);
|
|
83
|
+
}
|
|
84
|
+
while (map.size >= this.maxEntries) {
|
|
85
|
+
const oldest = map.keys().next().value;
|
|
86
|
+
if (oldest === undefined)
|
|
87
|
+
break;
|
|
88
|
+
map.delete(oldest);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Default per-route policies (tuneable). Keyed by a logical bucket name. */
|
|
93
|
+
export const DEFAULT_POLICIES = {
|
|
94
|
+
login: { max: 10, windowMs: 60_000 },
|
|
95
|
+
register: { max: 5, windowMs: 60_000 },
|
|
96
|
+
refresh: { max: 30, windowMs: 60_000 },
|
|
97
|
+
'oauth-token': { max: 30, windowMs: 60_000 },
|
|
98
|
+
// Session reprocess mints a FRESH durable run each call (dedup is deliberately bypassed), so an
|
|
99
|
+
// unthrottled caller can re-drive the same session's workflow without bound — a cost-DoS. Cap the
|
|
100
|
+
// reprocesses of one (tenant, session) per window; the route keys the bucket by `${tenant}:${session}`.
|
|
101
|
+
reprocess: { max: 5, windowMs: 60_000 },
|
|
102
|
+
// A manual trigger fire dispatches a declared action (agent run / handler). Within one firing-instant
|
|
103
|
+
// bucket a re-fire dedups, but distinct instants each dispatch — so an unthrottled caller could fire a
|
|
104
|
+
// named trigger repeatedly (a cost-DoS). Cap the fires of one (tenant, trigger) per window; the route
|
|
105
|
+
// keys the bucket by `${tenant}:${name}`.
|
|
106
|
+
'trigger-fire': { max: 30, windowMs: 60_000 },
|
|
107
|
+
// The invite-accept endpoint is UNAUTHENTICATED (a token bearer redeems) and can PROVISION an
|
|
108
|
+
// account, so throttle it per source IP to bound token-probing / account-creation abuse. The token
|
|
109
|
+
// is 256-bit (brute-force is infeasible regardless), but a per-source cap is cheap defense-in-depth.
|
|
110
|
+
'invite-accept': { max: 10, windowMs: 60_000 },
|
|
111
|
+
};
|
|
112
|
+
/** Duration of the refresh-reuse anti-DoS lock (a stale token cannot be a repeatable DoS). */
|
|
113
|
+
export const REUSE_LOCK_MS = 5 * 60_000;
|
|
114
|
+
/**
|
|
115
|
+
* The limiter facade used by the HTTP layer. `check(bucket, id)` returns whether the call is
|
|
116
|
+
* allowed; `lockSource`/`isLocked` back the refresh-reuse anti-DoS lock.
|
|
117
|
+
*/
|
|
118
|
+
export class RateLimiter {
|
|
119
|
+
store;
|
|
120
|
+
policies;
|
|
121
|
+
constructor(store = new InMemoryRateLimitStore(), policies = DEFAULT_POLICIES) {
|
|
122
|
+
this.store = store;
|
|
123
|
+
this.policies = policies;
|
|
124
|
+
}
|
|
125
|
+
/** True if this (bucket,id) is within its rate budget AND not locked. */
|
|
126
|
+
check(bucket, id) {
|
|
127
|
+
const key = `${bucket}:${id}`;
|
|
128
|
+
if (this.store.isLocked(key))
|
|
129
|
+
return { allowed: false, retryAfterMs: REUSE_LOCK_MS };
|
|
130
|
+
const policy = this.policies[bucket];
|
|
131
|
+
if (!policy)
|
|
132
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
133
|
+
const { count, resetAt } = this.store.hit(key, policy.windowMs);
|
|
134
|
+
if (count > policy.max)
|
|
135
|
+
return { allowed: false, retryAfterMs: Math.max(0, resetAt - Date.now()) };
|
|
136
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
137
|
+
}
|
|
138
|
+
/** Lock a source bucket (the refresh-reuse anti-DoS per-source lock). */
|
|
139
|
+
lockSource(bucket, id, ms = REUSE_LOCK_MS) {
|
|
140
|
+
this.store.lock(`${bucket}:${id}`, ms);
|
|
141
|
+
}
|
|
142
|
+
/** True if a source bucket is locked. */
|
|
143
|
+
isLocked(bucket, id) {
|
|
144
|
+
return this.store.isLocked(`${bucket}:${id}`);
|
|
145
|
+
}
|
|
146
|
+
/** Reset a bucket (e.g. on a successful authentication). */
|
|
147
|
+
reset(bucket, id) {
|
|
148
|
+
this.store.reset(`${bucket}:${id}`);
|
|
149
|
+
}
|
|
150
|
+
/** Clear ALL state (test isolation). No-op if the store does not support it. */
|
|
151
|
+
clearAll() {
|
|
152
|
+
if (this.store instanceof InMemoryRateLimitStore)
|
|
153
|
+
this.store.clearAll();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,OAAO,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,OAAO,sBAAsB;IAChB,OAAO,GAAG,IAAI,GAAG,EAA8C,CAAC;IAChE,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAClC,UAAU,CAAS;IACnB,GAAG,CAAe;IAEnC,YAAY,aAAqB,8BAA8B,EAAE,MAAoB,IAAI,CAAC,GAAG;QAC3F,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,QAAgB;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;YAC/B,gGAAgG;YAChG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YAChD,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAE,CAAC;YACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC;QACf,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,CAAC,GAAW,EAAE,EAAU;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAChC,CAAC;IAED,QAAQ,CAAC,GAAW;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACtC,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,GAAW;QACf,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,4EAA4E;IAC5E,QAAQ;QACN,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,2FAA2F;IAC3F,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAI,GAAmB,EAAE,GAAW,EAAE,QAA8B;QAC/E,IAAI,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU;YAAE,OAAO;QACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;YACzB,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG;gBAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACnC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YACvC,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM;YAChC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;CACF;AAUD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAoC;IAC/D,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACpC,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE;IACtC,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IACtC,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC5C,gGAAgG;IAChG,kGAAkG;IAClG,wGAAwG;IACxG,SAAS,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE;IACvC,sGAAsG;IACtG,uGAAuG;IACvG,sGAAsG;IACtG,0CAA0C;IAC1C,cAAc,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC7C,8FAA8F;IAC9F,mGAAmG;IACnG,qGAAqG;IACrG,eAAe,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;CAC/C,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC;AAExC;;;GAGG;AACH,MAAM,OAAO,WAAW;IACL,KAAK,CAAiB;IACtB,QAAQ,CAAkC;IAE3D,YAAY,QAAwB,IAAI,sBAAsB,EAAE,EAAE,QAAQ,GAAG,gBAAgB;QAC3F,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,MAAc,EAAE,EAAU;QAC9B,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,EAAE,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;QACrF,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACvD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChE,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG;YACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IAC5C,CAAC;IAED,yEAAyE;IACzE,UAAU,CAAC,MAAc,EAAE,EAAU,EAAE,EAAE,GAAG,aAAa;QACvD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,yCAAyC;IACzC,QAAQ,CAAC,MAAc,EAAE,EAAU;QACjC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,MAAc,EAAE,EAAU;QAC9B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,gFAAgF;IAChF,QAAQ;QACN,IAAI,IAAI,CAAC,KAAK,YAAY,sBAAsB;YAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1E,CAAC;CACF"}
|