@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,340 @@
1
+ import { execute, selectOne, selectRows } from "./query.js";
2
+ import { tableNames } from "./config.js";
3
+ //#region src/modules/mysql/schema.ts
4
+ /**
5
+ * The tables, as SQL.
6
+ *
7
+ * Two decisions in here are worth stating, because getting either wrong fails much later and looks like something
8
+ * else entirely:
9
+ *
10
+ * **Tokens are `TEXT`, not `VARCHAR`.** A session credential is a JWT, and a JWT's length is set by its `aud` claim
11
+ * — one host per environment. A stack with seven audiences mints a 549-character refresh token, so a `VARCHAR(512)`
12
+ * kills every login with "value too long" on the deployment that has the most environments and nowhere else. They
13
+ * are indexed by prefix instead; the comparison is still exact, MySQL rechecks the full value against the row.
14
+ *
15
+ * **Unix seconds are `BIGINT`, not `INT`.** A signed `INT` runs out on 19 January 2038, and the credential this
16
+ * schema stores that is allowed to live forever is exactly the one that would be written with a far-future date.
17
+ */
18
+ var CHARSET = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
19
+ var step1 = (t) => [
20
+ `CREATE TABLE IF NOT EXISTS ${t.account} (
21
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
22
+ username VARCHAR(191) NOT NULL,
23
+ email VARCHAR(191) NOT NULL,
24
+ password_hash VARCHAR(255) NULL,
25
+ status ENUM('active','inactive','blocked') NOT NULL DEFAULT 'active',
26
+ verified TINYINT(1) NOT NULL DEFAULT 0,
27
+ reset_token VARCHAR(191) NULL,
28
+ validation_token VARCHAR(191) NULL,
29
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
31
+ PRIMARY KEY (id),
32
+ UNIQUE KEY account_username (username),
33
+ UNIQUE KEY account_email (email),
34
+ KEY account_reset_token (reset_token),
35
+ KEY account_validation_token (validation_token)
36
+ ) ${CHARSET}`,
37
+ `CREATE TABLE IF NOT EXISTS ${t.session} (
38
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
39
+ account_id INT UNSIGNED NOT NULL,
40
+ token TEXT NOT NULL,
41
+ expires_at BIGINT NOT NULL,
42
+ refresh_token TEXT NULL,
43
+ refresh_expires_at BIGINT NULL,
44
+ user_agent VARCHAR(255) NULL,
45
+ ip VARCHAR(45) NULL,
46
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
47
+ PRIMARY KEY (id),
48
+ KEY session_account (account_id),
49
+ KEY session_token (token(191)),
50
+ KEY session_refresh_token (refresh_token(191)),
51
+ KEY session_expires_at (expires_at),
52
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE
53
+ ) ${CHARSET}`,
54
+ `CREATE TABLE IF NOT EXISTS ${t.role} (
55
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
56
+ name VARCHAR(64) NOT NULL,
57
+ description VARCHAR(255) NULL,
58
+ PRIMARY KEY (id),
59
+ UNIQUE KEY role_name (name)
60
+ ) ${CHARSET}`,
61
+ `CREATE TABLE IF NOT EXISTS ${t.permission} (
62
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
63
+ name VARCHAR(64) NOT NULL,
64
+ description VARCHAR(255) NULL,
65
+ PRIMARY KEY (id),
66
+ UNIQUE KEY permission_name (name)
67
+ ) ${CHARSET}`,
68
+ `CREATE TABLE IF NOT EXISTS ${t.rolePermission} (
69
+ role_id INT UNSIGNED NOT NULL,
70
+ permission_id INT UNSIGNED NOT NULL,
71
+ PRIMARY KEY (role_id, permission_id),
72
+ KEY role_permission_permission (permission_id),
73
+ FOREIGN KEY (role_id) REFERENCES ${t.role} (id) ON DELETE CASCADE,
74
+ FOREIGN KEY (permission_id) REFERENCES ${t.permission} (id) ON DELETE CASCADE
75
+ ) ${CHARSET}`,
76
+ `CREATE TABLE IF NOT EXISTS ${t.accountRole} (
77
+ account_id INT UNSIGNED NOT NULL,
78
+ role_id INT UNSIGNED NOT NULL,
79
+ PRIMARY KEY (account_id, role_id),
80
+ KEY account_role_role (role_id),
81
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE,
82
+ FOREIGN KEY (role_id) REFERENCES ${t.role} (id) ON DELETE CASCADE
83
+ ) ${CHARSET}`,
84
+ `CREATE TABLE IF NOT EXISTS ${t.spaceMember} (
85
+ space_id BIGINT UNSIGNED NOT NULL,
86
+ account_id INT UNSIGNED NOT NULL,
87
+ role_id INT UNSIGNED NOT NULL,
88
+ is_owner TINYINT(1) NOT NULL DEFAULT 0,
89
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
90
+ PRIMARY KEY (space_id, account_id),
91
+ KEY space_member_account (account_id),
92
+ KEY space_member_role (role_id),
93
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE,
94
+ FOREIGN KEY (role_id) REFERENCES ${t.role} (id)
95
+ ) ${CHARSET}`,
96
+ `CREATE TABLE IF NOT EXISTS ${t.spaceToken} (
97
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
98
+ space_id BIGINT UNSIGNED NOT NULL,
99
+ token TEXT NOT NULL,
100
+ scope ENUM('render','agent') NOT NULL DEFAULT 'render',
101
+ is_default TINYINT(1) NOT NULL DEFAULT 0,
102
+ origins TEXT NULL,
103
+ expires_at BIGINT NULL,
104
+ account_id INT UNSIGNED NULL,
105
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
106
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
107
+ PRIMARY KEY (id),
108
+ KEY space_token_space (space_id),
109
+ KEY space_token_token (token(191)),
110
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE
111
+ ) ${CHARSET}`
112
+ ];
113
+ /**
114
+ * Everything an account can be besides a password: an identity at another provider, a second factor, and a
115
+ * one-time code. Added as step 2 rather than folded into step 1 — step 1 is released.
116
+ */
117
+ var step2 = (t) => [
118
+ `CREATE TABLE IF NOT EXISTS ${t.identity} (
119
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
120
+ account_id INT UNSIGNED NOT NULL,
121
+ provider VARCHAR(64) NOT NULL,
122
+ subject VARCHAR(191) NOT NULL,
123
+ email VARCHAR(191) NULL,
124
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
125
+ PRIMARY KEY (id),
126
+ UNIQUE KEY identity_provider_subject (provider, subject),
127
+ KEY identity_account (account_id),
128
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE
129
+ ) ${CHARSET}`,
130
+ `CREATE TABLE IF NOT EXISTS ${t.mfa} (
131
+ account_id INT UNSIGNED NOT NULL,
132
+ secret VARCHAR(191) NOT NULL,
133
+ confirmed_at BIGINT NULL,
134
+ recovery_codes TEXT NULL,
135
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
136
+ PRIMARY KEY (account_id),
137
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE
138
+ ) ${CHARSET}`,
139
+ `CREATE TABLE IF NOT EXISTS ${t.otp} (
140
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT,
141
+ account_id INT UNSIGNED NULL,
142
+ purpose VARCHAR(32) NOT NULL,
143
+ identifier VARCHAR(191) NOT NULL,
144
+ code_hash VARCHAR(191) NOT NULL,
145
+ expires_at BIGINT NOT NULL,
146
+ attempts INT UNSIGNED NOT NULL DEFAULT 0,
147
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
148
+ PRIMARY KEY (id),
149
+ KEY otp_lookup (purpose, identifier),
150
+ KEY otp_expires_at (expires_at),
151
+ FOREIGN KEY (account_id) REFERENCES ${t.account} (id) ON DELETE CASCADE
152
+ ) ${CHARSET}`
153
+ ];
154
+ /**
155
+ * DDL that can be run twice.
156
+ *
157
+ * MySQL has `CREATE TABLE IF NOT EXISTS` and nothing else: `ADD COLUMN IF NOT EXISTS` is MariaDB's, and a plain
158
+ * `ADD COLUMN` on a second run is an error. That matters because MySQL cannot roll back DDL — a step that fails
159
+ * halfway has already applied some of its statements, and the retry has to survive meeting them again.
160
+ *
161
+ * So a conditional is built out of what MySQL does have: look in `information_schema`, and prepare either the real
162
+ * statement or a no-op. Emitted as separate statements because the driver sends one at a time.
163
+ *
164
+ * Every step after the first should be written with these. `schemaStatements()` still returns plain SQL, so a
165
+ * deployment migrating with its own tool gets the same guards.
166
+ */
167
+ var QUOTE = String.fromCharCode(39);
168
+ /** A SQL string literal. The statement being embedded is DDL, so it routinely contains quotes of its own. */
169
+ var literal = (value) => `${QUOTE}${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}${QUOTE}`;
170
+ var conditional = (test, statement) => [
171
+ `SET @plitzi_ddl := (SELECT IF(${test}, ${literal(statement)}, 'DO 0'))`,
172
+ "PREPARE plitzi_ddl_stmt FROM @plitzi_ddl",
173
+ "EXECUTE plitzi_ddl_stmt",
174
+ "DEALLOCATE PREPARE plitzi_ddl_stmt"
175
+ ];
176
+ var columnExists = (table, column) => `SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${table}' AND COLUMN_NAME = '${column}'`;
177
+ var indexExists = (table, index) => `SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${table}' AND INDEX_NAME = '${index}'`;
178
+ /** Adds a column unless it is already there. `definition` is everything after the column name. */
179
+ var addColumn = (table, column, definition) => conditional(`(${columnExists(table, column)}) = 0`, `ALTER TABLE \`${table}\` ADD COLUMN \`${column}\` ${definition}`);
180
+ /** Adds an index unless one of that name exists. `definition` is the parenthesised column list. */
181
+ var addIndex = (table, index, definition) => conditional(`(${indexExists(table, index)}) = 0`, `ALTER TABLE \`${table}\` ADD INDEX \`${index}\` ${definition}`);
182
+ /** The bare name, for the `information_schema` lookups above — they compare against a string, not an identifier. */
183
+ var unquote = (name) => name.replace(/`/g, "");
184
+ /**
185
+ * An address the account asked to move to, and the token that will confirm it.
186
+ *
187
+ * Parked in two columns rather than applied, so the account keeps a working sign-in identifier until the new one
188
+ * answers a confirmation — see `confirmEmailChange`. Written with the idempotent helpers, being a step after the
189
+ * first: this is the migration path every later change to these tables takes.
190
+ */
191
+ var step3 = (t) => {
192
+ const account = unquote(t.account);
193
+ return [
194
+ ...addColumn(account, "pending_email", "VARCHAR(191) NULL AFTER email"),
195
+ ...addColumn(account, "pending_email_token", "VARCHAR(191) NULL AFTER pending_email"),
196
+ ...addIndex(account, "account_pending_email_token", "(pending_email_token)")
197
+ ];
198
+ };
199
+ /** Append only. A released version is never edited — the deployments that already ran it would never see the change. */
200
+ var STEPS = [
201
+ {
202
+ version: 1,
203
+ statements: step1
204
+ },
205
+ {
206
+ version: 2,
207
+ statements: step2
208
+ },
209
+ {
210
+ version: 3,
211
+ statements: step3
212
+ }
213
+ ];
214
+ var SCHEMA_VERSION = STEPS[STEPS.length - 1].version;
215
+ /**
216
+ * Every statement this schema is made of, in order — for a deployment that runs `autoMigrate: false` and applies
217
+ * DDL through its own migration tool, which is the normal arrangement anywhere the application's database user is
218
+ * not allowed to alter tables.
219
+ */
220
+ var schemaStatements = (tables, fromVersion = 0) => STEPS.filter((step) => step.version > fromVersion).flatMap((step) => step.statements(tables));
221
+ /**
222
+ * The order tables must be dropped in, children first. Foreign keys make the wrong order an error rather than a
223
+ * silent mess, but the error names a constraint and not what to do about it.
224
+ */
225
+ var DROP_ORDER = [
226
+ "otp",
227
+ "mfa",
228
+ "identity",
229
+ "spaceToken",
230
+ "spaceMember",
231
+ "accountRole",
232
+ "rolePermission",
233
+ "session",
234
+ "account",
235
+ "role",
236
+ "permission",
237
+ "schemaVersion"
238
+ ];
239
+ /** Which of OUR tables the database already has. Asked of `information_schema`, scoped to this database. */
240
+ var existingTables = async (pool, prefix) => {
241
+ const wanted = Object.values(tableNames(prefix));
242
+ const placeholders = wanted.map(() => "?").join(", ");
243
+ return (await selectRows(pool, `SELECT TABLE_NAME AS name FROM information_schema.TABLES
244
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${placeholders})`, wanted)).map((row) => row.name);
245
+ };
246
+ var readVersion = async (pool, tables) => {
247
+ return (await selectOne(pool, `SELECT version FROM ${tables.schemaVersion} WHERE component = 'sdk-server-auth'`))?.version ?? 0;
248
+ };
249
+ /**
250
+ * Brings the schema up to date, once.
251
+ *
252
+ * The advisory lock is not decoration: an all-in-one deployment starts several roles against one database at the
253
+ * same moment, and without it they race to create the same tables and to bump the same version row. `IF NOT EXISTS`
254
+ * makes the statements survive that; the version bump would not.
255
+ */
256
+ var migrate = async (pool, tables, options) => {
257
+ const { prefix, adoptExisting = false, log } = options;
258
+ /**
259
+ * A database this schema has never been migrated into, that already has tables with these names, is almost
260
+ * certainly somebody else's — `role`, `permission` and `session` are among the most ordinary names there are, and
261
+ * this module is explicitly meant to be pointed at a database with unrelated tables in it.
262
+ *
263
+ * `CREATE TABLE IF NOT EXISTS` would quietly ADOPT them, and the failure would surface much later as a query
264
+ * against columns that are not there. Refused instead, naming the fix.
265
+ */
266
+ if (!adoptExisting) {
267
+ const present = await existingTables(pool, prefix);
268
+ const migrated = present.includes(tableNames(prefix).schemaVersion) && await readVersion(pool, tables) > 0;
269
+ if (present.length > 0 && !migrated) throw new Error(`@plitzi/sdk-server/mysql: this database already has tables named ${present.join(", ")}, and they were not created by this schema. Point it at another database, set \`tablePrefix\` so the names cannot collide, or pass \`adoptExisting: true\` if they really are this schema’s and you are re-attaching.`);
270
+ }
271
+ await execute(pool, `CREATE TABLE IF NOT EXISTS ${tables.schemaVersion} (
272
+ component VARCHAR(64) NOT NULL,
273
+ version INT UNSIGNED NOT NULL,
274
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
275
+ PRIMARY KEY (component)
276
+ ) ${CHARSET}`);
277
+ const connection = await pool.getConnection();
278
+ const lockName = `${tables.schemaVersion.replace(/`/g, "")}_sdk_server`.slice(0, 64);
279
+ try {
280
+ if ((await selectOne(connection, "SELECT GET_LOCK(?, 30) AS acquired", [lockName]))?.acquired !== 1) throw new Error(`@plitzi/sdk-server/mysql: timed out waiting for the schema lock "${lockName}". Another process is migrating this database, or one died holding the lock.`);
281
+ try {
282
+ const current = await readVersion(pool, tables);
283
+ /**
284
+ * The database is ahead of the code. That means an older process has been started against a schema a newer
285
+ * one migrated, and letting it run is worse than refusing: it would read and write rows shaped by rules it
286
+ * does not have. The usual cause is a rollback that rolled back the code and not the database.
287
+ */
288
+ if (current > SCHEMA_VERSION) throw new Error(`@plitzi/sdk-server/mysql: this database is at auth schema version ${current} and this build only knows ${SCHEMA_VERSION}. Upgrade @plitzi/sdk-server, or point this process at a database it understands.`);
289
+ if (current === SCHEMA_VERSION) return current;
290
+ /**
291
+ * One step at a time, with the version written after EACH.
292
+ *
293
+ * MySQL cannot roll back DDL, so a step that fails halfway leaves the database between two versions. Bumping
294
+ * once at the end would make the next run start over from `current` and re-apply the statements that had
295
+ * already succeeded — which is exactly when a non-idempotent `ADD COLUMN` turns a recoverable failure into a
296
+ * stuck deployment. Recording each step means a retry resumes at the one that failed.
297
+ */
298
+ for (const step of STEPS.filter((candidate) => candidate.version > current)) {
299
+ for (const statement of step.statements(tables)) await execute(connection, statement);
300
+ await execute(connection, `INSERT INTO ${tables.schemaVersion} (component, version) VALUES ('sdk-server-auth', ?)
301
+ ON DUPLICATE KEY UPDATE version = VALUES(version)`, [step.version]);
302
+ log?.(`[mysql] auth schema migrated to version ${step.version}`);
303
+ }
304
+ return SCHEMA_VERSION;
305
+ } finally {
306
+ await connection.query("SELECT RELEASE_LOCK(?)", [lockName]);
307
+ }
308
+ } finally {
309
+ connection.release();
310
+ }
311
+ };
312
+ /**
313
+ * Removes this schema and nothing else.
314
+ *
315
+ * For a deployment that pointed the store at a database it shares with other things and has now stopped using
316
+ * `@plitzi/sdk-server` — dropping the database would take the rest of their application with it, and working out
317
+ * which nine tables were ours is not something anybody should have to do by hand at that moment.
318
+ *
319
+ * It drops **only** the names this schema owns, children first so the foreign keys allow it, and it refuses to
320
+ * touch a database it was never migrated into: without that check, a typo in `tablePrefix` turns this into a tool
321
+ * that deletes somebody else's `role` table.
322
+ */
323
+ var dropSchema = async (pool, tables, options) => {
324
+ const { prefix, force = false, log } = options;
325
+ const present = await existingTables(pool, prefix);
326
+ if (present.length === 0) return [];
327
+ if (!force && !present.includes(tableNames(prefix).schemaVersion)) throw new Error(`@plitzi/sdk-server/mysql: refusing to drop ${present.join(", ")} — there is no ${tableNames(prefix).schemaVersion} table, so this schema was never migrated into this database and those tables are somebody else’s. Check \`tablePrefix\`, or pass \`force: true\` if you are certain.`);
328
+ const dropped = [];
329
+ for (const key of DROP_ORDER) {
330
+ const name = tableNames(prefix)[key];
331
+ if (present.includes(name)) {
332
+ await execute(pool, `DROP TABLE IF EXISTS ${tables[key]}`);
333
+ dropped.push(name);
334
+ }
335
+ }
336
+ log?.(`[mysql] dropped ${dropped.length} auth tables: ${dropped.join(", ")}`);
337
+ return dropped;
338
+ };
339
+ //#endregion
340
+ export { SCHEMA_VERSION, addColumn, addIndex, dropSchema, migrate, schemaStatements };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { Tables } from './config';
2
+ import { Queryable } from './query';
3
+ /** One signed-in device, as somebody looking at their own account should see it. Never the credential itself. */
4
+ export interface SessionSummary {
5
+ id: number;
6
+ userAgent?: string;
7
+ ip?: string;
8
+ /** Unix seconds. */
9
+ createdAt: number;
10
+ expiresAt: number;
11
+ /** This is the session asking. A device list without it invites someone to revoke the one they are using. */
12
+ current: boolean;
13
+ }
14
+ /**
15
+ * The sessions of one account, as a thing that can be looked at and taken away one at a time.
16
+ *
17
+ * This is what a `session` table buys over a token pair on the account row, and it is the whole reason to pay for
18
+ * it: somebody who left themselves signed in on a machine they no longer have can end that session and keep the
19
+ * ones they are using. Revoking everything is `clearSession({ userId })` and is a blunter instrument.
20
+ */
21
+ export declare const createSessionStore: (db: Queryable, t: Tables) => {
22
+ list: (accountId: number, currentToken?: string) => Promise<SessionSummary[]>;
23
+ /** Scoped by account as well as id, or one account could end another's session by guessing a number. */
24
+ revoke: (accountId: number, sessionId: number) => Promise<boolean>;
25
+ /** "Sign out everywhere else" — the one thing to do from a device you still trust after losing one you do not. */
26
+ revokeOthers: (accountId: number, currentToken: string) => Promise<number>;
27
+ /**
28
+ * Delete what has aged out. Nothing depends on this for correctness — an expired session is refused by its
29
+ * expiry, not by its absence — so it is housekeeping a deployment schedules, not a step in any request.
30
+ */
31
+ purgeExpired: () => Promise<number>;
32
+ };
33
+ export type SessionStore = ReturnType<typeof createSessionStore>;
@@ -0,0 +1,46 @@
1
+ import { execute, selectRows } from "./query.js";
2
+ //#region src/modules/mysql/sessions.ts
3
+ /**
4
+ * The sessions of one account, as a thing that can be looked at and taken away one at a time.
5
+ *
6
+ * This is what a `session` table buys over a token pair on the account row, and it is the whole reason to pay for
7
+ * it: somebody who left themselves signed in on a machine they no longer have can end that session and keep the
8
+ * ones they are using. Revoking everything is `clearSession({ userId })` and is a blunter instrument.
9
+ */
10
+ var createSessionStore = (db, t) => {
11
+ const list = async (accountId, currentToken) => {
12
+ return (await selectRows(db, `SELECT id, token, user_agent, ip, created_at, expires_at
13
+ FROM ${t.session}
14
+ WHERE account_id = ?
15
+ ORDER BY created_at DESC`, [accountId])).map((row) => ({
16
+ id: row.id,
17
+ ...row.user_agent ? { userAgent: row.user_agent } : {},
18
+ ...row.ip ? { ip: row.ip } : {},
19
+ createdAt: Math.floor(row.created_at.getTime() / 1e3),
20
+ expiresAt: row.expires_at,
21
+ current: currentToken !== void 0 && row.token === currentToken
22
+ }));
23
+ };
24
+ return {
25
+ list,
26
+ /** Scoped by account as well as id, or one account could end another's session by guessing a number. */
27
+ revoke: async (accountId, sessionId) => {
28
+ return (await execute(db, `DELETE FROM ${t.session} WHERE id = ? AND account_id = ?`, [sessionId, accountId])).affectedRows > 0;
29
+ },
30
+ /** "Sign out everywhere else" — the one thing to do from a device you still trust after losing one you do not. */
31
+ revokeOthers: async (accountId, currentToken) => {
32
+ return (await execute(db, `DELETE FROM ${t.session} WHERE account_id = ? AND token <> ?`, [accountId, currentToken])).affectedRows;
33
+ },
34
+ /**
35
+ * Delete what has aged out. Nothing depends on this for correctness — an expired session is refused by its
36
+ * expiry, not by its absence — so it is housekeeping a deployment schedules, not a step in any request.
37
+ */
38
+ purgeExpired: async () => {
39
+ const now = Math.floor(Date.now() / 1e3);
40
+ return (await execute(db, `DELETE FROM ${t.session}
41
+ WHERE expires_at < ? AND (refresh_expires_at IS NULL OR refresh_expires_at < ?)`, [now, now])).affectedRows;
42
+ }
43
+ };
44
+ };
45
+ //#endregion
46
+ export { createSessionStore };
@@ -0,0 +1,11 @@
1
+ import { Tables } from './config';
2
+ import { Queryable } from './query';
3
+ import { SpaceTokenAdapters } from '../../core/auth/spaceTokens';
4
+ /**
5
+ * The stored half of a space credential.
6
+ *
7
+ * Note what is NOT here: nothing creates a row. The space-token API rotates, re-scopes and revokes credentials for
8
+ * a space that already has one, and issuing a space's first credential happens when the space itself is created —
9
+ * which is the deployment's event, not auth's. `createSpaceToken` on the store's `admin` surface does it.
10
+ */
11
+ export declare const createSpaceTokenStore: (db: Queryable, t: Tables) => SpaceTokenAdapters;
@@ -0,0 +1,59 @@
1
+ import { execute, selectOne, selectRows } from "./query.js";
2
+ //#region src/modules/mysql/spaceTokens.ts
3
+ var COLUMNS = "id, token, scope, is_default, origins, expires_at, account_id, created_at, updated_at";
4
+ var seconds = (value) => Math.floor(value.getTime() / 1e3);
5
+ var toRecord = (row) => ({
6
+ id: row.id,
7
+ token: row.token,
8
+ scope: row.scope,
9
+ isDefault: row.is_default === 1,
10
+ origins: (row.origins ?? "").split(",").map((entry) => entry.trim()).filter(Boolean),
11
+ expiresAt: row.expires_at,
12
+ userId: row.account_id,
13
+ createdAt: seconds(row.created_at),
14
+ updatedAt: seconds(row.updated_at)
15
+ });
16
+ /**
17
+ * The stored half of a space credential.
18
+ *
19
+ * Note what is NOT here: nothing creates a row. The space-token API rotates, re-scopes and revokes credentials for
20
+ * a space that already has one, and issuing a space's first credential happens when the space itself is created —
21
+ * which is the deployment's event, not auth's. `createSpaceToken` on the store's `admin` surface does it.
22
+ */
23
+ var createSpaceTokenStore = (db, t) => ({
24
+ loadDefault: async (spaceId) => {
25
+ const row = await selectOne(db, `SELECT ${COLUMNS} FROM ${t.spaceToken} WHERE space_id = ? AND is_default = 1 LIMIT 1`, [spaceId]);
26
+ return row ? toRecord(row) : void 0;
27
+ },
28
+ /** Scoped by space as well as id: a token id from another space must not resolve, or it is an IDOR. */
29
+ find: async (spaceId, tokenId) => {
30
+ const row = await selectOne(db, `SELECT ${COLUMNS} FROM ${t.spaceToken} WHERE space_id = ? AND id = ? LIMIT 1`, [spaceId, tokenId]);
31
+ return row ? toRecord(row) : void 0;
32
+ },
33
+ list: async (spaceId) => {
34
+ return (await selectRows(db, `SELECT ${COLUMNS} FROM ${t.spaceToken} WHERE space_id = ? ORDER BY is_default DESC, id ASC`, [spaceId])).map(toRecord);
35
+ },
36
+ /**
37
+ * Only the fields the caller named. `origins` and `expiresAt` are each meaningful as "unchanged" and as "cleared",
38
+ * and a blanket UPDATE cannot tell those apart — it would wipe a domain list on every plain rotation.
39
+ */
40
+ save: async (id, values) => {
41
+ const columns = ["token = ?"];
42
+ const params = [values.token];
43
+ if (values.origins !== void 0) {
44
+ columns.push("origins = ?");
45
+ params.push(values.origins.join(","));
46
+ }
47
+ if (values.expiresAt !== void 0) {
48
+ columns.push("expires_at = ?");
49
+ params.push(values.expiresAt);
50
+ }
51
+ params.push(id);
52
+ await execute(db, `UPDATE ${t.spaceToken} SET ${columns.join(", ")} WHERE id = ?`, params);
53
+ },
54
+ remove: async (id) => {
55
+ await execute(db, `DELETE FROM ${t.spaceToken} WHERE id = ?`, [id]);
56
+ }
57
+ });
58
+ //#endregion
59
+ export { createSpaceTokenStore };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { ActionsModule } from '../actions';
2
+ import { ActionLookups } from '../actions/types';
3
+ import { ConnectorLookups } from '../connectors/resolver';
4
+ import { SSRAdapters } from '@plitzi/sdk-shared';
5
+ /**
6
+ * `getRscData`, built from the producers the server was already given — connectors, actions, or both.
7
+ *
8
+ * Every deployment that served server-driven elements wrote this same adapter: await the render payload, check the
9
+ * schema opted into RSC, then hand `resolveRscData` a resolver over the lookups — all of which are this package's
10
+ * own exports, assembled by the consumer only because nothing assembled them here. That made the lookups a thing
11
+ * you passed twice, once as config and once folded into an adapter, and put a copy of the enabled check in every
12
+ * deployment.
13
+ *
14
+ * Either half may be absent: a space whose server elements all name actions configures no connectors, and an
15
+ * element naming a producer this deployment does not have resolves to nothing rather than to an error — the same
16
+ * answer it gets for a connector the space never created.
17
+ *
18
+ * A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
19
+ */
20
+ export declare const connectorRscData: (lookups?: ConnectorLookups, actions?: {
21
+ lookups: ActionLookups;
22
+ module: ActionsModule;
23
+ },
24
+ /** The deployment's per-element ceiling, when it set one. `resolveRscData` decides the default. */
25
+ elementTimeoutMs?: number) => NonNullable<SSRAdapters["getRscData"]>;
@@ -0,0 +1,51 @@
1
+ import { createActionResolver } from "../actions/runtime/renderResolver.js";
2
+ import { createConnectorResolver } from "../connectors/resolver.js";
3
+ import { resolveRscData } from "./resolveRscData.js";
4
+ //#region src/modules/rsc/connectorRscData.ts
5
+ /**
6
+ * `getRscData`, built from the producers the server was already given — connectors, actions, or both.
7
+ *
8
+ * Every deployment that served server-driven elements wrote this same adapter: await the render payload, check the
9
+ * schema opted into RSC, then hand `resolveRscData` a resolver over the lookups — all of which are this package's
10
+ * own exports, assembled by the consumer only because nothing assembled them here. That made the lookups a thing
11
+ * you passed twice, once as config and once folded into an adapter, and put a copy of the enabled check in every
12
+ * deployment.
13
+ *
14
+ * Either half may be absent: a space whose server elements all name actions configures no connectors, and an
15
+ * element naming a producer this deployment does not have resolves to nothing rather than to an error — the same
16
+ * answer it gets for a connector the space never created.
17
+ *
18
+ * A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
19
+ */
20
+ var connectorRscData = (lookups, actions, elementTimeoutMs) => {
21
+ const resolveConnector = lookups ? createConnectorResolver(lookups) : void 0;
22
+ const resolveAction = actions ? createActionResolver(actions.lookups, actions.module) : void 0;
23
+ /**
24
+ * An element names ONE producer, and which one decides how its data is fetched.
25
+ *
26
+ * A connector is the declarative read and stays the default; an action is the read a manifest cannot express.
27
+ * Checked in that order so an element carrying both — a connector element an author later pointed at an action —
28
+ * keeps resolving the way its page already renders, instead of silently changing under them.
29
+ */
30
+ const resolveElement = async (context) => {
31
+ const attributes = context.element.attributes;
32
+ if (attributes.connector) return resolveConnector ? resolveConnector(context) : void 0;
33
+ return attributes.action && resolveAction ? resolveAction(context) : void 0;
34
+ };
35
+ return async ({ req, spaceId, environment, user, ids, loadOfflineData }) => {
36
+ const offlineData = await loadOfflineData();
37
+ if (!offlineData?.schema.rsc?.enabled) return {};
38
+ return resolveRscData({
39
+ schema: offlineData.schema,
40
+ req,
41
+ spaceId,
42
+ environment,
43
+ user,
44
+ ids,
45
+ resolveElement,
46
+ ...elementTimeoutMs === void 0 ? {} : { timeoutMs: elementTimeoutMs }
47
+ });
48
+ };
49
+ };
50
+ //#endregion
51
+ export { connectorRscData };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_TTL_MS } from "../../helpers/cache/defaults.js";
2
2
  import { buildRscCacheKey } from "../../helpers/cache/keys.js";
