@plitzi/sdk-server 0.33.2 → 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 +44 -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
@@ -1,3 +1,4 @@
1
+ import { csrfFailureMessage } from "./csrf.js";
1
2
  //#region src/core/auth/routes.ts
2
3
  var body = (req) => req.body ?? {};
3
4
  /** A body field as a string, whatever arrived. Handlers validate what they need; this only stops `[object Object]`. */
@@ -5,6 +6,32 @@ var field = (req, name) => {
5
6
  const value = body(req)[name];
6
7
  return typeof value === "string" ? value : "";
7
8
  };
9
+ /** A body field as a whole number. `NaN` for anything else, which every handler refuses as a missing id. */
10
+ var numberField = (req, name) => {
11
+ const value = body(req)[name];
12
+ if (typeof value === "number") return value;
13
+ return typeof value === "string" ? Number.parseInt(value, 10) : NaN;
14
+ };
15
+ var queryText = (req, name) => {
16
+ const value = req.query?.[name];
17
+ return typeof value === "string" && value ? value : void 0;
18
+ };
19
+ var queryNumber = (req, name) => {
20
+ const parsed = Number.parseInt(queryText(req, name) ?? "", 10);
21
+ return Number.isFinite(parsed) ? parsed : void 0;
22
+ };
23
+ /**
24
+ * Whatever arrived as `roles`, unexamined.
25
+ *
26
+ * Deliberately NOT coerced to a list here. It was, and a client sending `roles: "editor"` — the obvious mistake —
27
+ * got an empty list, which `setRoles` faithfully applied: every role removed, and a 200 saying it worked. The
28
+ * handler validates instead, so the wrong shape is a 400.
29
+ */
30
+ var rolesOf = (req) => body(req).roles;
31
+ var SUBJECT_OF = {
32
+ signIn: "signIn",
33
+ grant: "delegated"
34
+ };
8
35
  /**
9
36
  * The whole `/auth` surface as data: which method and path each flow answers on, what a caller must present, and what
10
37
  * to call.
@@ -24,6 +51,17 @@ var FLOWS = [
24
51
  requirement: "public",
25
52
  run: (api) => api.describe()
26
53
  },
54
+ (
55
+ /**
56
+ * A token to write with, for a page that has none. Public because signing IN needs one: without a token before
57
+ * anybody is authenticated, another site can log a visitor into an account it controls.
58
+ */
59
+ {
60
+ method: "GET",
61
+ path: "/csrf",
62
+ requirement: "public",
63
+ run: (api, cookies, req) => api.issueCsrf(cookies.resolveSessionToken(req))
64
+ }),
27
65
  {
28
66
  method: "GET",
29
67
  path: "/session",
@@ -34,18 +72,21 @@ var FLOWS = [
34
72
  method: "POST",
35
73
  path: "/login",
36
74
  requirement: "public",
37
- run: (api, _cookies, req) => api.login(body(req))
75
+ csrf: "signIn",
76
+ run: (api, _cookies, req) => api.login(body(req), req)
38
77
  },
39
78
  {
40
79
  method: "POST",
41
80
  path: "/refresh",
42
81
  requirement: "public",
43
- run: (api, cookies, req) => api.refresh(cookies.resolveRefreshToken(req, req.body))
82
+ csrf: "none",
83
+ run: (api, cookies, req) => api.refresh(cookies.resolveRefreshToken(req, req.body), req)
44
84
  },
45
85
  {
46
86
  method: "POST",
47
87
  path: "/logout",
48
88
  requirement: "public",
89
+ csrf: "none",
49
90
  run: (api, cookies, req) => api.logout({
50
91
  accessToken: cookies.resolveSessionToken(req),
51
92
  refreshToken: cookies.resolveRefreshToken(req, req.body)
@@ -61,44 +102,207 @@ var FLOWS = [
61
102
  method: "POST",
62
103
  path: "/exchange",
63
104
  requirement: "grant",
105
+ csrf: "grant",
64
106
  run: (api, _cookies, req) => api.exchange(field(req, "provider"), field(req, "token"), req)
65
107
  },
66
108
  {
67
109
  method: "POST",
68
110
  path: "/signup",
69
111
  requirement: "public",
112
+ csrf: "signIn",
70
113
  run: (api, _cookies, req) => api.signup(body(req))
71
114
  },
72
115
  {
73
116
  method: "POST",
74
117
  path: "/forgot-password",
75
118
  requirement: "public",
119
+ csrf: "signIn",
76
120
  run: (api, _cookies, req) => api.forgotPassword(field(req, "email"))
77
121
  },
78
122
  {
79
123
  method: "POST",
80
124
  path: "/reset-password",
81
125
  requirement: "public",
126
+ csrf: "signIn",
82
127
  run: (api, _cookies, req) => api.resetPassword(field(req, "token"), field(req, "password"))
83
128
  },
84
129
  {
85
130
  method: "POST",
86
131
  path: "/validate-account",
87
132
  requirement: "public",
133
+ csrf: "signIn",
88
134
  run: (api, _cookies, req) => api.validateAccount(field(req, "token"))
89
135
  },
136
+ {
137
+ /**
138
+ * Confirms an address change the account asked for. Public and `signIn`-guarded because the link lands in a
139
+ * mail client, and the browser that opens it may be one that has never seen this deployment.
140
+ */
141
+ method: "POST",
142
+ path: "/confirm-email",
143
+ requirement: "public",
144
+ csrf: "signIn",
145
+ run: (api, _cookies, req) => api.confirmEmailChange(field(req, "token"))
146
+ },
90
147
  {
91
148
  method: "POST",
92
149
  path: "/resend-verification-email",
93
150
  requirement: "public",
94
- run: (api, _cookies, req) => api.resendVerification(field(req, "email"))
151
+ csrf: "signIn",
152
+ run: (api, _cookies, req) => api.resendVerification(field(req, "email"), field(req, "redirect"))
153
+ },
154
+ {
155
+ method: "POST",
156
+ path: "/profile",
157
+ requirement: "actor",
158
+ run: (api, _cookies, req) => api.updateProfile(req.actor, body(req))
159
+ },
160
+ {
161
+ method: "POST",
162
+ path: "/password",
163
+ requirement: "actor",
164
+ run: (api, _cookies, req) => api.changePassword(req.actor, field(req, "currentPassword"), field(req, "password"))
165
+ },
166
+ {
167
+ method: "POST",
168
+ path: "/delete-account",
169
+ requirement: "actor",
170
+ run: (api, _cookies, req) => api.deleteSelf(req.actor, field(req, "password"))
171
+ },
172
+ {
173
+ method: "GET",
174
+ path: "/sessions",
175
+ requirement: "actor",
176
+ run: (api, _cookies, req) => api.listSessions(req.actor)
177
+ },
178
+ {
179
+ method: "POST",
180
+ path: "/sessions/revoke-one",
181
+ requirement: "actor",
182
+ run: (api, _cookies, req) => api.revokeSession(req.actor, numberField(req, "sessionId"))
183
+ },
184
+ {
185
+ method: "POST",
186
+ path: "/sessions/revoke-others",
187
+ requirement: "actor",
188
+ run: (api, _cookies, req) => api.revokeOtherSessions(req.actor)
189
+ },
190
+ {
191
+ method: "POST",
192
+ path: "/passwordless/request",
193
+ requirement: "public",
194
+ csrf: "signIn",
195
+ run: (api, _cookies, req) => api.passwordless.request(field(req, "email"), req)
196
+ },
197
+ {
198
+ method: "POST",
199
+ path: "/passwordless/complete",
200
+ requirement: "public",
201
+ csrf: "signIn",
202
+ run: (api, _cookies, req) => api.passwordless.complete(field(req, "email"), field(req, "code"), req)
203
+ },
204
+ {
205
+ method: "POST",
206
+ path: "/mfa/complete",
207
+ requirement: "public",
208
+ csrf: "signIn",
209
+ run: (api, _cookies, req) => api.completeMfa(field(req, "mfaToken"), field(req, "code"), req)
210
+ },
211
+ {
212
+ method: "GET",
213
+ path: "/mfa",
214
+ requirement: "actor",
215
+ run: (api, _cookies, req) => api.mfa.status(req.actor)
216
+ },
217
+ {
218
+ method: "POST",
219
+ path: "/mfa/begin",
220
+ requirement: "actor",
221
+ run: (api, _cookies, req) => api.mfa.begin(req.actor)
222
+ },
223
+ {
224
+ method: "POST",
225
+ path: "/mfa/confirm",
226
+ requirement: "actor",
227
+ run: (api, _cookies, req) => api.mfa.confirm(req.actor, field(req, "code"))
228
+ },
229
+ {
230
+ method: "POST",
231
+ path: "/mfa/disable",
232
+ requirement: "actor",
233
+ run: (api, _cookies, req) => api.mfa.disable(req.actor, field(req, "password"))
234
+ },
235
+ {
236
+ method: "GET",
237
+ path: "/admin/accounts",
238
+ requirement: "actor",
239
+ run: (api, _cookies, req) => api.admin.list(req.actor, {
240
+ search: queryText(req, "search"),
241
+ status: queryText(req, "status"),
242
+ limit: queryNumber(req, "limit"),
243
+ offset: queryNumber(req, "offset")
244
+ })
245
+ },
246
+ {
247
+ method: "GET",
248
+ path: "/admin/account",
249
+ requirement: "actor",
250
+ run: (api, _cookies, req) => api.admin.get(req.actor, queryNumber(req, "id") ?? NaN)
251
+ },
252
+ {
253
+ method: "POST",
254
+ path: "/admin/account/status",
255
+ requirement: "actor",
256
+ run: (api, _cookies, req) => api.admin.setStatus(req.actor, numberField(req, "userId"), field(req, "status"))
257
+ },
258
+ {
259
+ method: "POST",
260
+ path: "/admin/account/roles",
261
+ requirement: "actor",
262
+ run: (api, _cookies, req) => api.admin.setRoles(req.actor, numberField(req, "userId"), rolesOf(req))
263
+ },
264
+ {
265
+ method: "POST",
266
+ path: "/admin/account/delete",
267
+ requirement: "actor",
268
+ run: (api, _cookies, req) => api.admin.remove(req.actor, numberField(req, "userId"))
269
+ },
270
+ {
271
+ method: "POST",
272
+ path: "/admin/impersonate",
273
+ requirement: "actor",
274
+ run: (api, _cookies, req) => api.admin.impersonate(req.actor, numberField(req, "userId"), req)
95
275
  }
96
276
  ];
277
+ /** Whether this request has to present a token, which is `csrf`'s judgement — given what the flow does. */
278
+ var csrfNeeded = (csrf, policy, carrier) => policy === "none" ? false : csrf.required(carrier, policy ? SUBJECT_OF[policy] : "write");
97
279
  /** The flows, ready to mount. Which of them actually answer is decided by the API itself — no adapter, no endpoint —
98
280
  * so mounting all of them is correct even for a deployment that offers three. */
99
- var authRoutes = ({ api, cookies }) => FLOWS.map(({ run, ...route }) => ({
281
+ var authRoutes = ({ api, cookies, csrf }) => FLOWS.map(({ run, csrf: policy, ...route }) => ({
100
282
  ...route,
101
- handler: (req) => run(api, cookies, req)
283
+ handler: async (req) => {
284
+ /**
285
+ * Checked before the flow runs. The session the token must be bound to is whatever this request carries — an
286
+ * unbound token satisfies a signed-out caller, which is how signing in is protected when a deployment asks
287
+ * for that.
288
+ */
289
+ const carrier = {
290
+ ...req,
291
+ method: route.method
292
+ };
293
+ if (csrf && csrfNeeded(csrf, policy, carrier)) {
294
+ const result = csrf.verify(carrier, cookies.resolveSessionToken(req));
295
+ if (!result.ok) return {
296
+ ok: false,
297
+ status: 403,
298
+ body: {
299
+ error: csrfFailureMessage[result.reason],
300
+ reason: result.reason
301
+ }
302
+ };
303
+ }
304
+ return run(api, cookies, req);
305
+ }
102
306
  }));
103
307
  /**
104
308
  * The same flows as authorization rules, for the guard that runs in front of them.
@@ -120,10 +324,31 @@ var authPolicyRules = (prefix = "/auth") => {
120
324
  * to do that, and because getting this half wrong — writing the session but not its readable hint, or clearing one of
121
325
  * the three on sign-out — is the failure that looks like a session that exists and then does not.
122
326
  */
123
- var applySessionOutcome = (req, res, outcome, cookies) => {
124
- if (!outcome.ok) return;
125
- if (outcome.session) cookies.write(req, res, outcome.session);
126
- if (outcome.endSession) cookies.clear(req, res);
327
+ var applySessionOutcome = (req, res, outcome, cookies, csrf) => {
328
+ /**
329
+ * A refusal can still have something to say about the cookies see `endSession` on the failing outcome. It is
330
+ * the only thing it may say: nothing is granted from a flow that failed.
331
+ */
332
+ if (!outcome.ok) {
333
+ if (outcome.endSession) cookies.clear(req, res);
334
+ return;
335
+ }
336
+ if (outcome.session) {
337
+ cookies.write(req, res, outcome.session);
338
+ /**
339
+ * A fresh CSRF token, bound to the session that was just granted.
340
+ *
341
+ * Not optional housekeeping: the token a signed-out page was holding is bound to nobody, and every write it
342
+ * attempts after signing in would be refused as a mismatch. Re-issuing here is what makes signing in and then
343
+ * doing something work without the client knowing this exists.
344
+ */
345
+ csrf?.write(req, res, csrf.issue(outcome.session.token));
346
+ }
347
+ if (outcome.endSession) {
348
+ cookies.clear(req, res);
349
+ csrf?.clear(req, res);
350
+ }
351
+ if (outcome.csrf !== void 0) csrf?.write(req, res, outcome.csrf);
127
352
  };
128
353
  //#endregion
129
354
  export { applySessionOutcome, authPolicyRules, authRoutes };
@@ -22,6 +22,12 @@ export type SessionCookieParams = {
22
22
  * refuse one on a single-label host) and lax SameSite, because Secure + SameSite=None over plain http is dropped.
23
23
  */
24
24
  export declare const isLocalHost: (hostname: string) => boolean;
25
+ /**
26
+ * The last two labels of the host, so a session established on one sub-domain is carried by its siblings — an app
27
+ * and the API it talks to are rarely the same host. Local and IP hosts get no domain at all: browsers refuse a
28
+ * Domain on a single-label host, and one written for an address would never be sent back to it.
29
+ */
30
+ export declare const registrableDomain: (hostname: string) => string | undefined;
25
31
  /**
26
32
  * Everything about how a session cookie is written, derived from the request host unless the deployment says
27
33
  * otherwise. The defaults are the ones a server on a real domain needs: a cookie shared across sub-domains, and
@@ -38,6 +44,11 @@ export declare const sessionCookieParams: (hostname: string, config?: SSRAuthCoo
38
44
  * same question with `credentials: 'include'`.
39
45
  */
40
46
  export declare const sessionHintValue: (expiresAt: number, refreshExpiresAt?: number) => string;
47
+ /**
48
+ * Adds to `Set-Cookie` rather than replacing it. A handler may already have written one — clearing an OAuth flow
49
+ * before granting the session it produced is exactly that — and replacing the header would silently drop it.
50
+ */
51
+ export declare const appendCookies: (res: CookieSink, cookies: string[]) => void;
41
52
  /**
42
53
  * Writes a granted session onto the response: the credential itself, the refresh half confined to its own path, and
43
54
  * the readable hint. One call, because the three only make sense together — a hint that outlives its session sends
@@ -162,4 +162,4 @@ var createSessionCookies = (config) => ({
162
162
  }
163
163
  });
164
164
  //#endregion
165
- export { clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies };
165
+ export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, registrableDomain, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies };
@@ -25,6 +25,18 @@ export interface SpaceTokenAdapters {
25
25
  origins?: string[];
26
26
  expiresAt?: number | null;
27
27
  }) => Promise<void>;
28
+ /**
29
+ * Store a NEW credential beside the space's public one. Only the secret scopes are ever created this way — the
30
+ * public row is created with the space and rotated in place thereafter — so a deployment that issues none may
31
+ * leave it out and `issueHost` refuses.
32
+ */
33
+ create?: (values: {
34
+ spaceId: number;
35
+ token: string;
36
+ scope: SpaceScope;
37
+ origins: string[];
38
+ expiresAt: number | null;
39
+ }) => Promise<SpaceTokenRecord>;
28
40
  remove: (id: number) => Promise<void>;
29
41
  /**
30
42
  * The domain list is two policies at once — where the credential may be presented, and who may frame the space — so
@@ -91,6 +103,29 @@ export declare const createSpaceTokenApi: ({ tokens, adapters }: {
91
103
  domains: string[];
92
104
  warning: string;
93
105
  }>>;
106
+ /**
107
+ * Issue a **host** credential: the space's read access for a SERVER that renders it as its own.
108
+ *
109
+ * A separate credential rather than letting the public one do it, because the two are protected by different
110
+ * things and only one of them can be published. A `render` token is safe in a page precisely because it is held
111
+ * to the origins it declares — a check that only means anything when a browser is the one making the claim. A
112
+ * server has no origin to state, so admitting the public token without one would make a key lifted from anybody's
113
+ * published page enough to serve a byte-identical clone of their site. This one is admitted without an origin
114
+ * because possessing it is itself the proof, which is only true while it stays secret: it is returned once, never
115
+ * embedded, and revocable on its own row.
116
+ *
117
+ * `label` is what the owner will read in the list when deciding which one to revoke — the deployment it was
118
+ * issued to. It is stored where a render token keeps its domains, since a host credential is bound to none.
119
+ */
120
+ issueHost(context: SpaceTokenContext, label: unknown, { expiresAt }?: {
121
+ expiresAt?: number | null;
122
+ }): Promise<SpaceTokenOutcome<{
123
+ id: number;
124
+ token: string;
125
+ label: string;
126
+ expiresAt: number | null;
127
+ warning: string;
128
+ }>>;
94
129
  readDomains(context: SpaceTokenContext): Promise<SpaceTokenOutcome<{
95
130
  domains: string[];
96
131
  }>>;
@@ -116,8 +151,9 @@ export declare const createSpaceTokenApi: ({ tokens, adapters }: {
116
151
  }>>;
117
152
  /**
118
153
  * The space's credentials, so a leak can be seen and acted on. Never the secrets themselves: an `agent` grant
119
- * writes, and listing it would turn permission to read into a way to obtain it. The public one is published by
120
- * construction and has its own endpoint.
154
+ * writes and a `host` grant serves the space from somebody else's server, so listing either would turn
155
+ * permission to read into a way to obtain it. The public one is published by construction and has its own
156
+ * endpoint.
121
157
  */
122
158
  list(context: SpaceTokenContext): Promise<SpaceTokenOutcome<{
123
159
  tokens: SpaceTokenSummary[];
@@ -1,6 +1,12 @@
1
1
  import { normalizeDomain } from "./domains.js";
2
2
  //#region src/core/auth/spaceTokens.ts
3
3
  var ROTATION_WARNING = "The previous token stopped working. Every site embedding it must be redeployed.";
4
+ /**
5
+ * Said once, at the only moment it can be acted on. A host credential is the space's read access WITHOUT the origin
6
+ * check that makes the public one safe to publish, so what keeps it from being a way to clone the space is that it
7
+ * stays secret — and a caller who does not know it is never shown again will paste it into a page.
8
+ */
9
+ var HOST_SECRET_WARNING = "Store this now — it is not shown again. Unlike the public token it is NOT safe to embed in a page or commit to a public repository: anyone holding it can serve this space from their own server. Revoke it if it leaks.";
4
10
  var DAY_SECONDS = 86400;
5
11
  var now = () => Math.floor(Date.now() / 1e3);
6
12
  var notFound = {
@@ -81,6 +87,51 @@ var createSpaceTokenApi = ({ tokens, adapters }) => {
81
87
  }
82
88
  };
83
89
  },
90
+ /**
91
+ * Issue a **host** credential: the space's read access for a SERVER that renders it as its own.
92
+ *
93
+ * A separate credential rather than letting the public one do it, because the two are protected by different
94
+ * things and only one of them can be published. A `render` token is safe in a page precisely because it is held
95
+ * to the origins it declares — a check that only means anything when a browser is the one making the claim. A
96
+ * server has no origin to state, so admitting the public token without one would make a key lifted from anybody's
97
+ * published page enough to serve a byte-identical clone of their site. This one is admitted without an origin
98
+ * because possessing it is itself the proof, which is only true while it stays secret: it is returned once, never
99
+ * embedded, and revocable on its own row.
100
+ *
101
+ * `label` is what the owner will read in the list when deciding which one to revoke — the deployment it was
102
+ * issued to. It is stored where a render token keeps its domains, since a host credential is bound to none.
103
+ */
104
+ async issueHost(context, label, { expiresAt = null } = {}) {
105
+ if (!adapters.create) return {
106
+ ok: false,
107
+ status: 501,
108
+ body: { error: "This deployment does not issue host tokens" }
109
+ };
110
+ if (typeof label !== "string" || label.trim() === "") return {
111
+ ok: false,
112
+ status: 400,
113
+ body: { error: "label is required — name the deployment this credential is for, so it can be revoked" }
114
+ };
115
+ const name = label.trim();
116
+ const token = tokens.generateSpaceToken(context.spaceId, [name], "host", { expiresAt });
117
+ return {
118
+ ok: true,
119
+ status: 201,
120
+ body: {
121
+ id: (await adapters.create({
122
+ spaceId: context.spaceId,
123
+ token,
124
+ scope: "host",
125
+ origins: [name],
126
+ expiresAt
127
+ })).id,
128
+ token,
129
+ label: name,
130
+ expiresAt,
131
+ warning: HOST_SECRET_WARNING
132
+ }
133
+ };
134
+ },
84
135
  async readDomains(context) {
85
136
  const record = await adapters.loadDefault(context.spaceId);
86
137
  return record ? {
@@ -160,8 +211,9 @@ var createSpaceTokenApi = ({ tokens, adapters }) => {
160
211
  },
161
212
  /**
162
213
  * The space's credentials, so a leak can be seen and acted on. Never the secrets themselves: an `agent` grant
163
- * writes, and listing it would turn permission to read into a way to obtain it. The public one is published by
164
- * construction and has its own endpoint.
214
+ * writes and a `host` grant serves the space from somebody else's server, so listing either would turn
215
+ * permission to read into a way to obtain it. The public one is published by construction and has its own
216
+ * endpoint.
165
217
  */
166
218
  async list(context) {
167
219
  return {
@@ -0,0 +1,5 @@
1
+ import { ThrottleAttempt } from './api';
2
+ export declare const createMemoryRateLimit: () => ((attempt: ThrottleAttempt) => Promise<{
3
+ allowed: boolean;
4
+ retryAfter?: number;
5
+ }>);
@@ -0,0 +1,81 @@
1
+ //#region src/core/auth/throttle.ts
2
+ /** Attempts allowed per window, per key, per action. */
3
+ var LIMITS = {
4
+ /** Credential stuffing is the thing being priced out. Ten wrong passwords in five minutes is already a lot. */
5
+ login: {
6
+ attempts: 10,
7
+ windowSeconds: 300
8
+ },
9
+ signup: {
10
+ attempts: 5,
11
+ windowSeconds: 3600
12
+ },
13
+ /** Mailing flows are throttled by address, or the endpoint is a way to send somebody a hundred emails. */
14
+ forgotPassword: {
15
+ attempts: 5,
16
+ windowSeconds: 3600
17
+ },
18
+ /** By token: without this a reset token is guessable by trying, however opaque it is. */
19
+ resetPassword: {
20
+ attempts: 10,
21
+ windowSeconds: 3600
22
+ },
23
+ changePassword: {
24
+ attempts: 10,
25
+ windowSeconds: 300
26
+ },
27
+ exchange: {
28
+ attempts: 30,
29
+ windowSeconds: 300
30
+ },
31
+ /** Six digits is a million guesses, so the limit is what makes a second factor worth anything. */
32
+ mfa: {
33
+ attempts: 5,
34
+ windowSeconds: 300
35
+ },
36
+ passwordless: {
37
+ attempts: 5,
38
+ windowSeconds: 900
39
+ }
40
+ };
41
+ /** Stop the map growing without bound on a server nobody is attacking politely. */
42
+ var SWEEP_EVERY = 1e3;
43
+ var createMemoryRateLimit = () => {
44
+ const hits = /* @__PURE__ */ new Map();
45
+ let sinceSweep = 0;
46
+ const sweep = (now) => {
47
+ for (const [key, times] of hits) {
48
+ const action = key.slice(0, key.indexOf(":"));
49
+ const live = times.filter((at) => at > now - LIMITS[action].windowSeconds);
50
+ if (live.length === 0) hits.delete(key);
51
+ else hits.set(key, live);
52
+ }
53
+ };
54
+ return (attempt) => {
55
+ const { attempts, windowSeconds } = LIMITS[attempt.action];
56
+ const now = Math.floor(Date.now() / 1e3);
57
+ const key = `${attempt.action}:${attempt.key}`;
58
+ /**
59
+ * It worked, so nothing is being guarded against any more. Without this the counter cannot tell ten failures
60
+ * from ten sign-ins — the check runs before the password is examined — and an app that signs the same account
61
+ * in repeatedly would lock it out by succeeding.
62
+ */
63
+ if (attempt.succeeded) {
64
+ hits.delete(key);
65
+ return Promise.resolve({ allowed: true });
66
+ }
67
+ if (++sinceSweep >= SWEEP_EVERY) {
68
+ sinceSweep = 0;
69
+ sweep(now);
70
+ }
71
+ const recent = (hits.get(key) ?? []).filter((at) => at > now - windowSeconds);
72
+ if (recent.length >= attempts) return Promise.resolve({
73
+ allowed: false,
74
+ retryAfter: Math.max(1, recent[0] + windowSeconds - now)
75
+ });
76
+ hits.set(key, [...recent, now]);
77
+ return Promise.resolve({ allowed: true });
78
+ };
79
+ };
80
+ //#endregion
81
+ export { createMemoryRateLimit };
@@ -0,0 +1 @@
1
+ export {};