@plitzi/sdk-server 0.33.2 → 0.34.1

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 +54 -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 +259 -0
  273. /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
  274. /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { CredentialCarrier } from './credentials';
2
+ import { Csrf } from './csrf';
2
3
  import { Actor, Identity } from './identity';
3
- import { Tokens } from './tokens';
4
+ import { AuthFailure, Tokens } from './tokens';
4
5
  import { SSRSession } from '@plitzi/sdk-shared';
5
6
  /**
6
7
  * An account, as whatever stores accounts reports one. Deliberately not a user model: these are the only fields any
@@ -18,11 +19,81 @@ export interface AccountRecord {
18
19
  passwordHash?: string;
19
20
  /** Unix seconds, for the refresh credential this account currently holds. */
20
21
  refreshExpiresAt?: number;
22
+ /**
23
+ * Unix seconds the session being renewed BEGAN, for `lifetimes.session` — the cap on how long one may live
24
+ * however often it renews. Only `findByRefreshToken` needs to report it, and only a deployment that sets a cap.
25
+ *
26
+ * The renewal window is already an idle timeout: a session nobody refreshes dies with its refresh token. This is
27
+ * the other half, and without it a session that renews quietly renews forever.
28
+ */
29
+ sessionStartedAt?: number;
21
30
  }
22
31
  export interface AccountAccess {
23
32
  roles: string[];
24
33
  permissions: string[];
25
34
  }
35
+ /** What a session was created from, so a "your devices" list can name it. */
36
+ export interface SessionClient {
37
+ userAgent?: string;
38
+ ip?: string;
39
+ }
40
+ /**
41
+ * What an account is allowed to be. `inactive` is the account's own doing (deactivated, never confirmed);
42
+ * `blocked` is the deployment's. Neither may hold a session — the distinction is for whoever has to explain it.
43
+ */
44
+ export type AccountStatus = 'active' | 'inactive' | 'blocked';
45
+ /** One live session, as its owner may see it. Never the credential itself. */
46
+ export interface SessionSummary {
47
+ id: number;
48
+ userAgent?: string;
49
+ ip?: string;
50
+ createdAt: number;
51
+ expiresAt: number;
52
+ /** The session asking. A device list without it invites someone to revoke the one they are using. */
53
+ current: boolean;
54
+ }
55
+ /** A second factor as the store keeps it. `secret` is the TOTP seed; `recoveryCodes` are already hashed. */
56
+ export interface MfaRecord {
57
+ secret: string;
58
+ /** Unix seconds the enrolment was proven with a real code. Absent means started and never finished. */
59
+ confirmedAt?: number;
60
+ recoveryCodes?: string[];
61
+ }
62
+ /** Something worth writing down. Fed to an audit log, a webhook, a SIEM — whatever the deployment has. */
63
+ export interface SecurityEvent {
64
+ type: 'login' | 'login.failed' | 'login.mfa-required' | 'logout' | 'signup' | 'password.changed' | 'password.reset' | 'profile.changed' | 'email.changed' | 'account.deleted' | 'session.revoked' | 'mfa.enabled' | 'mfa.disabled' | 'mfa.failed' | 'admin.status-changed' | 'admin.roles-changed' | 'admin.account-deleted' | 'admin.impersonated';
65
+ /** Who it happened to. Absent when the attempt named nobody that exists. */
66
+ userId?: number;
67
+ /** Who did it, when that is somebody else — an administrator acting on an account. */
68
+ actorId?: number;
69
+ at: number;
70
+ detail?: Record<string, unknown>;
71
+ carrier?: CredentialCarrier;
72
+ }
73
+ export interface AccountQuery {
74
+ /** Matched against username and email. */
75
+ search?: string;
76
+ status?: AccountStatus;
77
+ limit?: number;
78
+ offset?: number;
79
+ }
80
+ /**
81
+ * Why a session is being written — which is not something the pair itself can say, and the difference matters to
82
+ * any store that keeps more than one session per account.
83
+ *
84
+ * A sign-in creates a session. A renewal **replaces one**, and a store that cannot tell them apart grows a row per
85
+ * renewal: a device list that fills with ghosts of the same browser, and a revoked session that comes back because
86
+ * the row it was meant to overwrite is still there. A store that keeps a single pair on the account row ignores
87
+ * this and overwrites either way, which is what it always did.
88
+ */
89
+ export interface SessionContext {
90
+ /** The credential being renewed. Absent for a fresh sign-in. */
91
+ replaces?: {
92
+ refreshToken?: string;
93
+ accessToken?: string;
94
+ };
95
+ client?: SessionClient;
96
+ }
26
97
  /**
27
98
  * The account store, as auth needs to see it. Every method is optional except the three the session cycle cannot do
28
99
  * without, and **what is absent decides what this server offers**: a deployment with no `createAccount` has no
@@ -30,8 +101,14 @@ export interface AccountAccess {
30
101
  * deployment with its own user table and one that signs everybody in through an external provider.
31
102
  */
