@selvajs/server 1.5.0-beta.5 → 1.5.0-beta.7
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 +1 -1
- package/dist/api/bodies.d.ts +0 -2
- package/dist/api/bodies.d.ts.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 +7 -7
- package/dist/api/idempotency.d.ts.map +1 -1
- package/dist/api/idempotency.js +8 -8
- package/dist/api/idempotency.js.map +1 -1
- package/dist/api/respond.js +1 -1
- 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/handlers/apiTokens.d.ts +13 -3
- package/dist/handlers/apiTokens.d.ts.map +1 -1
- package/dist/handlers/apiTokens.js +25 -18
- package/dist/handlers/apiTokens.js.map +1 -1
- package/dist/handlers/definitions.d.ts.map +1 -1
- package/dist/handlers/definitions.js +4 -2
- package/dist/handlers/definitions.js.map +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/me.d.ts.map +1 -1
- package/dist/handlers/me.js +5 -1
- package/dist/handlers/me.js.map +1 -1
- package/dist/http/api-token.d.ts +5 -6
- package/dist/http/api-token.d.ts.map +1 -1
- package/dist/http/api-token.js +6 -8
- 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 +135 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +415 -0
- package/dist/oauth/index.js.map +1 -0
- 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 +8 -6
- package/dist/tokens/api-token-holder.d.ts.map +1 -1
- package/dist/tokens/api-token-holder.js +5 -1
- package/dist/tokens/api-token-holder.js.map +1 -1
- package/dist/tokens/index.d.ts +1 -1
- 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 +9 -4
- 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/http/api-token.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ApiToken, AuthUser, IAuthProvider, IDataProvider, ILogger, RequestContext } from '@selvajs/platform';
|
|
2
2
|
import { ApiErrorCode } from '../api/errors.js';
|
|
3
3
|
import { type ApiTokenCodec } from '../tokens/api-token-codec.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type ApiTokenHolderPolicy } from '../tokens/api-token-holder.js';
|
|
5
5
|
export type ApiTokenResolution =
|
|
6
6
|
/** No API token on the request. Carry on with the cookie or proxy path. */
|
|
7
7
|
{
|
|
@@ -23,9 +23,8 @@ export interface ResolveApiTokenDeps {
|
|
|
23
23
|
/** Absent when the host hasn't configured API tokens: a sent key gets 503. */
|
|
24
24
|
codec?: ApiTokenCodec;
|
|
25
25
|
auth: Pick<IAuthProvider, 'getUser' | 'delegatedSession'>;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** The same policy passed as `SelvaDeps.tokens.mayHoldApiTokens`. Absent: keys aren't re-checked against one. */
|
|
26
|
+
data: Pick<IDataProvider, 'apiTokens' | 'orgs' | 'permissions'>;
|
|
27
|
+
/** The same policy passed as `SelvaDeps.tokens.mayHoldApiTokens`. Absent: the owner must still hold `manage_api_tokens`. */
|
|
29
28
|
mayHoldApiTokens?: ApiTokenHolderPolicy;
|
|
30
29
|
/** Receives background failures (the `lastUsedAt` write). Logs the token id only. */
|
|
31
30
|
log?: ILogger;
|
|
@@ -44,8 +43,8 @@ export interface ResolveApiTokenOptions {
|
|
|
44
43
|
* broken key and act as someone else.
|
|
45
44
|
*
|
|
46
45
|
* The owner is re-checked on every request (disabled, still a member of the
|
|
47
|
-
* token's org,
|
|
48
|
-
*
|
|
46
|
+
* token's org, still allowed to hold keys), then their live context is
|
|
47
|
+
* narrowed to the token's scopes.
|
|
49
48
|
*/
|
|
50
49
|
export declare function resolveApiToken(request: Request, deps: ResolveApiTokenDeps, { pathPrefix }?: ResolveApiTokenOptions): Promise<ApiTokenResolution>;
|
|
51
50
|
//# sourceMappingURL=api-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-token.d.ts","sourceRoot":"","sources":["../../src/http/api-token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,aAAa,EACb,OAAO,EACP,cAAc,EACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"api-token.d.ts","sourceRoot":"","sources":["../../src/http/api-token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,aAAa,EACb,OAAO,EACP,cAAc,EACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAoB,KAAK,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG5F,MAAM,MAAM,kBAAkB;AAC7B,2EAA2E;AACzE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE;AACtE,0EAA0E;GACxE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,mBAAmB;IACnC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,kBAAkB,CAAC,CAAC;IAC1D,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,GAAG,MAAM,GAAG,aAAa,CAAC,CAAC;IAChE,4HAA4H;IAC5H,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC,qFAAqF;IACrF,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACtC,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,mBAAmB,EACzB,EAAE,UAAuB,EAAE,GAAE,sBAA2B,GACtD,OAAO,CAAC,kBAAkB,CAAC,CAuE7B"}
|
package/dist/http/api-token.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SYSTEM_CONTEXT, narrowApiTokenContext } from '@selvajs/platform';
|
|
2
2
|
import { ApiErrorCode } from '../api/errors.js';
|
|
3
3
|
import { API_TOKEN_PREFIX, looksLikeApiToken } from '../tokens/api-token-codec.js';
|
|
4
|
+
import { canHoldApiTokens } from '../tokens/api-token-holder.js';
|
|
4
5
|
import { buildRequestContext } from './request-context.js';
|
|
5
6
|
/** `lastUsedAt` is written at most this often per token, so a busy key isn't a write per request. */
|
|
6
7
|
const LAST_USED_WRITE_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -31,8 +32,8 @@ const HOLDER_REFUSED = {
|
|
|
31
32
|
* broken key and act as someone else.
|
|
32
33
|
*
|
|
33
34
|
* The owner is re-checked on every request (disabled, still a member of the
|
|
34
|
-
* token's org,
|
|
35
|
-
*
|
|
35
|
+
* token's org, still allowed to hold keys), then their live context is
|
|
36
|
+
* narrowed to the token's scopes.
|
|
36
37
|
*/
|
|
37
38
|
export async function resolveApiToken(request, deps, { pathPrefix = '/api/v1/' } = {}) {
|
|
38
39
|
const url = new URL(request.url);
|
|
@@ -69,19 +70,16 @@ export async function resolveApiToken(request, deps, { pathPrefix = '/api/v1/' }
|
|
|
69
70
|
const token = await store.getByTokenHash(SYSTEM_CONTEXT, deps.codec.hashToken(raw));
|
|
70
71
|
if (!token)
|
|
71
72
|
return UNAUTHORIZED;
|
|
72
|
-
const policy = deps.mayHoldApiTokens;
|
|
73
73
|
const [user, member, platformPermissions] = await Promise.all([
|
|
74
74
|
deps.auth.getUser(token.userId),
|
|
75
75
|
deps.data.orgs.getOrgMember(SYSTEM_CONTEXT, token.orgId, token.userId),
|
|
76
|
-
// Read only for the
|
|
77
|
-
|
|
78
|
-
? deps.data.permissions.getFor(SYSTEM_CONTEXT, token.userId)
|
|
79
|
-
: []
|
|
76
|
+
// Read only for the holder check: narrowing drops platform permissions anyway.
|
|
77
|
+
deps.data.permissions.getFor(SYSTEM_CONTEXT, token.userId)
|
|
80
78
|
]);
|
|
81
79
|
if (!user || user.disabled || !member)
|
|
82
80
|
return UNAUTHORIZED;
|
|
83
81
|
const live = await buildRequestContext({ user, platformPermissions, membership: member, pinOrg: true }, { orgs: deps.data.orgs });
|
|
84
|
-
if (
|
|
82
|
+
if (!canHoldApiTokens(live, deps.mayHoldApiTokens))
|
|
85
83
|
return HOLDER_REFUSED;
|
|
86
84
|
let ctx = narrowApiTokenContext(live, token);
|
|
87
85
|
// Signed only after the user check above: a minted session can't be
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-token.js","sourceRoot":"","sources":["../../src/http/api-token.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACN,gBAAgB,EAChB,iBAAiB,EAEjB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"api-token.js","sourceRoot":"","sources":["../../src/http/api-token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit's form-submission CSRF check, with the two exemptions it can't
|
|
3
|
+
* express. A host turns the framework's own check off and calls this first.
|
|
4
|
+
*
|
|
5
|
+
* - A Selva API key in `Authorization`: a cross-site form can't set the header,
|
|
6
|
+
* and a `selva_` bearer never falls back to the cookie. API-token uploads are
|
|
7
|
+
* multipart with no `Origin`, which the framework check refuses. Any other
|
|
8
|
+
* `Authorization` (a proxy's id token) still gets checked: the request may run
|
|
9
|
+
* as the cookie session.
|
|
10
|
+
* - `exemptPaths`: endpoints that take a form body from servers, which send no
|
|
11
|
+
* `Origin` (the OAuth token endpoint, per RFC 6749).
|
|
12
|
+
*/
|
|
13
|
+
export interface CsrfOptions {
|
|
14
|
+
/** Exact paths that accept cross-site form bodies. */
|
|
15
|
+
exemptPaths?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function isCrossSiteFormSubmission(request: Request, options?: CsrfOptions): boolean;
|
|
18
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/http/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAcH,MAAM,WAAW,WAAW;IAC3B,sDAAsD;IACtD,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAS9F"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit's form-submission CSRF check, with the two exemptions it can't
|
|
3
|
+
* express. A host turns the framework's own check off and calls this first.
|
|
4
|
+
*
|
|
5
|
+
* - A Selva API key in `Authorization`: a cross-site form can't set the header,
|
|
6
|
+
* and a `selva_` bearer never falls back to the cookie. API-token uploads are
|
|
7
|
+
* multipart with no `Origin`, which the framework check refuses. Any other
|
|
8
|
+
* `Authorization` (a proxy's id token) still gets checked: the request may run
|
|
9
|
+
* as the cookie session.
|
|
10
|
+
* - `exemptPaths`: endpoints that take a form body from servers, which send no
|
|
11
|
+
* `Origin` (the OAuth token endpoint, per RFC 6749).
|
|
12
|
+
*/
|
|
13
|
+
import { API_TOKEN_PREFIX } from '../tokens/index.js';
|
|
14
|
+
const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
15
|
+
// The content types a browser form can send without a CORS preflight, plus
|
|
16
|
+
// SvelteKit's own binary form encoding.
|
|
17
|
+
const FORM_TYPES = [
|
|
18
|
+
'application/x-www-form-urlencoded',
|
|
19
|
+
'multipart/form-data',
|
|
20
|
+
'text/plain',
|
|
21
|
+
'application/x-sveltekit-formdata'
|
|
22
|
+
];
|
|
23
|
+
export function isCrossSiteFormSubmission(request, options = {}) {
|
|
24
|
+
if (!UNSAFE_METHODS.has(request.method))
|
|
25
|
+
return false;
|
|
26
|
+
const type = (request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
|
27
|
+
if (!FORM_TYPES.includes(type))
|
|
28
|
+
return false;
|
|
29
|
+
const bearer = /^Bearer\s+(\S+)/i.exec(request.headers.get('authorization') ?? '')?.[1];
|
|
30
|
+
if (bearer?.startsWith(API_TOKEN_PREFIX))
|
|
31
|
+
return false;
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
if (options.exemptPaths?.includes(url.pathname))
|
|
34
|
+
return false;
|
|
35
|
+
return request.headers.get('origin') !== url.origin;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=csrf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../../src/http/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AACnE,2EAA2E;AAC3E,wCAAwC;AACxC,MAAM,UAAU,GAAG;IAClB,mCAAmC;IACnC,qBAAqB;IACrB,YAAY;IACZ,kCAAkC;CAClC,CAAC;AAOF,MAAM,UAAU,yBAAyB,CAAC,OAAgB,EAAE,UAAuB,EAAE;IACpF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5F,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,MAAM,EAAE,UAAU,CAAC,gBAAgB,CAAC;QAAE,OAAO,KAAK,CAAC;IACvD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC;AACrD,CAAC"}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { safeRedirectTarget } from './redirect.js';
|
|
2
|
+
export { isCrossSiteFormSubmission, type CsrfOptions } from './csrf.js';
|
|
2
3
|
export { declaredBodySizeExceeds, type HeadersLike } from './body-size.js';
|
|
3
4
|
export { applySecurityHeaders, type SecurityHeaderOptions } from './security-headers.js';
|
|
4
5
|
export { createRouteClassifier, type RouteClassifier, type RouteClassifierConfig } from './route-classifier.js';
|
package/dist/http/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACN,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACN,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,MAAM,gBAAgB,CAAC"}
|
package/dist/http/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// HTTP hardening helpers — transport-agnostic request/response guards.
|
|
2
2
|
export { safeRedirectTarget } from './redirect.js';
|
|
3
|
+
export { isCrossSiteFormSubmission } from './csrf.js';
|
|
3
4
|
export { declaredBodySizeExceeds } from './body-size.js';
|
|
4
5
|
export { applySecurityHeaders } from './security-headers.js';
|
|
5
6
|
export { createRouteClassifier } from './route-classifier.js';
|
package/dist/http/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EAGpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAiC,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACN,eAAe,EAIf,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAoB,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EAGpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAiC,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACN,eAAe,EAIf,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 for MCP clients, with this instance as its own authorization server.
|
|
3
|
+
*
|
|
4
|
+
* The access token is an ordinary API token. The host mints it while the user is
|
|
5
|
+
* signed in, at consent, through the same path as Settings → API tokens; this
|
|
6
|
+
* module seals the raw key into the authorization code and hands it over at the
|
|
7
|
+
* token endpoint. So `/mcp` and `/api/v1` keep one credential type.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here is stored. A registered client's id is its own signed metadata,
|
|
10
|
+
* and a code is its own encrypted grant, bound to the issuing origin. The only
|
|
11
|
+
* state is the set of redeemed codes, in the host's memory, so on several
|
|
12
|
+
* replicas a code could be redeemed once per replica within its minute, and only
|
|
13
|
+
* by whoever holds the PKCE verifier.
|
|
14
|
+
*/
|
|
15
|
+
/** The scopes a grant can carry. Each maps to one org-wide API scope. */
|
|
16
|
+
export declare const OAUTH_SCOPES: readonly ["read", "solve"];
|
|
17
|
+
export type OAuthScope = (typeof OAUTH_SCOPES)[number];
|
|
18
|
+
export declare const MCP_PATH = "/mcp";
|
|
19
|
+
export declare class OAuthError extends Error {
|
|
20
|
+
/** RFC 6749 / 7591 error code. */
|
|
21
|
+
readonly error: string;
|
|
22
|
+
readonly status: number;
|
|
23
|
+
constructor(
|
|
24
|
+
/** RFC 6749 / 7591 error code. */
|
|
25
|
+
error: string, description: string, status?: number);
|
|
26
|
+
toBody(): {
|
|
27
|
+
error: string;
|
|
28
|
+
error_description: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface OAuthClient {
|
|
32
|
+
clientId: string;
|
|
33
|
+
/** Shown on the consent page. Unverified for a registered client: whatever it claimed. */
|
|
34
|
+
name: string;
|
|
35
|
+
redirectUris: string[];
|
|
36
|
+
}
|
|
37
|
+
/** A validated `/oauth/authorize` request, ready for the consent page. */
|
|
38
|
+
export interface AuthorizeRequest {
|
|
39
|
+
client: OAuthClient;
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
state?: string;
|
|
42
|
+
codeChallenge: string;
|
|
43
|
+
scopes: OAuthScope[];
|
|
44
|
+
resource: string;
|
|
45
|
+
}
|
|
46
|
+
/** What the host minted at consent. `secret` is the raw API key. */
|
|
47
|
+
export interface Grant {
|
|
48
|
+
tokenId: string;
|
|
49
|
+
secret: string;
|
|
50
|
+
scopes: OAuthScope[];
|
|
51
|
+
expiresAt: string;
|
|
52
|
+
}
|
|
53
|
+
export interface OAuthServerOptions {
|
|
54
|
+
/** This instance's origin, e.g. `https://selva.example.com`. */
|
|
55
|
+
origin: string;
|
|
56
|
+
/** `SELVA_HMAC_KEY`. Rotating it invalidates registered clients and live codes. */
|
|
57
|
+
secret: string;
|
|
58
|
+
/**
|
|
59
|
+
* Fetches a Client ID Metadata Document. The URL comes from an unknown client,
|
|
60
|
+
* so the host must guard it against SSRF. Without it, URL client ids are refused.
|
|
61
|
+
*/
|
|
62
|
+
fetchClientMetadata?: (url: string) => Promise<unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Redeemed code hashes and their expiry. Pass one map for the process so a
|
|
65
|
+
* rebuilt server (a new origin, a rotated secret) still refuses a replay.
|
|
66
|
+
*/
|
|
67
|
+
redeemedCodes?: Map<string, number>;
|
|
68
|
+
now?: () => number;
|
|
69
|
+
}
|
|
70
|
+
interface HttpResult {
|
|
71
|
+
status: number;
|
|
72
|
+
body: Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
export declare function mcpResourceUrl(origin: string): string;
|
|
75
|
+
export declare function resourceMetadataUrl(origin: string): string;
|
|
76
|
+
/** RFC 9728. Claude uses only the first authorization server. */
|
|
77
|
+
export declare function protectedResourceMetadata(origin: string): {
|
|
78
|
+
resource: string;
|
|
79
|
+
authorization_servers: string[];
|
|
80
|
+
scopes_supported: ("read" | "solve")[];
|
|
81
|
+
bearer_methods_supported: string[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* RFC 8414. Claude picks Client ID Metadata Documents only when both
|
|
85
|
+
* `client_id_metadata_document_supported` and the `none` auth method appear.
|
|
86
|
+
*/
|
|
87
|
+
export declare function authorizationServerMetadata(origin: string): {
|
|
88
|
+
issuer: string;
|
|
89
|
+
authorization_endpoint: string;
|
|
90
|
+
token_endpoint: string;
|
|
91
|
+
registration_endpoint: string;
|
|
92
|
+
response_types_supported: string[];
|
|
93
|
+
grant_types_supported: string[];
|
|
94
|
+
code_challenge_methods_supported: string[];
|
|
95
|
+
token_endpoint_auth_methods_supported: string[];
|
|
96
|
+
scopes_supported: ("read" | "solve")[];
|
|
97
|
+
client_id_metadata_document_supported: boolean;
|
|
98
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
99
|
+
};
|
|
100
|
+
export declare function createOAuthServer(options: OAuthServerOptions): {
|
|
101
|
+
register: (input: unknown) => HttpResult;
|
|
102
|
+
resolveClient: (clientId: string) => Promise<OAuthClient>;
|
|
103
|
+
parseAuthorizeRequest: (query: URLSearchParams) => Promise<AuthorizeRequest>;
|
|
104
|
+
approve: (request: AuthorizeRequest, grant: Grant) => string;
|
|
105
|
+
deny: (request: AuthorizeRequest) => string;
|
|
106
|
+
exchange: (form: URLSearchParams) => HttpResult;
|
|
107
|
+
protectedResourceMetadata: () => {
|
|
108
|
+
resource: string;
|
|
109
|
+
authorization_servers: string[];
|
|
110
|
+
scopes_supported: ("read" | "solve")[];
|
|
111
|
+
bearer_methods_supported: string[];
|
|
112
|
+
};
|
|
113
|
+
authorizationServerMetadata: () => {
|
|
114
|
+
issuer: string;
|
|
115
|
+
authorization_endpoint: string;
|
|
116
|
+
token_endpoint: string;
|
|
117
|
+
registration_endpoint: string;
|
|
118
|
+
response_types_supported: string[];
|
|
119
|
+
grant_types_supported: string[];
|
|
120
|
+
code_challenge_methods_supported: string[];
|
|
121
|
+
token_endpoint_auth_methods_supported: string[];
|
|
122
|
+
scopes_supported: ("read" | "solve")[];
|
|
123
|
+
client_id_metadata_document_supported: boolean;
|
|
124
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export type OAuthServer = ReturnType<typeof createOAuthServer>;
|
|
128
|
+
/** OAuth scopes to the API scopes a minted token gets. */
|
|
129
|
+
export declare function apiScopesFor(scopes: readonly OAuthScope[]): string[];
|
|
130
|
+
/** A redirect to a program on the user's own machine, which any local process can claim. */
|
|
131
|
+
export declare function isLoopbackRedirect(uri: string): boolean;
|
|
132
|
+
/** Exact, except a loopback redirect matches on any port (RFC 8252 §7.3). */
|
|
133
|
+
export declare function redirectMatches(registered: string, requested: string): boolean;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,yEAAyE;AACzE,eAAO,MAAM,YAAY,4BAA6B,CAAC;AACvD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,QAAQ,SAAS,CAAC;AAY/B,qBAAa,UAAW,SAAQ,KAAK;IAEnC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAEtB,QAAQ,CAAC,MAAM;;IAHf,kCAAkC;IACzB,KAAK,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACV,MAAM,SAAM;IAMtB,MAAM,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;CAGtD;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,oEAAoE;AACpE,MAAM,WAAW,KAAK;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD;;;OAGG;IACH,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,UAAU,UAAU;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,iEAAiE;AACjE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM;;;;;EAOvD;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;EAczD;AAMD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB;sBAgBnC,OAAO,KAAG,UAAU;8BA2BN,MAAM,KAAG,OAAO,CAAC,WAAW,CAAC;mCAmDxB,eAAe,KAAG,OAAO,CAAC,gBAAgB,CAAC;uBA+B7D,gBAAgB,SAAS,KAAK,KAAG,MAAM;oBAW1C,gBAAgB,KAAG,MAAM;qBAYxB,eAAe,KAAG,UAAU;;;;;;;;;;;;;;;;;;;;EAsEpD;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE/D,0DAA0D;AAC1D,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,EAAE,CAEpE;AAmED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAiB9E"}
|