@selvajs/server 1.5.0-beta.0 → 1.5.0-beta.10
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/access/guards.d.ts.map +1 -1
- package/dist/access/guards.js +11 -7
- package/dist/access/guards.js.map +1 -1
- package/dist/api/bodies.d.ts +0 -2
- package/dist/api/bodies.d.ts.map +1 -1
- package/dist/api/deps.d.ts +19 -1
- package/dist/api/deps.d.ts.map +1 -1
- package/dist/api/deps.js +4 -1
- package/dist/api/deps.js.map +1 -1
- package/dist/api/errors.d.ts +11 -0
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +15 -1
- package/dist/api/errors.js.map +1 -1
- package/dist/api/idempotency-http.d.ts.map +1 -0
- package/dist/api/idempotency-http.js.map +1 -0
- 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 +3 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/openapi.d.ts +84 -0
- package/dist/api/openapi.d.ts.map +1 -0
- package/dist/api/openapi.js +247 -0
- package/dist/api/openapi.js.map +1 -0
- 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 +14 -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/dist/api/scope.d.ts +3 -0
- package/dist/api/scope.d.ts.map +1 -1
- package/dist/api/scope.js +8 -2
- package/dist/api/scope.js.map +1 -1
- package/dist/compute/index.d.ts +0 -1
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +0 -1
- package/dist/compute/index.js.map +1 -1
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +7 -1
- package/dist/definitions/definition-service.js.map +1 -1
- package/dist/definitions/index.d.ts +1 -1
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/index.js +1 -1
- package/dist/definitions/index.js.map +1 -1
- package/dist/definitions/visibility.d.ts +35 -1
- package/dist/definitions/visibility.d.ts.map +1 -1
- package/dist/definitions/visibility.js +94 -7
- package/dist/definitions/visibility.js.map +1 -1
- package/dist/handlers/apiTokens.d.ts +59 -4
- package/dist/handlers/apiTokens.d.ts.map +1 -1
- package/dist/handlers/apiTokens.js +96 -21
- package/dist/handlers/apiTokens.js.map +1 -1
- package/dist/handlers/definitions.d.ts +7 -1
- package/dist/handlers/definitions.d.ts.map +1 -1
- package/dist/handlers/definitions.js +32 -9
- package/dist/handlers/definitions.js.map +1 -1
- package/dist/handlers/index.d.ts +2 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +2 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/me.d.ts.map +1 -1
- package/dist/handlers/me.js +9 -1
- package/dist/handlers/me.js.map +1 -1
- package/dist/handlers/oauthConsent.d.ts +37 -0
- package/dist/handlers/oauthConsent.d.ts.map +1 -0
- package/dist/handlers/oauthConsent.js +63 -0
- package/dist/handlers/oauthConsent.js.map +1 -0
- package/dist/handlers/orgMembers.d.ts.map +1 -1
- package/dist/handlers/orgMembers.js +2 -0
- package/dist/handlers/orgMembers.js.map +1 -1
- package/dist/handlers/projects.d.ts +5 -0
- package/dist/handlers/projects.d.ts.map +1 -1
- package/dist/handlers/projects.js +21 -4
- package/dist/handlers/projects.js.map +1 -1
- package/dist/handlers/services.d.ts +1 -1
- package/dist/handlers/services.d.ts.map +1 -1
- package/dist/handlers/services.js.map +1 -1
- package/dist/http/api-token.d.ts +12 -3
- package/dist/http/api-token.d.ts.map +1 -1
- package/dist/http/api-token.js +29 -6
- package/dist/http/api-token.js.map +1 -1
- package/dist/http/csrf.d.ts +18 -0
- package/dist/http/csrf.d.ts.map +1 -0
- package/dist/http/csrf.js +37 -0
- package/dist/http/csrf.js.map +1 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -0
- package/dist/http/index.js.map +1 -1
- package/dist/oauth/index.d.ts +160 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +465 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/organizations/index.d.ts +1 -0
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +1 -0
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/user-memberships.d.ts +11 -0
- package/dist/organizations/user-memberships.d.ts.map +1 -0
- package/dist/organizations/user-memberships.js +23 -0
- package/dist/organizations/user-memberships.js.map +1 -0
- package/dist/testing/harness.d.ts +3 -1
- package/dist/testing/harness.d.ts.map +1 -1
- package/dist/testing/harness.js +6 -1
- package/dist/testing/harness.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/scenarios.d.ts +6 -0
- package/dist/testing/scenarios.d.ts.map +1 -1
- package/dist/testing/scenarios.js +12 -1
- package/dist/testing/scenarios.js.map +1 -1
- package/dist/tokens/api-token-codec.d.ts +2 -2
- package/dist/tokens/api-token-codec.js +2 -2
- package/dist/tokens/api-token-holder.d.ts +30 -0
- package/dist/tokens/api-token-holder.d.ts.map +1 -0
- package/dist/tokens/api-token-holder.js +27 -0
- package/dist/tokens/api-token-holder.js.map +1 -0
- package/dist/tokens/index.d.ts +1 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +1 -0
- package/dist/tokens/index.js.map +1 -1
- package/package.json +16 -11
- package/dist/compute/idempotency-http.d.ts.map +0 -1
- package/dist/compute/idempotency-http.js.map +0 -1
- /package/dist/{compute → api}/idempotency-http.d.ts +0 -0
- /package/dist/{compute → api}/idempotency-http.js +0 -0
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,EACN,oBAAoB,EACpB,WAAW,EACX,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,MAAM,cAAc,CAAC;AACtB,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,8 +8,11 @@ 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';
|
|
15
|
+
export { buildOpenApiDocument, operationId } from './openapi.js';
|
|
13
16
|
export { parseListOptions, parseDefinitionListOptions } from './pagination.js';
|
|
14
17
|
export { SolveBodySchema, CreateProjectBodySchema, UpdateProjectBodySchema, AddProjectMemberBodySchema, UpdateProjectMemberBodySchema, CreateInviteBodySchema, OrgComputePatchBodySchema, UpdateOrgMemberBodySchema, CreateApiTokenBodySchema } from './bodies.js';
|
|
15
18
|
export { ShareLinkResponseSchema, CreatedShareLinkResponseSchema, InviteResponseSchema, CreatedInviteResponseSchema, OrgComputeServerResponseSchema, ComputeCatalogEntrySchema, OrgComputeResponseSchema, ApiTokenResponseSchema, CreatedApiTokenResponseSchema } from './responses-schema.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,EACN,oBAAoB,EACpB,WAAW,EAMX,MAAM,cAAc,CAAC;AACtB,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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds an OpenAPI 3.1 document for a host's `/api/v1` from its route registry.
|
|
3
|
+
*
|
|
4
|
+
* Shared so every host publishes the same shape: `@selvajs/mcp` reads
|
|
5
|
+
* `operationId`, `x-scope`, `x-internal` and the `Idempotency-Key` parameter
|
|
6
|
+
* from it, and nothing else.
|
|
7
|
+
*
|
|
8
|
+
* Request schemas come from `z.toJSONSchema` over the actual validators, so a
|
|
9
|
+
* renamed body field changes the spec on the next generate. Response schemas
|
|
10
|
+
* are not derived: handlers build payloads from store records with no Zod
|
|
11
|
+
* validator on the way out. They're described structurally (pagination
|
|
12
|
+
* envelope, error envelope) and resource bodies stay open; `returns` adds prose.
|
|
13
|
+
*/
|
|
14
|
+
import { type ZodType } from 'zod';
|
|
15
|
+
import { type ApiScopeAction } from '@selvajs/platform';
|
|
16
|
+
export type Json = string | number | boolean | null | Json[] | {
|
|
17
|
+
[k: string]: Json;
|
|
18
|
+
};
|
|
19
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
20
|
+
/**
|
|
21
|
+
* `collection` is the paginated `{ items, nextCursor? }` envelope and implies
|
|
22
|
+
* the `limit`/`cursor` query params. Mislabelling a list endpoint as `object`
|
|
23
|
+
* is how an unpaginated collection ships.
|
|
24
|
+
*/
|
|
25
|
+
export type ResponseKind = 'collection' | 'object' | 'empty' | 'binary';
|
|
26
|
+
export interface Endpoint {
|
|
27
|
+
method: HttpMethod;
|
|
28
|
+
/** OpenAPI path with `{param}` placeholders, not SvelteKit's `[param]`. */
|
|
29
|
+
path: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
/** The scope action the route checks. Emitted as `x-scope`; MCP tool annotations derive from it. */
|
|
32
|
+
scope: ApiScopeAction;
|
|
33
|
+
/** Excluded from published docs; may change without notice. */
|
|
34
|
+
internal?: boolean;
|
|
35
|
+
response: ResponseKind;
|
|
36
|
+
/** What a success body holds, in prose. */
|
|
37
|
+
returns?: string;
|
|
38
|
+
/** Success status when it isn't 200 (or 204 for `empty`). */
|
|
39
|
+
status?: number;
|
|
40
|
+
/** For `binary`. Defaults to `application/octet-stream`. */
|
|
41
|
+
contentType?: string;
|
|
42
|
+
requestBody?: ZodType;
|
|
43
|
+
/** Multipart form fields, for handlers that take `FormData` instead of JSON. */
|
|
44
|
+
multipart?: {
|
|
45
|
+
field: string;
|
|
46
|
+
required: boolean;
|
|
47
|
+
description: string;
|
|
48
|
+
}[];
|
|
49
|
+
/** Query params beyond the standard pagination set. */
|
|
50
|
+
query?: {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
}[];
|
|
54
|
+
/** Documented failure statuses beyond `alwaysErrors`. */
|
|
55
|
+
errors?: number[];
|
|
56
|
+
/** Honours `Idempotency-Key`; must match the route's `idempotent` mount option. */
|
|
57
|
+
idempotent?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface OpenApiOptions {
|
|
60
|
+
/** Route prefix every endpoint is served under, e.g. `/api/v1`. */
|
|
61
|
+
basePath: string;
|
|
62
|
+
info: {
|
|
63
|
+
title: string;
|
|
64
|
+
version: string;
|
|
65
|
+
description: string;
|
|
66
|
+
license?: {
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
securitySchemes: Record<string, Json>;
|
|
71
|
+
security: Json[];
|
|
72
|
+
/** Extra `components.schemas`, beside `Error` and `Page`. */
|
|
73
|
+
schemas?: Record<string, Json>;
|
|
74
|
+
/** Merged over the default per-status descriptions. */
|
|
75
|
+
errorDescriptions?: Record<number, string>;
|
|
76
|
+
/** Failures any operation can return. */
|
|
77
|
+
alwaysErrors?: number[];
|
|
78
|
+
/** Replaces the default `Idempotency-Key` description. */
|
|
79
|
+
idempotencyKeyDescription?: string;
|
|
80
|
+
}
|
|
81
|
+
/** `PATCH /jobs/{id}` → `patchJobsById`. Public contract: clients and MCP tools call by it. */
|
|
82
|
+
export declare function operationId(ep: Pick<Endpoint, 'method' | 'path'>): string;
|
|
83
|
+
export declare function buildOpenApiDocument(endpoints: Endpoint[], options: OpenApiOptions): Json;
|
|
84
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/api/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAK,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAG5F,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAErF,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAErE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,QAAQ;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oGAAoG;IACpG,KAAK,EAAE,cAAc,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;IACvB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gFAAgF;IAChF,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,uDAAuD;IACvD,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC1F,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/B,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,0DAA0D;IAC1D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACnC;AA+JD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG,MAAM,CAMzE;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAuGzF"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds an OpenAPI 3.1 document for a host's `/api/v1` from its route registry.
|
|
3
|
+
*
|
|
4
|
+
* Shared so every host publishes the same shape: `@selvajs/mcp` reads
|
|
5
|
+
* `operationId`, `x-scope`, `x-internal` and the `Idempotency-Key` parameter
|
|
6
|
+
* from it, and nothing else.
|
|
7
|
+
*
|
|
8
|
+
* Request schemas come from `z.toJSONSchema` over the actual validators, so a
|
|
9
|
+
* renamed body field changes the spec on the next generate. Response schemas
|
|
10
|
+
* are not derived: handlers build payloads from store records with no Zod
|
|
11
|
+
* validator on the way out. They're described structurally (pagination
|
|
12
|
+
* envelope, error envelope) and resource bodies stay open; `returns` adds prose.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from '@selvajs/platform';
|
|
16
|
+
import { ApiErrorCode } from './errors.js';
|
|
17
|
+
const DEFAULT_ERROR_DESCRIPTIONS = {
|
|
18
|
+
400: 'Validation failed.',
|
|
19
|
+
401: 'Missing or invalid credentials.',
|
|
20
|
+
403: 'Authenticated, but not permitted to perform this action. For an API token, `details.requiredScope` names the scope that would have passed.',
|
|
21
|
+
404: 'No such resource, or the caller cannot see it.',
|
|
22
|
+
409: 'The request conflicts with the current state.',
|
|
23
|
+
422: 'Well-formed but not processable.',
|
|
24
|
+
429: 'Rate limited. Retry after the interval in `Retry-After`.',
|
|
25
|
+
500: 'Unexpected server error.',
|
|
26
|
+
503: 'A dependency is unconfigured or unreachable. Retry later.'
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT_IDEMPOTENCY_KEY_DESCRIPTION = 'A client-chosen key, 1 to 255 characters. Repeating the request with the same key within the retention window replays the first successful response instead of running it again; the replay carries `Idempotency-Replayed: true`. Keys are scoped to the calling token (or user, for a browser session). Reusing a key with a different method, path or body returns `422`. A failed attempt is not kept, so a corrected retry runs. The store is per-process and in-memory: it absorbs retries, does not survive a restart, and is not shared between app instances.';
|
|
29
|
+
function requestBodySchema(schema) {
|
|
30
|
+
// io: 'input': fields with defaults are optional on the way in, unlike the output view.
|
|
31
|
+
const json = z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' });
|
|
32
|
+
// A nested schema inherits its dialect from the document; a $schema key
|
|
33
|
+
// here makes some validators treat the subtree as a separate document.
|
|
34
|
+
delete json.$schema;
|
|
35
|
+
return json;
|
|
36
|
+
}
|
|
37
|
+
function multipartSchema(fields) {
|
|
38
|
+
const properties = {};
|
|
39
|
+
const required = [];
|
|
40
|
+
for (const f of fields) {
|
|
41
|
+
properties[f.field] = { type: 'string', description: f.description };
|
|
42
|
+
if (f.required)
|
|
43
|
+
required.push(f.field);
|
|
44
|
+
}
|
|
45
|
+
return required.length
|
|
46
|
+
? { type: 'object', properties, required }
|
|
47
|
+
: { type: 'object', properties };
|
|
48
|
+
}
|
|
49
|
+
function responseFor(ep) {
|
|
50
|
+
if (ep.response === 'empty') {
|
|
51
|
+
return { '204': { description: ep.returns ?? 'Success. No content.' } };
|
|
52
|
+
}
|
|
53
|
+
const status = String(ep.status ?? 200);
|
|
54
|
+
if (ep.response === 'collection') {
|
|
55
|
+
return {
|
|
56
|
+
[status]: {
|
|
57
|
+
description: ep.returns ?? 'A page of results.',
|
|
58
|
+
content: {
|
|
59
|
+
'application/json': { schema: { $ref: '#/components/schemas/Page' } }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (ep.response === 'binary') {
|
|
65
|
+
return {
|
|
66
|
+
[status]: {
|
|
67
|
+
description: ep.returns ?? 'Binary payload.',
|
|
68
|
+
content: {
|
|
69
|
+
[ep.contentType ?? 'application/octet-stream']: {
|
|
70
|
+
schema: { type: 'string', format: 'binary' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
[status]: {
|
|
78
|
+
description: ep.returns ?? 'Success.',
|
|
79
|
+
content: { 'application/json': { schema: { type: 'object' } } }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function parametersFor(ep) {
|
|
84
|
+
const params = [];
|
|
85
|
+
for (const name of ep.path.matchAll(/\{(\w+)\}/g)) {
|
|
86
|
+
params.push({ name: name[1], in: 'path', required: true, schema: { type: 'string' } });
|
|
87
|
+
}
|
|
88
|
+
if (ep.response === 'collection') {
|
|
89
|
+
params.push({ $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }, { $ref: '#/components/parameters/orderBy' }, { $ref: '#/components/parameters/orderDir' });
|
|
90
|
+
}
|
|
91
|
+
for (const q of ep.query ?? []) {
|
|
92
|
+
params.push({
|
|
93
|
+
name: q.name,
|
|
94
|
+
in: 'query',
|
|
95
|
+
required: false,
|
|
96
|
+
description: q.description,
|
|
97
|
+
schema: { type: 'string' }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (ep.idempotent)
|
|
101
|
+
params.push({ $ref: '#/components/parameters/idempotencyKey' });
|
|
102
|
+
return params;
|
|
103
|
+
}
|
|
104
|
+
function operationFor(ep, errorDescriptions, alwaysErrors) {
|
|
105
|
+
const op = {
|
|
106
|
+
summary: ep.summary,
|
|
107
|
+
operationId: operationId(ep),
|
|
108
|
+
tags: [tagFor(ep.path)],
|
|
109
|
+
'x-scope': ep.scope
|
|
110
|
+
};
|
|
111
|
+
if (ep.internal)
|
|
112
|
+
op['x-internal'] = true;
|
|
113
|
+
const params = parametersFor(ep);
|
|
114
|
+
if (params.length)
|
|
115
|
+
op.parameters = params;
|
|
116
|
+
if (ep.requestBody) {
|
|
117
|
+
op.requestBody = {
|
|
118
|
+
required: true,
|
|
119
|
+
content: { 'application/json': { schema: requestBodySchema(ep.requestBody) } }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else if (ep.multipart) {
|
|
123
|
+
op.requestBody = {
|
|
124
|
+
required: true,
|
|
125
|
+
content: { 'multipart/form-data': { schema: multipartSchema(ep.multipart) } }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const responses = responseFor(ep);
|
|
129
|
+
const statuses = new Set([...(ep.errors ?? []), ...alwaysErrors]);
|
|
130
|
+
if (ep.idempotent)
|
|
131
|
+
statuses.add(422);
|
|
132
|
+
for (const status of [...statuses].sort((a, b) => a - b)) {
|
|
133
|
+
responses[String(status)] = {
|
|
134
|
+
description: errorDescriptions[status] ?? 'Error.',
|
|
135
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
op.responses = responses;
|
|
139
|
+
return op;
|
|
140
|
+
}
|
|
141
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
142
|
+
// A file-like segment (`/openapi.json`) drops its extension in names.
|
|
143
|
+
const stem = (s) => s.replace(/\..*$/, '');
|
|
144
|
+
function tagFor(path) {
|
|
145
|
+
return cap(stem(path.split('/')[1] ?? 'root'));
|
|
146
|
+
}
|
|
147
|
+
/** `PATCH /jobs/{id}` → `patchJobsById`. Public contract: clients and MCP tools call by it. */
|
|
148
|
+
export function operationId(ep) {
|
|
149
|
+
const segments = ep.path
|
|
150
|
+
.split('/')
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((s) => (s.startsWith('{') ? `By${cap(s.slice(1, -1))}` : cap(stem(s))));
|
|
153
|
+
return ep.method.toLowerCase() + segments.join('');
|
|
154
|
+
}
|
|
155
|
+
export function buildOpenApiDocument(endpoints, options) {
|
|
156
|
+
const errorDescriptions = { ...DEFAULT_ERROR_DESCRIPTIONS, ...options.errorDescriptions };
|
|
157
|
+
const alwaysErrors = options.alwaysErrors ?? [401, 403, 429, 500];
|
|
158
|
+
const paths = {};
|
|
159
|
+
for (const ep of endpoints) {
|
|
160
|
+
const path = `${options.basePath}${ep.path}`;
|
|
161
|
+
(paths[path] ??= {})[ep.method.toLowerCase()] = operationFor(ep, errorDescriptions, alwaysErrors);
|
|
162
|
+
}
|
|
163
|
+
const paginated = endpoints.some((ep) => ep.response === 'collection');
|
|
164
|
+
const parameters = {};
|
|
165
|
+
if (paginated) {
|
|
166
|
+
parameters.limit = {
|
|
167
|
+
name: 'limit',
|
|
168
|
+
in: 'query',
|
|
169
|
+
required: false,
|
|
170
|
+
description: `Page size. Out-of-range values clamp rather than fail, so a client walking a cursor is never stopped by pagination plumbing. Default ${DEFAULT_PAGE_LIMIT}.`,
|
|
171
|
+
schema: { type: 'integer', minimum: 1, maximum: MAX_PAGE_LIMIT }
|
|
172
|
+
};
|
|
173
|
+
parameters.cursor = {
|
|
174
|
+
name: 'cursor',
|
|
175
|
+
in: 'query',
|
|
176
|
+
required: false,
|
|
177
|
+
description: "An opaque cursor from a previous response's `nextCursor`. Do not construct or parse one.",
|
|
178
|
+
schema: { type: 'string' }
|
|
179
|
+
};
|
|
180
|
+
parameters.orderBy = {
|
|
181
|
+
name: 'orderBy',
|
|
182
|
+
in: 'query',
|
|
183
|
+
required: false,
|
|
184
|
+
schema: { type: 'string', enum: ['createdAt', 'updatedAt', 'name'] }
|
|
185
|
+
};
|
|
186
|
+
parameters.orderDir = {
|
|
187
|
+
name: 'orderDir',
|
|
188
|
+
in: 'query',
|
|
189
|
+
required: false,
|
|
190
|
+
schema: { type: 'string', enum: ['asc', 'desc'] }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (endpoints.some((ep) => ep.idempotent)) {
|
|
194
|
+
parameters.idempotencyKey = {
|
|
195
|
+
name: 'Idempotency-Key',
|
|
196
|
+
in: 'header',
|
|
197
|
+
required: false,
|
|
198
|
+
description: options.idempotencyKeyDescription ?? DEFAULT_IDEMPOTENCY_KEY_DESCRIPTION,
|
|
199
|
+
schema: { type: 'string', minLength: 1, maxLength: 255 }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const schemas = {};
|
|
203
|
+
if (paginated) {
|
|
204
|
+
schemas.Page = {
|
|
205
|
+
type: 'object',
|
|
206
|
+
description: 'The envelope every collection returns. `nextCursor` is absent on the last page.',
|
|
207
|
+
properties: {
|
|
208
|
+
items: { type: 'array', items: { type: 'object' } },
|
|
209
|
+
nextCursor: { type: 'string' }
|
|
210
|
+
},
|
|
211
|
+
required: ['items']
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
schemas.Error = {
|
|
215
|
+
type: 'object',
|
|
216
|
+
description: 'Every failure carries this shape. Branch on `code`, not on the human-readable `message`.',
|
|
217
|
+
properties: {
|
|
218
|
+
message: { type: 'string' },
|
|
219
|
+
code: { type: 'string', enum: Object.values(ApiErrorCode) },
|
|
220
|
+
fields: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
description: 'Per-field messages, keyed by dotted path. Present on validation failures.',
|
|
223
|
+
additionalProperties: { type: 'string' }
|
|
224
|
+
},
|
|
225
|
+
details: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
description: 'Machine-readable context. A scope refusal carries `requiredScope`, e.g. `write:org:<id>`.',
|
|
228
|
+
additionalProperties: { type: 'string' }
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
required: ['message', 'code']
|
|
232
|
+
};
|
|
233
|
+
Object.assign(schemas, options.schemas);
|
|
234
|
+
const components = { securitySchemes: options.securitySchemes };
|
|
235
|
+
if (Object.keys(parameters).length)
|
|
236
|
+
components.parameters = parameters;
|
|
237
|
+
components.schemas = schemas;
|
|
238
|
+
return {
|
|
239
|
+
openapi: '3.1.0',
|
|
240
|
+
info: options.info,
|
|
241
|
+
servers: [{ url: '/', description: 'The instance serving this document.' }],
|
|
242
|
+
security: options.security,
|
|
243
|
+
components,
|
|
244
|
+
paths: paths
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/api/openapi.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"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"}
|
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,18 @@ 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
|
+
* Skip the scope check: every API token may call the route, whatever its
|
|
35
|
+
* scopes. Only for a route whose whole effect is on the calling key itself.
|
|
36
|
+
*/
|
|
37
|
+
anyScope?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Honour `Idempotency-Key`: a repeat from the same caller replays the first
|
|
40
|
+
* response, and a key reused with a different body gets 422. Opt in per
|
|
41
|
+
* route; what is safe to replay differs.
|
|
42
|
+
*/
|
|
43
|
+
idempotent?: boolean;
|
|
39
44
|
}
|
|
40
45
|
/** 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>;
|
|
46
|
+
export declare function runHandler(handler: ApiHandler, req: ApiRequest, { fallback, mapError, action, scopeTarget, anyScope, idempotent }: RunHandlerOptions): Promise<Response>;
|
|
42
47
|
//# 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;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;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,QAAQ,EAAE,UAAU,EAAE,EAAE,iBAAiB,GAClF,OAAO,CAAC,QAAQ,CAAC,CA4EnB"}
|