3
+ import { readDraftToken } from "../../core/previewToken.js";
3
4
  import { createOfflineDataLoader } from "../../helpers/offlineDataLoader.js";
4
5
  //#region src/modules/rsc/handler.ts
5
6
  /** Bounds the reflected location: it only ever selects one of this space's own pages, but it is still input. */
@@ -72,12 +73,24 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
72
73
  const ids = idsRaw ? idsRaw.split(",").filter(Boolean).slice(0, 50).map((id) => id.slice(0, 128)) : void 0;
73
74
  const idsParam = ids?.join(",");
74
75
  const pageRequest = withPageLocation(req);
76
+ /**
77
+ * Whether this refresh belongs to somebody looking at a draft.
78
+ *
79
+ * The page render already refuses to meter or cache a draft, and before draft SESSIONS existed that was the whole
80
+ * story: a one-shot token was spent by the render, so no refresh could ever carry one. A reusable token can, and a
81
+ * page left open in a preview asks for data on its own — so without this, iterating on unsaved work would be
82
+ * billed as live traffic and would show up on the live view as visitors nobody has.
83
+ *
84
+ * The token is not resolved here: whether the draft is still in the store decides what the PAGE renders, and a
85
+ * refresh that arrives a second after it expired is still part of the same preview.
86
+ */
87
+ const previewing = readDraftToken(req) !== void 0;
75
88
  const ttlMs = config.rsc?.cacheTtlMs ?? DEFAULT_TTL_MS.rsc;
