@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.
Files changed (274) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +283 -8
  3. package/dist/actions.d.ts +62 -0
  4. package/dist/actions.js +12 -0
  5. package/dist/adapters/cloudAdapters.d.ts +72 -0
  6. package/dist/adapters/cloudAdapters.js +303 -0
  7. package/dist/adapters/space/index.d.ts +93 -0
  8. package/dist/adapters/space/index.js +122 -0
  9. package/dist/adapters/space/resolvers.d.ts +69 -0
  10. package/dist/adapters/space/resolvers.js +79 -0
  11. package/dist/adapters/space/space.test.d.ts +1 -0
  12. package/dist/adapters/space/types.d.ts +64 -0
  13. package/dist/adapters/space/types.js +41 -0
  14. package/dist/auth.d.ts +50 -0
  15. package/dist/auth.js +19 -0
  16. package/dist/core/auth/api.d.ts +400 -11
  17. package/dist/core/auth/api.js +1044 -50
  18. package/dist/core/auth/createAuth.d.ts +135 -30
  19. package/dist/core/auth/createAuth.js +96 -24
  20. package/dist/core/auth/credentials.d.ts +9 -3
  21. package/dist/core/auth/credentials.js +10 -4
  22. package/dist/core/auth/csrf.d.ts +89 -0
  23. package/dist/core/auth/csrf.js +234 -0
  24. package/dist/core/auth/csrf.test.d.ts +1 -0
  25. package/dist/core/auth/identity.d.ts +6 -0
  26. package/dist/core/auth/identity.js +37 -8
  27. package/dist/core/auth/lifecycle.test.d.ts +1 -0
  28. package/dist/core/auth/oauth/index.d.ts +8 -0
  29. package/dist/core/auth/oauth/index.js +13 -15
  30. package/dist/core/auth/passwords.d.ts +15 -0
  31. package/dist/core/auth/passwords.js +32 -0
  32. package/dist/core/auth/redirects.d.ts +18 -0
  33. package/dist/core/auth/redirects.js +31 -0
  34. package/dist/core/auth/redirects.test.d.ts +1 -0
  35. package/dist/core/auth/routes.d.ts +4 -2
  36. package/dist/core/auth/routes.js +234 -9
  37. package/dist/core/auth/session.d.ts +11 -0
  38. package/dist/core/auth/session.js +1 -1
  39. package/dist/core/auth/spaceTokens.d.ts +38 -2
  40. package/dist/core/auth/spaceTokens.js +54 -2
  41. package/dist/core/auth/throttle.d.ts +5 -0
  42. package/dist/core/auth/throttle.js +81 -0
  43. package/dist/core/auth/throttle.test.d.ts +1 -0
  44. package/dist/core/auth/tokens.d.ts +83 -12
  45. package/dist/core/auth/tokens.js +51 -9
  46. package/dist/core/auth/totp.d.ts +48 -0
  47. package/dist/core/auth/totp.js +118 -0
  48. package/dist/core/auth/totp.test.d.ts +1 -0
  49. package/dist/core/createServer.d.ts +0 -6
  50. package/dist/core/createServer.js +35 -3
  51. package/dist/core/fontStore.d.ts +33 -0
  52. package/dist/core/fontStore.js +53 -0
  53. package/dist/core/fontStore.test.d.ts +1 -0
  54. package/dist/core/handlers/authMiddleware.js +1 -1
  55. package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
  56. package/dist/core/handlers/authRouteHandlers.js +4 -3
  57. package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
  58. package/dist/core/handlers/csrfMiddleware.js +32 -0
  59. package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
  60. package/dist/core/handlers/oauthRouteHandlers.js +159 -0
  61. package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
  62. package/dist/core/handlers/socialRouteHandlers.js +129 -0
  63. package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
  64. package/dist/core/handlers/types.d.ts +22 -5
  65. package/dist/core/health.d.ts +11 -1
  66. package/dist/core/health.js +27 -3
  67. package/dist/core/http/dispatcher.d.ts +1 -1
  68. package/dist/core/http/dispatcher.js +20 -1
  69. package/dist/core/http/stages/authApi.js +1 -1
  70. package/dist/core/http/stages/fontAssets.d.ts +9 -0
  71. package/dist/core/http/stages/fontAssets.js +27 -0
  72. package/dist/core/http/stages/oauth.d.ts +19 -0
  73. package/dist/core/http/stages/oauth.js +118 -0
  74. package/dist/core/http/types.d.ts +12 -0
  75. package/dist/core/oauth/authorize.d.ts +22 -0
  76. package/dist/core/oauth/authorize.js +254 -0
  77. package/dist/core/oauth/authorize.test.d.ts +1 -0
  78. package/dist/core/oauth/challenge.d.ts +11 -0
  79. package/dist/core/oauth/challenge.js +33 -0
  80. package/dist/core/oauth/consentPage.d.ts +12 -0
  81. package/dist/core/oauth/consentPage.js +131 -0
  82. package/dist/core/oauth/metadata.d.ts +41 -0
  83. package/dist/core/oauth/metadata.js +71 -0
  84. package/dist/core/oauth/params.d.ts +5 -0
  85. package/dist/core/oauth/params.js +5 -0
  86. package/dist/core/oauth/pkce.d.ts +6 -0
  87. package/dist/core/oauth/pkce.js +15 -0
  88. package/dist/core/oauth/records.d.ts +72 -0
  89. package/dist/core/oauth/records.js +49 -0
  90. package/dist/core/oauth/register.d.ts +5 -0
  91. package/dist/core/oauth/register.js +55 -0
  92. package/dist/core/oauth/respond.d.ts +21 -0
  93. package/dist/core/oauth/respond.js +59 -0
  94. package/dist/core/oauth/token.d.ts +14 -0
  95. package/dist/core/oauth/token.js +130 -0
  96. package/dist/core/previewToken.d.ts +20 -0
  97. package/dist/core/previewToken.js +29 -1
  98. package/dist/core/server/pageServer.js +4 -1
  99. package/dist/core/services/action.d.ts +6 -4
  100. package/dist/core/services/action.js +103 -10
  101. package/dist/core/services/registry.js +2 -0
  102. package/dist/core/services/resolve.d.ts +8 -0
  103. package/dist/core/services/resolve.js +12 -1
  104. package/dist/core/staticFiles.js +1 -2
  105. package/dist/handlers.d.ts +7 -1
  106. package/dist/handlers.js +4 -1
  107. package/dist/helpers/buildResponseHelpers.d.ts +8 -0
  108. package/dist/helpers/buildResponseHelpers.js +9 -1
  109. package/dist/helpers/buildServerInfo.d.ts +8 -3
  110. package/dist/helpers/buildServerInfo.js +18 -14
  111. package/dist/helpers/cache/keys.d.ts +13 -0
  112. package/dist/helpers/cache/keys.js +12 -1
  113. package/dist/helpers/onAbort.d.ts +12 -0
  114. package/dist/helpers/onAbort.js +22 -0
  115. package/dist/helpers/onAbort.test.d.ts +1 -0
  116. package/dist/helpers/outboundGuard.d.ts +27 -0
  117. package/dist/helpers/outboundGuard.js +49 -0
  118. package/dist/helpers/outboundGuard.test.d.ts +1 -0
  119. package/dist/helpers/serverLog.d.ts +6 -3
  120. package/dist/helpers/serverLog.js +20 -3
  121. package/dist/index.d.ts +6 -0
  122. package/dist/index.js +9 -4
  123. package/dist/kernel.d.ts +5 -22
  124. package/dist/kernel.js +1 -14
  125. package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
  126. package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
  127. package/dist/modules/actions/index.d.ts +40 -0
  128. package/dist/modules/actions/index.js +46 -0
  129. package/dist/modules/actions/moduleFor.d.ts +3 -0
  130. package/dist/modules/actions/moduleFor.js +35 -0
  131. package/dist/modules/actions/moduleFor.test.d.ts +1 -0
  132. package/dist/modules/actions/runtime/check.d.ts +24 -0
  133. package/dist/modules/actions/runtime/check.js +111 -0
  134. package/dist/modules/actions/runtime/check.test.d.ts +1 -0
  135. package/dist/modules/actions/runtime/errors.d.ts +11 -0
  136. package/dist/modules/actions/runtime/errors.js +17 -0
  137. package/dist/modules/actions/runtime/guards.d.ts +126 -0
  138. package/dist/modules/actions/runtime/guards.js +246 -0
  139. package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
  140. package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
  141. package/dist/modules/actions/runtime/kvStore.js +41 -0
  142. package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
  143. package/dist/modules/actions/runtime/limits.d.ts +11 -0
  144. package/dist/modules/actions/runtime/limits.js +35 -0
  145. package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
  146. package/dist/modules/actions/runtime/memoryKv.js +77 -0
  147. package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
  148. package/dist/modules/actions/runtime/namespaceKv.js +19 -0
  149. package/dist/modules/actions/runtime/precheck.d.ts +27 -0
  150. package/dist/modules/actions/runtime/precheck.js +45 -0
  151. package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
  152. package/dist/modules/actions/runtime/renderResolver.js +130 -0
  153. package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
  154. package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
  155. package/dist/modules/actions/runtime/renderShare.js +36 -0
  156. package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
  157. package/dist/modules/actions/runtime/report.d.ts +14 -0
  158. package/dist/modules/actions/runtime/report.js +22 -0
  159. package/dist/modules/actions/runtime/runAction.d.ts +12 -0
  160. package/dist/modules/actions/runtime/runAction.js +405 -0
  161. package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
  162. package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
  163. package/dist/modules/actions/runtime/runLogger.js +66 -0
  164. package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
  165. package/dist/modules/actions/runtime/schedule.d.ts +35 -0
  166. package/dist/modules/actions/runtime/schedule.js +76 -0
  167. package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
  168. package/dist/modules/actions/runtime/scope.d.ts +40 -0
  169. package/dist/modules/actions/runtime/scope.js +103 -0
  170. package/dist/modules/actions/runtime/triggers.d.ts +12 -0
  171. package/dist/modules/actions/runtime/triggers.js +14 -0
  172. package/dist/modules/actions/taskCatalog.d.ts +26 -0
  173. package/dist/modules/actions/taskCatalog.js +24 -0
  174. package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
  175. package/dist/modules/actions/tasks/auth.d.ts +2 -0
  176. package/dist/modules/actions/tasks/auth.js +40 -0
  177. package/dist/modules/actions/tasks/builtins.d.ts +8 -0
  178. package/dist/modules/actions/tasks/builtins.js +24 -0
  179. package/dist/modules/actions/tasks/connector.d.ts +2 -0
  180. package/dist/modules/actions/tasks/connector.js +141 -0
  181. package/dist/modules/actions/tasks/db.d.ts +2 -0
  182. package/dist/modules/actions/tasks/db.js +59 -0
  183. package/dist/modules/actions/tasks/db.test.d.ts +1 -0
  184. package/dist/modules/actions/tasks/flow.d.ts +3 -0
  185. package/dist/modules/actions/tasks/flow.js +109 -0
  186. package/dist/modules/actions/tasks/helpers.d.ts +16 -0
  187. package/dist/modules/actions/tasks/helpers.js +30 -0
  188. package/dist/modules/actions/tasks/http.d.ts +2 -0
  189. package/dist/modules/actions/tasks/http.js +111 -0
  190. package/dist/modules/actions/tasks/http.test.d.ts +1 -0
  191. package/dist/modules/actions/tasks/kv.d.ts +2 -0
  192. package/dist/modules/actions/tasks/kv.js +102 -0
  193. package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
  194. package/dist/modules/actions/tasks/registry.d.ts +10 -0
  195. package/dist/modules/actions/tasks/registry.js +36 -0
  196. package/dist/modules/actions/tasks/transform.d.ts +2 -0
  197. package/dist/modules/actions/tasks/transform.js +34 -0
  198. package/dist/modules/actions/transport/callHandler.d.ts +24 -0
  199. package/dist/modules/actions/transport/callHandler.js +233 -0
  200. package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
  201. package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
  202. package/dist/modules/actions/transport/cancelHandler.js +23 -0
  203. package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
  204. package/dist/modules/actions/transport/catalogHandler.js +21 -0
  205. package/dist/modules/actions/transport/stream.d.ts +23 -0
  206. package/dist/modules/actions/transport/stream.js +100 -0
  207. package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
  208. package/dist/modules/actions/transport/verifySignature.js +72 -0
  209. package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
  210. package/dist/modules/actions/transport/webhookHandler.js +206 -0
  211. package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
  212. package/dist/modules/actions/types.d.ts +234 -0
  213. package/dist/modules/connectors/engine.js +4 -1
  214. package/dist/modules/connectors/resolver.d.ts +10 -2
  215. package/dist/modules/connectors/resolver.js +8 -3
  216. package/dist/modules/mysql/accounts.d.ts +5 -0
  217. package/dist/modules/mysql/accounts.js +365 -0
  218. package/dist/modules/mysql/accounts.test.d.ts +1 -0
  219. package/dist/modules/mysql/admin.d.ts +58 -0
  220. package/dist/modules/mysql/admin.js +133 -0
  221. package/dist/modules/mysql/config.d.ts +82 -0
  222. package/dist/modules/mysql/config.js +30 -0
  223. package/dist/modules/mysql/identities.d.ts +35 -0
  224. package/dist/modules/mysql/identities.js +43 -0
  225. package/dist/modules/mysql/index.d.ts +67 -0
  226. package/dist/modules/mysql/index.js +62 -0
  227. package/dist/modules/mysql/pool.d.ts +3 -0
  228. package/dist/modules/mysql/pool.js +88 -0
  229. package/dist/modules/mysql/query.d.ts +12 -0
  230. package/dist/modules/mysql/query.js +21 -0
  231. package/dist/modules/mysql/schema.d.ts +43 -0
  232. package/dist/modules/mysql/schema.js +340 -0
  233. package/dist/modules/mysql/schema.test.d.ts +1 -0
  234. package/dist/modules/mysql/sessions.d.ts +33 -0
  235. package/dist/modules/mysql/sessions.js +46 -0
  236. package/dist/modules/mysql/spaceTokens.d.ts +11 -0
  237. package/dist/modules/mysql/spaceTokens.js +59 -0
  238. package/dist/modules/mysql/store.integration.test.d.ts +1 -0
  239. package/dist/modules/rsc/connectorRscData.d.ts +25 -0
  240. package/dist/modules/rsc/connectorRscData.js +51 -0
  241. package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
  242. package/dist/modules/rsc/handler.js +16 -3
  243. package/dist/modules/rsc/matchRscPage.d.ts +14 -0
  244. package/dist/modules/rsc/matchRscPage.js +26 -0
  245. package/dist/modules/rsc/resolveRscData.d.ts +14 -1
  246. package/dist/modules/rsc/resolveRscData.js +23 -34
  247. package/dist/modules/ssr/Component.d.ts +6 -2
  248. package/dist/modules/ssr/Component.js +3 -1
  249. package/dist/modules/ssr/draftSession.test.d.ts +1 -0
  250. package/dist/modules/ssr/handler.js +15 -2
  251. package/dist/modules/ssr/loadPluginComponents.js +22 -5
  252. package/dist/modules/ssr/prepareRender.js +87 -8
  253. package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
  254. package/dist/modules/ssr/preview.d.ts +21 -8
  255. package/dist/modules/ssr/preview.js +38 -14
  256. package/dist/modules/ssr/registerExternalPlugins.js +2 -2
  257. package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
  258. package/dist/modules/ssr/resolvePageSeo.js +26 -0
  259. package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
  260. package/dist/modules/ssr/template.test.d.ts +1 -0
  261. package/dist/modules/ssr/views/template.ejs +71 -5
  262. package/dist/mysql.d.ts +26 -0
  263. package/dist/mysql.js +4 -0
  264. package/dist/oauth.d.ts +24 -0
  265. package/dist/oauth.js +10 -0
  266. package/dist/plugins/compile.d.ts +1 -0
  267. package/dist/plugins/compile.js +37 -3
  268. package/dist/plugins/manager.d.ts +40 -0
  269. package/dist/plugins/manager.js +113 -13
  270. package/dist/plugins/manager.test.d.ts +1 -0
  271. package/package.json +43 -14
  272. package/skills/plitzi-authoring/SKILL.md +252 -0
  273. /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
  274. /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,5 @@
1
+ //#region src/core/oauth/params.ts
2
+ var field = (params, name) => params[name] ?? "";
3
+ var optionalField = (params, name) => params[name] || void 0;
4
+ //#endregion
5
+ export { field, optionalField };
@@ -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
- * Write endpoint for server-driven providers. RSC is read-only, so this is the only path a page has to mutate a
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
- * Inert unless the consumer supplied the connector lookups: without them there is no manifest to authorize a
7
- * write against, and guessing would be the whole vulnerability.
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>;