32
103
  export interface AccountAdapters {
33
- /** Persist a freshly minted pair. Storing it is what retires the previous one — the whole of rotation. */
34
- saveSession: (userId: number, session: SSRSession) => Promise<void>;
104
+ /**
105
+ * Persist a freshly minted pair.
106
+ *
107
+ * `context` says whether this is a new session or one replacing another — see {@link SessionContext}. A store
108
+ * with one pair per account may ignore it and overwrite; a store with a `session` table must not, or every
109
+ * renewal leaves a row behind.
110
+ */
111
+ saveSession: (userId: number, session: SSRSession, context?: SessionContext) => Promise<void>;
35
112
  /** Clear the pair, by whichever half the caller holds, or by account. */
36
113
  clearSession: (target: {
37
114
  accessToken?: string;
@@ -40,6 +117,40 @@ export interface AccountAdapters {
40
117
  }) => Promise<void>;
41
118
  /** Global roles and permissions, for the body a grant answers with. */
42
119
  loadAccess: (userId: number) => Promise<AccountAccess>;
120
+ /** By id. Needed wherever a flow acts on the account already signed in — changing a password, deleting itself. */
121
+ findById?: (userId: number) => Promise<AccountRecord | undefined>;
122
+ /**
123
+ * Change what an account says about itself. Only the keys present are touched — an absent `email` means "leave
124
+ * it", never "clear it", which is what an implementation writing every column unconditionally would do.
125
+ */
126
+ updateAccount?: (userId: number, changes: {
127
+ username?: string;
128
+ email?: string;
129
+ }) => Promise<AccountRecord>;
130
+ /**
131
+ * Suspend, block or restore. Separate from `deleteAccount` because they are different acts with different
132
+ * consequences: a suspension is reversible and keeps everything the account made.
133
+ */
134
+ setStatus?: (userId: number, status: AccountStatus) => Promise<void>;
135
+ /**
136
+ * Erase the account. Whether that means deleting a row or anonymising one is the deployment's call and it is a
137
+ * real one — content, audit logs and invoices usually have to outlive the person. Either way it must end every
138
+ * session, which deleting the account's sessions does.
139
+ */
140
+ deleteAccount?: (userId: number) => Promise<void>;
141
+ /** Page through accounts, for an administrator. `total` is the count before the page was taken. */
142
+ listAccounts?: (query: AccountQuery) => Promise<{
143
+ accounts: AccountRecord[];
144
+ total: number;
145
+ }>;
146
+ /** Replace an account's global roles with exactly these. */
147
+ setRoles?: (userId: number, roles: string[]) => Promise<void>;
148
+ /** The account's live sessions. `currentToken` marks the one asking, so a device list can say "this device". */
149
+ listSessions?: (userId: number, currentToken?: string) => Promise<SessionSummary[]>;
150
+ /** End one session. Scoped by account: a session id from another account must not resolve. */
151
+ revokeSession?: (userId: number, sessionId: number) => Promise<boolean>;
152
+ /** End every session except the one asking. */
153
+ revokeOtherSessions?: (userId: number, currentToken: string) => Promise<number>;
43
154
  findByUsername?: (username: string) => Promise<AccountRecord | undefined>;
44
155
  findByRefreshToken?: (token: string) => Promise<AccountRecord | undefined>;
45
156
  createAccount?: (account: {
@@ -53,7 +164,51 @@ export interface AccountAdapters {
53
164
  findByResetToken?: (token: string) => Promise<AccountRecord | undefined>;
54
165
  setValidationToken?: (userId: number, token: string) => Promise<void>;
55
166
  findByValidationToken?: (token: string) => Promise<AccountRecord | undefined>;
56
- markVerified?: (userId: number) => Promise<void>;
167
+ /**
168
+ * Park an address the account asked to move to, with the token that will confirm it. It is NOT the account's
169
+ * address yet, and nothing may sign in with it — the whole point is that the old one keeps working until the new
170
+ * one is proven, so a typo is a nuisance instead of a lockout.
171
+ *
172
+ * Supplying these three (with `sendMail`) is what turns an email change into a confirmed one. A deployment that
173
+ * leaves them out changes the address on the spot, which is the simpler thing and a legitimate choice.
174
+ */
175
+ setPendingEmail?: (userId: number, email: string, token: string) => Promise<void>;
176
+ /** The account waiting on this confirmation, and the address it is waiting for. */
177
+ findByPendingEmail?: (token: string) => Promise<{
178
+ account: AccountRecord;
179
+ email: string;
180
+ } | undefined>;
181
+ /** Forget the parked address — it was confirmed, superseded, or the account was closed. */
182
+ clearPendingEmail?: (userId: number) => Promise<void>;
183
+ /**
184
+ * Confirm or un-confirm the address. Both directions from one adapter, because changing an email has to be able
185
+ * to take verification away — a `markVerified` that only ever set it made that impossible to express.
186
+ */
187
+ setVerified?: (userId: number, verified: boolean) => Promise<void>;
188
+ /**
189
+ * A one-time code, hashed. `purpose` separates a sign-in link from anything else that uses the same table, and
190
+ * `identifier` is what it was issued against — an email address, usually.
191
+ */
192
+ saveOtp?: (code: {
193
+ purpose: string;
194
+ identifier: string;
195
+ codeHash: string;
196
+ expiresAt: number;
197
+ userId?: number;
198
+ }) => Promise<void>;
199
+ /** The live code for this purpose and identifier, if there is one. Expired rows may be returned or not. */
200
+ findOtp?: (purpose: string, identifier: string) => Promise<{
201
+ id: number;
202
+ codeHash: string;
203
+ expiresAt: number;
204
+ userId?: number;
205
+ } | undefined>;
206
+ /** Spend it. Deleting rather than flagging: a spent code that still exists is one a lookup can forget to exclude. */
207
+ consumeOtp?: (id: number) => Promise<void>;
208
+ /** The second factor for this account, if it has one. Without it, no MFA is offered at all. */
209
+ loadMfa?: (userId: number) => Promise<MfaRecord | undefined>;
210
+ saveMfa?: (userId: number, record: MfaRecord) => Promise<void>;
211
+ deleteMfa?: (userId: number) => Promise<void>;
57
212
  /** Called for validation and password-reset mail. Without it neither flow is offered. */
58
213
  sendMail?: (message: {
59
214
  to: string;
@@ -70,15 +225,110 @@ export interface AccountAdapters {
70
225
  status?: number;
71
226
  } | undefined>;
72
227
  }
228
+ /** What a flow was asked to do, for the throttle below. */
229
+ export type ThrottledAction = 'login' | 'signup' | 'forgotPassword' | 'resetPassword' | 'changePassword' | 'exchange' | 'mfa' | 'passwordless';
230
+ export interface ThrottleAttempt {
231
+ action: ThrottledAction;
232
+ /** What is being attempted against — a username, an email, a token. Never a password. */
233
+ key: string;
234
+ carrier?: CredentialCarrier;
235
+ /**
236
+ * Not a question but a report: this one worked, so whatever was counted against the key can be forgotten. The
237
+ * return value is ignored.
238
+ *
239
+ * It matters because the check happens BEFORE the password is examined — which is what makes a throttled attempt
240
+ * cost no hash — so without this the counter cannot tell ten failures from ten sign-ins. An app that signs the
241
+ * same account in repeatedly would lock it out by succeeding. A limiter that ignores this is simply stricter.
242
+ */
243
+ succeeded?: boolean;
244
+ }
245
+ export interface PasswordPolicy {
246
+ /**
247
+ * Default 8, which is NIST SP 800-63B's floor. There are deliberately no composition rules: requiring a digit
248
+ * and a symbol measurably produces `Password1!` and nothing safer.
249
+ */
250
+ minLength?: number;
251
+ /** Anything else this deployment decides — a breach-list lookup, a strength estimator. Return why, or nothing. */
252
+ validate?: (password: string, context: {
253
+ username?: string;
254
+ email?: string;
255
+ }) => Promise<string | undefined>;
256
+ }
73
257
  export interface AuthApiConfig {
74
- /** Compare a password against a stored hash. A deployment picks its own algorithm; nothing here assumes one. */
258
+ /**
259
+ * How passwords are hashed and checked. **Defaults to scrypt from the standard library**, so a deployment that
260
+ * has no opinion does not have to have one — and, more to the point, cannot end up with password login quietly
261
+ * switched off because it did not supply a function it had no reason to know about.
262
+ *
263
+ * Supply both to keep an existing algorithm: a store full of bcrypt hashes needs bcrypt.
264
+ */
75
265
  verifyPassword?: (plain: string, hash: string) => Promise<boolean>;
76
266
  hashPassword?: (plain: string) => Promise<string>;
77
- /** Opaque, single-use strings for validation and reset links. */
267
+ /** Opaque, single-use strings for validation and reset links. Defaults to 128 random bits, hex. */
78
268
  generateToken?: () => string;
79
269
  /** Whether a new account may sign in immediately or has to confirm its address first. */
80
270
  verifyOnSignup?: boolean;
271
+ /**
272
+ * The global capability an administrator must hold to act on somebody else's account. Named rather than fixed,
273
+ * because what a deployment calls its permissions is its own vocabulary. Default `userManage`.
274
+ */
275
+ adminPermission?: string;
276
+ /**
277
+ * The capability that lets somebody obtain a session AS another account, and the switch that offers the flow at
278
+ * all: **absent, there is no impersonation** and `/auth/admin/impersonate` answers 404.
279
+ *
280
+ * Deliberately its own permission rather than part of `adminPermission`. Support staff who can suspend an account
281
+ * and support staff who can become one are not the same grant, and defaulting the second to the first would hand
282
+ * it out to everybody who already had the first — a power that appears from an upgrade nobody read.
283
+ */
284
+ impersonationPermission?: string;
285
+ /** What a password has to be. Applied wherever one is set: signing up, resetting, changing. */
286
+ password?: PasswordPolicy;
287
+ /**
288
+ * May this attempt proceed? Answered **in memory by default**, so no deployment is unthrottled by omission.
289
+ *
290
+ * There used to be no default, on the reasoning that where the counter lives is a deployment decision. It is —
291
+ * but the consequence of leaving it out was that the ordinary deployment shipped an unmetered password oracle,
292
+ * because nobody configures an option they have not read about. The counting is per process; supply this to put
293
+ * one counter behind the whole fleet, which is what a cluster wants.
294
+ *
295
+ * A refusal is a 429 with `retryAfter`, raised before any password is checked so it costs no hash.
296
+ */
297
+ rateLimit?: (attempt: ThrottleAttempt) => Promise<boolean | {
298
+ allowed: boolean;
299
+ retryAfter?: number;
300
+ }>;
301
+ /** Where a failed delivery is reported. Defaults to `console.error`; it is never thrown — see `deliver`. */
302
+ onMailError?: (error: unknown, message: {
303
+ to: string;
304
+ template: string;
305
+ }) => void;
306
+ /**
307
+ * Every act worth recording, as it happens.
308
+ *
309
+ * An audit trail, a webhook, an alert on ten failed sign-ins — all the same feed, and none of them something the
310
+ * server should decide the shape of. Never awaited and never allowed to fail a request: a logging outage must
311
+ * not become an authentication outage.
312
+ */
313
+ onEvent?: (event: SecurityEvent) => void;
314
+ /** What an authenticator app calls this deployment when somebody enrols. Defaults to the token issuer. */
315
+ mfaIssuer?: string;
316
+ /** Set by `createAuth`, so `GET /auth/csrf` can mint one. Nothing here enforces the check — the routes do. */
317
+ csrf?: Csrf;
81
318
  }
319
+ /** What an exchange came to, before it is turned into either an HTTP body or a rendered page's session. */
320
+ export type ExchangeResult = {
321
+ ok: true;
322
+ account: AccountRecord;
323
+ access: AccountAccess;
324
+ session: SSRSession;
325
+ } | {
326
+ ok: false;
327
+ offered: boolean;
328
+ status: number;
329
+ error: string;
330
+ reason?: AuthFailure;
331
+ };
82
332
  /** What a handler answers: a body, and optionally what should happen to the session cookies. */
83
333
  export type AuthOutcome = {
84
334
  ok: true;
@@ -86,10 +336,19 @@ export type AuthOutcome = {
86
336
  body: object;
87
337
  session?: SSRSession;
88
338
  endSession?: boolean;
89
- } | {
339
+ csrf?: string;
340
+ }
341
+ /**
342
+ * `endSession` on a REFUSAL is not a contradiction: it is how a flow says "what this browser is holding is
343
+ * dead, stop holding it". Without it the cookies survive the refusal that proved them worthless, and the
344
+ * readable hint — which outlives the access token on purpose, to advertise the renewal window — sends the page
345
+ * back to renew on every single load. One 401 per page view, forever, for a session that ended weeks ago.
346
+ */
347
+ | {
90
348
  ok: false;
91
349
  status: number;
92
350
  body: object;
351
+ endSession?: boolean;
93
352
  };
94
353
  /**
95
354
  * The HTTP surface of authentication, as functions rather than routes.
@@ -112,6 +371,17 @@ export declare const createAuthApi: ({ tokens, identity, adapters, config }: {
112
371
  passwordReset: boolean;
113
372
  emailVerification: boolean;
114
373
  exchange: boolean;
374
+ mfa: boolean;
375
+ passwordless: boolean;
376
+ profile: boolean;
377
+ passwordChange: boolean;
378
+ accountDeletion: boolean;
379
+ sessionList: boolean;
380
+ administration: boolean;
381
+ /** Changing an address by confirming it, rather than on the spot. Needs somewhere to park it and a way to write. */
382
+ emailChange: boolean;
383
+ /** Off unless the deployment named the capability it takes — see `impersonationPermission`. */
384
+ impersonation: boolean;
115
385
  };
116
386
  /** What a client reads to know which of the flows below actually exist here. */
117
387
  describe: () => AuthOutcome;
@@ -121,8 +391,39 @@ export declare const createAuthApi: ({ tokens, identity, adapters, config }: {
121
391
  * signed-out from the refusal itself. It costs no query: the actor was resolved before routing.
122
392
  */
123
393
  session: (actor?: Actor) => AuthOutcome;
124
- login: (credentials: Record<string, unknown>) => Promise<AuthOutcome>;
125
- refresh: (refreshToken?: string) => Promise<AuthOutcome>;
394
+ login: (credentials: Record<string, unknown>, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
395
+ /**
396
+ * The second half of a sign-in that owed a factor.
397
+ *
398
+ * Takes a TOTP code or a recovery code. A recovery code is SPENT — removed from the stored list — because one
399
+ * that survives being used is a password with extra steps.
400
+ */
401
+ completeMfa: (mfaToken: string, code: string, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
402
+ /**
403
+ * Signing in with a code sent to an email address, and no password at all.
404
+ *
405
+ * Two decisions worth stating. **The request answers the same whether the address exists or not** — anything
406
+ * else turns this endpoint into a way to ask which addresses have accounts. And **it never creates an
407
+ * account**: a sign-in flow that silently registers whoever asks is a different feature, and one a deployment
408
+ * should choose deliberately rather than inherit.
409
+ */
410
+ passwordless: {
411
+ request: (email: string, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
412
+ complete: (email: string, code: string, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
413
+ };
414
+ /**
415
+ * Everything about this account's second factor. `begin` hands back a secret and the URI an app scans; nothing
416
+ * is in force until `confirm` proves a code from it.
417
+ */
418
+ mfa: {
419
+ status: (actor: Actor | undefined) => Promise<AuthOutcome>;
420
+ begin: (actor: Actor | undefined) => Promise<AuthOutcome>;
421
+ /** Proves the app was actually set up, and only then does the factor start being required. */
422
+ confirm: (actor: Actor | undefined, code: string) => Promise<AuthOutcome>;
423
+ /** Removing a factor is a security downgrade, so it asks for the password the way deleting an account does. */
424
+ disable: (actor: Actor | undefined, password?: string) => Promise<AuthOutcome>;
425
+ };
426
+ refresh: (refreshToken?: string, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
126
427
  /** Reachable with an expired access token on purpose, or a live refresh token could never be revoked. */
127
428
  logout: (credentials: {
128
429
  accessToken?: string;
@@ -135,14 +436,102 @@ export declare const createAuthApi: ({ tokens, identity, adapters, config }: {
135
436
  */
136
437
  revokeSessions: (userId?: number) => Promise<AuthOutcome>;
137
438
  exchange: (provider: string, token: string, carrier: CredentialCarrier) => Promise<AuthOutcome>;
439
+ /**
440
+ * The same exchange, answering in accounts rather than in HTTP.
441
+ *
442
+ * A page server needs who came back and the session they got; the flow above needs a body and a status. They
443
+ * are the same act, and the translation between them has one correct answer — so it is made once, here, rather
444
+ * than by every deployment that wires an external identity provider into a rendered page.
445
+ */
446
+ exchangeAccount: (provider: string, token: string, carrier: CredentialCarrier) => Promise<ExchangeResult>;
138
447
  signup: (fields: Record<string, unknown>) => Promise<AuthOutcome>;
139
448
  /** Answers the same either way: whether an address has an account here is not something a stranger may probe. */
140
449
  forgotPassword: (email: string) => Promise<AuthOutcome>;
141
450
  resetPassword: (token: string, password: string) => Promise<AuthOutcome>;
142
451
  validateAccount: (token: string) => Promise<AuthOutcome>;
143
- resendVerification: (email: string) => Promise<AuthOutcome>;
452
+ resendVerification: (email: string, redirect?: string) => Promise<AuthOutcome>;
453
+ /**
454
+ * Change what an account says about itself.
455
+ *
456
+ * Both fields are sign-in identifiers, so both are checked for collision first — a store that only has a unique
457
+ * index reports the clash as a driver error five hundred lines away.
458
+ */
459
+ updateProfile: (actor: Actor | undefined, fields: Record<string, unknown>) => Promise<AuthOutcome>;
460
+ /**
461
+ * Takes the parked address, having proved somebody reads it.
462
+ *
463
+ * Public, and it has to be: the link is opened from a mail client, in whatever browser that happens to be, with
464
+ * no session. The token is the whole credential — which is why it is single-use and why confirming also marks
465
+ * the account verified: an address that answered a link is an address that was proven.
466
+ */
467
+ confirmEmailChange: (token: string) => Promise<AuthOutcome>;
468
+ /**
469
+ * Change a password, having proved you know the current one.
470
+ *
471
+ * Then **every other session ends**. Changing a password is what somebody does when they think a credential
472
+ * escaped, and one that leaves the other sessions signed in has not done the thing they asked for. The session
473
+ * making the change survives, or the act of securing the account would sign them out of it.
474
+ */
475
+ changePassword: (actor: Actor | undefined, currentPassword: string, newPassword: string) => Promise<AuthOutcome>;
476
+ /**
477
+ * Close the account.
478
+ *
479
+ * Confirmed with the password when there is one — this is irreversible, and a borrowed session should not be
480
+ * able to do it. An account with no password (one that signs in through a provider) cannot be asked, so the
481
+ * session alone has to be enough.
482
+ */
483
+ deleteSelf: (actor: Actor | undefined, password?: string) => Promise<AuthOutcome>;
484
+ /** The devices this account is signed in on, with the one asking marked. */
485
+ listSessions: (actor: Actor | undefined) => Promise<AuthOutcome>;
486
+ /** End one of them. Scoped to the caller's own account, or a session id would be an IDOR. */
487
+ revokeSession: (actor: Actor | undefined, sessionId: number) => Promise<AuthOutcome>;
488
+ /** "Sign out everywhere else" — the thing to do from a device you still hold after losing one you do not. */
489
+ revokeOtherSessions: (actor: Actor | undefined) => Promise<AuthOutcome>;
490
+ admin: {
491
+ list: (actor: Actor | undefined, query: AccountQuery) => Promise<AuthOutcome>;
492
+ get: (actor: Actor | undefined, userId: number) => Promise<AuthOutcome>;
493
+ /**
494
+ * Suspend, block or restore an account — and **end its sessions when it stops being active**.
495
+ *
496
+ * That second half is the whole point. A ban that leaves the credential working is not a ban, it is a note
497
+ * in a database; the person stays signed in until their token happens to lapse.
498
+ */
499
+ setStatus: (actor: Actor | undefined, userId: number, status: AccountStatus) => Promise<AuthOutcome>;
500
+ setRoles: (actor: Actor | undefined, userId: number, roles: unknown) => Promise<AuthOutcome>;
501
+ remove: (actor: Actor | undefined, userId: number) => Promise<AuthOutcome>;
502
+ /**
503
+ * A session AS somebody else, for support that has to see what they see.
504
+ *
505
+ * Three properties make it something a deployment can live with, and all three are deliberate:
506
+ *
507
+ * - **It says so.** The credential carries `act` (RFC 8693), so every request made with it can be told from
508
+ * one the account holder made — `Actor.impersonatedBy` is that claim, read back. A borrowed session that
509
+ * is indistinguishable from a real one turns an audit log into fiction.
510
+ * - **It is short and cannot renew.** Fifteen minutes by default and no refresh token, so it expires rather
511
+ * than becoming a permanent second key to somebody's account.
512
+ * - **It is off unless asked for**, behind its own permission — see `impersonationPermission`.
513
+ *
514
+ * The session is answered in the BODY and no cookie is written. Swapping the administrator's own session
515
+ * cookie for this one would sign them out of the account they administer from, and getting back would mean
516
+ * signing in again; whoever asked decides where to put a credential they were handed.
517
+ *
518
+ * One caveat a deployment has to know: a store that keeps a single session per account — no `session` table —
519
+ * will overwrite the subject's own session with this one, signing them out. Anything with per-session rows,
520
+ * which is what `createMysqlStore` builds, does not.
521
+ */
522
+ impersonate: (actor: Actor | undefined, userId: number, carrier?: CredentialCarrier) => Promise<AuthOutcome>;
523
+ };
524
+ /**
525
+ * A CSRF token for whoever is asking, bound to their session when they have one. Answered as a body AND as the
526
+ * cookie the binding writes, so a page can take whichever it finds easier to read.
527
+ */
528
+ issueCsrf: (sessionToken?: string) => AuthOutcome;
144
529
  /** Exposed so a deployment can mint a session outside the flows above — a rendered page's login form. */
145
- issueSession: (userId: number) => Promise<SSRSession>;
530
+ issueSession: (userId: number, context?: SessionContext, options?: {
531
+ actingAs?: number;
532
+ ttlSeconds?: number;
533
+ renewable?: boolean;
534
+ }) => Promise<SSRSession>;
146
535
  resolveActor: (carrier: CredentialCarrier) => Promise<import('./identity').ActorResult>;
147
536
  };
148
537
  export type AuthApi = ReturnType<typeof createAuthApi>;