76
89
  const isAuthenticated = !!req.ctx.user;
77
- const cacheControl = environment === "main" ? "no-store" : isAuthenticated ? `private, max-age=${Math.floor(ttlMs / 1e3)}` : `public, max-age=${Math.floor(ttlMs / 1e3)}`;
78
- const cacheKey = environment !== "main" ? buildRscCacheKey(spaceId, environment, revision, req.ctx.user?.id, idsParam, req) : void 0;
90
+ const cacheControl = environment === "main" || previewing ? "no-store" : isAuthenticated ? `private, max-age=${Math.floor(ttlMs / 1e3)}` : `public, max-age=${Math.floor(ttlMs / 1e3)}`;
91
+ const cacheKey = environment !== "main" && !previewing ? buildRscCacheKey(spaceId, environment, revision, req.ctx.user?.id, idsParam, req) : void 0;
79
92
  const cached = cacheKey ? cache?.get(cacheKey) : void 0;
80
- await meterRsc(req, config, spaceId, environment, revision, !!cached);
93
+ if (!previewing) await meterRsc(req, config, spaceId, environment, revision, !!cached);
81
94
  if (cached) {
82
95
  res.setHeader("Content-Type", "application/json; charset=utf-8");
83
96
  res.setHeader("Cache-Control", cacheControl);
@@ -0,0 +1,14 @@
1
+ import { Schema, SSRUser } from '@plitzi/sdk-shared';
2
+ export type RscPageMatch = {
3
+ pageId: string;
4
+ routeParams: Record<string, string | undefined>;
5
+ };
6
+ /**
7
+ * Which page of the schema a URL addresses, matched with the same matcher the client router uses — so the element
8
+ * set and the route params a read resolves against are the ones the browser will see.
9
+ *
10
+ * Its own function because two callers need the answer: the read itself, and the page render deciding beforehand
11
+ * whether there is anything on this page worth asking an adapter for. `undefined` when the URL matches no page,
12
+ * which is also nothing to resolve.
13
+ */
14
+ export declare const matchRscPage: (schema: Schema, path: string, user: SSRUser | undefined) => RscPageMatch | undefined;
@@ -0,0 +1,26 @@
1
+ import { getPaths, matchRoutePath } from "@plitzi/sdk-shared/navigation";
2
+ //#region src/modules/rsc/matchRscPage.ts
3
+ /**
4
+ * Which page of the schema a URL addresses, matched with the same matcher the client router uses — so the element
5
+ * set and the route params a read resolves against are the ones the browser will see.
6
+ *
7
+ * Its own function because two callers need the answer: the read itself, and the page render deciding beforehand
8
+ * whether there is anything on this page worth asking an adapter for. `undefined` when the URL matches no page,
9
+ * which is also nothing to resolve.
10
+ */
11
+ var matchRscPage = (schema, path, user) => {
12
+ const pages = schema.pages.reduce((acum, pageId) => {
13
+ const page = schema.flat[pageId];
14
+ if (page) acum[pageId] = page;
15
+ return acum;
16
+ }, {});
17
+ const paths = getPaths(pages, schema.pageFolders, !!user);
18
+ const { pageId, pathMatch } = matchRoutePath(paths, path, !!user);
19
+ if (!pageId) return;
20
+ return {
21
+ pageId,
22
+ routeParams: pathMatch?.params ?? {}
23
+ };
24
+ };
25
+ //#endregion
26
+ export { matchRscPage };