@selvajs/server 1.5.0-beta.1 → 1.5.0-beta.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 +17 -17
- package/dist/api/deps.d.ts +13 -1
- package/dist/api/deps.d.ts.map +1 -1
- package/dist/api/deps.js +3 -1
- package/dist/api/deps.js.map +1 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +13 -1
- package/dist/api/errors.js.map +1 -1
- package/dist/api/idempotency.d.ts +58 -0
- package/dist/api/idempotency.d.ts.map +1 -0
- package/dist/api/idempotency.js +130 -0
- package/dist/api/idempotency.js.map +1 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/rate-limit.d.ts +57 -0
- package/dist/api/rate-limit.d.ts.map +1 -0
- package/dist/api/rate-limit.js +108 -0
- package/dist/api/rate-limit.js.map +1 -0
- package/dist/api/respond.d.ts +9 -9
- package/dist/api/respond.d.ts.map +1 -1
- package/dist/api/respond.js +49 -17
- package/dist/api/respond.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -26,23 +26,23 @@ pnpm add @selvajs/server
|
|
|
26
26
|
**There is no root export.** Import from a subpath — `import … from '@selvajs/server'` fails to
|
|
27
27
|
resolve, on purpose, so it stays visible which slice a consumer depends on.
|
|
28
28
|
|
|
29
|
-
| Export | Contents
|
|
30
|
-
| ------------------------------- |
|
|
31
|
-
| `@selvajs/server/compute` | Compute limits, rate limiting, idempotency, SSRF guard, remote-definition fetch
|
|
32
|
-
| `@selvajs/server/definitions` | Definition service, compute-schema extraction, render loader
|
|
33
|
-
| `@selvajs/server/providers` | Env-driven provider selection over a caller-supplied registry
|
|
34
|
-
| `@selvajs/server/tokens` | HMAC codec for capability-URL tokens (share links, invites)
|
|
35
|
-
| `@selvajs/server/errors` | Sentry-backed `IErrorReporter`
|
|
36
|
-
| `@selvajs/server/logging` | pino-backed `ILogger` + request-id correlation
|
|
37
|
-
| `@selvajs/server/http` | Security headers, body-size and redirect guards, route classification
|
|
38
|
-
| `@selvajs/server/access` | Builds the input platform's project access rules consume
|
|
39
|
-
| `@selvajs/server/ops` | Channel-aware semver comparison
|
|
40
|
-
| `@selvajs/server/api` | Transport-free API core: handler contract,
|
|
41
|
-
| `@selvajs/server/handlers` | The API handlers: business logic behind each endpoint, bound via `runHandler`
|
|
42
|
-
| `@selvajs/server/projects` | Project writes whose rules two routes must agree on
|
|
43
|
-
| `@selvajs/server/organizations` | Org-level domain services spanning more than one provider
|
|
44
|
-
| `@selvajs/server/notifications` | `SmtpNotificationProvider`, SMTP config, invite delivery
|
|
45
|
-
| `@selvajs/server/testing` | Test harness for the transport-free API handlers
|
|
29
|
+
| Export | Contents |
|
|
30
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `@selvajs/server/compute` | Compute limits, rate limiting, idempotency, SSRF guard, remote-definition fetch |
|
|
32
|
+
| `@selvajs/server/definitions` | Definition service, compute-schema extraction, render loader |
|
|
33
|
+
| `@selvajs/server/providers` | Env-driven provider selection over a caller-supplied registry |
|
|
34
|
+
| `@selvajs/server/tokens` | HMAC codec for capability-URL tokens (share links, invites) |
|
|
35
|
+
| `@selvajs/server/errors` | Sentry-backed `IErrorReporter` |
|
|
36
|
+
| `@selvajs/server/logging` | pino-backed `ILogger` + request-id correlation |
|
|
37
|
+
| `@selvajs/server/http` | Security headers, body-size and redirect guards, route classification |
|
|
38
|
+
| `@selvajs/server/access` | Builds the input platform's project access rules consume |
|
|
39
|
+
| `@selvajs/server/ops` | Channel-aware semver comparison |
|
|
40
|
+
| `@selvajs/server/api` | Transport-free API core: handler contract, deps, errors, token scope, per-token rate limit, idempotency |
|
|
41
|
+
| `@selvajs/server/handlers` | The API handlers: business logic behind each endpoint, bound via `runHandler` |
|
|
42
|
+
| `@selvajs/server/projects` | Project writes whose rules two routes must agree on |
|
|
43
|
+
| `@selvajs/server/organizations` | Org-level domain services spanning more than one provider |
|
|
44
|
+
| `@selvajs/server/notifications` | `SmtpNotificationProvider`, SMTP config, invite delivery |
|
|
45
|
+
| `@selvajs/server/testing` | Test harness for the transport-free API handlers |
|
|
46
46
|
|
|
47
47
|
Requires a `@selvajs/platform` provider (e.g. `@selvajs/local-provider` or
|
|
48
48
|
`@selvajs/supabase-provider`) for data/storage/auth.
|
package/dist/api/deps.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type { DefinitionService } from '../definitions/definition-service.js';
|
|
|
15
15
|
import type { OrgAssetService } from '../organizations/org-asset-service.js';
|
|
16
16
|
import type { TokenCodec } from '../tokens/token-codec.js';
|
|
17
17
|
import type { ApiTokenCodec } from '../tokens/api-token-codec.js';
|
|
18
|
+
import type { ApiRateLimiter } from './rate-limit.js';
|
|
19
|
+
import type { ApiIdempotencyStore } from './idempotency.js';
|
|
18
20
|
import type { IAuthProvider, IDataProvider, IEventSink, INotificationProvider, IStorageProvider, SelvaConfig, SelvaFlags } from '@selvajs/platform';
|
|
19
21
|
export interface SelvaDeps {
|
|
20
22
|
auth: IAuthProvider;
|
|
@@ -90,6 +92,14 @@ export interface SelvaDeps {
|
|
|
90
92
|
* handlers report `not-configured` rather than failing the write.
|
|
91
93
|
*/
|
|
92
94
|
notifications?: INotificationProvider;
|
|
95
|
+
/**
|
|
96
|
+
* Per-token request limit `runHandler` charges. Absent: the process-wide
|
|
97
|
+
* default (`DEFAULT_API_RATE_LIMIT`). `null`: no limit. A host's own limiter
|
|
98
|
+
* must outlive the request, since deps are rebuilt per request.
|
|
99
|
+
*/
|
|
100
|
+
apiRateLimiter?: ApiRateLimiter | null;
|
|
101
|
+
/** Replay store for routes mounted with `idempotent: true`. Absent: the process-wide default. */
|
|
102
|
+
idempotency?: ApiIdempotencyStore;
|
|
93
103
|
/** Instance display name, used where a record has none. */
|
|
94
104
|
instanceName: string;
|
|
95
105
|
/** Where a person manages their API tokens, linked from the new-key email. Host-relative. */
|
|
@@ -130,12 +140,14 @@ export interface SelvaDeps {
|
|
|
130
140
|
* `tokens` arrives in an options object rather than as a third positional, so
|
|
131
141
|
* the two composed bags cannot be swapped at a call site.
|
|
132
142
|
*/
|
|
133
|
-
export declare function depsFromConfig(config: SelvaConfig, services?: SelvaDeps['services'], { tokens, uploadLimits, evictComputeClient, notifications, instanceName, apiTokenSettingsPath }?: {
|
|
143
|
+
export declare function depsFromConfig(config: SelvaConfig, services?: SelvaDeps['services'], { tokens, uploadLimits, evictComputeClient, notifications, instanceName, apiTokenSettingsPath, apiRateLimiter, idempotency }?: {
|
|
134
144
|
tokens?: SelvaDeps['tokens'];
|
|
135
145
|
uploadLimits?: Partial<SelvaDeps['uploadLimits']>;
|
|
136
146
|
evictComputeClient?: SelvaDeps['evictComputeClient'];
|
|
137
147
|
notifications?: INotificationProvider;
|
|
138
148
|
instanceName?: string;
|
|
139
149
|
apiTokenSettingsPath?: string;
|
|
150
|
+
apiRateLimiter?: ApiRateLimiter | null;
|
|
151
|
+
idempotency?: ApiIdempotencyStore;
|
|
140
152
|
}): SelvaDeps;
|
|
141
153
|
//# sourceMappingURL=deps.d.ts.map
|
package/dist/api/deps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/api/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EACX,aAAa,EACb,aAAa,EACb,UAAU,EACV,qBAAqB,EACrB,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,uEAAuE;IACvE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,uCAAuC;IACvC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACpC,0DAA0D;IAC1D,cAAc,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC7C,aAAa,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC9C,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACxC,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,qBAAqB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC9D;;;;;;;OAOG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,UAAU,KAAK,OAAO,CAAC;IAC1C;;;;;;;;;;;;;OAaG;IACH,MAAM,EAAE;QACP,UAAU,CAAC,EAAE,UAAU,CAAC;QACxB,OAAO,CAAC,EAAE,UAAU,CAAC;QACrB,gEAAgE;QAChE,SAAS,CAAC,EAAE,aAAa,CAAC;KAC1B,CAAC;IACF;;;;;;;;OAQG;IACH,YAAY,EAAE;QACb,qBAAqB,EAAE,MAAM,CAAC;QAC9B,gBAAgB,EAAE,MAAM,CAAC;KACzB,CAAC;IACF;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;OAWG;IACH,kBAAkB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,EAAE;QACT,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,SAAS,CAAC,EAAE,eAAe,CAAC;KAC5B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,WAAW,EACnB,QAAQ,GAAE,SAAS,CAAC,UAAU,CAAM,EACpC,EACC,MAAW,EACX,YAAY,EACZ,kBAA6B,EAC7B,aAAa,EACb,YAAsB,EACtB,oBAAyC,EACzC,GAAE;IACF,MAAM,CAAC,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC;IAClD,kBAAkB,CAAC,EAAE,SAAS,CAAC,oBAAoB,CAAC,CAAC;IACrD,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/api/deps.ts"],"names":[],"mappings":"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"}
|
package/dist/api/deps.js
CHANGED
|
@@ -21,7 +21,7 @@ import { isFlagEnabled, NoopEventSink } from '@selvajs/platform';
|
|
|
21
21
|
* `tokens` arrives in an options object rather than as a third positional, so
|
|
22
22
|
* the two composed bags cannot be swapped at a call site.
|
|
23
23
|
*/
|
|
24
|
-
export function depsFromConfig(config, services = {}, { tokens = {}, uploadLimits, evictComputeClient = () => { }, notifications, instanceName = 'Selva', apiTokenSettingsPath = '/settings/tokens' } = {}) {
|
|
24
|
+
export function depsFromConfig(config, services = {}, { tokens = {}, uploadLimits, evictComputeClient = () => { }, notifications, instanceName = 'Selva', apiTokenSettingsPath = '/settings/tokens', apiRateLimiter, idempotency } = {}) {
|
|
25
25
|
const { data } = config;
|
|
26
26
|
return {
|
|
27
27
|
auth: config.auth,
|
|
@@ -50,6 +50,8 @@ export function depsFromConfig(config, services = {}, { tokens = {}, uploadLimit
|
|
|
50
50
|
notifications,
|
|
51
51
|
instanceName,
|
|
52
52
|
apiTokenSettingsPath,
|
|
53
|
+
apiRateLimiter,
|
|
54
|
+
idempotency,
|
|
53
55
|
services
|
|
54
56
|
};
|
|
55
57
|
}
|
package/dist/api/deps.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/api/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/api/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAmIjE;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC7B,MAAmB,EACnB,WAAkC,EAAE,EACpC,EACC,MAAM,GAAG,EAAE,EACX,YAAY,EACZ,kBAAkB,GAAG,GAAG,EAAE,GAAE,CAAC,EAC7B,aAAa,EACb,YAAY,GAAG,OAAO,EACtB,oBAAoB,GAAG,kBAAkB,EACzC,cAAc,EACd,WAAW,KAUR,EAAE;IAEN,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IACxB,OAAO;QACN,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,cAAc,EAAE,IAAI,CAAC,WAAW;QAChC,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,qBAAqB,EAAE,IAAI,CAAC,qBAAqB;QACjD,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,IAAI,aAAa,EAAE;QAC5C,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC;QAC3C,MAAM;QACN,qEAAqE;QACrE,sEAAsE;QACtE,sEAAsE;QACtE,YAAY,EAAE;YACb,qBAAqB,EAAE,YAAY,EAAE,qBAAqB,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI;YAC9E,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI;SACpE;QACD,kBAAkB;QAClB,aAAa;QACb,YAAY;QACZ,oBAAoB;QACpB,cAAc;QACd,WAAW;QACX,QAAQ;KACR,CAAC;AACH,CAAC"}
|
package/dist/api/errors.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare const ApiErrorCode: {
|
|
|
20
20
|
readonly INTERNAL: "INTERNAL";
|
|
21
21
|
/** API tokens are switched off or misconfigured here; browser sessions still work. */
|
|
22
22
|
readonly API_TOKENS_UNAVAILABLE: "API_TOKENS_UNAVAILABLE";
|
|
23
|
+
/** Too many requests, or a share link's solve cap is spent. `Retry-After`, when sent, says when to retry. */
|
|
24
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
23
25
|
};
|
|
24
26
|
export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode];
|
|
25
27
|
export declare class ApiError extends Error {
|
|
@@ -31,6 +33,13 @@ export declare class ApiError extends Error {
|
|
|
31
33
|
readonly details?: Record<string, string>;
|
|
32
34
|
constructor(status: number, code: ApiErrorCode, message: string, fields?: Record<string, string>, details?: Record<string, string>);
|
|
33
35
|
}
|
|
36
|
+
export interface ApiErrorBody {
|
|
37
|
+
message: string;
|
|
38
|
+
code: ApiErrorCode;
|
|
39
|
+
fields?: Record<string, string>;
|
|
40
|
+
details?: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
export declare function toErrorBody(err: ApiError): ApiErrorBody;
|
|
34
43
|
export declare function isApiError(err: unknown): err is ApiError;
|
|
35
44
|
/** Raise a structured API error. The only way handlers should fail. */
|
|
36
45
|
export declare function apiError(status: number, code: ApiErrorCode, message: string, fields?: Record<string, string>, details?: Record<string, string>): never;
|
package/dist/api/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,eAAO,MAAM,YAAY;;;;;;;;;;IAUxB,sFAAsF;;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,eAAO,MAAM,YAAY;;;;;;;;;;IAUxB,sFAAsF;;IAEtF,6GAA6G;;CAEpG,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,qBAAa,QAAS,SAAQ,KAAK;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAGzC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CASjC;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,YAAY,CAKvD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,QAAQ,CAExD;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,KAAK,CAEP;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAqB1D"}
|
package/dist/api/errors.js
CHANGED
|
@@ -19,7 +19,9 @@ export const ApiErrorCode = {
|
|
|
19
19
|
SETUP_REQUIRED: 'SETUP_REQUIRED',
|
|
20
20
|
INTERNAL: 'INTERNAL',
|
|
21
21
|
/** API tokens are switched off or misconfigured here; browser sessions still work. */
|
|
22
|
-
API_TOKENS_UNAVAILABLE: 'API_TOKENS_UNAVAILABLE'
|
|
22
|
+
API_TOKENS_UNAVAILABLE: 'API_TOKENS_UNAVAILABLE',
|
|
23
|
+
/** Too many requests, or a share link's solve cap is spent. `Retry-After`, when sent, says when to retry. */
|
|
24
|
+
RATE_LIMITED: 'RATE_LIMITED'
|
|
23
25
|
};
|
|
24
26
|
export class ApiError extends Error {
|
|
25
27
|
status;
|
|
@@ -37,6 +39,14 @@ export class ApiError extends Error {
|
|
|
37
39
|
this.details = details;
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
export function toErrorBody(err) {
|
|
43
|
+
const body = { message: err.message, code: err.code };
|
|
44
|
+
if (err.fields)
|
|
45
|
+
body.fields = err.fields;
|
|
46
|
+
if (err.details)
|
|
47
|
+
body.details = err.details;
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
40
50
|
export function isApiError(err) {
|
|
41
51
|
return err instanceof ApiError;
|
|
42
52
|
}
|
|
@@ -59,6 +69,8 @@ export function codeForStatus(status) {
|
|
|
59
69
|
return ApiErrorCode.CONFLICT;
|
|
60
70
|
case 422:
|
|
61
71
|
return ApiErrorCode.UNPROCESSABLE;
|
|
72
|
+
case 429:
|
|
73
|
+
return ApiErrorCode.RATE_LIMITED;
|
|
62
74
|
case 503:
|
|
63
75
|
return ApiErrorCode.COMPUTE_UNAVAILABLE;
|
|
64
76
|
default:
|
package/dist/api/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,iBAAiB,EAAE,mBAAmB;IACtC,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,UAAU;IACpB,aAAa,EAAE,eAAe;IAC9B,mBAAmB,EAAE,qBAAqB;IAC1C,cAAc,EAAE,gBAAgB;IAChC,QAAQ,EAAE,UAAU;IACpB,sFAAsF;IACtF,sBAAsB,EAAE,wBAAwB;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,iBAAiB,EAAE,mBAAmB;IACtC,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,UAAU;IACpB,aAAa,EAAE,eAAe;IAC9B,mBAAmB,EAAE,qBAAqB;IAC1C,cAAc,EAAE,gBAAgB;IAChC,QAAQ,EAAE,UAAU;IACpB,sFAAsF;IACtF,sBAAsB,EAAE,wBAAwB;IAChD,6GAA6G;IAC7G,YAAY,EAAE,cAAc;CACnB,CAAC;AAIX,MAAM,OAAO,QAAS,SAAQ,KAAK;IACzB,MAAM,CAAS;IACf,IAAI,CAAe;IAC5B,sFAAsF;IAC7E,MAAM,CAA0B;IACzC,yEAAyE;IAChE,OAAO,CAA0B;IAE1C,YACC,MAAc,EACd,IAAkB,EAClB,OAAe,EACf,MAA+B,EAC/B,OAAgC;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AASD,MAAM,UAAU,WAAW,CAAC,GAAa;IACxC,MAAM,IAAI,GAAiB,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IACpE,IAAI,GAAG,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IACzC,IAAI,GAAG,CAAC,OAAO;QAAE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5C,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAY;IACtC,OAAO,GAAG,YAAY,QAAQ,CAAC;AAChC,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CACvB,MAAc,EACd,IAAkB,EAClB,OAAe,EACf,MAA+B,EAC/B,OAAgC;IAEhC,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAC5D,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,aAAa,CAAC,MAAc;IAC3C,QAAQ,MAAM,EAAE,CAAC;QAChB,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,iBAAiB,CAAC;QACvC,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,YAAY,CAAC;QAClC,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,SAAS,CAAC;QAC/B,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,SAAS,CAAC;QAC/B,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,QAAQ,CAAC;QAC9B,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,aAAa,CAAC;QACnC,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,YAAY,CAAC;QAClC,KAAK,GAAG;YACP,OAAO,YAAY,CAAC,mBAAmB,CAAC;QACzC;YACC,OAAO,YAAY,CAAC,QAAQ,CAAC;IAC/B,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Idempotency-Key` for mounted routes that opt in with `idempotent: true`.
|
|
3
|
+
*
|
|
4
|
+
* Built on the solve route's store and wire contract (`../compute/idempotency*`):
|
|
5
|
+
* the store absorbs in-flight and near-term retries in this process; it is not
|
|
6
|
+
* a durable record. A create that must never duplicate also needs the key on
|
|
7
|
+
* the created row under a unique constraint, written in the same transaction.
|
|
8
|
+
*
|
|
9
|
+
* Each entry carries a fingerprint of method, path and body, so a client that
|
|
10
|
+
* reuses a key for a different request gets 422 instead of someone else's
|
|
11
|
+
* replay. Only responses below 400 are kept: a failed attempt frees the key so
|
|
12
|
+
* a corrected retry can run.
|
|
13
|
+
*/
|
|
14
|
+
import type { RequestContext } from '@selvajs/platform';
|
|
15
|
+
import { type IdempotencyStore } from '../compute/idempotency.js';
|
|
16
|
+
import { type StoredResponse } from '../compute/idempotency-http.js';
|
|
17
|
+
export declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
18
|
+
export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
19
|
+
/** Covers a client's retry-on-timeout window without turning into a result cache. */
|
|
20
|
+
export declare const DEFAULT_API_IDEMPOTENCY_TTL_MS: number;
|
|
21
|
+
export interface IdempotentEntry {
|
|
22
|
+
fingerprint: string;
|
|
23
|
+
response: StoredResponse;
|
|
24
|
+
}
|
|
25
|
+
export type ApiIdempotencyStore = IdempotencyStore<IdempotentEntry>;
|
|
26
|
+
export declare function createApiIdempotencyStore(ttlMs?: number): ApiIdempotencyStore;
|
|
27
|
+
/** The store `depsFromConfig` wires when the host passes none. One per process. */
|
|
28
|
+
export declare function defaultApiIdempotencyStore(): ApiIdempotencyStore;
|
|
29
|
+
/**
|
|
30
|
+
* The narrowest identity a key is namespaced by: the token for a token
|
|
31
|
+
* request, so two keys of one user never share replays, else the user.
|
|
32
|
+
*/
|
|
33
|
+
export declare function idempotencyCallerId(ctx: RequestContext): string;
|
|
34
|
+
/** Read and validate the client's key. `null` when the header is absent. */
|
|
35
|
+
export declare function readIdempotencyKey(request: Request): string | null;
|
|
36
|
+
/** SHA-256 over method, path and body, hex. */
|
|
37
|
+
export declare function requestFingerprint(method: string, path: string, body: ArrayBuffer | string): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* The body `requestFingerprint` should hash, read from a clone so the handler
|
|
40
|
+
* can still consume the original.
|
|
41
|
+
*
|
|
42
|
+
* Multipart is canonicalized to its fields and file contents: clients pick a
|
|
43
|
+
* fresh random boundary per request, so the raw bytes of an identical retry
|
|
44
|
+
* never match.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fingerprintBody(request: Request): Promise<ArrayBuffer | string>;
|
|
47
|
+
/**
|
|
48
|
+
* Run `fn` once per `(callerId, clientKey)`, replaying its response to repeats
|
|
49
|
+
* with `Idempotency-Replayed: true`. Throws 422 when the key was used for a
|
|
50
|
+
* different request. Errors `fn` throws propagate and free the key.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runIdempotent({ store, callerId, clientKey, fingerprint }: {
|
|
53
|
+
store: ApiIdempotencyStore;
|
|
54
|
+
callerId: string;
|
|
55
|
+
clientKey: string;
|
|
56
|
+
fingerprint: string;
|
|
57
|
+
}, fn: () => Promise<Response>): Promise<Response>;
|
|
58
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/api/idempotency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC1F,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,gCAAgC,CAAC;AAGxC,eAAO,MAAM,sBAAsB,oBAAoB,CAAC;AACxD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,qFAAqF;AACrF,eAAO,MAAM,8BAA8B,QAAa,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,CAAC,eAAe,CAAC,CAAC;AAEpE,wBAAgB,yBAAyB,CACxC,KAAK,SAAiC,GACpC,mBAAmB,CAErB;AAID,mFAAmF;AACnF,wBAAgB,0BAA0B,IAAI,mBAAmB,CAEhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAWlE;AAOD,+CAA+C;AAC/C,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,GAAG,MAAM,GACxB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC,CAsBrF;AAmBD;;;;GAIG;AACH,wBAAsB,aAAa,CAClC,EACC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,WAAW,EACX,EAAE;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAC3F,EAAE,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GACzB,OAAO,CAAC,QAAQ,CAAC,CAiBnB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Idempotency-Key` for mounted routes that opt in with `idempotent: true`.
|
|
3
|
+
*
|
|
4
|
+
* Built on the solve route's store and wire contract (`../compute/idempotency*`):
|
|
5
|
+
* the store absorbs in-flight and near-term retries in this process; it is not
|
|
6
|
+
* a durable record. A create that must never duplicate also needs the key on
|
|
7
|
+
* the created row under a unique constraint, written in the same transaction.
|
|
8
|
+
*
|
|
9
|
+
* Each entry carries a fingerprint of method, path and body, so a client that
|
|
10
|
+
* reuses a key for a different request gets 422 instead of someone else's
|
|
11
|
+
* replay. Only responses below 400 are kept: a failed attempt frees the key so
|
|
12
|
+
* a corrected retry can run.
|
|
13
|
+
*/
|
|
14
|
+
import { createIdempotencyStore } from '../compute/idempotency.js';
|
|
15
|
+
import { fromStoredResponse, idempotencyKey, toStoredResponse } from '../compute/idempotency-http.js';
|
|
16
|
+
import { apiError, ApiErrorCode } from './errors.js';
|
|
17
|
+
export const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';
|
|
18
|
+
export const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
19
|
+
/** Covers a client's retry-on-timeout window without turning into a result cache. */
|
|
20
|
+
export const DEFAULT_API_IDEMPOTENCY_TTL_MS = 5 * 60_000;
|
|
21
|
+
export function createApiIdempotencyStore(ttlMs = DEFAULT_API_IDEMPOTENCY_TTL_MS) {
|
|
22
|
+
return createIdempotencyStore({ ttlMs });
|
|
23
|
+
}
|
|
24
|
+
let sharedDefault;
|
|
25
|
+
/** The store `depsFromConfig` wires when the host passes none. One per process. */
|
|
26
|
+
export function defaultApiIdempotencyStore() {
|
|
27
|
+
return (sharedDefault ??= createApiIdempotencyStore());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The narrowest identity a key is namespaced by: the token for a token
|
|
31
|
+
* request, so two keys of one user never share replays, else the user.
|
|
32
|
+
*/
|
|
33
|
+
export function idempotencyCallerId(ctx) {
|
|
34
|
+
return ctx.apiScope ? `token:${ctx.apiScope.tokenId}` : `user:${ctx.userId}`;
|
|
35
|
+
}
|
|
36
|
+
/** Read and validate the client's key. `null` when the header is absent. */
|
|
37
|
+
export function readIdempotencyKey(request) {
|
|
38
|
+
const key = request.headers.get(IDEMPOTENCY_KEY_HEADER);
|
|
39
|
+
if (key === null)
|
|
40
|
+
return null;
|
|
41
|
+
if (key.length === 0 || key.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
42
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, `${IDEMPOTENCY_KEY_HEADER} must be 1 to ${MAX_IDEMPOTENCY_KEY_LENGTH} characters.`);
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
async function sha256Hex(bytes) {
|
|
47
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
48
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
49
|
+
}
|
|
50
|
+
/** SHA-256 over method, path and body, hex. */
|
|
51
|
+
export async function requestFingerprint(method, path, body) {
|
|
52
|
+
const head = new TextEncoder().encode(`${method.toUpperCase()} ${path}\n`);
|
|
53
|
+
const tail = typeof body === 'string' ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
54
|
+
const bytes = new Uint8Array(head.length + tail.length);
|
|
55
|
+
bytes.set(head);
|
|
56
|
+
bytes.set(tail, head.length);
|
|
57
|
+
return sha256Hex(bytes);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The body `requestFingerprint` should hash, read from a clone so the handler
|
|
61
|
+
* can still consume the original.
|
|
62
|
+
*
|
|
63
|
+
* Multipart is canonicalized to its fields and file contents: clients pick a
|
|
64
|
+
* fresh random boundary per request, so the raw bytes of an identical retry
|
|
65
|
+
* never match.
|
|
66
|
+
*/
|
|
67
|
+
export async function fingerprintBody(request) {
|
|
68
|
+
const type = request.headers.get('content-type') ?? '';
|
|
69
|
+
if (!type.toLowerCase().startsWith('multipart/form-data')) {
|
|
70
|
+
return request.clone().arrayBuffer();
|
|
71
|
+
}
|
|
72
|
+
let form;
|
|
73
|
+
try {
|
|
74
|
+
form = await request.clone().formData();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Malformed: hash the raw bytes and let the handler refuse it.
|
|
78
|
+
return request.clone().arrayBuffer();
|
|
79
|
+
}
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const [name, value] of form) {
|
|
82
|
+
parts.push([
|
|
83
|
+
name,
|
|
84
|
+
typeof value === 'string'
|
|
85
|
+
? value
|
|
86
|
+
: { file: value.name, type: value.type, sha256: await sha256Hex(await value.arrayBuffer()) }
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
return JSON.stringify(parts);
|
|
90
|
+
}
|
|
91
|
+
class NotReplayable {
|
|
92
|
+
fingerprint;
|
|
93
|
+
response;
|
|
94
|
+
constructor(fingerprint, response) {
|
|
95
|
+
this.fingerprint = fingerprint;
|
|
96
|
+
this.response = response;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertSameRequest(stored, fingerprint) {
|
|
100
|
+
if (stored !== fingerprint) {
|
|
101
|
+
apiError(422, ApiErrorCode.UNPROCESSABLE, `This ${IDEMPOTENCY_KEY_HEADER} was already used for a different request.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run `fn` once per `(callerId, clientKey)`, replaying its response to repeats
|
|
106
|
+
* with `Idempotency-Replayed: true`. Throws 422 when the key was used for a
|
|
107
|
+
* different request. Errors `fn` throws propagate and free the key.
|
|
108
|
+
*/
|
|
109
|
+
export async function runIdempotent({ store, callerId, clientKey, fingerprint }, fn) {
|
|
110
|
+
let outcome;
|
|
111
|
+
try {
|
|
112
|
+
outcome = await store.run(idempotencyKey(callerId, clientKey), async () => {
|
|
113
|
+
const res = await fn();
|
|
114
|
+
const response = await toStoredResponse(res);
|
|
115
|
+
if (res.status >= 400)
|
|
116
|
+
throw new NotReplayable(fingerprint, response);
|
|
117
|
+
return { fingerprint, response };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (!(err instanceof NotReplayable))
|
|
122
|
+
throw err;
|
|
123
|
+
// A concurrent retry joining the failed attempt receives the same failure.
|
|
124
|
+
assertSameRequest(err.fingerprint, fingerprint);
|
|
125
|
+
return fromStoredResponse(err.response, false);
|
|
126
|
+
}
|
|
127
|
+
assertSameRequest(outcome.value.fingerprint, fingerprint);
|
|
128
|
+
return fromStoredResponse(outcome.value.response, outcome.replayed);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/api/idempotency.ts"],"names":[],"mappings":"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"}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { collection, created, noContent } from './responses.js';
|
|
|
5
5
|
export { shaped, shapedCollection } from './shaped.js';
|
|
6
6
|
export { formText, parseBody, parseParam, requireCaller, requireParams, requireUpload, throwZodError } from './request.js';
|
|
7
7
|
export { runHandler, toErrorBody, type ApiErrorBody, type RunHandlerOptions } from './respond.js';
|
|
8
|
+
export { createApiRateLimiter, defaultApiRateLimiter, resolveApiRateLimitConfig, chargeApiRateLimit, rateLimitHeaders, rateLimitedResponse, withRateLimitHeaders, DEFAULT_API_RATE_LIMIT, type ApiRateLimiter, type ApiRateLimitConfig, type ApiRateLimitVerdict } from './rate-limit.js';
|
|
9
|
+
export { createApiIdempotencyStore, defaultApiIdempotencyStore, idempotencyCallerId, readIdempotencyKey, requestFingerprint, runIdempotent, IDEMPOTENCY_KEY_HEADER, MAX_IDEMPOTENCY_KEY_LENGTH, DEFAULT_API_IDEMPOTENCY_TTL_MS, type ApiIdempotencyStore, type IdempotentEntry } from './idempotency.js';
|
|
8
10
|
export { assertScope, actionForMethod, scopeRefusalsToday, projectScopeTarget, definitionScopeTarget } from './scope.js';
|
|
9
11
|
export { mapCoreError } from './map-core-error.js';
|
|
10
12
|
export { parseListOptions, parseDefinitionListOptions } from './pagination.js';
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1F,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACN,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,aAAa,EACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EACN,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACN,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,uBAAuB,EACvB,8BAA8B,EAC9B,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,6BAA6B,EAC7B,KAAK,iBAAiB,EACtB,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1F,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACN,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,aAAa,EACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EACN,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,yBAAyB,EACzB,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,0BAA0B,EAC1B,8BAA8B,EAC9B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACN,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,uBAAuB,EACvB,8BAA8B,EAC9B,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,6BAA6B,EAC7B,KAAK,iBAAiB,EACtB,MAAM,uBAAuB,CAAC"}
|
package/dist/api/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export { collection, created, noContent } from './responses.js';
|
|
|
8
8
|
export { shaped, shapedCollection } from './shaped.js';
|
|
9
9
|
export { formText, parseBody, parseParam, requireCaller, requireParams, requireUpload, throwZodError } from './request.js';
|
|
10
10
|
export { runHandler, toErrorBody } from './respond.js';
|
|
11
|
+
export { createApiRateLimiter, defaultApiRateLimiter, resolveApiRateLimitConfig, chargeApiRateLimit, rateLimitHeaders, rateLimitedResponse, withRateLimitHeaders, DEFAULT_API_RATE_LIMIT } from './rate-limit.js';
|
|
12
|
+
export { createApiIdempotencyStore, defaultApiIdempotencyStore, idempotencyCallerId, readIdempotencyKey, requestFingerprint, runIdempotent, IDEMPOTENCY_KEY_HEADER, MAX_IDEMPOTENCY_KEY_LENGTH, DEFAULT_API_IDEMPOTENCY_TTL_MS } from './idempotency.js';
|
|
11
13
|
export { assertScope, actionForMethod, scopeRefusalsToday, projectScopeTarget, definitionScopeTarget } from './scope.js';
|
|
12
14
|
export { mapCoreError } from './map-core-error.js';
|
|
13
15
|
export { parseListOptions, parseDefinitionListOptions } from './pagination.js';
|
package/dist/api/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,8EAA8E;AAC9E,kEAAkE;AAElE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1F,OAAO,EAAE,cAAc,EAAkB,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACN,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,aAAa,EACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,UAAU,EAAE,WAAW,EAA6C,MAAM,cAAc,CAAC;AAClG,OAAO,EACN,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACN,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,uBAAuB,EACvB,8BAA8B,EAC9B,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,6BAA6B,EAE7B,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,8EAA8E;AAC9E,kEAAkE;AAElE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1F,OAAO,EAAE,cAAc,EAAkB,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACN,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,aAAa,EACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,UAAU,EAAE,WAAW,EAA6C,MAAM,cAAc,CAAC;AAClG,OAAO,EACN,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EAItB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,yBAAyB,EACzB,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,0BAA0B,EAC1B,8BAA8B,EAG9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACN,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,uBAAuB,EACvB,8BAA8B,EAC9B,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,6BAA6B,EAE7B,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general per-token request limit `runHandler` applies to every API-token
|
|
3
|
+
* request. Browser sessions are not charged: their abuse surface is the
|
|
4
|
+
* per-user compute bucket and the login limiter, not request volume.
|
|
5
|
+
*
|
|
6
|
+
* Solves charge this bucket and the owner's `user:{id}` compute bucket, so ten
|
|
7
|
+
* keys of one user never buy ten times the compute.
|
|
8
|
+
*
|
|
9
|
+
* The default limiter is process-local (fixed window over
|
|
10
|
+
* `createComputeRateLimiter`), so N instances admit N times the rate. A host
|
|
11
|
+
* that needs one shared budget implements `ApiRateLimiter` over its own store.
|
|
12
|
+
*/
|
|
13
|
+
import type { ILogger, RequestContext } from '@selvajs/platform';
|
|
14
|
+
import { type EnvRecord } from '../compute/limits.js';
|
|
15
|
+
export interface ApiRateLimitVerdict {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
/** Requests admitted per window. */
|
|
18
|
+
limit: number;
|
|
19
|
+
/** Requests left in the current window after this one. */
|
|
20
|
+
remaining: number;
|
|
21
|
+
/** Seconds until the window resets, when refused. */
|
|
22
|
+
retryAfter?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Records one request against `key`. Async so a shared-store implementation fits. */
|
|
25
|
+
export interface ApiRateLimiter {
|
|
26
|
+
hit(key: string): ApiRateLimitVerdict | Promise<ApiRateLimitVerdict>;
|
|
27
|
+
}
|
|
28
|
+
export interface ApiRateLimitConfig {
|
|
29
|
+
windowMs: number;
|
|
30
|
+
/** `0` disables the limit. */
|
|
31
|
+
maxPerWindow: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_API_RATE_LIMIT: Readonly<ApiRateLimitConfig>;
|
|
34
|
+
/**
|
|
35
|
+
* Read `API_TOKEN_RATE_LIMIT_MAX` (requests per window, `0` = off) and
|
|
36
|
+
* `API_TOKEN_RATE_LIMIT_WINDOW_MS` from an injected env map.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveApiRateLimitConfig(env: EnvRecord, logger?: ILogger): ApiRateLimitConfig;
|
|
39
|
+
/** Process-local fixed-window limiter. Returns `null` when `maxPerWindow` is 0. */
|
|
40
|
+
export declare function createApiRateLimiter(config?: ApiRateLimitConfig): ApiRateLimiter | null;
|
|
41
|
+
/**
|
|
42
|
+
* The limiter `depsFromConfig` wires when the host passes none. One per
|
|
43
|
+
* process: deps are rebuilt per request, and a limiter per request counts
|
|
44
|
+
* nothing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function defaultApiRateLimiter(): ApiRateLimiter | null;
|
|
47
|
+
/**
|
|
48
|
+
* Charge one request to the caller's token. `undefined` for a session caller
|
|
49
|
+
* or when no limiter is wired.
|
|
50
|
+
*/
|
|
51
|
+
export declare function chargeApiRateLimit(ctx: RequestContext | undefined, limiter: ApiRateLimiter | null | undefined): Promise<ApiRateLimitVerdict | undefined>;
|
|
52
|
+
export declare function rateLimitHeaders(verdict: ApiRateLimitVerdict): Record<string, string>;
|
|
53
|
+
/** The 429 envelope, with `Retry-After` and the `RateLimit-*` headers. */
|
|
54
|
+
export declare function rateLimitedResponse(verdict: ApiRateLimitVerdict, message?: string): Response;
|
|
55
|
+
/** Stamp the `RateLimit-*` headers onto a response. No-op without a verdict. */
|
|
56
|
+
export declare function withRateLimitHeaders(res: Response, verdict: ApiRateLimitVerdict | undefined): Response;
|
|
57
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/api/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjE,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG3F,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,kBAAkB,CAG9D,CAAC;AAEH;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAe9F;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CACnC,MAAM,GAAE,kBAA2C,GACjD,cAAc,GAAG,IAAI,CAevB;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,GAAG,IAAI,CAG7D;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,cAAc,GAAG,SAAS,EAC/B,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,GACxC,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAG1C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOrF;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,SAAgF,GACrF,QAAQ,CAOV;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAAG,SAAS,GACtC,QAAQ,CAgBV"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general per-token request limit `runHandler` applies to every API-token
|
|
3
|
+
* request. Browser sessions are not charged: their abuse surface is the
|
|
4
|
+
* per-user compute bucket and the login limiter, not request volume.
|
|
5
|
+
*
|
|
6
|
+
* Solves charge this bucket and the owner's `user:{id}` compute bucket, so ten
|
|
7
|
+
* keys of one user never buy ten times the compute.
|
|
8
|
+
*
|
|
9
|
+
* The default limiter is process-local (fixed window over
|
|
10
|
+
* `createComputeRateLimiter`), so N instances admit N times the rate. A host
|
|
11
|
+
* that needs one shared budget implements `ApiRateLimiter` over its own store.
|
|
12
|
+
*/
|
|
13
|
+
import { createComputeRateLimiter } from '../compute/rate-limit.js';
|
|
14
|
+
import { readNonNegativeInt, readPositiveInt } from '../compute/limits.js';
|
|
15
|
+
import { ApiError, ApiErrorCode, toErrorBody } from './errors.js';
|
|
16
|
+
export const DEFAULT_API_RATE_LIMIT = Object.freeze({
|
|
17
|
+
windowMs: 60_000,
|
|
18
|
+
maxPerWindow: 600
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Read `API_TOKEN_RATE_LIMIT_MAX` (requests per window, `0` = off) and
|
|
22
|
+
* `API_TOKEN_RATE_LIMIT_WINDOW_MS` from an injected env map.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveApiRateLimitConfig(env, logger) {
|
|
25
|
+
return {
|
|
26
|
+
windowMs: readPositiveInt(env, 'API_TOKEN_RATE_LIMIT_WINDOW_MS', DEFAULT_API_RATE_LIMIT.windowMs, logger),
|
|
27
|
+
maxPerWindow: readNonNegativeInt(env, 'API_TOKEN_RATE_LIMIT_MAX', DEFAULT_API_RATE_LIMIT.maxPerWindow, logger)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Process-local fixed-window limiter. Returns `null` when `maxPerWindow` is 0. */
|
|
31
|
+
export function createApiRateLimiter(config = DEFAULT_API_RATE_LIMIT) {
|
|
32
|
+
if (config.maxPerWindow <= 0)
|
|
33
|
+
return null;
|
|
34
|
+
const inner = createComputeRateLimiter(config);
|
|
35
|
+
const limit = config.maxPerWindow;
|
|
36
|
+
return {
|
|
37
|
+
hit(key) {
|
|
38
|
+
const result = inner.check(key);
|
|
39
|
+
return {
|
|
40
|
+
allowed: result.allowed,
|
|
41
|
+
limit,
|
|
42
|
+
remaining: Math.max(0, limit - inner.count(key)),
|
|
43
|
+
retryAfter: result.retryAfter
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
let sharedDefault;
|
|
49
|
+
/**
|
|
50
|
+
* The limiter `depsFromConfig` wires when the host passes none. One per
|
|
51
|
+
* process: deps are rebuilt per request, and a limiter per request counts
|
|
52
|
+
* nothing.
|
|
53
|
+
*/
|
|
54
|
+
export function defaultApiRateLimiter() {
|
|
55
|
+
if (sharedDefault === undefined)
|
|
56
|
+
sharedDefault = createApiRateLimiter();
|
|
57
|
+
return sharedDefault;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Charge one request to the caller's token. `undefined` for a session caller
|
|
61
|
+
* or when no limiter is wired.
|
|
62
|
+
*/
|
|
63
|
+
export async function chargeApiRateLimit(ctx, limiter) {
|
|
64
|
+
if (!ctx?.apiScope || !limiter)
|
|
65
|
+
return undefined;
|
|
66
|
+
return limiter.hit(`token:${ctx.apiScope.tokenId}`);
|
|
67
|
+
}
|
|
68
|
+
export function rateLimitHeaders(verdict) {
|
|
69
|
+
const headers = {
|
|
70
|
+
'RateLimit-Limit': String(verdict.limit),
|
|
71
|
+
'RateLimit-Remaining': String(verdict.remaining)
|
|
72
|
+
};
|
|
73
|
+
if (!verdict.allowed)
|
|
74
|
+
headers['Retry-After'] = String(verdict.retryAfter ?? 1);
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
/** The 429 envelope, with `Retry-After` and the `RateLimit-*` headers. */
|
|
78
|
+
export function rateLimitedResponse(verdict, message = `Too many requests for this API token. Retry in ${verdict.retryAfter ?? 1}s.`) {
|
|
79
|
+
const retryAfter = String(verdict.retryAfter ?? 1);
|
|
80
|
+
const err = new ApiError(429, ApiErrorCode.RATE_LIMITED, message, undefined, { retryAfter });
|
|
81
|
+
return new Response(JSON.stringify(toErrorBody(err)), {
|
|
82
|
+
status: 429,
|
|
83
|
+
headers: { 'content-type': 'application/json', ...rateLimitHeaders(verdict) }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Stamp the `RateLimit-*` headers onto a response. No-op without a verdict. */
|
|
87
|
+
export function withRateLimitHeaders(res, verdict) {
|
|
88
|
+
if (!verdict)
|
|
89
|
+
return res;
|
|
90
|
+
const headers = rateLimitHeaders(verdict);
|
|
91
|
+
try {
|
|
92
|
+
for (const [k, v] of Object.entries(headers))
|
|
93
|
+
res.headers.set(k, v);
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A fetched or otherwise immutable response: rewrap rather than drop the headers.
|
|
98
|
+
const copy = new Headers(res.headers);
|
|
99
|
+
for (const [k, v] of Object.entries(headers))
|
|
100
|
+
copy.set(k, v);
|
|
101
|
+
return new Response(res.body, {
|
|
102
|
+
status: res.status,
|
|
103
|
+
statusText: res.statusText,
|
|
104
|
+
headers: copy
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/api/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAkB,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuBlE,MAAM,CAAC,MAAM,sBAAsB,GAAiC,MAAM,CAAC,MAAM,CAAC;IACjF,QAAQ,EAAE,MAAM;IAChB,YAAY,EAAE,GAAG;CACjB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAc,EAAE,MAAgB;IACzE,OAAO;QACN,QAAQ,EAAE,eAAe,CACxB,GAAG,EACH,gCAAgC,EAChC,sBAAsB,CAAC,QAAQ,EAC/B,MAAM,CACN;QACD,YAAY,EAAE,kBAAkB,CAC/B,GAAG,EACH,0BAA0B,EAC1B,sBAAsB,CAAC,YAAY,EACnC,MAAM,CACN;KACD,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,oBAAoB,CACnC,SAA6B,sBAAsB;IAEnD,IAAI,MAAM,CAAC,YAAY,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,KAAK,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC;IAClC,OAAO;QACN,GAAG,CAAC,GAAG;YACN,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAChC,OAAO;gBACN,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,KAAK;gBACL,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAChD,UAAU,EAAE,MAAM,CAAC,UAAU;aAC7B,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC;AAED,IAAI,aAAgD,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB;IACpC,IAAI,aAAa,KAAK,SAAS;QAAE,aAAa,GAAG,oBAAoB,EAAE,CAAC;IACxE,OAAO,aAAa,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,GAA+B,EAC/B,OAA0C;IAE1C,IAAI,CAAC,GAAG,EAAE,QAAQ,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IACjD,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAA4B;IAC5D,MAAM,OAAO,GAA2B;QACvC,iBAAiB,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;QACxC,qBAAqB,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;KAChD,CAAC;IACF,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;IAC/E,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CAClC,OAA4B,EAC5B,OAAO,GAAG,kDAAkD,OAAO,CAAC,UAAU,IAAI,CAAC,IAAI;IAEvF,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;IACnD,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAC7F,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE;QACrD,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,EAAE;KAC7E,CAAC,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CACnC,GAAa,EACb,OAAwC;IAExC,IAAI,CAAC,OAAO;QAAE,OAAO,GAAG,CAAC;IACzB,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,CAAC;QACJ,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpE,OAAO,GAAG,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACR,kFAAkF;QAClF,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7D,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;YAC7B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,OAAO,EAAE,IAAI;SACb,CAAC,CAAC;IACJ,CAAC;AACF,CAAC"}
|
package/dist/api/respond.d.ts
CHANGED
|
@@ -7,16 +7,10 @@
|
|
|
7
7
|
* to build an `ApiRequest` and call `runHandler`; one that does not can reuse
|
|
8
8
|
* `toErrorBody` and serialize itself.
|
|
9
9
|
*/
|
|
10
|
-
import { ApiError
|
|
10
|
+
import { ApiError } from './errors.js';
|
|
11
11
|
import type { ApiScopeAction, ScopeTarget } from '@selvajs/platform';
|
|
12
12
|
import type { ApiHandler, ApiRequest } from './types.js';
|
|
13
|
-
export
|
|
14
|
-
message: string;
|
|
15
|
-
code: ApiErrorCode;
|
|
16
|
-
fields?: Record<string, string>;
|
|
17
|
-
details?: Record<string, string>;
|
|
18
|
-
}
|
|
19
|
-
export declare function toErrorBody(err: ApiError): ApiErrorBody;
|
|
13
|
+
export { toErrorBody, type ApiErrorBody } from './errors.js';
|
|
20
14
|
export interface RunHandlerOptions {
|
|
21
15
|
/** The 500 message when the handler throws something unmapped. */
|
|
22
16
|
fallback: string;
|
|
@@ -36,7 +30,13 @@ export interface RunHandlerOptions {
|
|
|
36
30
|
* org-level, which project- and definition-scoped keys never pass.
|
|
37
31
|
*/
|
|
38
32
|
scopeTarget?: (req: ApiRequest) => ScopeTarget | Promise<ScopeTarget>;
|
|
33
|
+
/**
|
|
34
|
+
* Honour `Idempotency-Key`: a repeat from the same caller replays the first
|
|
35
|
+
* response, and a key reused with a different body gets 422. Opt in per
|
|
36
|
+
* route; what is safe to replay differs.
|
|
37
|
+
*/
|
|
38
|
+
idempotent?: boolean;
|
|
39
39
|
}
|
|
40
40
|
/** Run a handler and serialize whatever comes out, including failures. */
|
|
41
|
-
export declare function runHandler(handler: ApiHandler, req: ApiRequest, { fallback, mapError, action, scopeTarget }: RunHandlerOptions): Promise<Response>;
|
|
41
|
+
export declare function runHandler(handler: ApiHandler, req: ApiRequest, { fallback, mapError, action, scopeTarget, idempotent }: RunHandlerOptions): Promise<Response>;
|
|
42
42
|
//# sourceMappingURL=respond.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"respond.d.ts","sourceRoot":"","sources":["../../src/api/respond.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"respond.d.ts","sourceRoot":"","sources":["../../src/api/respond.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAyC,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AAiBtE,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAc7D,MAAM,WAAW,iBAAiB;IACjC,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,SAAS,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACtE;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,0EAA0E;AAC1E,wBAAsB,UAAU,CAC/B,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,UAAU,EACf,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,iBAAiB,GACxE,OAAO,CAAC,QAAQ,CAAC,CA4EnB"}
|
package/dist/api/respond.js
CHANGED
|
@@ -7,16 +7,11 @@
|
|
|
7
7
|
* to build an `ApiRequest` and call `runHandler`; one that does not can reuse
|
|
8
8
|
* `toErrorBody` and serialize itself.
|
|
9
9
|
*/
|
|
10
|
-
import { ApiErrorCode, isApiError } from './errors.js';
|
|
10
|
+
import { ApiErrorCode, isApiError, toErrorBody } from './errors.js';
|
|
11
11
|
import { actionForMethod, assertScope } from './scope.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
body.fields = err.fields;
|
|
16
|
-
if (err.details)
|
|
17
|
-
body.details = err.details;
|
|
18
|
-
return body;
|
|
19
|
-
}
|
|
12
|
+
import { chargeApiRateLimit, defaultApiRateLimiter, rateLimitedResponse, withRateLimitHeaders } from './rate-limit.js';
|
|
13
|
+
import { defaultApiIdempotencyStore, fingerprintBody, idempotencyCallerId, readIdempotencyKey, requestFingerprint, runIdempotent } from './idempotency.js';
|
|
14
|
+
export { toErrorBody } from './errors.js';
|
|
20
15
|
function toResponse(result) {
|
|
21
16
|
if (result instanceof Response)
|
|
22
17
|
return result;
|
|
@@ -29,14 +24,8 @@ function toResponse(result) {
|
|
|
29
24
|
});
|
|
30
25
|
}
|
|
31
26
|
/** Run a handler and serialize whatever comes out, including failures. */
|
|
32
|
-
export async function runHandler(handler, req, { fallback, mapError, action, scopeTarget }) {
|
|
33
|
-
|
|
34
|
-
if (req.ctx?.apiScope) {
|
|
35
|
-
assertScope(req.ctx, action ?? actionForMethod(req.request.method), scopeTarget ? await scopeTarget(req) : undefined);
|
|
36
|
-
}
|
|
37
|
-
return toResponse(await handler(req));
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
27
|
+
export async function runHandler(handler, req, { fallback, mapError, action, scopeTarget, idempotent }) {
|
|
28
|
+
const fail = (err) => {
|
|
40
29
|
if (isApiError(err)) {
|
|
41
30
|
return toResponse({ status: err.status, body: toErrorBody(err) });
|
|
42
31
|
}
|
|
@@ -55,6 +44,49 @@ export async function runHandler(handler, req, { fallback, mapError, action, sco
|
|
|
55
44
|
status: 500,
|
|
56
45
|
body: { message: fallback, code: ApiErrorCode.INTERNAL }
|
|
57
46
|
});
|
|
47
|
+
};
|
|
48
|
+
let verdict;
|
|
49
|
+
try {
|
|
50
|
+
// Charged before the scope check: `scopeTarget` may read the database,
|
|
51
|
+
// and a throttled key must not reach it.
|
|
52
|
+
const limiter = req.deps.apiRateLimiter === undefined ? defaultApiRateLimiter() : req.deps.apiRateLimiter;
|
|
53
|
+
verdict = await chargeApiRateLimit(req.ctx, limiter);
|
|
54
|
+
if (verdict && !verdict.allowed)
|
|
55
|
+
return rateLimitedResponse(verdict);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return fail(err);
|
|
59
|
+
}
|
|
60
|
+
let res;
|
|
61
|
+
try {
|
|
62
|
+
if (req.ctx?.apiScope) {
|
|
63
|
+
assertScope(req.ctx, action ?? actionForMethod(req.request.method), scopeTarget ? await scopeTarget(req) : undefined);
|
|
64
|
+
}
|
|
65
|
+
const run = async () => {
|
|
66
|
+
try {
|
|
67
|
+
return toResponse(await handler(req));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return fail(err);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const clientKey = idempotent && req.ctx ? readIdempotencyKey(req.request) : null;
|
|
74
|
+
if (clientKey && req.ctx) {
|
|
75
|
+
const fingerprint = await requestFingerprint(req.request.method, req.url.pathname + req.url.search, await fingerprintBody(req.request));
|
|
76
|
+
res = await runIdempotent({
|
|
77
|
+
store: req.deps.idempotency ?? defaultApiIdempotencyStore(),
|
|
78
|
+
callerId: idempotencyCallerId(req.ctx),
|
|
79
|
+
clientKey,
|
|
80
|
+
fingerprint
|
|
81
|
+
}, run);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
res = await run();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
res = fail(err);
|
|
58
89
|
}
|
|
90
|
+
return withRateLimitHeaders(res, verdict);
|
|
59
91
|
}
|
|
60
92
|
//# sourceMappingURL=respond.js.map
|
package/dist/api/respond.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"respond.js","sourceRoot":"","sources":["../../src/api/respond.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAY,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"respond.js","sourceRoot":"","sources":["../../src/api/respond.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/server",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.2",
|
|
4
4
|
"description": "Transport-agnostic server building blocks for apps built on the Selva engine (limits, rate limiting, SSRF guard, remote-definition fetch, definition service + render loader); the solve core lives in @selvajs/solve/server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
@@ -112,8 +112,8 @@
|
|
|
112
112
|
"zod": "^4.6.5",
|
|
113
113
|
"@selvajs/compute": "^4.2.1",
|
|
114
114
|
"@selvajs/notifications": "^0.3.0-beta.1",
|
|
115
|
-
"@selvajs/
|
|
116
|
-
"@selvajs/
|
|
115
|
+
"@selvajs/platform": "^0.21.0-beta.1",
|
|
116
|
+
"@selvajs/schemas": "^5.2.0"
|
|
117
117
|
},
|
|
118
118
|
"peerDependencies": {
|
|
119
119
|
"@sentry/node": ">=8",
|