@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
@@ -18,11 +18,40 @@ export declare const SCOPES: {
18
18
  readonly widget: "widget";
19
19
  readonly spaceRender: "space:render";
20
20
  readonly spaceAgent: "space:agent";
21
+ /**
22
+ * A SERVER that renders this space as its own — self-hosting. Secret by construction, which is the whole reason it
23
+ * exists apart from `space:render`: see {@link SpaceScope}.
24
+ */
25
+ readonly spaceHost: "space:host";
26
+ /** A sign-in that got the password right and still owes a second factor. Carries no authority of its own. */
27
+ readonly mfa: "mfa";
21
28
  };
22
29
  export type TokenScope = (typeof SCOPES)[keyof typeof SCOPES];
23
- export type SpaceScope = 'render' | 'agent';
30
+ /**
31
+ * What a space credential IS, and the distinction the other two are defined against.
32
+ *
33
+ * `render` is **public**: it ships in the clear inside every published page, so anybody who views source has it. The
34
+ * only thing that keeps a copied one from working is that a browser is made to state where it is presenting from and
35
+ * that statement is checked — a control that exists solely because browsers enforce it, and that means nothing to a
36
+ * client which simply does not send the header.
37
+ *
38
+ * `host` is that same read access for a server, and it is **secret**: shown once when it is issued, never embedded in
39
+ * a page, held only by the deployment operator. Self-hosting needs a credential that survives having no Origin to
40
+ * check, and a public one cannot be it — a render key lifted from someone else's page would otherwise be enough to
41
+ * serve a byte-identical clone of their site from anywhere.
42
+ *
43
+ * `agent` writes, on behalf of the member who consented.
44
+ */
45
+ export type SpaceScope = 'render' | 'agent' | 'host';
46
+ /** The scopes that are safe to present with no `Origin` header, because they are not public to begin with. */
47
+ export declare const OFF_BROWSER_SPACE_SCOPES: readonly SpaceScope[];
24
48
  /** Why a credential was refused. A reason rather than a status, because each transport maps it onto its own. */
25
- export type AuthFailure = 'missing' | 'malformed' | 'expired' | 'outdated' | 'revoked' | 'wrong-type' | 'scope-mismatch' | 'issuer-not-allowed' | 'origin-not-allowed' | 'domain-not-allowed' | 'inactive';
49
+ export type AuthFailure = 'missing' | 'malformed' | 'expired' | 'outdated' | 'revoked' | 'wrong-type' | 'scope-mismatch' | 'issuer-not-allowed' | 'origin-not-allowed' | 'domain-not-allowed' | 'inactive'
50
+ /**
51
+ * The password was right and the address has never answered. Separate from `inactive` because it is the one
52
+ * refusal whose owner can clear it themselves, and a screen that cannot tell them apart has to guess.
53
+ */
54
+ | 'unverified';
26
55
  export declare const authFailureMessage: Record<AuthFailure, string>;
27
56
  /**
28
57
  * How long each credential lives. One rule, applied by how much damage the credential can do and whether something
@@ -40,8 +69,25 @@ export declare const DEFAULT_LIFETIMES: {
40
69
  readonly refresh: number;
41
70
  readonly agent: number;
42
71
  readonly widget: number;
72
+ /**
73
+ * Long enough to fetch a code out of an app, short enough that a stolen one is worthless. It authorises nothing
74
+ * on its own — only finishing the sign-in it belongs to, which still needs the code.
75
+ */
76
+ readonly mfaChallenge: 300;
77
+ /**
78
+ * The longest a session may live no matter how often it renews, counted from when it began. `0` means no cap,
79
+ * which is the default and what most deployments want: the renewal window is already an idle timeout, since a
80
+ * session nobody refreshes dies with its refresh token. This is the other half — the one that ends a session
81
+ * that has been renewing quietly for a year.
82
+ */
83
+ readonly session: 0;
43
84
  };
