@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
@@ -0,0 +1,365 @@
1
+ import { execute, selectOne, selectRows } from "./query.js";
2
+ import { createSessionStore } from "./sessions.js";
3
+ //#region src/modules/mysql/accounts.ts
4
+ var ACCOUNT_COLUMNS = "a.id, a.username, a.email, a.password_hash, a.status, a.verified";
5
+ var toRecord = (row) => ({
6
+ id: row.id,
7
+ username: row.username,
8
+ email: row.email,
9
+ active: row.status === "active",
10
+ verified: row.verified === 1,
11
+ ...row.password_hash ? { passwordHash: row.password_hash } : {},
12
+ ...row.refresh_expires_at !== null && row.refresh_expires_at !== void 0 ? { refreshExpiresAt: row.refresh_expires_at } : {},
13
+ ...row.session_started_at !== null && row.session_started_at !== void 0 ? { sessionStartedAt: row.session_started_at } : {}
14
+ });
15
+ /**
16
+ * An opaque single-use token, as a lookup key. Empty is not one.
17
+ *
18
+ * Clearing a reset token is written as `setResetToken(userId, '')` — the flows say so — and an empty string stored
19
+ * as an empty string is a value that MATCHES: one lookup would then return whichever account was cleared first, and
20
+ * a password-reset link with no token in it would work. Stored as NULL, and never looked up when blank.
21
+ */
22
+ var asLookup = (token) => token.trim() === "" ? void 0 : token;
23
+ var accessRows = (rows) => {
24
+ const roles = /* @__PURE__ */ new Set();
25
+ const permissions = /* @__PURE__ */ new Set();
26
+ for (const row of rows) {
27
+ roles.add(row.role);
28
+ if (row.permission) permissions.add(row.permission);
29
+ }
30
+ return {
31
+ roles: [...roles],
32
+ permissions: [...permissions]
33
+ };
34
+ };
35
+ var createAccountStore = (db, t) => {
36
+ const sessions = createSessionStore(db, t);
37
+ const findBy = (column, value) => selectOne(db, `SELECT ${ACCOUNT_COLUMNS} FROM ${t.account} a WHERE a.${column} = ? LIMIT 1`, [value]);
38
+ const loadAccess = async (userId) => accessRows(await selectRows(db, `SELECT r.name AS role, p.name AS permission
39
+ FROM ${t.accountRole} ar
40
+ JOIN ${t.role} r ON r.id = ar.role_id
41
+ LEFT JOIN ${t.rolePermission} rp ON rp.role_id = r.id
42
+ LEFT JOIN ${t.permission} p ON p.id = rp.permission_id
43
+ WHERE ar.account_id = ?`, [userId]));
44
+ return {
45
+ /**
46
+ * Looked up BY THE TOKEN, which is what makes a session revocable: writing a new pair over the row retires the
47
+ * previous one, so a credential that no longer matches is dead however valid its signature still is.
48
+ *
49
+ * A suspended account resolves to nothing at all rather than to an actor the caller then has to remember to
50
+ * check — deactivating somebody has to end the session they are already holding, not just the next login.
51
+ */
52
+ findAccountByToken: async (token) => {
53
+ const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, s.expires_at AS session_expires_at
54
+ FROM ${t.session} s
55
+ JOIN ${t.account} a ON a.id = s.account_id
56
+ WHERE s.token = ? AND a.status = 'active'
57
+ LIMIT 1`, [token]);
58
+ if (!row) return;
59
+ const { roles, permissions } = await loadAccess(row.id);
60
+ return {
61
+ id: row.id,
62
+ username: row.username,
63
+ email: row.email,
64
+ verified: row.verified === 1,
65
+ roles,
66
+ permissions,
67
+ token,
68
+ expiresAt: row.session_expires_at ?? 0
69
+ };
70
+ },
71
+ findSpaceToken: async (token) => {
72
+ const row = await selectOne(db, `SELECT space_id, scope, account_id FROM ${t.spaceToken} WHERE token = ? LIMIT 1`, [token]);
73
+ if (!row) return;
74
+ return {
75
+ spaceId: row.space_id,
76
+ scope: row.scope,
77
+ ...row.account_id !== null ? { userId: row.account_id } : {}
78
+ };
79
+ },
80
+ findMembership: async (userId, spaceId) => {
81
+ const rows = await selectRows(db, `SELECT sm.is_owner, r.name AS role, p.name AS permission
82
+ FROM ${t.spaceMember} sm
83
+ JOIN ${t.role} r ON r.id = sm.role_id
84
+ LEFT JOIN ${t.rolePermission} rp ON rp.role_id = r.id
85
+ LEFT JOIN ${t.permission} p ON p.id = rp.permission_id
86
+ WHERE sm.account_id = ? AND sm.space_id = ?`, [userId, spaceId]);
87
+ if (rows.length === 0) return;
88
+ const { roles, permissions } = accessRows(rows);
89
+ return {
90
+ spaceId,
91
+ isOwner: rows[0].is_owner === 1,
92
+ role: roles[0],
93
+ permissions
94
+ };
95
+ },
96
+ loadAccess,
97
+ /**
98
+ * A sign-in adds a session; a renewal replaces the one it names.
99
+ *
100
+ * The replace is an UPDATE on purpose, not a delete-then-insert: it keeps the row's identity, so the device
101
+ * somebody sees in their session list is still the same device after it renews, and a session revoked in the
102
+ * instant between the two cannot come back. When the update matches nothing — the session was revoked while
103
+ * this renewal was in flight — **no row is written**. The caller walks away with a credential that is not
104
+ * stored, which is a signed-out user rather than a revoked session resurrected.
105
+ */
106
+ saveSession: async (userId, session, context) => {
107
+ const replaces = context?.replaces;
108
+ if (replaces?.refreshToken !== void 0 || replaces?.accessToken !== void 0) {
109
+ const column = replaces.refreshToken !== void 0 ? "refresh_token" : "token";
110
+ const value = replaces.refreshToken ?? replaces.accessToken;
111
+ await execute(db, `UPDATE ${t.session}
112
+ SET token = ?, expires_at = ?, refresh_token = ?, refresh_expires_at = ?
113
+ WHERE ${column} = ? AND account_id = ?`, [
114
+ session.token,
115
+ session.expiresAt,
116
+ session.refreshToken ?? null,
117
+ session.refreshExpiresAt ?? null,
118
+ value,
119
+ userId
120
+ ]);
121
+ return;
122
+ }
123
+ await execute(db, `INSERT INTO ${t.session} (account_id, token, expires_at, refresh_token, refresh_expires_at, user_agent, ip)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
125
+ userId,
126
+ session.token,
127
+ session.expiresAt,
128
+ session.refreshToken ?? null,
129
+ session.refreshExpiresAt ?? null,
130
+ context?.client?.userAgent?.slice(0, 255) ?? null,
131
+ context?.client?.ip ?? null
132
+ ]);
133
+ },
134
+ /**
135
+ * By whichever half the caller is holding, or every session of an account.
136
+ *
137
+ * An empty target clears NOTHING — a `WHERE` assembled from no conditions is a `WHERE` that matches every row,
138
+ * and the statement that would sign out an entire deployment must not be reachable by leaving an argument off.
139
+ */
140
+ clearSession: async (target) => {
141
+ const conditions = [];
142
+ const params = [];
143
+ if (target.userId !== void 0) {
144
+ conditions.push("account_id = ?");
145
+ params.push(target.userId);
146
+ }
147
+ if (target.accessToken !== void 0) {
148
+ conditions.push("token = ?");
149
+ params.push(target.accessToken);
150
+ }
151
+ if (target.refreshToken !== void 0) {
152
+ conditions.push("refresh_token = ?");
153
+ params.push(target.refreshToken);
154
+ }
155
+ if (conditions.length === 0) return;
156
+ await execute(db, `DELETE FROM ${t.session} WHERE ${conditions.join(" OR ")}`, params);
157
+ },
158
+ findByUsername: async (username) => {
159
+ const row = await findBy("username", username);
160
+ return row ? toRecord(row) : void 0;
161
+ },
162
+ findByEmail: async (email) => {
163
+ const row = await findBy("email", email);
164
+ return row ? toRecord(row) : void 0;
165
+ },
166
+ /**
167
+ * `refreshExpiresAt` comes off the ROW. A store that keeps it inside a session object and forgets to lift it out
168
+ * has every renewal refused as expired — which does not fail at login, it fails a day later, silently, when the
169
+ * access token ages out and the session that looked fine simply ends.
170
+ */
171
+ findByRefreshToken: async (token) => {
172
+ const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, s.refresh_expires_at, UNIX_TIMESTAMP(s.created_at) AS session_started_at
173
+ FROM ${t.session} s
174
+ JOIN ${t.account} a ON a.id = s.account_id
175
+ WHERE s.refresh_token = ?
176
+ LIMIT 1`, [token]);
177
+ return row ? toRecord(row) : void 0;
178
+ },
179
+ createAccount: async (account) => {
180
+ return {
181
+ id: (await execute(db, `INSERT INTO ${t.account} (username, email, password_hash) VALUES (?, ?, ?)`, [
182
+ account.username,
183
+ account.email,
184
+ account.passwordHash
185
+ ])).insertId,
186
+ username: account.username,
187
+ email: account.email,
188
+ active: true,
189
+ verified: false,
190
+ passwordHash: account.passwordHash
191
+ };
192
+ },
193
+ setPassword: async (userId, passwordHash) => {
194
+ await execute(db, `UPDATE ${t.account} SET password_hash = ? WHERE id = ?`, [passwordHash, userId]);
195
+ },
196
+ setResetToken: async (userId, token) => {
197
+ await execute(db, `UPDATE ${t.account} SET reset_token = ? WHERE id = ?`, [asLookup(token) ?? null, userId]);
198
+ },
199
+ findByResetToken: async (token) => {
200
+ const lookup = asLookup(token);
201
+ if (!lookup) return;
202
+ const row = await findBy("reset_token", lookup);
203
+ return row ? toRecord(row) : void 0;
204
+ },
205
+ setValidationToken: async (userId, token) => {
206
+ await execute(db, `UPDATE ${t.account} SET validation_token = ? WHERE id = ?`, [asLookup(token) ?? null, userId]);
207
+ },
208
+ findByValidationToken: async (token) => {
209
+ const lookup = asLookup(token);
210
+ if (!lookup) return;
211
+ const row = await findBy("validation_token", lookup);
212
+ return row ? toRecord(row) : void 0;
213
+ },
214
+ /**
215
+ * The address is parked in its own column, so nothing that resolves an account by email can find it: it is not
216
+ * this account's address until it is confirmed, and a lookup that matched it would let an unconfirmed change
217
+ * take over a sign-in identifier.
218
+ */
219
+ setPendingEmail: async (userId, email, token) => {
220
+ await execute(db, `UPDATE ${t.account} SET pending_email = ?, pending_email_token = ? WHERE id = ?`, [
221
+ email,
222
+ asLookup(token) ?? null,
223
+ userId
224
+ ]);
225
+ },
226
+ findByPendingEmail: async (token) => {
227
+ const lookup = asLookup(token);
228
+ if (!lookup) return;
229
+ const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, a.pending_email FROM ${t.account} a WHERE a.pending_email_token = ? LIMIT 1`, [lookup]);
230
+ return row?.pending_email ? {
231
+ account: toRecord(row),
232
+ email: row.pending_email
233
+ } : void 0;
234
+ },
235
+ clearPendingEmail: async (userId) => {
236
+ await execute(db, `UPDATE ${t.account} SET pending_email = NULL, pending_email_token = NULL WHERE id = ?`, [userId]);
237
+ },
238
+ setVerified: async (userId, verified) => {
239
+ await execute(db, `UPDATE ${t.account} SET verified = ?, validation_token = NULL WHERE id = ?`, [verified ? 1 : 0, userId]);
240
+ },
241
+ findById: async (userId) => {
242
+ const row = await findBy("id", userId);
243
+ return row ? toRecord(row) : void 0;
244
+ },
245
+ /**
246
+ * Only the columns the caller named. A blanket UPDATE cannot tell "leave the email alone" from "clear it", and
247
+ * a profile form that posts one field would wipe the other.
248
+ */
249
+ updateAccount: async (userId, changes) => {
250
+ const columns = [];
251
+ const params = [];
252
+ if (changes.username !== void 0) {
253
+ columns.push("username = ?");
254
+ params.push(changes.username);
255
+ }
256
+ if (changes.email !== void 0) {
257
+ columns.push("email = ?");
258
+ params.push(changes.email);
259
+ }
260
+ if (columns.length > 0) await execute(db, `UPDATE ${t.account} SET ${columns.join(", ")} WHERE id = ?`, [...params, userId]);
261
+ const row = await findBy("id", userId);
262
+ if (!row) throw new Error(`@plitzi/sdk-server/mysql: account ${userId} disappeared while being updated.`);
263
+ return toRecord(row);
264
+ },
265
+ setStatus: async (userId, status) => {
266
+ await execute(db, `UPDATE ${t.account} SET status = ? WHERE id = ?`, [status, userId]);
267
+ },
268
+ /**
269
+ * The row goes, and its sessions with it — the foreign key cascades, so there is no window in which a deleted
270
+ * account still has a working credential. A deployment that has to keep the row for its content or its invoices
271
+ * replaces this adapter with one that anonymises instead; the sessions still have to go.
272
+ */
273
+ deleteAccount: async (userId) => {
274
+ await execute(db, `DELETE FROM ${t.account} WHERE id = ?`, [userId]);
275
+ },
276
+ listAccounts: async (query) => {
277
+ const conditions = [];
278
+ const params = [];
279
+ if (query.search) {
280
+ conditions.push("(a.username LIKE ? OR a.email LIKE ?)");
281
+ const like = `%${query.search.replace(/[%_\\]/g, (character) => `\\${character}`)}%`;
282
+ params.push(like, like);
283
+ }
284
+ if (query.status) {
285
+ conditions.push("a.status = ?");
286
+ params.push(query.status);
287
+ }
288
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
289
+ const limit = Math.min(Math.max(query.limit ?? 50, 1), 200);
290
+ const offset = Math.max(query.offset ?? 0, 0);
291
+ const counted = await selectOne(db, `SELECT COUNT(*) AS total FROM ${t.account} a ${where}`, params);
292
+ return {
293
+ accounts: (await selectRows(db, `SELECT ${ACCOUNT_COLUMNS} FROM ${t.account} a ${where} ORDER BY a.id ASC LIMIT ? OFFSET ?`, [
294
+ ...params,
295
+ limit,
296
+ offset
297
+ ])).map(toRecord),
298
+ total: counted?.total ?? 0
299
+ };
300
+ },
301
+ /** Exactly these roles, which means the ones not named are taken away. Unknown names are ignored, not created. */
302
+ setRoles: async (userId, roles) => {
303
+ await execute(db, `DELETE FROM ${t.accountRole} WHERE account_id = ?`, [userId]);
304
+ if (roles.length === 0) return;
305
+ const placeholders = roles.map(() => "?").join(", ");
306
+ await execute(db, `INSERT INTO ${t.accountRole} (account_id, role_id)
307
+ SELECT ?, id FROM ${t.role} WHERE name IN (${placeholders})`, [userId, ...roles]);
308
+ },
309
+ saveOtp: async (code) => {
310
+ await execute(db, `DELETE FROM ${t.otp} WHERE purpose = ? AND identifier = ?`, [code.purpose, code.identifier]);
311
+ await execute(db, `INSERT INTO ${t.otp} (account_id, purpose, identifier, code_hash, expires_at) VALUES (?, ?, ?, ?, ?)`, [
312
+ code.userId ?? null,
313
+ code.purpose,
314
+ code.identifier,
315
+ code.codeHash,
316
+ code.expiresAt
317
+ ]);
318
+ },
319
+ findOtp: async (purpose, identifier) => {
320
+ const row = await selectOne(db, `SELECT id, code_hash, expires_at, account_id FROM ${t.otp}
321
+ WHERE purpose = ? AND identifier = ? ORDER BY id DESC LIMIT 1`, [purpose, identifier]);
322
+ if (!row) return;
323
+ return {
324
+ id: row.id,
325
+ codeHash: row.code_hash,
326
+ expiresAt: row.expires_at,
327
+ ...row.account_id !== null ? { userId: row.account_id } : {}
328
+ };
329
+ },
330
+ consumeOtp: async (id) => {
331
+ await execute(db, `DELETE FROM ${t.otp} WHERE id = ?`, [id]);
332
+ },
333
+ /**
334
+ * The second factor. `recovery_codes` is a newline-separated list of digests, not a JSON blob: it is only ever
335
+ * read whole and written whole, and a text column that a human can read in a crisis beats one they cannot.
336
+ */
337
+ loadMfa: async (userId) => {
338
+ const row = await selectOne(db, `SELECT secret, confirmed_at, recovery_codes FROM ${t.mfa} WHERE account_id = ? LIMIT 1`, [userId]);
339
+ if (!row) return;
340
+ return {
341
+ secret: row.secret,
342
+ ...row.confirmed_at !== null ? { confirmedAt: row.confirmed_at } : {},
343
+ recoveryCodes: (row.recovery_codes ?? "").split("\n").filter(Boolean)
344
+ };
345
+ },
346
+ saveMfa: async (userId, mfaRecord) => {
347
+ await execute(db, `INSERT INTO ${t.mfa} (account_id, secret, confirmed_at, recovery_codes) VALUES (?, ?, ?, ?)
348
+ ON DUPLICATE KEY UPDATE
349
+ secret = VALUES(secret), confirmed_at = VALUES(confirmed_at), recovery_codes = VALUES(recovery_codes)`, [
350
+ userId,
351
+ mfaRecord.secret,
352
+ mfaRecord.confirmedAt ?? null,
353
+ (mfaRecord.recoveryCodes ?? []).join("\n")
354
+ ]);
355
+ },
356
+ deleteMfa: async (userId) => {
357
+ await execute(db, `DELETE FROM ${t.mfa} WHERE account_id = ?`, [userId]);
358
+ },
359
+ listSessions: (userId, currentToken) => sessions.list(userId, currentToken),
360
+ revokeSession: (userId, sessionId) => sessions.revoke(userId, sessionId),
361
+ revokeOtherSessions: (userId, currentToken) => sessions.revokeOthers(userId, currentToken)
362
+ };
363
+ };
364
+ //#endregion
365
+ export { createAccountStore };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { Tables } from './config';
2
+ import { SpaceScope } from '../../core/auth/tokens';
3
+ import { Pool } from 'mysql2/promise';
4
+ export interface AccountSeed {
5
+ username: string;
6
+ email: string;
7
+ /** Plain text, hashed here. Omit for an account that signs in through an identity provider and has no password. */
8
+ password?: string;
9
+ verified?: boolean;
10
+ /** Global roles. They must exist — `ensureRole` first, or an unknown name is a mistake worth failing on. */
11
+ roles?: string[];
12
+ }
13
+ export interface SpaceTokenSeed {
14
+ token: string;
15
+ scope?: SpaceScope;
16
+ isDefault?: boolean;
17
+ origins?: string[];
18
+ expiresAt?: number | null;
19
+ accountId?: number;
20
+ }
21
+ /**
22
+ * Everything that puts rows in these tables that no request ever will.
23
+ *
24
+ * The auth flows read accounts, roles and memberships; almost none of them create one. Signing up creates an
25
+ * account and that is the whole of it — a role, a permission, a space membership and a space's first credential all
26
+ * come from somewhere else: a seed script, an admin screen, the moment a space is created. That somewhere else
27
+ * needs statements, and a deployment writing them by hand against a schema it did not design is a deployment about
28
+ * to get a join wrong.
29
+ *
30
+ * Every operation is idempotent, because the place they are called from is usually a startup path that runs again.
31
+ */
32
+ export declare const createAdmin: (pool: Pool, t: Tables) => {
33
+ ensurePermissions: (names: string[]) => Promise<void>;
34
+ ensureRole: (name: string, options?: {
35
+ permissions?: string[];
36
+ description?: string;
37
+ }) => Promise<number>;
38
+ grantRole: (accountId: number, role: string) => Promise<void>;
39
+ revokeRole: (accountId: number, role: string) => Promise<void>;
40
+ ensureAccount: (seed: AccountSeed) => Promise<number>;
41
+ /** Membership of one space, with the role that decides what they may do inside it. */
42
+ addMember: (spaceId: number, accountId: number, role: string, options?: {
43
+ owner?: boolean;
44
+ }) => Promise<void>;
45
+ removeMember: (spaceId: number, accountId: number) => Promise<void>;
46
+ listMembers: (spaceId: number) => Promise<{
47
+ accountId: number;
48
+ username: string;
49
+ role: string;
50
+ owner: boolean;
51
+ }[]>;
52
+ /**
53
+ * A space's credential row. Replaces the default one rather than adding a second: exactly one row per space
54
+ * carries `is_default`, and two of them make `loadDefault` answer whichever the database happened to return.
55
+ */
56
+ createSpaceToken: (spaceId: number, seed: SpaceTokenSeed) => Promise<number>;
57
+ };
58
+ export type MysqlAdmin = ReturnType<typeof createAdmin>;
@@ -0,0 +1,133 @@
1
+ import { hashPassword } from "../../core/auth/passwords.js";
2
+ import { execute, selectOne, selectRows } from "./query.js";
3
+ //#region src/modules/mysql/admin.ts
4
+ /**
5
+ * Everything that puts rows in these tables that no request ever will.
6
+ *
7
+ * The auth flows read accounts, roles and memberships; almost none of them create one. Signing up creates an
8
+ * account and that is the whole of it — a role, a permission, a space membership and a space's first credential all
9
+ * come from somewhere else: a seed script, an admin screen, the moment a space is created. That somewhere else
10
+ * needs statements, and a deployment writing them by hand against a schema it did not design is a deployment about
11
+ * to get a join wrong.
12
+ *
13
+ * Every operation is idempotent, because the place they are called from is usually a startup path that runs again.
14
+ */
15
+ var createAdmin = (pool, t) => {
16
+ const idOf = async (table, name) => {
17
+ return (await selectOne(pool, `SELECT id FROM ${table} WHERE name = ? LIMIT 1`, [name]))?.id;
18
+ };
19
+ const ensurePermissions = async (names) => {
20
+ if (names.length === 0) return;
21
+ const values = names.map(() => "(?)").join(", ");
22
+ await execute(pool, `INSERT IGNORE INTO ${t.permission} (name) VALUES ${values}`, names);
23
+ };
24
+ /**
25
+ * Declarative: the permission list given is the permission list the role ends up with. A role that only ever
26
+ * gained permissions could never lose one, and the first thing anybody does with a role is take something away.
27
+ */
28
+ const ensureRole = async (name, options = {}) => {
29
+ const { permissions = [], description } = options;
30
+ await execute(pool, `INSERT INTO ${t.role} (name, description) VALUES (?, ?)
31
+ ON DUPLICATE KEY UPDATE description = COALESCE(VALUES(description), description)`, [name, description ?? null]);
32
+ const roleId = await idOf(t.role, name);
33
+ if (roleId === void 0) throw new Error(`@plitzi/sdk-server/mysql: role "${name}" could not be read back after being written.`);
34
+ await ensurePermissions(permissions);
35
+ const connection = await pool.getConnection();
36
+ try {
37
+ await connection.beginTransaction();
38
+ await execute(connection, `DELETE FROM ${t.rolePermission} WHERE role_id = ?`, [roleId]);
39
+ if (permissions.length > 0) {
40
+ const placeholders = permissions.map(() => "?").join(", ");
41
+ await execute(connection, `INSERT INTO ${t.rolePermission} (role_id, permission_id)
42
+ SELECT ?, id FROM ${t.permission} WHERE name IN (${placeholders})`, [roleId, ...permissions]);
43
+ }
44
+ await connection.commit();
45
+ } catch (error) {
46
+ await connection.rollback();
47
+ throw error;
48
+ } finally {
49
+ connection.release();
50
+ }
51
+ return roleId;
52
+ };
53
+ const requireRole = async (name) => {
54
+ const roleId = await idOf(t.role, name);
55
+ if (roleId === void 0) throw new Error(`@plitzi/sdk-server/mysql: no role named "${name}". Create it with ensureRole() first.`);
56
+ return roleId;
57
+ };
58
+ const grantRole = async (accountId, role) => {
59
+ const roleId = await requireRole(role);
60
+ await execute(pool, `INSERT IGNORE INTO ${t.accountRole} (account_id, role_id) VALUES (?, ?)`, [accountId, roleId]);
61
+ };
62
+ const revokeRole = async (accountId, role) => {
63
+ const roleId = await requireRole(role);
64
+ await execute(pool, `DELETE FROM ${t.accountRole} WHERE account_id = ? AND role_id = ?`, [accountId, roleId]);
65
+ };
66
+ /**
67
+ * Keyed by username. The password is re-hashed on every call so a seed stays the source of truth for it — which
68
+ * is what a seed is for, and the reason this must never be pointed at a database where people change their own.
69
+ */
70
+ const ensureAccount = async (seed) => {
71
+ const passwordHash = seed.password === void 0 ? null : await hashPassword(seed.password);
72
+ await execute(pool, `INSERT INTO ${t.account} (username, email, password_hash, verified) VALUES (?, ?, ?, ?)
73
+ ON DUPLICATE KEY UPDATE
74
+ email = VALUES(email),
75
+ password_hash = COALESCE(VALUES(password_hash), password_hash),
76
+ verified = VALUES(verified)`, [
77
+ seed.username,
78
+ seed.email,
79
+ passwordHash,
80
+ seed.verified ? 1 : 0
81
+ ]);
82
+ const row = await selectOne(pool, `SELECT id FROM ${t.account} WHERE username = ? LIMIT 1`, [seed.username]);
83
+ if (!row) throw new Error(`@plitzi/sdk-server/mysql: account "${seed.username}" could not be read back after writing.`);
84
+ for (const role of seed.roles ?? []) await grantRole(row.id, role);
85
+ return row.id;
86
+ };
87
+ return {
88
+ ensurePermissions,
89
+ ensureRole,
90
+ grantRole,
91
+ revokeRole,
92
+ ensureAccount,
93
+ /** Membership of one space, with the role that decides what they may do inside it. */
94
+ addMember: async (spaceId, accountId, role, options = {}) => {
95
+ const roleId = await requireRole(role);
96
+ await execute(pool, `INSERT INTO ${t.spaceMember} (space_id, account_id, role_id, is_owner) VALUES (?, ?, ?, ?)
97
+ ON DUPLICATE KEY UPDATE role_id = VALUES(role_id), is_owner = VALUES(is_owner)`, [
98
+ spaceId,
99
+ accountId,
100
+ roleId,
101
+ options.owner ? 1 : 0
102
+ ]);
103
+ },
104
+ removeMember: async (spaceId, accountId) => {
105
+ await execute(pool, `DELETE FROM ${t.spaceMember} WHERE space_id = ? AND account_id = ?`, [spaceId, accountId]);
106
+ },
107
+ listMembers: (spaceId) => selectRows(pool, `SELECT sm.account_id AS accountId, a.username, r.name AS role, sm.is_owner AS owner
108
+ FROM ${t.spaceMember} sm
109
+ JOIN ${t.account} a ON a.id = sm.account_id
110
+ JOIN ${t.role} r ON r.id = sm.role_id
111
+ WHERE sm.space_id = ?`, [spaceId]),
112
+ /**
113
+ * A space's credential row. Replaces the default one rather than adding a second: exactly one row per space
114
+ * carries `is_default`, and two of them make `loadDefault` answer whichever the database happened to return.
115
+ */
116
+ createSpaceToken: async (spaceId, seed) => {
117
+ const isDefault = seed.isDefault ?? true;
118
+ if (isDefault) await execute(pool, `DELETE FROM ${t.spaceToken} WHERE space_id = ? AND is_default = 1`, [spaceId]);
119
+ return (await execute(pool, `INSERT INTO ${t.spaceToken} (space_id, token, scope, is_default, origins, expires_at, account_id)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
121
+ spaceId,
122
+ seed.token,
123
+ seed.scope ?? "render",
124
+ isDefault ? 1 : 0,
125
+ (seed.origins ?? []).join(","),
126
+ seed.expiresAt ?? null,
127
+ seed.accountId ?? null
128
+ ])).insertId;
129
+ }
130
+ };
131
+ };
132
+ //#endregion
133
+ export { createAdmin };
@@ -0,0 +1,82 @@
1
+ import { Pool, PoolOptions } from 'mysql2/promise';
2
+ /**
3
+ * Where the database is, and what this deployment calls the tables in it.
4
+ *
5
+ * `url` and the discrete fields are two ways of saying the same thing — a hosted database hands you one string, a
6
+ * docker-compose hands you five values — and the discrete fields win where both are given, so a single override
7
+ * does not mean re-assembling the URL.
8
+ */
9
+ export interface MysqlConfig {
10
+ /** `mysql://user:password@host:port/database`. Whatever a managed database hands you, unedited. */
11
+ url?: string;
12
+ host?: string;
13
+ port?: number;
14
+ user?: string;
15
+ password?: string;
16
+ database?: string;
17
+ /** Passed to the driver as given. `true` turns TLS on with the system CA set. */
18
+ ssl?: PoolOptions['ssl'];
19
+ connectionLimit?: number;
20
+ /**
21
+ * Prefix for every table below. **Required**, and deliberately so.
22
+ *
23
+ * `account`, `role`, `permission` and `session` are among the most ordinary table names there are, and this
24
+ * module exists to be pointed at a database that has other things in it. Optional, the default was the empty
25
+ * string — the one value guaranteed to collide — so the guard that refuses to adopt somebody else's tables fired
26
+ * on the common case rather than the dangerous one. A prefix you chose makes a collision mean what it says.
27
+ *
28
+ * `'plitzi_'` is a fine answer. So is your product's name. Say it once: nothing outside this module ever writes
29
+ * a table name.
30
+ */
31
+ tablePrefix: string;
32
+ /**
33
+ * Create and update the tables at startup. On by default, which is what makes a fresh database work with no step
34
+ * before it. A deployment whose database user cannot run DDL — the usual arrangement in production — turns it off
35
+ * and applies the statements from `schemaStatements()` through whatever it migrates with.
36
+ */
37
+ autoMigrate?: boolean;
38
+ /**
39
+ * Create the database itself if the server has not got it, before connecting to it. Off by default and meant for
40
+ * development, where "point it at a MySQL and go" is the whole appeal and the alternative is one `CREATE DATABASE`
41
+ * that nobody remembers until the first run fails. In production the database exists before the application does,
42
+ * made by somebody holding rights the application should not have.
43
+ */
44
+ ensureDatabase?: boolean;
45
+ /**
46
+ * Use tables with these names that already exist and were not created here.
47
+ *
48
+ * Off by default, and the default is the safe one: this module is meant to be pointed at a database that has
49
+ * unrelated tables in it, and `role`, `permission` and `session` are among the most ordinary names there are.
50
+ * `CREATE TABLE IF NOT EXISTS` would silently adopt somebody else's, and the failure would surface much later as
51
+ * a query against columns that are not there. Turn it on only when re-attaching to tables that really are this
52
+ * schema's — a restored backup whose `schema_version` row was lost, say.
53
+ */
54
+ adoptExisting?: boolean;
55
+ /** An existing pool, for a deployment that already has one. Given, nothing here creates or ends a connection. */
56
+ pool?: Pool;
57
+ log?: (message: string) => void;
58
+ }
59
+ /**
60
+ * The tables the auth kernel needs. Named after the kernel's own vocabulary — an `AccountRecord` is stored in
61
+ * `account` — rather than after any one deployment's user table, which is a thing it maps ONTO these.
62
+ */
63
+ export declare const TABLE_NAMES: {
64
+ readonly schemaVersion: "schema_version";
65
+ readonly account: "account";
66
+ readonly session: "session";
67
+ readonly identity: "account_identity";
68
+ readonly mfa: "account_mfa";
69
+ readonly otp: "account_otp";
70
+ readonly role: "role";
71
+ readonly permission: "permission";
72
+ readonly rolePermission: "role_permission";
73
+ readonly accountRole: "account_role";
74
+ readonly spaceMember: "space_member";
75
+ readonly spaceToken: "space_token";
76
+ };
77
+ export type TableKey = keyof typeof TABLE_NAMES;
78
+ /** Table names, already prefixed and already backticked — the only form the rest of the module ever sees. */
79
+ export type Tables = Record<TableKey, string>;
80
+ export declare const resolveTables: (prefix: string) => Tables;
81
+ /** The bare, unquoted names — for a migration tool, or an error that has to say which table it meant. */
82
+ export declare const tableNames: (prefix: string) => Record<TableKey, string>;
@@ -0,0 +1,30 @@
1
+ //#region src/modules/mysql/config.ts
2
+ /**
3
+ * The tables the auth kernel needs. Named after the kernel's own vocabulary — an `AccountRecord` is stored in
4
+ * `account` — rather than after any one deployment's user table, which is a thing it maps ONTO these.
5
+ */
6
+ var TABLE_NAMES = {
7
+ schemaVersion: "schema_version",
8
+ account: "account",
9
+ session: "session",
10
+ identity: "account_identity",
11
+ mfa: "account_mfa",
12
+ otp: "account_otp",
13
+ role: "role",
14
+ permission: "permission",
15
+ rolePermission: "role_permission",
16
+ accountRole: "account_role",
17
+ spaceMember: "space_member",
18
+ spaceToken: "space_token"
19
+ };
20
+ var resolveTables = (prefix) => {
21
+ const entries = Object.entries(TABLE_NAMES).map(([key, name]) => [key, `\`${prefix}${name}\``]);
22
+ return Object.fromEntries(entries);
23
+ };
24
+ /** The bare, unquoted names — for a migration tool, or an error that has to say which table it meant. */
25
+ var tableNames = (prefix) => {
26
+ const entries = Object.entries(TABLE_NAMES).map(([key, name]) => [key, `${prefix}${name}`]);
27
+ return Object.fromEntries(entries);
28
+ };
29
+ //#endregion
30
+ export { TABLE_NAMES, resolveTables, tableNames };