@plitzi/sdk-server 0.33.1 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +283 -8
- package/dist/actions.d.ts +62 -0
- package/dist/actions.js +12 -0
- package/dist/adapters/cloudAdapters.d.ts +72 -0
- package/dist/adapters/cloudAdapters.js +303 -0
- package/dist/adapters/space/index.d.ts +93 -0
- package/dist/adapters/space/index.js +122 -0
- package/dist/adapters/space/resolvers.d.ts +69 -0
- package/dist/adapters/space/resolvers.js +79 -0
- package/dist/adapters/space/space.test.d.ts +1 -0
- package/dist/adapters/space/types.d.ts +64 -0
- package/dist/adapters/space/types.js +41 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +19 -0
- package/dist/core/auth/api.d.ts +400 -11
- package/dist/core/auth/api.js +1044 -50
- package/dist/core/auth/createAuth.d.ts +135 -30
- package/dist/core/auth/createAuth.js +96 -24
- package/dist/core/auth/credentials.d.ts +9 -3
- package/dist/core/auth/credentials.js +10 -4
- package/dist/core/auth/csrf.d.ts +89 -0
- package/dist/core/auth/csrf.js +234 -0
- package/dist/core/auth/csrf.test.d.ts +1 -0
- package/dist/core/auth/identity.d.ts +6 -0
- package/dist/core/auth/identity.js +37 -8
- package/dist/core/auth/lifecycle.test.d.ts +1 -0
- package/dist/core/auth/oauth/index.d.ts +8 -0
- package/dist/core/auth/oauth/index.js +13 -15
- package/dist/core/auth/passwords.d.ts +15 -0
- package/dist/core/auth/passwords.js +32 -0
- package/dist/core/auth/redirects.d.ts +18 -0
- package/dist/core/auth/redirects.js +31 -0
- package/dist/core/auth/redirects.test.d.ts +1 -0
- package/dist/core/auth/routes.d.ts +4 -2
- package/dist/core/auth/routes.js +234 -9
- package/dist/core/auth/session.d.ts +11 -0
- package/dist/core/auth/session.js +1 -1
- package/dist/core/auth/spaceTokens.d.ts +38 -2
- package/dist/core/auth/spaceTokens.js +54 -2
- package/dist/core/auth/throttle.d.ts +5 -0
- package/dist/core/auth/throttle.js +81 -0
- package/dist/core/auth/throttle.test.d.ts +1 -0
- package/dist/core/auth/tokens.d.ts +83 -12
- package/dist/core/auth/tokens.js +51 -9
- package/dist/core/auth/totp.d.ts +48 -0
- package/dist/core/auth/totp.js +118 -0
- package/dist/core/auth/totp.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +0 -6
- package/dist/core/createServer.js +35 -3
- package/dist/core/fontStore.d.ts +33 -0
- package/dist/core/fontStore.js +53 -0
- package/dist/core/fontStore.test.d.ts +1 -0
- package/dist/core/handlers/authMiddleware.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
- package/dist/core/handlers/authRouteHandlers.js +4 -3
- package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
- package/dist/core/handlers/csrfMiddleware.js +32 -0
- package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
- package/dist/core/handlers/oauthRouteHandlers.js +159 -0
- package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
- package/dist/core/handlers/socialRouteHandlers.js +129 -0
- package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
- package/dist/core/handlers/types.d.ts +22 -5
- package/dist/core/health.d.ts +11 -1
- package/dist/core/health.js +27 -3
- package/dist/core/http/dispatcher.d.ts +1 -1
- package/dist/core/http/dispatcher.js +20 -1
- package/dist/core/http/stages/authApi.js +1 -1
- package/dist/core/http/stages/fontAssets.d.ts +9 -0
- package/dist/core/http/stages/fontAssets.js +27 -0
- package/dist/core/http/stages/oauth.d.ts +19 -0
- package/dist/core/http/stages/oauth.js +118 -0
- package/dist/core/http/types.d.ts +12 -0
- package/dist/core/oauth/authorize.d.ts +22 -0
- package/dist/core/oauth/authorize.js +254 -0
- package/dist/core/oauth/authorize.test.d.ts +1 -0
- package/dist/core/oauth/challenge.d.ts +11 -0
- package/dist/core/oauth/challenge.js +33 -0
- package/dist/core/oauth/consentPage.d.ts +12 -0
- package/dist/core/oauth/consentPage.js +131 -0
- package/dist/core/oauth/metadata.d.ts +41 -0
- package/dist/core/oauth/metadata.js +71 -0
- package/dist/core/oauth/params.d.ts +5 -0
- package/dist/core/oauth/params.js +5 -0
- package/dist/core/oauth/pkce.d.ts +6 -0
- package/dist/core/oauth/pkce.js +15 -0
- package/dist/core/oauth/records.d.ts +72 -0
- package/dist/core/oauth/records.js +49 -0
- package/dist/core/oauth/register.d.ts +5 -0
- package/dist/core/oauth/register.js +55 -0
- package/dist/core/oauth/respond.d.ts +21 -0
- package/dist/core/oauth/respond.js +59 -0
- package/dist/core/oauth/token.d.ts +14 -0
- package/dist/core/oauth/token.js +130 -0
- package/dist/core/previewToken.d.ts +20 -0
- package/dist/core/previewToken.js +29 -1
- package/dist/core/server/pageServer.js +4 -1
- package/dist/core/services/action.d.ts +6 -4
- package/dist/core/services/action.js +103 -10
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/resolve.d.ts +8 -0
- package/dist/core/services/resolve.js +12 -1
- package/dist/core/staticFiles.js +1 -2
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +4 -1
- package/dist/helpers/buildResponseHelpers.d.ts +8 -0
- package/dist/helpers/buildResponseHelpers.js +9 -1
- package/dist/helpers/buildServerInfo.d.ts +8 -3
- package/dist/helpers/buildServerInfo.js +18 -14
- package/dist/helpers/cache/keys.d.ts +13 -0
- package/dist/helpers/cache/keys.js +12 -1
- package/dist/helpers/onAbort.d.ts +12 -0
- package/dist/helpers/onAbort.js +22 -0
- package/dist/helpers/onAbort.test.d.ts +1 -0
- package/dist/helpers/outboundGuard.d.ts +27 -0
- package/dist/helpers/outboundGuard.js +49 -0
- package/dist/helpers/outboundGuard.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +6 -3
- package/dist/helpers/serverLog.js +20 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -4
- package/dist/kernel.d.ts +5 -22
- package/dist/kernel.js +1 -14
- package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
- package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
- package/dist/modules/actions/index.d.ts +40 -0
- package/dist/modules/actions/index.js +46 -0
- package/dist/modules/actions/moduleFor.d.ts +3 -0
- package/dist/modules/actions/moduleFor.js +35 -0
- package/dist/modules/actions/moduleFor.test.d.ts +1 -0
- package/dist/modules/actions/runtime/check.d.ts +24 -0
- package/dist/modules/actions/runtime/check.js +111 -0
- package/dist/modules/actions/runtime/check.test.d.ts +1 -0
- package/dist/modules/actions/runtime/errors.d.ts +11 -0
- package/dist/modules/actions/runtime/errors.js +17 -0
- package/dist/modules/actions/runtime/guards.d.ts +126 -0
- package/dist/modules/actions/runtime/guards.js +246 -0
- package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
- package/dist/modules/actions/runtime/kvStore.js +41 -0
- package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
- package/dist/modules/actions/runtime/limits.d.ts +11 -0
- package/dist/modules/actions/runtime/limits.js +35 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
- package/dist/modules/actions/runtime/memoryKv.js +77 -0
- package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
- package/dist/modules/actions/runtime/namespaceKv.js +19 -0
- package/dist/modules/actions/runtime/precheck.d.ts +27 -0
- package/dist/modules/actions/runtime/precheck.js +45 -0
- package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
- package/dist/modules/actions/runtime/renderResolver.js +130 -0
- package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
- package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
- package/dist/modules/actions/runtime/renderShare.js +36 -0
- package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
- package/dist/modules/actions/runtime/report.d.ts +14 -0
- package/dist/modules/actions/runtime/report.js +22 -0
- package/dist/modules/actions/runtime/runAction.d.ts +12 -0
- package/dist/modules/actions/runtime/runAction.js +405 -0
- package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
- package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
- package/dist/modules/actions/runtime/runLogger.js +66 -0
- package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
- package/dist/modules/actions/runtime/schedule.d.ts +35 -0
- package/dist/modules/actions/runtime/schedule.js +76 -0
- package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
- package/dist/modules/actions/runtime/scope.d.ts +40 -0
- package/dist/modules/actions/runtime/scope.js +103 -0
- package/dist/modules/actions/runtime/triggers.d.ts +12 -0
- package/dist/modules/actions/runtime/triggers.js +14 -0
- package/dist/modules/actions/taskCatalog.d.ts +26 -0
- package/dist/modules/actions/taskCatalog.js +24 -0
- package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
- package/dist/modules/actions/tasks/auth.d.ts +2 -0
- package/dist/modules/actions/tasks/auth.js +40 -0
- package/dist/modules/actions/tasks/builtins.d.ts +8 -0
- package/dist/modules/actions/tasks/builtins.js +24 -0
- package/dist/modules/actions/tasks/connector.d.ts +2 -0
- package/dist/modules/actions/tasks/connector.js +141 -0
- package/dist/modules/actions/tasks/db.d.ts +2 -0
- package/dist/modules/actions/tasks/db.js +59 -0
- package/dist/modules/actions/tasks/db.test.d.ts +1 -0
- package/dist/modules/actions/tasks/flow.d.ts +3 -0
- package/dist/modules/actions/tasks/flow.js +109 -0
- package/dist/modules/actions/tasks/helpers.d.ts +16 -0
- package/dist/modules/actions/tasks/helpers.js +30 -0
- package/dist/modules/actions/tasks/http.d.ts +2 -0
- package/dist/modules/actions/tasks/http.js +111 -0
- package/dist/modules/actions/tasks/http.test.d.ts +1 -0
- package/dist/modules/actions/tasks/kv.d.ts +2 -0
- package/dist/modules/actions/tasks/kv.js +102 -0
- package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -0
- package/dist/modules/actions/tasks/registry.js +36 -0
- package/dist/modules/actions/tasks/transform.d.ts +2 -0
- package/dist/modules/actions/tasks/transform.js +34 -0
- package/dist/modules/actions/transport/callHandler.d.ts +24 -0
- package/dist/modules/actions/transport/callHandler.js +233 -0
- package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
- package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
- package/dist/modules/actions/transport/cancelHandler.js +23 -0
- package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
- package/dist/modules/actions/transport/catalogHandler.js +21 -0
- package/dist/modules/actions/transport/stream.d.ts +23 -0
- package/dist/modules/actions/transport/stream.js +100 -0
- package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
- package/dist/modules/actions/transport/verifySignature.js +72 -0
- package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
- package/dist/modules/actions/transport/webhookHandler.js +206 -0
- package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
- package/dist/modules/actions/types.d.ts +234 -0
- package/dist/modules/connectors/engine.js +4 -1
- package/dist/modules/connectors/resolver.d.ts +10 -2
- package/dist/modules/connectors/resolver.js +8 -3
- package/dist/modules/mysql/accounts.d.ts +5 -0
- package/dist/modules/mysql/accounts.js +365 -0
- package/dist/modules/mysql/accounts.test.d.ts +1 -0
- package/dist/modules/mysql/admin.d.ts +58 -0
- package/dist/modules/mysql/admin.js +133 -0
- package/dist/modules/mysql/config.d.ts +82 -0
- package/dist/modules/mysql/config.js +30 -0
- package/dist/modules/mysql/identities.d.ts +35 -0
- package/dist/modules/mysql/identities.js +43 -0
- package/dist/modules/mysql/index.d.ts +67 -0
- package/dist/modules/mysql/index.js +62 -0
- package/dist/modules/mysql/pool.d.ts +3 -0
- package/dist/modules/mysql/pool.js +88 -0
- package/dist/modules/mysql/query.d.ts +12 -0
- package/dist/modules/mysql/query.js +21 -0
- package/dist/modules/mysql/schema.d.ts +43 -0
- package/dist/modules/mysql/schema.js +340 -0
- package/dist/modules/mysql/schema.test.d.ts +1 -0
- package/dist/modules/mysql/sessions.d.ts +33 -0
- package/dist/modules/mysql/sessions.js +46 -0
- package/dist/modules/mysql/spaceTokens.d.ts +11 -0
- package/dist/modules/mysql/spaceTokens.js +59 -0
- package/dist/modules/mysql/store.integration.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +25 -0
- package/dist/modules/rsc/connectorRscData.js +51 -0
- package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
- package/dist/modules/rsc/handler.js +16 -3
- package/dist/modules/rsc/matchRscPage.d.ts +14 -0
- package/dist/modules/rsc/matchRscPage.js +26 -0
- package/dist/modules/rsc/resolveRscData.d.ts +14 -1
- package/dist/modules/rsc/resolveRscData.js +23 -34
- package/dist/modules/ssr/Component.d.ts +6 -2
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/draftSession.test.d.ts +1 -0
- package/dist/modules/ssr/handler.js +15 -2
- package/dist/modules/ssr/loadPluginComponents.js +22 -5
- package/dist/modules/ssr/prepareRender.js +87 -8
- package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
- package/dist/modules/ssr/preview.d.ts +21 -8
- package/dist/modules/ssr/preview.js +38 -14
- package/dist/modules/ssr/registerExternalPlugins.js +2 -2
- package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
- package/dist/modules/ssr/resolvePageSeo.js +26 -0
- package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
- package/dist/modules/ssr/template.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +71 -5
- package/dist/mysql.d.ts +26 -0
- package/dist/mysql.js +4 -0
- package/dist/oauth.d.ts +24 -0
- package/dist/oauth.js +10 -0
- package/dist/plugins/compile.d.ts +1 -0
- package/dist/plugins/compile.js +37 -3
- package/dist/plugins/manager.d.ts +40 -0
- package/dist/plugins/manager.js +113 -13
- package/dist/plugins/manager.test.d.ts +1 -0
- package/package.json +43 -14
- package/skills/plitzi-authoring/SKILL.md +252 -0
- /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
- /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { requestOrigin } from "@plitzi/sdk-server/kernel";
|
|
2
|
+
//#region src/core/oauth/metadata.ts
|
|
3
|
+
var AUTHORIZE_PATH = "/authorize";
|
|
4
|
+
var TOKEN_PATH = "/token";
|
|
5
|
+
var REVOKE_PATH = "/revoke";
|
|
6
|
+
var REGISTER_PATH = "/register";
|
|
7
|
+
var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
8
|
+
var AUTHORIZATION_SERVER_PATH = "/.well-known/oauth-authorization-server";
|
|
9
|
+
var DEFAULT_SCOPES = ["plitzi"];
|
|
10
|
+
/** The identifier the two documents agree on. This server is both the protected resource and its own
|
|
11
|
+
* authorization server, so one origin names both. Derived from the request unless pinned, which keeps a
|
|
12
|
+
* deployment correct across its dev, staging and production hosts without per-environment config. */
|
|
13
|
+
var issuerOf = (config, req) => config.issuer ?? requestOrigin(req);
|
|
14
|
+
var scopesOf = (config) => config.scopes ?? DEFAULT_SCOPES;
|
|
15
|
+
var canonicalPath = (path) => path.replace(/\/+$/, "");
|
|
16
|
+
/** Where the challenge points a host, for an MCP endpoint served at `req.path`. RFC 9728 §3.1 appends the
|
|
17
|
+
* resource's own path to the well-known path, and a host that reads the document back expects its `resource` to
|
|
18
|
+
* name the URL it was configured with — so a server mounted at /mcp must be pointed at the suffixed document,
|
|
19
|
+
* not the bare one. */
|
|
20
|
+
var resourceMetadataUrl = (config, req) => `${issuerOf(config, req)}${PROTECTED_RESOURCE_PATH}${canonicalPath(req.path)}`;
|
|
21
|
+
/** RFC 9728. The document Claude Desktop asks for FIRST, and the one whose absence ends the flow before anything
|
|
22
|
+
* else is tried — a 404 here is what a host reports as `mcp_auth_start_failed`.
|
|
23
|
+
*
|
|
24
|
+
* `resource` echoes the path the document was asked for: Claude requires it to match the server URL the user
|
|
25
|
+
* typed, path included, and a dedicated MCP server answers JSON-RPC on every path — so `/.well-known/…/mcp`
|
|
26
|
+
* describes `https://host/mcp` while the bare path describes the origin.
|
|
27
|
+
*
|
|
28
|
+
* A server mounted at the ORIGIN is `https://host/`, WITH the trailing slash: a host parses the URL the user
|
|
29
|
+
* typed before it does anything with it, and an empty path renders as `/` — Claude's connector was observed
|
|
30
|
+
* asking for exactly `resource=https%3A%2F%2Fhost%2F`, so that is the form the document must claim.
|
|
31
|
+
*
|
|
32
|
+
* It does NOT make a bare-origin connector work, and nothing here can. Claude's remote-connector service will
|
|
33
|
+
* not open a session against an MCP endpoint whose URL has no path: the whole grant succeeds — discovery,
|
|
34
|
+
* registration, consent, a 200 from /token with the scope it asked for — and then it reports
|
|
35
|
+
* `McpAuthorizationError` ("the integration rejected the credentials it just issued") without ever presenting
|
|
36
|
+
* the bearer. Verified against the same deployment, minutes apart: at /mcp the token is followed by
|
|
37
|
+
* `initialize`, at / nothing follows it, in the ingress log as well as this server's. Claiming `https://host/mcp`
|
|
38
|
+
* from the bare document would only fail sooner — a client checks that the resource it is handed is no MORE
|
|
39
|
+
* specific than the URL it holds. Publish the connector URL WITH its path. */
|
|
40
|
+
var protectedResourceMetadata = (config, req) => {
|
|
41
|
+
const issuer = issuerOf(config, req);
|
|
42
|
+
return {
|
|
43
|
+
resource: `${issuer}${canonicalPath(req.path.slice(37)) || "/"}`,
|
|
44
|
+
authorization_servers: [issuer],
|
|
45
|
+
scopes_supported: scopesOf(config),
|
|
46
|
+
bearer_methods_supported: ["header"]
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** RFC 8414. Public clients with PKCE only: a desktop host stores no secret, so `none` is the sole endpoint auth
|
|
50
|
+
* method and S256 the sole challenge method. `offline_access` is advertised whenever refresh grants are issued,
|
|
51
|
+
* which is the signal a host looks for before asking for one. */
|
|
52
|
+
var authorizationServerMetadata = (config, req) => {
|
|
53
|
+
const issuer = issuerOf(config, req);
|
|
54
|
+
const refreshes = config.refreshTtlSeconds !== 0;
|
|
55
|
+
const grantTypes = refreshes ? ["authorization_code", "refresh_token"] : ["authorization_code"];
|
|
56
|
+
const scopes = refreshes ? [...scopesOf(config), "offline_access"] : scopesOf(config);
|
|
57
|
+
return {
|
|
58
|
+
issuer,
|
|
59
|
+
authorization_endpoint: `${issuer}${AUTHORIZE_PATH}`,
|
|
60
|
+
token_endpoint: `${issuer}${TOKEN_PATH}`,
|
|
61
|
+
registration_endpoint: `${issuer}${REGISTER_PATH}`,
|
|
62
|
+
revocation_endpoint: `${issuer}${REVOKE_PATH}`,
|
|
63
|
+
response_types_supported: ["code"],
|
|
64
|
+
grant_types_supported: grantTypes,
|
|
65
|
+
code_challenge_methods_supported: ["S256"],
|
|
66
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
67
|
+
scopes_supported: scopes
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { AUTHORIZATION_SERVER_PATH, AUTHORIZE_PATH, PROTECTED_RESOURCE_PATH, REGISTER_PATH, REVOKE_PATH, TOKEN_PATH, authorizationServerMetadata, issuerOf, protectedResourceMetadata, resourceMetadataUrl, scopesOf };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Query-string or form fields as they arrive from a client: every name is optional, and an absent one is not
|
|
2
|
+
* distinguishable from an empty one — both mean "the client did not send this". */
|
|
3
|
+
export type OAuthParams = Record<string, string | undefined>;
|
|
4
|
+
export declare const field: (params: OAuthParams, name: string) => string;
|
|
5
|
+
export declare const optionalField: (params: OAuthParams, name: string) => string | undefined;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** An unguessable, URL-safe identifier for the values this layer hands out (codes, refresh tokens, client ids).
|
|
2
|
+
* 256 bits from the CSPRNG, so they are also unguessable by anyone who collects a few of them. */
|
|
3
|
+
export declare const randomId: () => string;
|
|
4
|
+
/** RFC 7636 S256, the only method this server accepts — `plain` puts the verifier on the wire, and a public
|
|
5
|
+
* client on a desktop host has no other proof it is the one that started the flow. */
|
|
6
|
+
export declare const verifyChallenge: (verifier: string, challenge: string) => boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
//#region src/core/oauth/pkce.ts
|
|
3
|
+
/** An unguessable, URL-safe identifier for the values this layer hands out (codes, refresh tokens, client ids).
|
|
4
|
+
* 256 bits from the CSPRNG, so they are also unguessable by anyone who collects a few of them. */
|
|
5
|
+
var randomId = () => randomBytes(32).toString("base64url");
|
|
6
|
+
/** RFC 7636 S256, the only method this server accepts — `plain` puts the verifier on the wire, and a public
|
|
7
|
+
* client on a desktop host has no other proof it is the one that started the flow. */
|
|
8
|
+
var verifyChallenge = (verifier, challenge) => {
|
|
9
|
+
if (!verifier || !challenge) return false;
|
|
10
|
+
const computed = Buffer.from(createHash("sha256").update(verifier).digest("base64url"));
|
|
11
|
+
const expected = Buffer.from(challenge);
|
|
12
|
+
return computed.length === expected.length && timingSafeEqual(computed, expected);
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { randomId, verifyChallenge };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { OAuthGrantTarget, OAuthStore, OAuthUser } from '@plitzi/sdk-shared';
|
|
2
|
+
/** A client that registered itself through RFC 7591. Public clients only — a desktop host cannot keep a secret,
|
|
3
|
+
* so the token endpoint authenticates the exchange with PKCE instead. */
|
|
4
|
+
export type ClientRecord = {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientName: string;
|
|
7
|
+
redirectUris: string[];
|
|
8
|
+
/** When this registration was first handed out, so re-registering the same client keeps reporting its real age. */
|
|
9
|
+
issuedAt: number;
|
|
10
|
+
};
|
|
11
|
+
/** The authorization request, parked while the user logs in. `challenge` binds the eventual code to the client
|
|
12
|
+
* that started the flow. */
|
|
13
|
+
export type PendingRecord = {
|
|
14
|
+
clientId: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
challenge: string;
|
|
17
|
+
state?: string;
|
|
18
|
+
scope?: string;
|
|
19
|
+
user: OAuthUser;
|
|
20
|
+
};
|
|
21
|
+
/** A code, redeemable once, for the token it already resolves to. Minting the bearer at consent time (rather than
|
|
22
|
+
* at redemption) means a failure the user could act on — no space, revoked access — surfaces on the consent
|
|
23
|
+
* screen instead of as an opaque `invalid_grant` inside the host. */
|
|
24
|
+
export type CodeRecord = {
|
|
25
|
+
clientId: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
challenge: string;
|
|
28
|
+
token: string;
|
|
29
|
+
expiresInSeconds?: number;
|
|
30
|
+
scope?: string;
|
|
31
|
+
user: OAuthUser;
|
|
32
|
+
target: OAuthGrantTarget;
|
|
33
|
+
};
|
|
34
|
+
/** A refresh grant: everything needed to mint a fresh bearer without asking the user again. */
|
|
35
|
+
export type RefreshRecord = {
|
|
36
|
+
clientId: string;
|
|
37
|
+
scope?: string;
|
|
38
|
+
user: OAuthUser;
|
|
39
|
+
target: OAuthGrantTarget;
|
|
40
|
+
};
|
|
41
|
+
/** A bearer this server handed out. The token a client holds is an opaque handle minted here, and this record is
|
|
42
|
+
* what it stands for — including `credential`, the thing the deployment's own adapters understand, which never
|
|
43
|
+
* leaves the server: a platform token is usually good against more than the MCP endpoint, and a remote host has
|
|
44
|
+
* no business holding one. The record expires with the bearer, which is what turns an expired one into the 401 a
|
|
45
|
+
* host answers by refreshing; dropping it early revokes the bearer outright. */
|
|
46
|
+
export type AccessRecord = {
|
|
47
|
+
/** What {@link OAuthAdapters.issueToken} returned — swapped back onto the request once the bearer checks out.
|
|
48
|
+
* Absent only in a record written before the bearer and the credential were separate things, where the bearer
|
|
49
|
+
* WAS the credential: a live connector must survive that upgrade, so the reader falls back to the token itself. */
|
|
50
|
+
credential?: string;
|
|
51
|
+
clientId: string;
|
|
52
|
+
user: OAuthUser;
|
|
53
|
+
target: OAuthGrantTarget;
|
|
54
|
+
};
|
|
55
|
+
export declare const putClient: (store: OAuthStore, client: ClientRecord) => Promise<void>;
|
|
56
|
+
export declare const getClient: (store: OAuthStore, clientId: string) => Promise<ClientRecord | undefined>;
|
|
57
|
+
/** The id already handed out for an identical registration. Keyed by what the client asked to be registered AS,
|
|
58
|
+
* so the same request always resolves to the same client — see handleRegister for why that matters. */
|
|
59
|
+
export declare const getClientByFingerprint: (store: OAuthStore, fingerprint: string) => Promise<ClientRecord | undefined>;
|
|
60
|
+
export declare const putClientFingerprint: (store: OAuthStore, fingerprint: string, clientId: string) => Promise<void>;
|
|
61
|
+
export declare const putPending: (store: OAuthStore, id: string, pending: PendingRecord) => Promise<void>;
|
|
62
|
+
export declare const getPending: (store: OAuthStore, id: string) => Promise<PendingRecord | undefined>;
|
|
63
|
+
export declare const dropPending: (store: OAuthStore, id: string) => Promise<void>;
|
|
64
|
+
export declare const putCode: (store: OAuthStore, code: string, record: CodeRecord, ttlSeconds: number) => Promise<void>;
|
|
65
|
+
export declare const getCode: (store: OAuthStore, code: string) => Promise<CodeRecord | undefined>;
|
|
66
|
+
export declare const dropCode: (store: OAuthStore, code: string) => Promise<void>;
|
|
67
|
+
export declare const putRefresh: (store: OAuthStore, token: string, record: RefreshRecord, ttlSeconds: number) => Promise<void>;
|
|
68
|
+
export declare const getRefresh: (store: OAuthStore, token: string) => Promise<RefreshRecord | undefined>;
|
|
69
|
+
export declare const dropRefresh: (store: OAuthStore, token: string) => Promise<void>;
|
|
70
|
+
export declare const dropAccess: (store: OAuthStore, token: string) => Promise<void>;
|
|
71
|
+
export declare const putAccess: (store: OAuthStore, token: string, record: AccessRecord, ttlSeconds: number) => Promise<void>;
|
|
72
|
+
export declare const getAccess: (store: OAuthStore, token: string) => Promise<AccessRecord | undefined>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/core/oauth/records.ts
|
|
2
|
+
var CLIENT_TTL_SECONDS = 7776e3;
|
|
3
|
+
var PENDING_TTL_SECONDS = 600;
|
|
4
|
+
var keyOf = (kind, id) => `oauth:${kind}:${id}`;
|
|
5
|
+
var readJson = async (store, kind, id) => {
|
|
6
|
+
const raw = await store.get(keyOf(kind, id));
|
|
7
|
+
if (!raw) return;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
} catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var writeJson = async (store, kind, id, value, ttlSeconds) => {
|
|
15
|
+
await store.put(keyOf(kind, id), JSON.stringify(value), ttlSeconds);
|
|
16
|
+
};
|
|
17
|
+
var putClient = (store, client) => writeJson(store, "client", client.clientId, client, CLIENT_TTL_SECONDS);
|
|
18
|
+
var getClient = (store, clientId) => readJson(store, "client", clientId);
|
|
19
|
+
/** The id already handed out for an identical registration. Keyed by what the client asked to be registered AS,
|
|
20
|
+
* so the same request always resolves to the same client — see handleRegister for why that matters. */
|
|
21
|
+
var getClientByFingerprint = async (store, fingerprint) => {
|
|
22
|
+
const clientId = await store.get(keyOf("clientfp", fingerprint));
|
|
23
|
+
return clientId ? getClient(store, clientId) : void 0;
|
|
24
|
+
};
|
|
25
|
+
var putClientFingerprint = async (store, fingerprint, clientId) => {
|
|
26
|
+
await store.put(keyOf("clientfp", fingerprint), clientId, CLIENT_TTL_SECONDS);
|
|
27
|
+
};
|
|
28
|
+
var putPending = (store, id, pending) => writeJson(store, "pending", id, pending, PENDING_TTL_SECONDS);
|
|
29
|
+
var getPending = (store, id) => readJson(store, "pending", id);
|
|
30
|
+
var dropPending = async (store, id) => {
|
|
31
|
+
await store.drop(keyOf("pending", id));
|
|
32
|
+
};
|
|
33
|
+
var putCode = (store, code, record, ttlSeconds) => writeJson(store, "code", code, record, ttlSeconds);
|
|
34
|
+
var getCode = (store, code) => readJson(store, "code", code);
|
|
35
|
+
var dropCode = async (store, code) => {
|
|
36
|
+
await store.drop(keyOf("code", code));
|
|
37
|
+
};
|
|
38
|
+
var putRefresh = (store, token, record, ttlSeconds) => writeJson(store, "refresh", token, record, ttlSeconds);
|
|
39
|
+
var getRefresh = (store, token) => readJson(store, "refresh", token);
|
|
40
|
+
var dropRefresh = async (store, token) => {
|
|
41
|
+
await store.drop(keyOf("refresh", token));
|
|
42
|
+
};
|
|
43
|
+
var dropAccess = async (store, token) => {
|
|
44
|
+
await store.drop(keyOf("access", token));
|
|
45
|
+
};
|
|
46
|
+
var putAccess = (store, token, record, ttlSeconds) => writeJson(store, "access", token, record, ttlSeconds);
|
|
47
|
+
var getAccess = (store, token) => readJson(store, "access", token);
|
|
48
|
+
//#endregion
|
|
49
|
+
export { dropAccess, dropCode, dropPending, dropRefresh, getAccess, getClient, getClientByFingerprint, getCode, getPending, getRefresh, putAccess, putClient, putClientFingerprint, putCode, putPending, putRefresh };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OAuthConfig, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
2
|
+
/** RFC 7591 dynamic client registration. A remote host has no way to be configured into this server ahead of
|
|
3
|
+
* time, so it registers itself on first connect; the record it gets back is only ever used to pin the redirect
|
|
4
|
+
* target, since a public client authenticates with PKCE rather than with credentials. */
|
|
5
|
+
export declare const handleRegister: (config: OAuthConfig, res: SSRResponseHelpers, body: unknown) => Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomId } from "./pkce.js";
|
|
2
|
+
import { getClientByFingerprint, putClient, putClientFingerprint } from "./records.js";
|
|
3
|
+
import { sendErrorJson, sendJson } from "./respond.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/core/oauth/register.ts
|
|
6
|
+
var isUsableRedirectUri = (value) => {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(value);
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (url.protocol === "https:") return true;
|
|
14
|
+
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
15
|
+
};
|
|
16
|
+
var stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
17
|
+
var nowSeconds = () => Math.floor(Date.now() / 1e3);
|
|
18
|
+
var fingerprintOf = (clientName, redirectUris) => createHash("sha256").update(JSON.stringify([clientName, [...redirectUris].sort()])).digest("base64url");
|
|
19
|
+
/** RFC 7591 dynamic client registration. A remote host has no way to be configured into this server ahead of
|
|
20
|
+
* time, so it registers itself on first connect; the record it gets back is only ever used to pin the redirect
|
|
21
|
+
* target, since a public client authenticates with PKCE rather than with credentials. */
|
|
22
|
+
var handleRegister = async (config, res, body) => {
|
|
23
|
+
if (typeof body !== "object" || body === null) {
|
|
24
|
+
sendErrorJson(res, 400, "invalid_request", "Expected a JSON client metadata object.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const metadata = body;
|
|
28
|
+
const redirectUris = stringList(metadata["redirect_uris"]).filter(isUsableRedirectUri);
|
|
29
|
+
if (redirectUris.length === 0) {
|
|
30
|
+
sendErrorJson(res, 400, "invalid_request", "redirect_uris must list at least one https (or loopback) URI.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const clientName = typeof metadata["client_name"] === "string" ? metadata["client_name"] : "MCP client";
|
|
34
|
+
const { store } = config.adapters;
|
|
35
|
+
const fingerprint = fingerprintOf(clientName, redirectUris);
|
|
36
|
+
const client = await getClientByFingerprint(store, fingerprint) ?? {
|
|
37
|
+
clientId: randomId(),
|
|
38
|
+
clientName,
|
|
39
|
+
redirectUris,
|
|
40
|
+
issuedAt: nowSeconds()
|
|
41
|
+
};
|
|
42
|
+
await putClient(store, client);
|
|
43
|
+
await putClientFingerprint(store, fingerprint, client.clientId);
|
|
44
|
+
sendJson(res, 201, {
|
|
45
|
+
client_id: client.clientId,
|
|
46
|
+
client_id_issued_at: client.issuedAt,
|
|
47
|
+
client_name: clientName,
|
|
48
|
+
redirect_uris: redirectUris,
|
|
49
|
+
grant_types: config.refreshTtlSeconds === 0 ? ["authorization_code"] : ["authorization_code", "refresh_token"],
|
|
50
|
+
response_types: ["code"],
|
|
51
|
+
token_endpoint_auth_method: "none"
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { handleRegister };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
2
|
+
/** The error codes RFC 6749 defines for the responses this server produces. */
|
|
3
|
+
export type OAuthErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'unsupported_response_type' | 'invalid_scope' | 'access_denied' | 'server_error';
|
|
4
|
+
export declare const sendJson: (res: SSRResponseHelpers, status: number, body: unknown) => void;
|
|
5
|
+
export declare const sendHtml: (res: SSRResponseHelpers, status: number, html: string) => void;
|
|
6
|
+
export declare const sendErrorJson: (res: SSRResponseHelpers, status: number, error: OAuthErrorCode, description: string) => void;
|
|
7
|
+
/** Shown when the authorization request itself is unusable — an unknown client, a `redirect_uri` that is not
|
|
8
|
+
* registered. Redirecting those back would turn the endpoint into an open redirector. */
|
|
9
|
+
export declare const sendErrorPage: (res: SSRResponseHelpers, title: string, detail: string) => void;
|
|
10
|
+
/** The other half of RFC 6749 error handling: once the redirect target is known to be legitimate, failures go
|
|
11
|
+
* back to the client so the host can report them, carrying `state` so it can match the response to its request. */
|
|
12
|
+
export declare const redirectWithError: (res: SSRResponseHelpers, redirectUri: string, error: OAuthErrorCode, description: string, state?: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Send the browser somewhere else entirely, carrying where to come back to.
|
|
15
|
+
*
|
|
16
|
+
* Used for the hand-off to the sign-in screen. The destination is passed as `redirect` — the same param the auth
|
|
17
|
+
* space reads — and it is the WHOLE authorization request, so resuming is an ordinary navigation and this server
|
|
18
|
+
* keeps no session of its own for a flow that is one page long.
|
|
19
|
+
*/
|
|
20
|
+
export declare const redirectToSignIn: (res: SSRResponseHelpers, signInUrl: string, returnTo: string) => void;
|
|
21
|
+
export declare const redirectWithCode: (res: SSRResponseHelpers, redirectUri: string, code: string, state?: string) => void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { renderErrorPage } from "./consentPage.js";
|
|
2
|
+
//#region src/core/oauth/respond.ts
|
|
3
|
+
var sendJson = (res, status, body) => {
|
|
4
|
+
res.setStatus(status);
|
|
5
|
+
res.setHeader("Content-Type", "application/json");
|
|
6
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
7
|
+
res.send(JSON.stringify(body));
|
|
8
|
+
};
|
|
9
|
+
var sendHtml = (res, status, html) => {
|
|
10
|
+
res.setStatus(status);
|
|
11
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
12
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
13
|
+
res.send(html);
|
|
14
|
+
};
|
|
15
|
+
var sendErrorJson = (res, status, error, description) => sendJson(res, status, {
|
|
16
|
+
error,
|
|
17
|
+
error_description: description
|
|
18
|
+
});
|
|
19
|
+
/** Shown when the authorization request itself is unusable — an unknown client, a `redirect_uri` that is not
|
|
20
|
+
* registered. Redirecting those back would turn the endpoint into an open redirector. */
|
|
21
|
+
var sendErrorPage = (res, title, detail) => sendHtml(res, 400, renderErrorPage(title, detail));
|
|
22
|
+
/** The other half of RFC 6749 error handling: once the redirect target is known to be legitimate, failures go
|
|
23
|
+
* back to the client so the host can report them, carrying `state` so it can match the response to its request. */
|
|
24
|
+
var redirectWithError = (res, redirectUri, error, description, state) => {
|
|
25
|
+
const url = new URL(redirectUri);
|
|
26
|
+
url.searchParams.set("error", error);
|
|
27
|
+
url.searchParams.set("error_description", description);
|
|
28
|
+
if (state !== void 0) url.searchParams.set("state", state);
|
|
29
|
+
res.setStatus(302);
|
|
30
|
+
res.setHeader("Location", url.toString());
|
|
31
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
32
|
+
res.end();
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Send the browser somewhere else entirely, carrying where to come back to.
|
|
36
|
+
*
|
|
37
|
+
* Used for the hand-off to the sign-in screen. The destination is passed as `redirect` — the same param the auth
|
|
38
|
+
* space reads — and it is the WHOLE authorization request, so resuming is an ordinary navigation and this server
|
|
39
|
+
* keeps no session of its own for a flow that is one page long.
|
|
40
|
+
*/
|
|
41
|
+
var redirectToSignIn = (res, signInUrl, returnTo) => {
|
|
42
|
+
const url = new URL(signInUrl);
|
|
43
|
+
url.searchParams.set("redirect", returnTo);
|
|
44
|
+
res.setStatus(302);
|
|
45
|
+
res.setHeader("Location", url.toString());
|
|
46
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
47
|
+
res.end();
|
|
48
|
+
};
|
|
49
|
+
var redirectWithCode = (res, redirectUri, code, state) => {
|
|
50
|
+
const url = new URL(redirectUri);
|
|
51
|
+
url.searchParams.set("code", code);
|
|
52
|
+
if (state !== void 0) url.searchParams.set("state", state);
|
|
53
|
+
res.setStatus(302);
|
|
54
|
+
res.setHeader("Location", url.toString());
|
|
55
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
56
|
+
res.end();
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { redirectToSignIn, redirectWithCode, redirectWithError, sendErrorJson, sendErrorPage, sendHtml, sendJson };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OAuthParams } from './params';
|
|
2
|
+
import { OAuthConfig, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
3
|
+
/**
|
|
4
|
+
* POST /revoke — RFC 7009. Ends a grant, which is what signing out of a native client has to do.
|
|
5
|
+
*
|
|
6
|
+
* Without it, signing out ends the SESSION and leaves the refresh token that can mint another one: the app has
|
|
7
|
+
* deleted its copy, but a leaked one stays good until its TTL. The endpoint answers 200 whatever happens, as the
|
|
8
|
+
* RFC requires — a client cannot be told whether a token it presented was real, and a sign-out that reports a
|
|
9
|
+
* failure is one people ignore.
|
|
10
|
+
*/
|
|
11
|
+
export declare const handleRevoke: (config: OAuthConfig, res: SSRResponseHelpers, params: OAuthParams) => Promise<void>;
|
|
12
|
+
/** POST /token. Public clients only, so there is no client authentication to check — PKCE is what proves the
|
|
13
|
+
* caller is the one that started the flow. */
|
|
14
|
+
export declare const handleToken: (config: OAuthConfig, res: SSRResponseHelpers, params: OAuthParams) => Promise<void>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { scopesOf } from "./metadata.js";
|
|
2
|
+
import { field, optionalField } from "./params.js";
|
|
3
|
+
import { randomId, verifyChallenge } from "./pkce.js";
|
|
4
|
+
import { dropAccess, dropCode, dropRefresh, getCode, getRefresh, putAccess, putRefresh } from "./records.js";
|
|
5
|
+
import { sendErrorJson, sendJson } from "./respond.js";
|
|
6
|
+
//#region src/core/oauth/token.ts
|
|
7
|
+
var DEFAULT_REFRESH_TTL_SECONDS = 2592e3;
|
|
8
|
+
var DEFAULT_ACCESS_TTL_SECONDS = 2592e3;
|
|
9
|
+
var refreshTtlOf = (config) => config.refreshTtlSeconds ?? DEFAULT_REFRESH_TTL_SECONDS;
|
|
10
|
+
var scopeOf = (config, requested) => requested ?? scopesOf(config).join(" ");
|
|
11
|
+
/** The token response, plus a rotated refresh grant when refresh is enabled. Rotation is unconditional: a refresh
|
|
12
|
+
* token is a long-lived credential, so the one just used never stays valid. */
|
|
13
|
+
var sendTokens = async (config, res, credential, expiresInSeconds, grant) => {
|
|
14
|
+
const ttl = refreshTtlOf(config);
|
|
15
|
+
/**
|
|
16
|
+
* What the client actually receives, and the two answers are a real policy choice.
|
|
17
|
+
*
|
|
18
|
+
* By default it is a HANDLE to the grant, not the credential behind it: what `issueToken` minted is usually good
|
|
19
|
+
* against more of the platform than this endpoint, so it stays on this side of the boundary and the resource
|
|
20
|
+
* side swaps it back. That is right for a connector holding a space token.
|
|
21
|
+
*
|
|
22
|
+
* `directTokens` gives the client the credential itself, for the case where the credential is exactly what the
|
|
23
|
+
* client should hold — a native app granted the person's own SESSION. There the handle buys nothing and costs:
|
|
24
|
+
* a second thing to revoke, and a store read on every request to an API that already knows how to read a
|
|
25
|
+
* session. Either way an access record is written, because it is also what lets the resource side recognise a
|
|
26
|
+
* bearer this server issued.
|
|
27
|
+
*/
|
|
28
|
+
const bearer = config.directTokens ? credential : randomId();
|
|
29
|
+
await putAccess(config.adapters.store, bearer, {
|
|
30
|
+
credential,
|
|
31
|
+
clientId: grant.clientId,
|
|
32
|
+
user: grant.user,
|
|
33
|
+
target: grant.target
|
|
34
|
+
}, Math.max(expiresInSeconds ?? DEFAULT_ACCESS_TTL_SECONDS, 1));
|
|
35
|
+
const body = {
|
|
36
|
+
access_token: bearer,
|
|
37
|
+
token_type: "Bearer",
|
|
38
|
+
scope: scopeOf(config, grant.scope)
|
|
39
|
+
};
|
|
40
|
+
if (expiresInSeconds !== void 0) body["expires_in"] = expiresInSeconds;
|
|
41
|
+
if (ttl > 0) {
|
|
42
|
+
const refreshToken = randomId();
|
|
43
|
+
await putRefresh(config.adapters.store, refreshToken, grant, ttl);
|
|
44
|
+
body["refresh_token"] = refreshToken;
|
|
45
|
+
}
|
|
46
|
+
sendJson(res, 200, body);
|
|
47
|
+
};
|
|
48
|
+
var exchangeCode = async (config, res, params) => {
|
|
49
|
+
const code = field(params, "code");
|
|
50
|
+
const record = code ? await getCode(config.adapters.store, code) : void 0;
|
|
51
|
+
if (!record) {
|
|
52
|
+
sendErrorJson(res, 400, "invalid_grant", "The authorization code is unknown or has expired.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await dropCode(config.adapters.store, code);
|
|
56
|
+
if (record.clientId !== field(params, "client_id")) {
|
|
57
|
+
sendErrorJson(res, 400, "invalid_grant", "The authorization code was issued to another client.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const redirectUri = optionalField(params, "redirect_uri");
|
|
61
|
+
if (redirectUri !== void 0 && redirectUri !== record.redirectUri) {
|
|
62
|
+
sendErrorJson(res, 400, "invalid_grant", "redirect_uri does not match the authorization request.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!verifyChallenge(field(params, "code_verifier"), record.challenge)) {
|
|
66
|
+
sendErrorJson(res, 400, "invalid_grant", "The PKCE code verifier does not match the challenge.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await sendTokens(config, res, record.token, record.expiresInSeconds, {
|
|
70
|
+
clientId: record.clientId,
|
|
71
|
+
scope: record.scope,
|
|
72
|
+
user: record.user,
|
|
73
|
+
target: record.target
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
var exchangeRefresh = async (config, res, params) => {
|
|
77
|
+
if (refreshTtlOf(config) === 0) {
|
|
78
|
+
sendErrorJson(res, 400, "unsupported_grant_type", "This server issues no refresh tokens.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const refreshToken = field(params, "refresh_token");
|
|
82
|
+
const record = refreshToken ? await getRefresh(config.adapters.store, refreshToken) : void 0;
|
|
83
|
+
if (!record) {
|
|
84
|
+
sendErrorJson(res, 400, "invalid_grant", "The refresh token is unknown or has expired.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await dropRefresh(config.adapters.store, refreshToken);
|
|
88
|
+
if (record.clientId !== field(params, "client_id")) {
|
|
89
|
+
sendErrorJson(res, 400, "invalid_grant", "The refresh token was issued to another client.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const issued = await config.adapters.issueToken(record.user, record.target);
|
|
93
|
+
if (!issued) {
|
|
94
|
+
sendErrorJson(res, 400, "invalid_grant", "The account no longer has access to this resource.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await sendTokens(config, res, issued.token, issued.expiresInSeconds, record);
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* POST /revoke — RFC 7009. Ends a grant, which is what signing out of a native client has to do.
|
|
101
|
+
*
|
|
102
|
+
* Without it, signing out ends the SESSION and leaves the refresh token that can mint another one: the app has
|
|
103
|
+
* deleted its copy, but a leaked one stays good until its TTL. The endpoint answers 200 whatever happens, as the
|
|
104
|
+
* RFC requires — a client cannot be told whether a token it presented was real, and a sign-out that reports a
|
|
105
|
+
* failure is one people ignore.
|
|
106
|
+
*/
|
|
107
|
+
var handleRevoke = async (config, res, params) => {
|
|
108
|
+
const token = optionalField(params, "token");
|
|
109
|
+
if (token) {
|
|
110
|
+
await dropRefresh(config.adapters.store, token);
|
|
111
|
+
await dropAccess(config.adapters.store, token);
|
|
112
|
+
}
|
|
113
|
+
res.setStatus(200);
|
|
114
|
+
res.end();
|
|
115
|
+
};
|
|
116
|
+
/** POST /token. Public clients only, so there is no client authentication to check — PKCE is what proves the
|
|
117
|
+
* caller is the one that started the flow. */
|
|
118
|
+
var handleToken = async (config, res, params) => {
|
|
119
|
+
switch (field(params, "grant_type")) {
|
|
120
|
+
case "authorization_code":
|
|
121
|
+
await exchangeCode(config, res, params);
|
|
122
|
+
return;
|
|
123
|
+
case "refresh_token":
|
|
124
|
+
await exchangeRefresh(config, res, params);
|
|
125
|
+
return;
|
|
126
|
+
default: sendErrorJson(res, 400, "unsupported_grant_type", "Supported grants: authorization_code, refresh_token.");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
export { handleRevoke, handleToken };
|
|
@@ -1 +1,21 @@
|
|
|
1
|
+
import { SSRRequest } from '@plitzi/sdk-shared';
|
|
1
2
|
export declare const PREVIEW_TOKEN_PARAM = "__pt";
|
|
3
|
+
/**
|
|
4
|
+
* Where a REUSABLE draft token is remembered for the rest of the visit.
|
|
5
|
+
*
|
|
6
|
+
* The query param alone previews one URL, which is all a screenshot ever needed. A person iterating does more than
|
|
7
|
+
* look at one URL: they follow a link, the SDK asks `/_rsc` for fresh data, they press back. Every one of those is a
|
|
8
|
+
* request without the param, and answering the saved space to a browser that is looking at a draft is the kind of
|
|
9
|
+
* confusion that gets reported as "the preview randomly reverts".
|
|
10
|
+
*
|
|
11
|
+
* `HttpOnly` because nothing in the page has any use for it, and `SameSite=Lax` so another site cannot cause a draft
|
|
12
|
+
* to be resolved in a visitor's browser. It is set only for a token the store minted as reusable.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PREVIEW_COOKIE = "plitzi_draft";
|
|
15
|
+
/**
|
|
16
|
+
* The draft this request is looking at, whichever way it says so.
|
|
17
|
+
*
|
|
18
|
+
* The query param wins: it is how a session is STARTED, and re-minting while an older one is still in the cookie has
|
|
19
|
+
* to answer the new draft or the loop shows stale work.
|
|
20
|
+
*/
|
|
21
|
+
export declare const readDraftToken: (req: SSRRequest) => string | undefined;
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
//#region src/core/previewToken.ts
|
|
2
2
|
var PREVIEW_TOKEN_PARAM = "__pt";
|
|
3
|
+
/**
|
|
4
|
+
* Where a REUSABLE draft token is remembered for the rest of the visit.
|
|
5
|
+
*
|
|
6
|
+
* The query param alone previews one URL, which is all a screenshot ever needed. A person iterating does more than
|
|
7
|
+
* look at one URL: they follow a link, the SDK asks `/_rsc` for fresh data, they press back. Every one of those is a
|
|
8
|
+
* request without the param, and answering the saved space to a browser that is looking at a draft is the kind of
|
|
9
|
+
* confusion that gets reported as "the preview randomly reverts".
|
|
10
|
+
*
|
|
11
|
+
* `HttpOnly` because nothing in the page has any use for it, and `SameSite=Lax` so another site cannot cause a draft
|
|
12
|
+
* to be resolved in a visitor's browser. It is set only for a token the store minted as reusable.
|
|
13
|
+
*/
|
|
14
|
+
var PREVIEW_COOKIE = "plitzi_draft";
|
|
15
|
+
var cookieValue = (header, name) => {
|
|
16
|
+
if (!header) return;
|
|
17
|
+
for (const part of header.split(";")) {
|
|
18
|
+
const separator = part.indexOf("=");
|
|
19
|
+
if (separator > 0 && part.slice(0, separator).trim() === name) return decodeURIComponent(part.slice(separator + 1).trim());
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The draft this request is looking at, whichever way it says so.
|
|
24
|
+
*
|
|
25
|
+
* The query param wins: it is how a session is STARTED, and re-minting while an older one is still in the cookie has
|
|
26
|
+
* to answer the new draft or the loop shows stale work.
|
|
27
|
+
*/
|
|
28
|
+
var readDraftToken = (req) => {
|
|
29
|
+
return req.query["__pt"] || cookieValue(req.headers.cookie, "plitzi_draft");
|
|
30
|
+
};
|
|
3
31
|
//#endregion
|
|
4
|
-
export { PREVIEW_TOKEN_PARAM };
|
|
32
|
+
export { PREVIEW_COOKIE, PREVIEW_TOKEN_PARAM, readDraftToken };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { actionsModuleFor } from "../../modules/actions/moduleFor.js";
|
|
1
2
|
import { createHttpServer } from "./baseServer.js";
|
|
2
3
|
import { DEFAULT_TTL_MS } from "../../helpers/cache/defaults.js";
|
|
3
4
|
import { buildCacheManager } from "../../helpers/cache/cacheManager.js";
|
|
@@ -30,6 +31,7 @@ var createPageServer = (config, services, extensions) => {
|
|
|
30
31
|
invalidatePluginComponentCache();
|
|
31
32
|
}
|
|
32
33
|
};
|
|
34
|
+
const actions = actionsModuleFor(config);
|
|
33
35
|
const stages = buildPagePipeline(services, extensions);
|
|
34
36
|
const makeHandlerForPort = (port) => {
|
|
35
37
|
const buildContext = (raw, rawRes, req, res) => ({
|
|
@@ -41,7 +43,8 @@ var createPageServer = (config, services, extensions) => {
|
|
|
41
43
|
port,
|
|
42
44
|
renderFn,
|
|
43
45
|
caches,
|
|
44
|
-
pluginManager
|
|
46
|
+
pluginManager,
|
|
47
|
+
actions
|
|
45
48
|
});
|
|
46
49
|
return makeHandler("SSR", buildContext, stages, config.compression);
|
|
47
50
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { SSRContext, Stage } from '../http/types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* connected backend — and it answers POST on its own path alone, never falling through to SSR.
|
|
3
|
+
* The action endpoint: server-driven provider writes, and server actions.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Two addressing modes on one path, because both are "the browser asks the server to change something, naming it
|
|
6
|
+
* rather than addressing it". An element-addressed body reaches a connector's declared write; an action-addressed
|
|
7
|
+
* one runs a stored flow. Each mode is inert unless its own half of the config is present, so a deployment that
|
|
8
|
+
* configures neither has no endpoint at all — and guessing what an unconfigured one should do would be the whole
|
|
9
|
+
* vulnerability.
|
|
8
10
|
*/
|
|
9
11
|
export declare const actionStage: Stage<SSRContext>;
|