44
- export type TokenLifetimes = Partial<typeof DEFAULT_LIFETIMES>;
85
+ /**
86
+ * Seconds, per credential. Written as a record of `number` rather than `Partial<typeof DEFAULT_LIFETIMES>`: the
87
+ * defaults are `as const`, so that shape narrowed every field to the literal it happens to default to and made
88
+ * `{ access: 3600 }` a type error. The option existed and could not be used.
89
+ */
90
+ export type TokenLifetimes = Partial<Record<keyof typeof DEFAULT_LIFETIMES, number>>;
45
91
  export type TokenConfig = {
46
92
  secret: string;
47
93
  /** What this deployment mints under. Also accepted on the way back in, along with `alsoAccept`. */
@@ -70,13 +116,33 @@ interface BaseClaims {
70
116
  export interface UserTokenPayload extends BaseClaims {
71
117
  scope: typeof SCOPES.user;
72
118
  sub: string;
119
+ /**
120
+ * Who is really behind this credential, when it is not the subject: an administrator acting AS somebody. `act` is
121
+ * the registered claim for exactly that (RFC 8693 §4.1), so a log line, an audit trail or a downstream service can
122
+ * tell a session somebody was given from one they signed into — and nothing has to invent a claim to say it.
123
+ */
124
+ act?: {
125
+ sub: string;
126
+ };
127
+ }
128
+ /** What a session credential is, beyond who it is for. */
129
+ export interface UserTokenOptions {
130
+ /** The id of whoever obtained this session on the subject's behalf. Becomes the `act` claim. */
131
+ actingAs?: number | string;
132
+ /** Overrides `lifetimes.access`. A borrowed session is worth keeping short. */
133
+ ttlSeconds?: number;
73
134
  }
74
135
  export interface RefreshTokenPayload extends BaseClaims {
75
136
  scope: typeof SCOPES.refresh;
76
137
  sub: string;
77
138
  }
139
+ /** Half a sign-in: the password is proven, the second factor is not. Authorises finishing that login and nothing else. */
140
+ export interface MfaChallengePayload extends BaseClaims {
141
+ scope: typeof SCOPES.mfa;
142
+ sub: string;
143
+ }
78
144
  export interface SpaceTokenPayload extends BaseClaims {
79
- scope: typeof SCOPES.spaceRender | typeof SCOPES.spaceAgent;
145
+ scope: typeof SCOPES.spaceRender | typeof SCOPES.spaceAgent | typeof SCOPES.spaceHost;
80
146
  sub: string;
81
147
  /** Web origins the space may be embedded on. No registered claim covers this. */
82
148
  origins: string[];
@@ -85,7 +151,7 @@ export interface SpaceTokenPayload extends BaseClaims {
85
151
  export interface WidgetTokenPayload extends BaseClaims {
86
152
  scope: typeof SCOPES.widget;
87
153
  }
88
- export type TokenPayload = UserTokenPayload | RefreshTokenPayload | SpaceTokenPayload | WidgetTokenPayload;
154
+ export type TokenPayload = UserTokenPayload | RefreshTokenPayload | SpaceTokenPayload | WidgetTokenPayload | MfaChallengePayload;
89
155
  export type VerifyResult<T> = {
90
156
  ok: true;
91
157
  payload: T;
@@ -108,15 +174,14 @@ export interface SpaceTokenOptions {
108
174
  * server issues under their own and nothing they mint is worth anything here, or the other way round.
109
175
  */
110
176
  export declare const createTokens: (config: TokenConfig) => {
111
- lifetimes: {
112
- access: 86400;
113
- refresh: number;
114
- agent: number;
115
- widget: number;
116
- };
117
- generateUserToken: (userId: number | string) => string;
177
+ lifetimes: Record<"session" | "refresh" | "widget" | "agent" | "access" | "mfaChallenge", number>;
178
+ /** What this deployment mints under. Read by anything that has to name the deployment — an enrolment URI, say. */
179
+ issuer: string;
180
+ generateUserToken: (userId: number | string, { actingAs, ttlSeconds }?: UserTokenOptions) => string;
118
181
  generateRefreshToken: (userId: number | string) => string;
119
182
  generateWidgetToken: () => string;
183
+ /** Says "this person proved their password"; nothing more, and only for a few minutes. */
184
+ generateMfaChallenge: (userId: number | string) => string;
120
185
  /**
121
186
  * A `render` token may live forever, and defaults to it, because it is embedded in a published site — often a
122
187
  * SPA deployed once and left alone — where an expiry is a scheduled outage: the site breaks weeks later with
@@ -127,10 +192,16 @@ export declare const createTokens: (config: TokenConfig) => {
127
192
  *
128
193
  * An `agent` grant is the opposite default and always carries a lifetime: it writes, and it is held by a
129
194
  * third-party host.
195
+ *
196
+ * A `host` grant follows `render`: it only reads, it is held by the space owner's own deployment, and an expiry
197
+ * there is the same scheduled outage — except the site goes quietly STALE rather than dark, because a
198
+ * self-hosted renderer keeps serving the last copy it fetched. Being secret, it is worth rotating; being
199
+ * revocable, it does not need a deadline to be withdrawn.
130
200
  */
131
201
  generateSpaceToken: (spaceId: number | string, origins: string[], scope?: SpaceScope, { expiresAt }?: SpaceTokenOptions) => string;
132
202
  verifyUserToken: (token: string) => VerifyResult<UserTokenPayload>;
133
203
  verifyRefreshToken: (token: string) => VerifyResult<RefreshTokenPayload>;
204
+ verifyMfaChallenge: (token: string) => VerifyResult<MfaChallengePayload>;
134
205
  verifyWidgetToken: (token: string) => VerifyResult<WidgetTokenPayload>;
135
206
  verifySpaceToken: (token: string) => VerifyResult<SpaceTokenPayload & {
136
207
  spaceId: number;
@@ -19,8 +19,17 @@ var SCOPES = {
19
19
  refresh: "refresh",
20
20
  widget: "widget",
21
21
  spaceRender: "space:render",
22
- spaceAgent: "space:agent"
22
+ spaceAgent: "space:agent",
23
+ /**
24
+ * A SERVER that renders this space as its own — self-hosting. Secret by construction, which is the whole reason it
25
+ * exists apart from `space:render`: see {@link SpaceScope}.
26
+ */
27
+ spaceHost: "space:host",
28
+ /** A sign-in that got the password right and still owes a second factor. Carries no authority of its own. */
29
+ mfa: "mfa"
23
30
  };
31
+ /** The scopes that are safe to present with no `Origin` header, because they are not public to begin with. */
32
+ var OFF_BROWSER_SPACE_SCOPES = ["agent", "host"];
24
33
  var authFailureMessage = {
25
34
  missing: "Authentication required",
26
35
  malformed: "Token Invalid",
@@ -32,7 +41,8 @@ var authFailureMessage = {
32
41
  "issuer-not-allowed": "Token was issued by another environment",
33
42
  "origin-not-allowed": "Access Not Authorized",
34
43
  "domain-not-allowed": "This token is not allowed on this domain",
35
- inactive: "Account is not active"
44
+ inactive: "Account is not active",
45
+ unverified: "Account is not verified"
36
46
  };
37
47
  var DAY = 86400;
38
48
  /**
@@ -50,15 +60,29 @@ var DEFAULT_LIFETIMES = {
50
60
  access: DAY,
51
61
  refresh: 30 * DAY,
52
62
  agent: 30 * DAY,
53
- widget: 30 * DAY
63
+ widget: 30 * DAY,
64
+ /**
65
+ * Long enough to fetch a code out of an app, short enough that a stolen one is worthless. It authorises nothing
66
+ * on its own — only finishing the sign-in it belongs to, which still needs the code.
67
+ */
68
+ mfaChallenge: 300,
69
+ /**
70
+ * The longest a session may live no matter how often it renews, counted from when it began. `0` means no cap,
71
+ * which is the default and what most deployments want: the renewal window is already an idle timeout, since a
72
+ * session nobody refreshes dies with its refresh token. This is the other half — the one that ends a session
73
+ * that has been renewing quietly for a year.
74
+ */
75
+ session: 0
54
76
  };
55
77
  var spaceScopeToClaim = {
56
78
  render: SCOPES.spaceRender,
57
- agent: SCOPES.spaceAgent
79
+ agent: SCOPES.spaceAgent,
80
+ host: SCOPES.spaceHost
58
81
  };
59
82
  var claimToSpaceScope = {
60
83
  [SCOPES.spaceRender]: "render",
61
- [SCOPES.spaceAgent]: "agent"
84
+ [SCOPES.spaceAgent]: "agent",
85
+ [SCOPES.spaceHost]: "host"
62
86
  };
63
87
  var isLegacyShape = (payload) => "data" in payload || "version" in payload;
64
88
  var subjectId = (payload) => {
@@ -146,15 +170,23 @@ var createTokens = (config) => {
146
170
  };
147
171
  return {
148
172
  lifetimes,
149
- generateUserToken: (userId) => sign({
173
+ /** What this deployment mints under. Read by anything that has to name the deployment — an enrolment URI, say. */
174
+ issuer: config.issuer,
175
+ generateUserToken: (userId, { actingAs, ttlSeconds } = {}) => sign({
150
176
  sub: String(userId),
151
- ...baseClaims(SCOPES.user, lifetimes.access)
177
+ ...actingAs === void 0 ? {} : { act: { sub: String(actingAs) } },
178
+ ...baseClaims(SCOPES.user, ttlSeconds ?? lifetimes.access)
152
179
  }),
153
180
  generateRefreshToken: (userId) => sign({
154
181
  sub: String(userId),
155
182
  ...baseClaims(SCOPES.refresh, lifetimes.refresh)
156
183
  }),
157
184
  generateWidgetToken: () => sign(baseClaims(SCOPES.widget, lifetimes.widget)),
185
+ /** Says "this person proved their password"; nothing more, and only for a few minutes. */
186
+ generateMfaChallenge: (userId) => sign({
187
+ sub: String(userId),
188
+ ...baseClaims(SCOPES.mfa, lifetimes.mfaChallenge)
189
+ }),
158
190
  /**
159
191
  * A `render` token may live forever, and defaults to it, because it is embedded in a published site — often a
160
192
  * SPA deployed once and left alone — where an expiry is a scheduled outage: the site breaks weeks later with
@@ -165,6 +197,11 @@ var createTokens = (config) => {
165
197
  *
166
198
  * An `agent` grant is the opposite default and always carries a lifetime: it writes, and it is held by a
167
199
  * third-party host.
200
+ *
201
+ * A `host` grant follows `render`: it only reads, it is held by the space owner's own deployment, and an expiry
202
+ * there is the same scheduled outage — except the site goes quietly STALE rather than dark, because a
203
+ * self-hosted renderer keeps serving the last copy it fetched. Being secret, it is worth rotating; being
204
+ * revocable, it does not need a deadline to be withdrawn.
168
205
  */
169
206
  generateSpaceToken: (spaceId, origins, scope = "render", { expiresAt } = {}) => {
170
207
  const defaultTtl = scope === "agent" ? lifetimes.agent : void 0;
@@ -177,9 +214,14 @@ var createTokens = (config) => {
177
214
  },
178
215
  verifyUserToken: (token) => withSubject(verifyScoped(token, [SCOPES.user])),
179
216
  verifyRefreshToken: (token) => withSubject(verifyScoped(token, [SCOPES.refresh])),
217
+ verifyMfaChallenge: (token) => withSubject(verifyScoped(token, [SCOPES.mfa])),
180
218
  verifyWidgetToken: (token) => verifyScoped(token, [SCOPES.widget]),
181
219
  verifySpaceToken: (token) => {
182
- const result = withSubject(verifyScoped(token, [SCOPES.spaceRender, SCOPES.spaceAgent]));
220
+ const result = withSubject(verifyScoped(token, [
221
+ SCOPES.spaceRender,
222
+ SCOPES.spaceAgent,
223
+ SCOPES.spaceHost
224
+ ]));
183
225
  if (!result.ok) return result;
184
226
  const spaceId = subjectId(result.payload);
185
227
  const spaceScope = claimToSpaceScope[result.payload.scope];
@@ -218,4 +260,4 @@ var createTokens = (config) => {
218
260
  /** The user id a session or refresh token is about. */
219
261
  var userIdOf = (payload) => Number(payload.sub);
220
262
  //#endregion
221
- export { DEFAULT_LIFETIMES, SCOPES, authFailureMessage, createTokens, userIdOf };
263
+ export { DEFAULT_LIFETIMES, OFF_BROWSER_SPACE_SCOPES, SCOPES, authFailureMessage, createTokens, userIdOf };
@@ -0,0 +1,48 @@
1
+ /** A 20-byte secret, which is what RFC 4226 recommends for HMAC-SHA1 and what apps are sized for. */
2
+ export declare const generateTotpSecret: () => string;
3
+ /** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
4
+ export declare const totpCode: (secret: string, at?: number) => string;
5
+ /**
6
+ * Is this the code?
7
+ *
8
+ * `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
9
+ * and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
10
+ * trade and the same one Google Authenticator's own guidance makes.
11
+ *
12
+ * Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
13
+ * right, which turns a million-guess space into ten guesses per position.
14
+ */
15
+ export declare const verifyTotp: (secret: string, code: string, options?: {
16
+ window?: number;
17
+ at?: number;
18
+ }) => boolean;
19
+ /**
20
+ * The URI an authenticator app scans. `issuer` appears twice by convention — in the label and as a parameter —
21
+ * because different apps read different ones, and an entry that says only an email address is unidentifiable in a
22
+ * list of thirty.
23
+ */
24
+ export declare const totpUri: ({ secret, account, issuer }: {
25
+ secret: string;
26
+ account: string;
27
+ issuer: string;
28
+ }) => string;
29
+ /**
30
+ * An unbiased random string over an alphabet.
31
+ *
32
+ * `randomInt` rejection-samples, which is the whole point: the obvious `byte % alphabet.length` over-represents the
33
+ * first `256 % length` characters, and a token that gates an account has to draw every character with equal
34
+ * probability. Hand-rolling that correctly is a known trap, so it is written once here.
35
+ *
36
+ * Default alphabet is Crockford-ish base32 — no `0`/`O`/`1`/`I`, so a code read aloud or off paper survives.
37
+ */
38
+ export declare const randomCode: (length: number, alphabet?: string) => string;
39
+ /**
40
+ * Recovery codes, for the phone that went in a river.
41
+ *
42
+ * Ten of them, each 40 bits of entropy in a shape somebody can read off paper. Generated once and shown once: they
43
+ * are stored hashed, so a deployment that loses them cannot print them again, which is the property that makes
44
+ * them worth having.
45
+ */
46
+ export declare const generateRecoveryCodes: (count?: number) => string[];
47
+ /** Normalised before hashing so that case and the hyphen a person types or omits do not decide whether it works. */
48
+ export declare const normalizeRecoveryCode: (code: string) => string;
@@ -0,0 +1,118 @@
1
+ import { createHmac, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
2
+ //#region src/core/auth/totp.ts
3
+ /**
4
+ * Time-based one-time passwords, RFC 6238, on the standard library.
5
+ *
6
+ * Six digits over HMAC-SHA1 in thirty-second steps, because that is what every authenticator app assumes and the
7
+ * point of this is that Google Authenticator, 1Password and Aegis all work without being told anything. The RFC
8
+ * allows SHA-256 and other digit counts; almost nothing in the wild reads them off the enrolment URI.
9
+ */
10
+ var DIGITS = 6;
11
+ var STEP_SECONDS = 30;
12
+ /** RFC 4648 base32, which is the alphabet authenticator apps expect a secret in. No padding: apps dislike it. */
13
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
14
+ var base32Encode = (bytes) => {
15
+ let bits = 0;
16
+ let value = 0;
17
+ let output = "";
18
+ for (const byte of bytes) {
19
+ value = value << 8 | byte;
20
+ bits += 8;
21
+ while (bits >= 5) {
22
+ output += ALPHABET[value >>> bits - 5 & 31];
23
+ bits -= 5;
24
+ }
25
+ }
26
+ if (bits > 0) output += ALPHABET[value << 5 - bits & 31];
27
+ return output;
28
+ };
29
+ var base32Decode = (secret) => {
30
+ const clean = secret.toUpperCase().replace(/[^A-Z2-7]/g, "");
31
+ const bytes = [];
32
+ let bits = 0;
33
+ let value = 0;
34
+ for (const character of clean) {
35
+ value = value << 5 | ALPHABET.indexOf(character);
36
+ bits += 5;
37
+ if (bits >= 8) {
38
+ bytes.push(value >>> bits - 8 & 255);
39
+ bits -= 8;
40
+ }
41
+ }
42
+ return Buffer.from(bytes);
43
+ };
44
+ /** A 20-byte secret, which is what RFC 4226 recommends for HMAC-SHA1 and what apps are sized for. */
45
+ var generateTotpSecret = () => base32Encode(randomBytes(20));
46
+ var codeAt = (secret, counter) => {
47
+ const buffer = Buffer.alloc(8);
48
+ buffer.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
49
+ buffer.writeUInt32BE(counter >>> 0, 4);
50
+ const digest = createHmac("sha1", base32Decode(secret)).update(buffer).digest();
51
+ const offset = digest[digest.length - 1] & 15;
52
+ const binary = (digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255;
53
+ return String(binary % 10 ** DIGITS).padStart(DIGITS, "0");
54
+ };
55
+ /** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
56
+ var totpCode = (secret, at = Date.now()) => codeAt(secret, Math.floor(at / 1e3 / STEP_SECONDS));
57
+ /**
58
+ * Is this the code?
59
+ *
60
+ * `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
61
+ * and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
62
+ * trade and the same one Google Authenticator's own guidance makes.
63
+ *
64
+ * Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
65
+ * right, which turns a million-guess space into ten guesses per position.
66
+ */
67
+ var verifyTotp = (secret, code, options = {}) => {
68
+ const { window = 1, at = Date.now() } = options;
69
+ const candidate = code.trim();
70
+ if (!/^\d{6}$/.test(candidate)) return false;
71
+ const counter = Math.floor(at / 1e3 / STEP_SECONDS);
72
+ const supplied = Buffer.from(candidate);
73
+ let matched = false;
74
+ for (let drift = -window; drift <= window; drift++) {
75
+ const expected = Buffer.from(codeAt(secret, counter + drift));
76
+ matched = timingSafeEqual(expected, supplied) || matched;
77
+ }
78
+ return matched;
79
+ };
80
+ /**
81
+ * The URI an authenticator app scans. `issuer` appears twice by convention — in the label and as a parameter —
82
+ * because different apps read different ones, and an entry that says only an email address is unidentifiable in a
83
+ * list of thirty.
84
+ */
85
+ var totpUri = ({ secret, account, issuer }) => {
86
+ return `otpauth://totp/${encodeURIComponent(`${issuer}:${account}`)}?${new URLSearchParams({
87
+ secret,
88
+ issuer,
89
+ algorithm: "SHA1",
90
+ digits: String(DIGITS),
91
+ period: "30"
92
+ }).toString()}`;
93
+ };
94
+ /**
95
+ * An unbiased random string over an alphabet.
96
+ *
97
+ * `randomInt` rejection-samples, which is the whole point: the obvious `byte % alphabet.length` over-represents the
98
+ * first `256 % length` characters, and a token that gates an account has to draw every character with equal
99
+ * probability. Hand-rolling that correctly is a known trap, so it is written once here.
100
+ *
101
+ * Default alphabet is Crockford-ish base32 — no `0`/`O`/`1`/`I`, so a code read aloud or off paper survives.
102
+ */
103
+ var randomCode = (length, alphabet = ALPHABET) => Array.from({ length }, () => alphabet[randomInt(alphabet.length)]).join("");
104
+ /**
105
+ * Recovery codes, for the phone that went in a river.
106
+ *
107
+ * Ten of them, each 40 bits of entropy in a shape somebody can read off paper. Generated once and shown once: they
108
+ * are stored hashed, so a deployment that loses them cannot print them again, which is the property that makes
109
+ * them worth having.
110
+ */
111
+ var generateRecoveryCodes = (count = 10) => Array.from({ length: count }, () => {
112
+ const digits = randomCode(10);
113
+ return `${digits.slice(0, 5)}-${digits.slice(5)}`;
114
+ });
115
+ /** Normalised before hashing so that case and the hyphen a person types or omits do not decide whether it works. */
116
+ var normalizeRecoveryCode = (code) => code.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
117
+ //#endregion
118
+ export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpUri, verifyTotp };
@@ -0,0 +1 @@
1
+ export {};
@@ -20,10 +20,4 @@ export type ServerConfig = Omit<SSRServerConfig, 'adapters'> & {
20
20
  */
21
21
  auth?: Auth;
22
22
  };
23
- /** The server this package makes: pages and RSC, mounting whatever the config enables, plus any stages a
24
- * companion package contributes through `extensions`. The MCP endpoint, the widget proxy and draft-preview
25
- * arrive that way from `@plitzi/sdk-mcp`, which a page-only deployment never installs.
26
- *
27
- * A dedicated MCP server is `createServer` from `@plitzi/sdk-mcp` — it builds none of the render template,
28
- * caches or plugin manager this one does. */
29
23
  export declare const createServer: ({ auth, ...config }: ServerConfig, extensions?: PipelineExtensions) => SSRServer;
@@ -1,3 +1,5 @@
1
+ import { actionsModuleFor } from "../modules/actions/moduleFor.js";
2
+ import { connectorRscData } from "../modules/rsc/connectorRscData.js";
1
3
  import { createAuthApiStage } from "./http/stages/authApi.js";
2
4
  import { resolveServices } from "./services/resolve.js";
3
5
  import { createPageServer } from "./server/pageServer.js";
@@ -8,11 +10,41 @@ import { createPageServer } from "./server/pageServer.js";
8
10
  *
9
11
  * A dedicated MCP server is `createServer` from `@plitzi/sdk-mcp` — it builds none of the render template,
10
12
  * caches or plugin manager this one does. */
13
+ /**
14
+ * Fills in `getRscData` from whatever can produce a server element's data — `connectors`, `action.lookups`, or
15
+ * both — when the deployment did not write one.
16
+ *
17
+ * The lookups are already here and the assembly is entirely this package's — a resolver over `resolveRscData`.
18
+ * Leaving it out meant every deployment passed the same lookups twice: once as config, for the write endpoint,
19
+ * and once folded by hand into an adapter, for the read.
20
+ *
21
+ * Actions count on their own: a space whose server elements name actions rather than connectors has nothing to
22
+ * configure under `connectors`, and keying this on that alone left its `render` elements resolving to nothing
23
+ * with no configuration missing anywhere.
24
+ */
25
+ var withConnectorRsc = (config) => {
26
+ if (config.adapters.getRscData || !config.connectors && !config.action?.lookups) return config;
27
+ const module = actionsModuleFor(config);
28
+ const actions = module && config.action?.lookups ? {
29
+ lookups: config.action.lookups,
30
+ module
31
+ } : void 0;
32
+ return {
33
+ ...config,
34
+ adapters: {
35
+ ...config.adapters,
36
+ getRscData: connectorRscData(config.connectors, actions, config.rsc?.elementTimeoutMs)
37
+ }
38
+ };
39
+ };
11
40
  var createServer = ({ auth, ...config }, extensions) => {
12
- if (!auth) return createPageServer(config, resolveServices(config), extensions);
41
+ if (!auth) {
42
+ const resolvedConfig = withConnectorRsc(config);
43
+ return createPageServer(resolvedConfig, resolveServices(resolvedConfig), extensions);
44
+ }
13
45
  const entries = Object.entries(config.adapters);
14
46
  const supplied = Object.fromEntries(entries.filter(([, value]) => value !== void 0));
15
- const resolved = {
47
+ const resolved = withConnectorRsc({
16
48
  loginPath: false,
17
49
  logoutPath: false,
18
50
  exchangePath: false,
@@ -22,7 +54,7 @@ var createServer = ({ auth, ...config }, extensions) => {
22
54
  ...auth.ssrAdapters,
23
55
  ...supplied
24
56
  }
25
- };
57
+ });
26
58
  const withAuthRoutes = {
27
59
  ...extensions,
28
60
  preAuth: [...extensions?.preAuth ?? [], createAuthApiStage(auth, auth.basePath)]
@@ -0,0 +1,33 @@
1
+ import { SpaceFont } from '@plitzi/sdk-shared';
2
+ /** One file in the store, as the caller needs to address it afterwards. */
3
+ export type StoredFont = {
4
+ /** Store-relative, and exactly what goes into a `hosted` font's `files[].path`. */
5
+ path: string;
6
+ size: number;
7
+ format: 'woff2' | 'woff';
8
+ };
9
+ /**
10
+ * Where a deployment keeps the font files a space uploaded.
11
+ *
12
+ * Deliberately not a URL builder: addressing is `fontUrlResolver(baseUrl)`, which every render surface already
13
+ * shares, and a store that also decided URLs would put this deployment's origin inside a document that travels to
14
+ * other deployments (see `HostedFont`). Nor does it read bytes back — a local store's files are served off disk by
15
+ * `fontAssetsStage` and a cloud store's by its CDN, so nothing would ever call it.
16
+ */
17
+ export type FontStore = {
18
+ list: (spaceId: number) => Promise<StoredFont[]>;
19
+ put: (spaceId: number, file: {
20
+ name: string;
21
+ body: Buffer;
22
+ format: 'woff2' | 'woff';
23
+ }) => Promise<StoredFont>;
24
+ remove: (storePath: string) => Promise<void>;
25
+ };
26
+ /**
27
+ * The store a self-hosted server gets for nothing: a directory on its own disk, served by this server at
28
+ * `/fonts/*`. No object storage, no credentials, no account — and it works with no network at all, which is the
29
+ * state a self-hosted deployment is allowed to be in.
30
+ */
31
+ export declare const createLocalFontStore: (rootDir: string) => FontStore;
32
+ /** Every store path a manifest names, so a deployment can tell what is still referenced from what is orphaned. */
33
+ export declare const hostedPathsOf: (fonts: SpaceFont[]) => string[];
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/core/fontStore.ts
4
+ /** `<spaceId>/<name>` — the space segment is what keeps one space's files out of another's. */
5
+ var storePathOf = (spaceId, name) => `${spaceId}/${name}`;
6
+ /**
7
+ * A path that came from a document or a request, resolved inside the store and nowhere else.
8
+ *
9
+ * The manifest is data a user can write, so `..` and absolute paths reach here; without this check a font path
10
+ * would be a way to read any file the process can.
11
+ */
12
+ var resolveInside = (rootDir, storePath) => {
13
+ const resolvedRoot = path.resolve(rootDir);
14
+ const filePath = path.resolve(resolvedRoot, storePath);
15
+ return filePath.startsWith(resolvedRoot + path.sep) ? filePath : void 0;
16
+ };
17
+ /**
18
+ * The store a self-hosted server gets for nothing: a directory on its own disk, served by this server at
19
+ * `/fonts/*`. No object storage, no credentials, no account — and it works with no network at all, which is the
20
+ * state a self-hosted deployment is allowed to be in.
21
+ */
22
+ var createLocalFontStore = (rootDir) => ({
23
+ list: (spaceId) => {
24
+ const dir = path.resolve(rootDir, String(spaceId));
25
+ if (!fs.existsSync(dir)) return Promise.resolve([]);
26
+ return Promise.resolve(fs.readdirSync(dir).filter((name) => name.endsWith(".woff2") || name.endsWith(".woff")).map((name) => ({
27
+ path: storePathOf(spaceId, name),
28
+ size: fs.statSync(path.join(dir, name)).size,
29
+ format: name.endsWith(".woff2") ? "woff2" : "woff"
30
+ })));
31
+ },
32
+ put: (spaceId, file) => {
33
+ const dir = path.resolve(rootDir, String(spaceId));
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ const filePath = resolveInside(rootDir, storePathOf(spaceId, file.name));
36
+ if (!filePath) return Promise.reject(/* @__PURE__ */ new Error("Font name escapes the store"));
37
+ fs.writeFileSync(filePath, file.body);
38
+ return Promise.resolve({
39
+ path: storePathOf(spaceId, file.name),
40
+ size: file.body.byteLength,
41
+ format: file.format
42
+ });
43
+ },
44
+ remove: (storePath) => {
45
+ const filePath = resolveInside(rootDir, storePath);
46
+ if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
47
+ return Promise.resolve();
48
+ }
49
+ });
50
+ /** Every store path a manifest names, so a deployment can tell what is still referenced from what is orphaned. */
51
+ var hostedPathsOf = (fonts) => fonts.flatMap((font) => font.source === "hosted" ? font.files.map((file) => file.path) : []);
52
+ //#endregion
53
+ export { createLocalFontStore, hostedPathsOf };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
- import { authFailureMessage } from "../auth/tokens.js";
2
1
  import { createAuthorizer } from "../auth/authorize.js";
2
+ import { authFailureMessage } from "../auth/tokens.js";
3
3
  //#region src/core/handlers/authMiddleware.ts
4
4
  /**
5
5
  * The authorizer, as middleware: `(req, res, next)`.
@@ -1,9 +1,17 @@
1
1
  import { HttpRoute, RouterLike } from './types';
2
2
  import { AuthApi } from '../auth/api';
3
+ import { Csrf } from '../auth/csrf';
3
4
  import { SessionCookies } from '../auth/session';
4
5
  export interface AuthRouteHandlersOptions {
5
6
  api: AuthApi;
6
7
  cookies: SessionCookies;
8
+ /**
9
+ * Enforces the CSRF check on these flows, and re-issues the token when a session is granted. Pass `auth.csrf`.
10
+ *
11
+ * Omitting it leaves the `/auth` surface unprotected, which is why `createAuth` hands one over by default — a
12
+ * deployment that means to go without says so with `csrf: false` there rather than by forgetting here.
13
+ */
14
+ csrf?: Csrf;
7
15
  /** Reports a flow that threw. Without it the failure goes to `console.error`; the caller still gets a 500. */
8
16
  onError?: (error: unknown, context: {
9
17
  method: string;
@@ -33,7 +41,7 @@ export interface AuthRouteHandlersOptions {
33
41
  * Which flows actually answer is decided by the adapters, not here — no `createAccount`, no signup, and the route
34
42
  * reports 404 rather than failing at runtime. Mounting all of them is correct for a deployment that offers three.
35
43
  */
36
- export declare const createAuthRouteHandlers: ({ api, cookies, onError }: AuthRouteHandlersOptions) => HttpRoute[];
44
+ export declare const createAuthRouteHandlers: ({ api, cookies, csrf, onError }: AuthRouteHandlersOptions) => HttpRoute[];
37
45
  /**
38
46
  * {@link createAuthRouteHandlers}, hung on anything with `get` and `post`.
39
47
  *
@@ -37,16 +37,17 @@ var carrier = (req) => ({
37
37
  * Which flows actually answer is decided by the adapters, not here — no `createAccount`, no signup, and the route
38
38
  * reports 404 rather than failing at runtime. Mounting all of them is correct for a deployment that offers three.
39
39
  */
40
- var createAuthRouteHandlers = ({ api, cookies, onError }) => authRoutes({
40
+ var createAuthRouteHandlers = ({ api, cookies, csrf, onError }) => authRoutes({
41
41
  api,
42
- cookies
42
+ cookies,
43
+ csrf
43
44
  }).map(({ method, path, handler }) => ({
44
45
  method,
45
46
  path,
46
47
  handle: async (req, res) => {
47
48
  try {
48
49
  const outcome = await handler(carrier(req));
49
- applySessionOutcome(req, res, outcome, cookies);
50
+ applySessionOutcome(req, res, outcome, cookies, csrf);
50
51
  res.status(outcome.ok ? outcome.status ?? 200 : outcome.status).json(outcome.body);
51
52
  } catch (error) {
52
53
  if (onError) onError(error, {