@storyshelf/app 0.5.3

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,132 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { startBranchGcTimer } from "./retention-timer.mjs";
5
+ import { AdapterLifecycleError, bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns, validateBootAssembly } from "@storyshelf/core/adapter/setup";
6
+ import { sanitizeErrorText } from "@storyshelf/core/utils";
7
+ //#region src/lifecycle.ts
8
+ function trackSettlement(cell, promise) {
9
+ promise.then((result) => {
10
+ cell.settled = result;
11
+ }, () => {
12
+ cell.settled = {
13
+ ok: false,
14
+ failures: []
15
+ };
16
+ });
17
+ }
18
+ /** Kick the eager background setup run (resolves; never rejects). */
19
+ function kickSetup(options, ctx, logger, cell) {
20
+ cell.ready = (async () => {
21
+ const invalid = validateBootAssembly(options);
22
+ if (invalid.length > 0) {
23
+ logger.error({ failures: invalid }, "adapter validation failed");
24
+ return {
25
+ ok: false,
26
+ failures: invalid
27
+ };
28
+ }
29
+ const authFailed = await runAuthSetup(options);
30
+ if (authFailed) {
31
+ logger.error({ failures: authFailed.failures }, "auth setup failed");
32
+ return authFailed;
33
+ }
34
+ return await runAdapterSetups(collectSetups(options), ctx, logger);
35
+ })();
36
+ trackSettlement(cell, cell.ready);
37
+ }
38
+ /** Login method count, or null when the engine throws during introspection. */
39
+ function loginMethodCount(auth) {
40
+ try {
41
+ return auth.loginMethods().length;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Log which auth mode the server boots with (open servers stay visible). */
47
+ function logAuthMode(options, logger) {
48
+ if (!options.auth) {
49
+ logger.info("auth disabled (local default; every route is open)");
50
+ return;
51
+ }
52
+ const methods = loginMethodCount(options.auth);
53
+ if (methods === null) {
54
+ logger.warn("auth loginMethods() threw during boot logging");
55
+ return;
56
+ }
57
+ logger.info({ loginMethods: methods }, "auth enabled");
58
+ }
59
+ /** Kick eager setup and attach the `app.lifecycle` namespace (single run). */
60
+ function attachLifecycle(app, options, runtime, cell) {
61
+ const setupCtx = {
62
+ config: runtime.config,
63
+ logger: runtime.logger
64
+ };
65
+ bindAdapterLoggers(options, runtime.logger);
66
+ logAuthMode(options, runtime.logger);
67
+ kickSetup(options, setupCtx, runtime.logger, cell);
68
+ const timer = startBranchGcInterval(options, runtime);
69
+ Object.assign(app, { lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer) });
70
+ }
71
+ function startBranchGcInterval(options, runtime) {
72
+ return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger);
73
+ }
74
+ /** Run auth boot validation, returning a failure result instead of throwing. */
75
+ async function runAuthSetup(options) {
76
+ try {
77
+ await options.auth?.setup?.();
78
+ return null;
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ return {
82
+ ok: false,
83
+ failures: [{
84
+ category: "auth",
85
+ kind: "auth",
86
+ name: "auth",
87
+ error: sanitizeErrorText(message, 500)
88
+ }]
89
+ };
90
+ }
91
+ }
92
+ /** Validation failure settled on the cell — null when the assembly is sound. */
93
+ function invalidSources(options, cell) {
94
+ const invalid = validateBootAssembly(options);
95
+ if (invalid.length === 0) return null;
96
+ const result = {
97
+ ok: false,
98
+ failures: invalid
99
+ };
100
+ cell.ready = Promise.resolve(result);
101
+ cell.settled = result;
102
+ return result;
103
+ }
104
+ /** Build the `app.lifecycle` namespace closing over one settlement cell. */
105
+ function createLifecycle(options, ctx, logger, cell, timer = null) {
106
+ return {
107
+ get ready() {
108
+ return cell.ready;
109
+ },
110
+ logger,
111
+ setup: async () => {
112
+ const blocked = invalidSources(options, cell);
113
+ if (blocked) throw new AdapterLifecycleError("setup", blocked.failures);
114
+ const result = await runAdapterSetups(collectSetups(options), ctx, logger);
115
+ cell.ready = Promise.resolve(result);
116
+ cell.settled = result;
117
+ if (!result.ok) throw new AdapterLifecycleError("setup", result.failures);
118
+ },
119
+ teardown: async () => {
120
+ timer?.stop();
121
+ const result = await runAdapterTeardowns(collectTeardowns(options), ctx, logger);
122
+ await options.observability?.shutdown().catch((error) => {
123
+ logger.error({ err: error }, "observability shutdown failed");
124
+ });
125
+ if (!result.ok) throw new AdapterLifecycleError("teardown", result.failures);
126
+ }
127
+ };
128
+ }
129
+ //#endregion
130
+ export { attachLifecycle };
131
+
132
+ //# sourceMappingURL=lifecycle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import type { AdapterSetupContext } from \"@storyshelf/core/adapter/metadata\";\nimport {\n AdapterLifecycleError,\n bindAdapterLoggers,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter, ShelfLifecycle } from \"./app-types.ts\";\nimport { startBranchGcTimer } from \"./retention-timer.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\n\n/** Mutable setup-settlement cell shared by the gate, health, and `setup()`. */\nexport interface LifecycleCell {\n ready: Promise<AdapterSetupResult>;\n settled: AdapterSetupResult | null;\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterSetupResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** Kick the eager background setup run (resolves; never rejects). */\nfunction kickSetup(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n // Auth is not an adapter: its one-shot boot validation runs first, then\n // the adapter setups. A failing secret check fails readiness, like others.\n cell.ready = (async (): Promise<AdapterSetupResult> => {\n const invalid = validateBootAssembly(options);\n if (invalid.length > 0) {\n logger.error({ failures: invalid }, \"adapter validation failed\");\n return { ok: false, failures: invalid };\n }\n const authFailed = await runAuthSetup(options);\n if (authFailed) {\n logger.error({ failures: authFailed.failures }, \"auth setup failed\");\n return authFailed;\n }\n return await runAdapterSetups(collectSetups(options), ctx, logger);\n })();\n trackSettlement(cell, cell.ready);\n}\n\n/** Login method count, or null when the engine throws during introspection. */\nfunction loginMethodCount(auth: NonNullable<ShelfOptions[\"auth\"]>): number | null {\n try {\n return auth.loginMethods().length;\n } catch {\n return null;\n }\n}\n\n/** Log which auth mode the server boots with (open servers stay visible). */\nfunction logAuthMode(options: ShelfOptions, logger: Logger): void {\n if (!options.auth) {\n logger.info(\"auth disabled (local default; every route is open)\");\n return;\n }\n const methods = loginMethodCount(options.auth);\n if (methods === null) {\n logger.warn(\"auth loginMethods() threw during boot logging\");\n return;\n }\n logger.info({ loginMethods: methods }, \"auth enabled\");\n}\n\n/** Kick eager setup and attach the `app.lifecycle` namespace (single run). */\nexport function attachLifecycle(\n app: ShelfRouter,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const setupCtx: AdapterSetupContext = { config: runtime.config, logger: runtime.logger };\n bindAdapterLoggers(options, runtime.logger);\n logAuthMode(options, runtime.logger);\n kickSetup(options, setupCtx, runtime.logger, cell);\n const timer = startBranchGcInterval(options, runtime);\n Object.assign(app, {\n lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer),\n });\n}\n\nfunction startBranchGcInterval(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): { stop(): void } | null {\n return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger);\n}\n\n/** Run auth boot validation, returning a failure result instead of throwing. */\nasync function runAuthSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n try {\n await options.auth?.setup?.();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"auth\", kind: \"auth\", name: \"auth\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Validation failure settled on the cell — null when the assembly is sound. */\nfunction invalidSources(options: ShelfOptions, cell: LifecycleCell): AdapterSetupResult | null {\n const invalid = validateBootAssembly(options);\n if (invalid.length === 0) {\n return null;\n }\n const result: AdapterSetupResult = { ok: false, failures: invalid };\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n return result;\n}\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n timer: { stop(): void } | null = null,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n logger,\n setup: async () => {\n const blocked = invalidSources(options, cell);\n if (blocked) {\n throw new AdapterLifecycleError(\"setup\", blocked.failures);\n }\n const result = await runAdapterSetups(collectSetups(options), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"setup\", result.failures);\n }\n },\n teardown: async () => {\n timer?.stop();\n const result = await runAdapterTeardowns(collectTeardowns(options), ctx, logger);\n await options.observability?.shutdown().catch((error: unknown) => {\n logger.error({ err: error }, \"observability shutdown failed\");\n });\n if (!result.ok) {\n throw new AdapterLifecycleError(\"teardown\", result.failures);\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAwBA,SAAS,gBAAgB,MAAqB,SAA4C;CACxF,QAAQ,MACL,WAAW;EACV,KAAK,UAAU;CACjB,SACM;EACJ,KAAK,UAAU;GAAE,IAAI;GAAO,UAAU,CAAC;EAAE;CAC3C,CACF;AACF;;AAGA,SAAS,UACP,SACA,KACA,QACA,MACM;CAGN,KAAK,SAAS,YAAyC;EACrD,MAAM,UAAU,qBAAqB,OAAO;EAC5C,IAAI,QAAQ,SAAS,GAAG;GACtB,OAAO,MAAM,EAAE,UAAU,QAAQ,GAAG,2BAA2B;GAC/D,OAAO;IAAE,IAAI;IAAO,UAAU;GAAQ;EACxC;EACA,MAAM,aAAa,MAAM,aAAa,OAAO;EAC7C,IAAI,YAAY;GACd,OAAO,MAAM,EAAE,UAAU,WAAW,SAAS,GAAG,mBAAmB;GACnE,OAAO;EACT;EACA,OAAO,MAAM,iBAAiB,cAAc,OAAO,GAAG,KAAK,MAAM;CACnE,EAAA,CAAG;CACH,gBAAgB,MAAM,KAAK,KAAK;AAClC;;AAGA,SAAS,iBAAiB,MAAwD;CAChF,IAAI;EACF,OAAO,KAAK,aAAa,CAAC,CAAC;CAC7B,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,YAAY,SAAuB,QAAsB;CAChE,IAAI,CAAC,QAAQ,MAAM;EACjB,OAAO,KAAK,oDAAoD;EAChE;CACF;CACA,MAAM,UAAU,iBAAiB,QAAQ,IAAI;CAC7C,IAAI,YAAY,MAAM;EACpB,OAAO,KAAK,+CAA+C;EAC3D;CACF;CACA,OAAO,KAAK,EAAE,cAAc,QAAQ,GAAG,cAAc;AACvD;;AAGA,SAAgB,gBACd,KACA,SACA,SACA,MACM;CACN,MAAM,WAAgC;EAAE,QAAQ,QAAQ;EAAQ,QAAQ,QAAQ;CAAO;CACvF,mBAAmB,SAAS,QAAQ,MAAM;CAC1C,YAAY,SAAS,QAAQ,MAAM;CACnC,UAAU,SAAS,UAAU,QAAQ,QAAQ,IAAI;CACjD,MAAM,QAAQ,sBAAsB,SAAS,OAAO;CACpD,OAAO,OAAO,KAAK,EACjB,WAAW,gBAAgB,SAAS,UAAU,QAAQ,QAAQ,MAAM,KAAK,EAC3E,CAAC;AACH;AAEA,SAAS,sBACP,SACA,SACyB;CACzB,OAAO,mBAAmB,QAAQ,UAAU,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,MAAM;AAC7F;;AAGA,eAAe,aAAa,SAA2D;CACrF,IAAI;EACF,MAAM,QAAQ,MAAM,QAAQ;EAC5B,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,OAAO;GACL,IAAI;GACJ,UAAU,CACR;IAAE,UAAU;IAAQ,MAAM;IAAQ,MAAM;IAAQ,OAAO,kBAAkB,SAAS,GAAG;GAAE,CACzF;EACF;CACF;AACF;;AAEA,SAAS,eAAe,SAAuB,MAAgD;CAC7F,MAAM,UAAU,qBAAqB,OAAO;CAC5C,IAAI,QAAQ,WAAW,GACrB,OAAO;CAET,MAAM,SAA6B;EAAE,IAAI;EAAO,UAAU;CAAQ;CAClE,KAAK,QAAQ,QAAQ,QAAQ,MAAM;CACnC,KAAK,UAAU;CACf,OAAO;AACT;;AAEA,SAAS,gBACP,SACA,KACA,QACA,MACA,QAAiC,MACjB;CAChB,OAAO;EACL,IAAI,QAAQ;GACV,OAAO,KAAK;EACd;EACA;EACA,OAAO,YAAY;GACjB,MAAM,UAAU,eAAe,SAAS,IAAI;GAC5C,IAAI,SACF,MAAM,IAAI,sBAAsB,SAAS,QAAQ,QAAQ;GAE3D,MAAM,SAAS,MAAM,iBAAiB,cAAc,OAAO,GAAG,KAAK,MAAM;GACzE,KAAK,QAAQ,QAAQ,QAAQ,MAAM;GACnC,KAAK,UAAU;GACf,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,sBAAsB,SAAS,OAAO,QAAQ;EAE5D;EACA,UAAU,YAAY;GACpB,OAAO,KAAK;GACZ,MAAM,SAAS,MAAM,oBAAoB,iBAAiB,OAAO,GAAG,KAAK,MAAM;GAC/E,MAAM,QAAQ,eAAe,SAAS,CAAC,CAAC,OAAO,UAAmB;IAChE,OAAO,MAAM,EAAE,KAAK,MAAM,GAAG,+BAA+B;GAC9D,CAAC;GACD,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,sBAAsB,YAAY,OAAO,QAAQ;EAE/D;CACF;AACF"}
@@ -0,0 +1,36 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ //#region src/middleware/auth-gate.ts
6
+ /** Resolve the request user from the auth adapter (null when auth is off or a token is used). */
7
+ async function resolveRequestUser(c, auth) {
8
+ if (!auth) return null;
9
+ if (c.req.header("authorization")) return null;
10
+ return await auth.check(c.req.raw);
11
+ }
12
+ /**
13
+ * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML
14
+ * requests redirect to login. API/auth routes answer 401/403 themselves, and
15
+ * published-Storybook routes enforce their own auth (public builds are
16
+ * viewable without a session, ADR 0011).
17
+ */
18
+ function authGate() {
19
+ return async (c, next) => {
20
+ const { user, authEnabled: isAuthEnabled } = getStore();
21
+ const { path } = c.req;
22
+ if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/.well-known/") || path.startsWith("/projects/") && path.includes("/storybook") || /^\/projects\/[^/]+\/?$/u.test(path)) {
23
+ await next();
24
+ return;
25
+ }
26
+ if (c.req.header("HX-Request") === "true") {
27
+ c.header("HX-Redirect", "/auth/login");
28
+ return c.body(null, 204);
29
+ }
30
+ return c.redirect("/auth/login", 302);
31
+ };
32
+ }
33
+ //#endregion
34
+ export { authGate, resolveRequestUser };
35
+
36
+ //# sourceMappingURL=auth-gate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/auth\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: Auth,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n path.startsWith(\"/.well-known/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\")) ||\n /^\\/projects\\/[^/]+\\/?$/u.test(path)\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":";;;;;;AAMA,eAAsB,mBACpB,GACA,MAC0B;CAC1B,IAAI,CAAC,MACH,OAAO;CAET,IAAI,EAAE,IAAI,OAAO,eAAe,GAC9B,OAAO;CAET,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,GAAG;AACnC;;;;;;;AAQA,SAAgB,WAAW;CAEzB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,MAAM,aAAa,kBAAkB,SAAS;EACtD,MAAM,EAAE,SAAS,EAAE;EACnB,IACE,CAAC,iBACD,QACA,KAAK,WAAW,OAAO,KACvB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,UAAU,KAC1B,KAAK,WAAW,eAAe,KAC9B,KAAK,WAAW,YAAY,KAAK,KAAK,SAAS,YAAY,KAC5D,0BAA0B,KAAK,IAAI,GACnC;GACA,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;GACzC,EAAE,OAAO,eAAe,aAAa;GACrC,OAAO,EAAE,KAAK,MAAM,GAAG;EACzB;EACA,OAAO,EAAE,SAAS,eAAe,GAAG;CACtC;AACF"}
@@ -0,0 +1,75 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { timingSafeEqualString } from "@storyshelf/core/utils";
5
+ import { SESSION_COOKIE } from "@storyshelf/core/types";
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ //#region src/middleware/csrf.ts
8
+ const DEFAULT_SESSION = "default";
9
+ const FALLBACK_SECRET = randomBytes(32).toString("hex");
10
+ /** Use the app secret when provided so tokens survive restarts and instances. */
11
+ function resolveSecret(secret) {
12
+ return secret ?? FALLBACK_SECRET;
13
+ }
14
+ function generateToken(secret, sessionId) {
15
+ const payload = `${sessionId}:${Date.now().toString(36)}`;
16
+ return `${payload}:${createHash("sha256").update(`${secret}:${payload}`).digest("hex").slice(0, 16)}`;
17
+ }
18
+ function verifyToken(secret, token, sessionId) {
19
+ const parts = token.split(":");
20
+ if (parts.length !== 3) return false;
21
+ const [payloadTimestamp, signature] = parts.slice(1);
22
+ if (!payloadTimestamp || !signature) return false;
23
+ const expectedSignature = createHash("sha256").update(`${secret}:${sessionId}:${payloadTimestamp}`).digest("hex").slice(0, 16);
24
+ if (!timingSafeEqualString(signature, expectedSignature)) return false;
25
+ const timestamp = Number.parseInt(payloadTimestamp, 36);
26
+ const age = Date.now() - timestamp;
27
+ return age >= 0 && age < 864e5;
28
+ }
29
+ /**
30
+ * Session id for CSRF binding: the raw session cookie value, or "default"
31
+ * for anonymous requests (login/invite forms have no session yet). A token
32
+ * minted for one session never validates for another.
33
+ */
34
+ function sessionIdFrom(c) {
35
+ const header = c.req.header("cookie") ?? "";
36
+ for (const part of header.split(";")) {
37
+ const eqIndex = part.indexOf("=");
38
+ if (eqIndex !== -1 && part.slice(0, eqIndex).trim() === SESSION_COOKIE) {
39
+ const value = part.slice(eqIndex + 1).trim();
40
+ if (value) return value;
41
+ }
42
+ }
43
+ return DEFAULT_SESSION;
44
+ }
45
+ /** Read a CSRF token from the request header or form body (never the URL: query tokens leak via logs, history, and Referer). */
46
+ async function tokenFromRequest(c) {
47
+ const header = c.req.header("x-csrf-token");
48
+ if (header) return header;
49
+ if (!(c.req.header("content-type") ?? "").includes("form")) return;
50
+ const value = (await c.req.parseBody())["csrf_token"];
51
+ return typeof value === "string" ? value : void 0;
52
+ }
53
+ /** Hono middleware issuing CSRF tokens on safe methods and verifying them on writes. */
54
+ function csrf(secret) {
55
+ const resolvedSecret = resolveSecret(secret);
56
+ return async (c, next) => {
57
+ const { method } = c.req;
58
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
59
+ c.header("X-CSRF-Token", generateToken(resolvedSecret, sessionIdFrom(c)));
60
+ await next();
61
+ return;
62
+ }
63
+ const token = await tokenFromRequest(c);
64
+ if (!token || !verifyToken(resolvedSecret, token, sessionIdFrom(c))) return c.json({ error: "Invalid CSRF token" }, 403);
65
+ await next();
66
+ };
67
+ }
68
+ /** Generate a CSRF token for the given session (bound to the same secret as `csrf`). */
69
+ function getCsrfToken(secret, sessionId = DEFAULT_SESSION) {
70
+ return generateToken(resolveSecret(secret), sessionId);
71
+ }
72
+ //#endregion
73
+ export { csrf, getCsrfToken, sessionIdFrom };
74
+
75
+ //# sourceMappingURL=csrf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["import { SESSION_COOKIE } from \"@storyshelf/core/types\";\nimport { timingSafeEqualString } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport { createHash, randomBytes } from \"node:crypto\";\n\nconst DEFAULT_SESSION = \"default\";\nconst FALLBACK_SECRET = randomBytes(32).toString(\"hex\");\n\n/** Use the app secret when provided so tokens survive restarts and instances. */\nfunction resolveSecret(secret?: string): string {\n return secret ?? FALLBACK_SECRET;\n}\n\nfunction generateToken(secret: string, sessionId: string): string {\n const timestamp = Date.now().toString(36);\n const payload = `${sessionId}:${timestamp}`;\n const signature = createHash(\"sha256\").update(`${secret}:${payload}`).digest(\"hex\").slice(0, 16);\n return `${payload}:${signature}`;\n}\n\nfunction verifyToken(secret: string, token: string, sessionId: string): boolean {\n const parts = token.split(\":\");\n if (parts.length !== 3) {\n return false;\n }\n const [payloadTimestamp, signature] = parts.slice(1);\n if (!payloadTimestamp || !signature) {\n return false;\n }\n const expectedSignature = createHash(\"sha256\")\n .update(`${secret}:${sessionId}:${payloadTimestamp}`)\n .digest(\"hex\")\n .slice(0, 16);\n if (!timingSafeEqualString(signature, expectedSignature)) {\n return false;\n }\n const timestamp = Number.parseInt(payloadTimestamp, 36);\n const age = Date.now() - timestamp;\n // Same-millisecond tokens are fresh (>= 0), not from the future.\n return age >= 0 && age < 24 * 60 * 60 * 1000;\n}\n\n/**\n * Session id for CSRF binding: the raw session cookie value, or \"default\"\n * for anonymous requests (login/invite forms have no session yet). A token\n * minted for one session never validates for another.\n */\nexport function sessionIdFrom(c: Pick<Context, \"req\">): string {\n const header = c.req.header(\"cookie\") ?? \"\";\n for (const part of header.split(\";\")) {\n const eqIndex = part.indexOf(\"=\");\n if (eqIndex !== -1 && part.slice(0, eqIndex).trim() === SESSION_COOKIE) {\n const value = part.slice(eqIndex + 1).trim();\n if (value) {\n return value;\n }\n }\n }\n return DEFAULT_SESSION;\n}\n\n/** Read a CSRF token from the request header or form body (never the URL: query tokens leak via logs, history, and Referer). */\nasync function tokenFromRequest(c: Context): Promise<string | undefined> {\n const header = c.req.header(\"x-csrf-token\");\n if (header) {\n return header;\n }\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (!contentType.includes(\"form\")) {\n return undefined;\n }\n const body = await c.req.parseBody();\n const value = body[\"csrf_token\"];\n return typeof value === \"string\" ? value : undefined;\n}\n\n/** Hono middleware issuing CSRF tokens on safe methods and verifying them on writes. */\nexport function csrf(secret?: string) {\n const resolvedSecret = resolveSecret(secret);\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { method } = c.req;\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n c.header(\"X-CSRF-Token\", generateToken(resolvedSecret, sessionIdFrom(c)));\n await next();\n return;\n }\n const token = await tokenFromRequest(c);\n if (!token || !verifyToken(resolvedSecret, token, sessionIdFrom(c))) {\n return c.json({ error: \"Invalid CSRF token\" }, 403);\n }\n await next();\n };\n}\n\n/** Generate a CSRF token for the given session (bound to the same secret as `csrf`). */\nexport function getCsrfToken(secret?: string, sessionId: string = DEFAULT_SESSION): string {\n return generateToken(resolveSecret(secret), sessionId);\n}\n"],"mappings":";;;;;;;AAKA,MAAM,kBAAkB;AACxB,MAAM,kBAAkB,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;;AAGtD,SAAS,cAAc,QAAyB;CAC9C,OAAO,UAAU;AACnB;AAEA,SAAS,cAAc,QAAgB,WAA2B;CAEhE,MAAM,UAAU,GAAG,UAAU,GADX,KAAK,IAAI,CAAC,CAAC,SAAS,EACE;CAExC,OAAO,GAAG,QAAQ,GADA,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAChE;AAC/B;AAEA,SAAS,YAAY,QAAgB,OAAe,WAA4B;CAC9E,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,WAAW,GACnB,OAAO;CAET,MAAM,CAAC,kBAAkB,aAAa,MAAM,MAAM,CAAC;CACnD,IAAI,CAAC,oBAAoB,CAAC,WACxB,OAAO;CAET,MAAM,oBAAoB,WAAW,QAAQ,CAAC,CAC3C,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,kBAAkB,CAAC,CACpD,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EAAE;CACd,IAAI,CAAC,sBAAsB,WAAW,iBAAiB,GACrD,OAAO;CAET,MAAM,YAAY,OAAO,SAAS,kBAAkB,EAAE;CACtD,MAAM,MAAM,KAAK,IAAI,IAAI;CAEzB,OAAO,OAAO,KAAK,MAAM;AAC3B;;;;;;AAOA,SAAgB,cAAc,GAAiC;CAC7D,MAAM,SAAS,EAAE,IAAI,OAAO,QAAQ,KAAK;CACzC,KAAK,MAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;EACpC,MAAM,UAAU,KAAK,QAAQ,GAAG;EAChC,IAAI,YAAY,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,MAAM,gBAAgB;GACtE,MAAM,QAAQ,KAAK,MAAM,UAAU,CAAC,CAAC,CAAC,KAAK;GAC3C,IAAI,OACF,OAAO;EAEX;CACF;CACA,OAAO;AACT;;AAGA,eAAe,iBAAiB,GAAyC;CACvE,MAAM,SAAS,EAAE,IAAI,OAAO,cAAc;CAC1C,IAAI,QACF,OAAO;CAGT,IAAI,EADgB,EAAE,IAAI,OAAO,cAAc,KAAK,GAAA,CACnC,SAAS,MAAM,GAC9B;CAGF,MAAM,SAAQ,MADK,EAAE,IAAI,UAAU,EAAA,CAChB;CACnB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;;AAGA,SAAgB,KAAK,QAAiB;CACpC,MAAM,iBAAiB,cAAc,MAAM;CAE3C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,WAAW,EAAE;EACrB,IAAI,WAAW,SAAS,WAAW,UAAU,WAAW,WAAW;GACjE,EAAE,OAAO,gBAAgB,cAAc,gBAAgB,cAAc,CAAC,CAAC,CAAC;GACxE,MAAM,KAAK;GACX;EACF;EACA,MAAM,QAAQ,MAAM,iBAAiB,CAAC;EACtC,IAAI,CAAC,SAAS,CAAC,YAAY,gBAAgB,OAAO,cAAc,CAAC,CAAC,GAChE,OAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,GAAG,GAAG;EAEpD,MAAM,KAAK;CACb;AACF;;AAGA,SAAgB,aAAa,QAAiB,YAAoB,iBAAyB;CACzF,OAAO,cAAc,cAAc,MAAM,GAAG,SAAS;AACvD"}
@@ -0,0 +1,68 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/middleware/rate-limit.ts
5
+ /** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */
6
+ function peerIp(c) {
7
+ const env = c.env ?? {};
8
+ const address = env.incoming?.socket?.remoteAddress ?? env.server?.incoming?.socket?.remoteAddress;
9
+ return typeof address === "string" && address ? address : void 0;
10
+ }
11
+ /**
12
+ * Rate-limit identity for one bucket prefix: direct peer first, then the
13
+ * first `X-Forwarded-For` entry (client-controlled, so only a fallback),
14
+ * then `X-Real-IP`, then a shared bucket. Deployments behind proxies with
15
+ * many users behind one peer IP share that peer's budget.
16
+ */
17
+ function rateLimitKey(prefix) {
18
+ return (c) => {
19
+ const peer = peerIp(c);
20
+ if (peer) return `${prefix}:${peer}`;
21
+ const forwarded = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
22
+ if (forwarded) return `${prefix}:fwd:${forwarded}`;
23
+ const real = c.req.header("x-real-ip")?.trim();
24
+ if (real) return `${prefix}:fwd:${real}`;
25
+ return `${prefix}:anonymous`;
26
+ };
27
+ }
28
+ const stores = /* @__PURE__ */ new Map();
29
+ /** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */
30
+ const MAX_BUCKETS = 1e4;
31
+ function cleanup() {
32
+ const now = Date.now();
33
+ for (const [key, entry] of stores) if (now > entry.resetAt) stores.delete(key);
34
+ }
35
+ setInterval(cleanup, 6e4).unref();
36
+ /** Hono middleware limiting requests per key within a sliding window. */
37
+ function rateLimit(options) {
38
+ const { windowMs, max, keyGenerator } = options;
39
+ return async (c, next) => {
40
+ const key = keyGenerator ? keyGenerator(c) : rateLimitKey("api")(c);
41
+ const now = Date.now();
42
+ let entry = stores.get(key);
43
+ if (!entry || now > entry.resetAt) {
44
+ entry = {
45
+ count: 0,
46
+ resetAt: now + windowMs
47
+ };
48
+ if (stores.size >= MAX_BUCKETS) {
49
+ const oldest = stores.keys().next();
50
+ if (!oldest.done) stores.delete(oldest.value);
51
+ }
52
+ stores.set(key, entry);
53
+ }
54
+ entry.count += 1;
55
+ if (entry.count > max) {
56
+ const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
57
+ c.header("Retry-After", String(retryAfter));
58
+ return c.json({ error: "Too many requests" }, 429);
59
+ }
60
+ c.header("X-RateLimit-Limit", String(max));
61
+ c.header("X-RateLimit-Remaining", String(Math.max(0, max - entry.count)));
62
+ await next();
63
+ };
64
+ }
65
+ //#endregion
66
+ export { rateLimit, rateLimitKey };
67
+
68
+ //# sourceMappingURL=rate-limit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\ninterface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\n/** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */\nfunction peerIp(c: Pick<Context, \"env\">): string | undefined {\n const env = (c.env ?? {}) as {\n incoming?: { socket?: { remoteAddress?: unknown } };\n server?: { incoming?: { socket?: { remoteAddress?: unknown } } };\n };\n const address =\n env.incoming?.socket?.remoteAddress ?? env.server?.incoming?.socket?.remoteAddress;\n return typeof address === \"string\" && address ? address : undefined;\n}\n\n/**\n * Rate-limit identity for one bucket prefix: direct peer first, then the\n * first `X-Forwarded-For` entry (client-controlled, so only a fallback),\n * then `X-Real-IP`, then a shared bucket. Deployments behind proxies with\n * many users behind one peer IP share that peer's budget.\n */\nexport function rateLimitKey(prefix: string): (c: Context) => string {\n return (c) => {\n const peer = peerIp(c);\n if (peer) {\n return `${prefix}:${peer}`;\n }\n const forwarded = c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim();\n if (forwarded) {\n return `${prefix}:fwd:${forwarded}`;\n }\n const real = c.req.header(\"x-real-ip\")?.trim();\n if (real) {\n return `${prefix}:fwd:${real}`;\n }\n return `${prefix}:anonymous`;\n };\n}\n\nconst stores = new Map<string, RateLimitEntry>();\n\n/** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */\nconst MAX_BUCKETS = 10_000;\n\nfunction cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n}\n\nsetInterval(cleanup, 60_000).unref();\n\n/** Hono middleware limiting requests per key within a sliding window. */\nexport function rateLimit(options: RateLimitOptions) {\n const { windowMs, max, keyGenerator } = options;\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const key = keyGenerator ? keyGenerator(c) : rateLimitKey(\"api\")(c);\n const now = Date.now();\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n if (stores.size >= MAX_BUCKETS) {\n const oldest = stores.keys().next();\n if (!oldest.done) {\n stores.delete(oldest.value);\n }\n }\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000);\n c.header(\"Retry-After\", String(retryAfter));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n await next();\n };\n}\n"],"mappings":";;;;;AAcA,SAAS,OAAO,GAA6C;CAC3D,MAAM,MAAO,EAAE,OAAO,CAAC;CAIvB,MAAM,UACJ,IAAI,UAAU,QAAQ,iBAAiB,IAAI,QAAQ,UAAU,QAAQ;CACvE,OAAO,OAAO,YAAY,YAAY,UAAU,UAAU,KAAA;AAC5D;;;;;;;AAQA,SAAgB,aAAa,QAAwC;CACnE,QAAQ,MAAM;EACZ,MAAM,OAAO,OAAO,CAAC;EACrB,IAAI,MACF,OAAO,GAAG,OAAO,GAAG;EAEtB,MAAM,YAAY,EAAE,IAAI,OAAO,iBAAiB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;EACvE,IAAI,WACF,OAAO,GAAG,OAAO,OAAO;EAE1B,MAAM,OAAO,EAAE,IAAI,OAAO,WAAW,CAAC,EAAE,KAAK;EAC7C,IAAI,MACF,OAAO,GAAG,OAAO,OAAO;EAE1B,OAAO,GAAG,OAAO;CACnB;AACF;AAEA,MAAM,yBAAS,IAAI,IAA4B;;AAG/C,MAAM,cAAc;AAEpB,SAAS,UAAgB;CACvB,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;AAGvB;AAEA,YAAY,SAAS,GAAM,CAAC,CAAC,MAAM;;AAGnC,SAAgB,UAAU,SAA2B;CACnD,MAAM,EAAE,UAAU,KAAK,iBAAiB;CAExC,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,MAAM,eAAe,aAAa,CAAC,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC;EAClE,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,QAAQ,OAAO,IAAI,GAAG;EAC1B,IAAI,CAAC,SAAS,MAAM,MAAM,SAAS;GACjC,QAAQ;IAAE,OAAO;IAAG,SAAS,MAAM;GAAS;GAC5C,IAAI,OAAO,QAAQ,aAAa;IAC9B,MAAM,SAAS,OAAO,KAAK,CAAC,CAAC,KAAK;IAClC,IAAI,CAAC,OAAO,MACV,OAAO,OAAO,OAAO,KAAK;GAE9B;GACA,OAAO,IAAI,KAAK,KAAK;EACvB;EACA,MAAM,SAAS;EACf,IAAI,MAAM,QAAQ,KAAK;GACrB,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU,OAAO,GAAI;GACzD,EAAE,OAAO,eAAe,OAAO,UAAU,CAAC;GAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;EACnD;EACA,EAAE,OAAO,qBAAqB,OAAO,GAAG,CAAC;EACzC,EAAE,OAAO,yBAAyB,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,CAAC;EACxE,MAAM,KAAK;CACb;AACF"}
@@ -0,0 +1,30 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/middleware/request-log.ts
5
+ /** Hono middleware logging request start/end with duration. */
6
+ function requestLogging(logger) {
7
+ return async (c, next) => {
8
+ const started = performance.now();
9
+ const id = c.get("requestId");
10
+ logger.info({
11
+ reqId: id,
12
+ method: c.req.method,
13
+ url: c.req.path
14
+ }, "request start");
15
+ await next();
16
+ const userId = c.get("userId");
17
+ logger.info({
18
+ reqId: id,
19
+ method: c.req.method,
20
+ url: c.req.path,
21
+ status: c.res.status,
22
+ durationMs: Math.round(performance.now() - started),
23
+ ...userId ? { userId } : {}
24
+ }, "request end");
25
+ };
26
+ }
27
+ //#endregion
28
+ export { requestLogging };
29
+
30
+ //# sourceMappingURL=request-log.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport type { Context, Next } from \"hono\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }, \"request start\");\n await next();\n const userId = c.get(\"userId\") as string | null | undefined;\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: c.req.path,\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n ...(userId ? { userId } : {}),\n },\n \"request end\",\n );\n };\n}\n"],"mappings":";;;;;AAIA,SAAgB,eAAe,QAAgB;CAE7C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,KAAK,EAAE,IAAI,WAAW;EAC5B,OAAO,KAAK;GAAE,OAAO;GAAI,QAAQ,EAAE,IAAI;GAAQ,KAAK,EAAE,IAAI;EAAK,GAAG,eAAe;EACjF,MAAM,KAAK;EACX,MAAM,SAAS,EAAE,IAAI,QAAQ;EAC7B,OAAO,KACL;GACE,OAAO;GACP,QAAQ,EAAE,IAAI;GACd,KAAK,EAAE,IAAI;GACX,QAAQ,EAAE,IAAI;GACd,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;GAClD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,GACA,aACF;CACF;AACF"}
@@ -0,0 +1,39 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { runWithTimings } from "@storyshelf/core/utils";
5
+ import { setMetric, timing } from "hono/timing";
6
+ //#region src/middleware/server-timing.ts
7
+ /**
8
+ * Opt-in `Server-Timing` response headers over Hono's timing middleware.
9
+ *
10
+ * Enabled only when `ShelfConfig.serverTiming` is true (default off — some
11
+ * deployments prefer no timing headers). The `total` metric matches the
12
+ * `http.server` span; `db`/`storage` are roll-ups of the instrumented
13
+ * adapter calls in this request (same family names as the `db.*` spans;
14
+ * per-operation detail stays in traces). Background capture work never
15
+ * appears here — it has no response; see attempt logs instead.
16
+ */
17
+ /** Create the Server-Timing middleware (pass-through unless enabled). */
18
+ function serverTiming(enabled) {
19
+ const inner = timing({
20
+ total: true,
21
+ enabled: false
22
+ });
23
+ return async (c, next) => {
24
+ if (!enabled) {
25
+ await next();
26
+ return;
27
+ }
28
+ const { timings } = await runWithTimings(async () => {
29
+ await inner(c, next);
30
+ });
31
+ for (const [name, ms] of Object.entries(timings).toSorted((a, b) => a[0].localeCompare(b[0]))) setMetric(c, name, ms);
32
+ const headers = c.get("metric")?.headers ?? [];
33
+ if (headers.length > 0) c.res.headers.append("Server-Timing", headers.join(","));
34
+ };
35
+ }
36
+ //#endregion
37
+ export { serverTiming };
38
+
39
+ //# sourceMappingURL=server-timing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-timing.mjs","names":[],"sources":["../../src/middleware/server-timing.ts"],"sourcesContent":["/**\n * Opt-in `Server-Timing` response headers over Hono's timing middleware.\n *\n * Enabled only when `ShelfConfig.serverTiming` is true (default off — some\n * deployments prefer no timing headers). The `total` metric matches the\n * `http.server` span; `db`/`storage` are roll-ups of the instrumented\n * adapter calls in this request (same family names as the `db.*` spans;\n * per-operation detail stays in traces). Background capture work never\n * appears here — it has no response; see attempt logs instead.\n */\nimport { runWithTimings } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport { setMetric, timing } from \"hono/timing\";\n\n/** Create the Server-Timing middleware (pass-through unless enabled). */\nexport function serverTiming(enabled: boolean) {\n const inner = timing({ total: true, enabled: false });\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (!enabled) {\n await next();\n return;\n }\n const { timings } = await runWithTimings(async () => {\n await inner(c as never, next);\n });\n for (const [name, ms] of Object.entries(timings).toSorted((a, b) => a[0].localeCompare(b[0]))) {\n setMetric(c, name, ms);\n }\n const headers = (c.get(\"metric\") as { headers: string[] } | undefined)?.headers ?? [];\n if (headers.length > 0) {\n c.res.headers.append(\"Server-Timing\", headers.join(\",\"));\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAeA,SAAgB,aAAa,SAAkB;CAC7C,MAAM,QAAQ,OAAO;EAAE,OAAO;EAAM,SAAS;CAAM,CAAC;CAEpD,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,CAAC,SAAS;GACZ,MAAM,KAAK;GACX;EACF;EACA,MAAM,EAAE,YAAY,MAAM,eAAe,YAAY;GACnD,MAAM,MAAM,GAAY,IAAI;EAC9B,CAAC;EACD,KAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,OAAO,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,GAC1F,UAAU,GAAG,MAAM,EAAE;EAEvB,MAAM,UAAW,EAAE,IAAI,QAAQ,CAAC,EAAwC,WAAW,CAAC;EACpF,IAAI,QAAQ,SAAS,GACnB,EAAE,IAAI,QAAQ,OAAO,iBAAiB,QAAQ,KAAK,GAAG,CAAC;CAE3D;AACF"}
@@ -0,0 +1,37 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { sanitizeErrorText } from "@storyshelf/core/utils";
5
+ //#region src/middleware/setup-gate.ts
6
+ /** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */
7
+ function publicFailures(result) {
8
+ return result.failures.map((failure) => ({
9
+ category: failure.category,
10
+ kind: failure.kind,
11
+ error: sanitizeErrorText(failure.error, 500)
12
+ }));
13
+ }
14
+ /**
15
+ * Block requests until adapter setup settles; answer 503 with the per-adapter
16
+ * failures when setup failed. Health probes and the admin System page are
17
+ * exempt (they report setup state themselves instead of being masked by
18
+ * the gate — both are site-admin-gated where it matters).
19
+ */
20
+ function setupGate(getReady) {
21
+ return async (c, next) => {
22
+ if (c.req.path === "/api/v1/health" || c.req.path === "/admin") {
23
+ await next();
24
+ return;
25
+ }
26
+ const result = await getReady();
27
+ if (!result.ok) return c.json({
28
+ error: "Adapters failed setup",
29
+ failures: publicFailures(result)
30
+ }, 503);
31
+ await next();
32
+ };
33
+ }
34
+ //#endregion
35
+ export { setupGate };
36
+
37
+ //# sourceMappingURL=setup-gate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-gate.mjs","names":[],"sources":["../../src/middleware/setup-gate.ts"],"sourcesContent":["import type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport type { QueueWiring } from \"../capture-setup.ts\";\nimport type { ServerRuntime } from \"../runtime.ts\";\n\nexport interface MiddlewareWiring extends ServerRuntime, QueueWiring {\n options: ShelfOptions;\n getReady: () => Promise<AdapterSetupResult>;\n}\n\n/** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */\nfunction publicFailures(\n result: AdapterSetupResult,\n): { category: string; kind: string; error: string }[] {\n return result.failures.map((failure) => ({\n category: failure.category,\n kind: failure.kind,\n error: sanitizeErrorText(failure.error, 500),\n }));\n}\n\n/**\n * Block requests until adapter setup settles; answer 503 with the per-adapter\n * failures when setup failed. Health probes and the admin System page are\n * exempt (they report setup state themselves instead of being masked by\n * the gate — both are site-admin-gated where it matters).\n */\nexport function setupGate(getReady: () => Promise<AdapterSetupResult>) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (c.req.path === \"/api/v1/health\" || c.req.path === \"/admin\") {\n await next();\n return;\n }\n const result = await getReady();\n if (!result.ok) {\n return c.json({ error: \"Adapters failed setup\", failures: publicFailures(result) }, 503);\n }\n await next();\n };\n}\n"],"mappings":";;;;;;AAaA,SAAS,eACP,QACqD;CACrD,OAAO,OAAO,SAAS,KAAK,aAAa;EACvC,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,OAAO,kBAAkB,QAAQ,OAAO,GAAG;CAC7C,EAAE;AACJ;;;;;;;AAQA,SAAgB,UAAU,UAA6C;CAErE,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,EAAE,IAAI,SAAS,oBAAoB,EAAE,IAAI,SAAS,UAAU;GAC9D,MAAM,KAAK;GACX;EACF;EACA,MAAM,SAAS,MAAM,SAAS;EAC9B,IAAI,CAAC,OAAO,IACV,OAAO,EAAE,KAAK;GAAE,OAAO;GAAyB,UAAU,eAAe,MAAM;EAAE,GAAG,GAAG;EAEzF,MAAM,KAAK;CACb;AACF"}
@@ -0,0 +1,33 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { runWithStore } from "../store.mjs";
5
+ import { sessionIdFrom } from "./csrf.mjs";
6
+ //#region src/middleware/store-scope.ts
7
+ /** Hono middleware running downstream handlers inside the request store. */
8
+ function storeScope(deps) {
9
+ return async (c, next) => {
10
+ const user = await deps.resolveUser(c);
11
+ c.set("userId", user?.id ?? null);
12
+ await runWithStore({
13
+ db: deps.db,
14
+ storage: deps.storage,
15
+ config: deps.config,
16
+ ui: deps.ui,
17
+ logger: deps.logger,
18
+ user,
19
+ authEnabled: deps.authEnabled,
20
+ sessionId: sessionIdFrom(c),
21
+ enqueueCapture: deps.enqueueCapture,
22
+ captureQueue: deps.captureQueue,
23
+ gitHosts: deps.gitHosts,
24
+ notifiers: deps.notifiers
25
+ }, async () => {
26
+ await next();
27
+ });
28
+ };
29
+ }
30
+ //#endregion
31
+ export { storeScope };
32
+
33
+ //# sourceMappingURL=store-scope.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport { runWithStore } from \"../store.ts\";\nimport { sessionIdFrom } from \"./csrf.ts\";\n\n/** Dependencies scoped into the request store for router handlers. */\nexport interface StoreScopeDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n notifiers: NotifierProvider[];\n resolveUser: (c: Context) => Promise<AuthUser | null>;\n}\n\n/** Hono middleware running downstream handlers inside the request store. */\nexport function storeScope(deps: StoreScopeDeps) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const user = await deps.resolveUser(c);\n c.set(\"userId\", user?.id ?? null);\n await runWithStore(\n {\n db: deps.db,\n storage: deps.storage,\n config: deps.config,\n ui: deps.ui,\n logger: deps.logger,\n user,\n authEnabled: deps.authEnabled,\n sessionId: sessionIdFrom(c),\n enqueueCapture: deps.enqueueCapture,\n captureQueue: deps.captureQueue,\n gitHosts: deps.gitHosts,\n notifiers: deps.notifiers,\n },\n async () => {\n await next();\n },\n );\n };\n}\n"],"mappings":";;;;;;;AA4BA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,OAAO,MAAM,KAAK,YAAY,CAAC;EACrC,EAAE,IAAI,UAAU,MAAM,MAAM,IAAI;EAChC,MAAM,aACJ;GACE,IAAI,KAAK;GACT,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,QAAQ,KAAK;GACb;GACA,aAAa,KAAK;GAClB,WAAW,cAAc,CAAC;GAC1B,gBAAgB,KAAK;GACrB,cAAc,KAAK;GACnB,UAAU,KAAK;GACf,WAAW,KAAK;EAClB,GACA,YAAY;GACV,MAAM,KAAK;EACb,CACF;CACF;AACF"}
@@ -0,0 +1,17 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
4
+ import { ShelfConfig, UIConfig } from "@storyshelf/core/config";
5
+ import { Logger } from "@storyshelf/core/logger";
6
+ import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
7
+ import "@storyshelf/core/schema";
8
+ //#region src/notify.d.ts
9
+ /**
10
+ * Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth
11
+ * events out to admin channels. Safe to call anywhere — outside a request
12
+ * scope (e.g. boot) it resolves to a no-op instead of throwing.
13
+ */
14
+ declare function createAuthSystemHook(): (event: "sys:user-created" | "sys:invite-issued", data: Record<string, unknown>) => Promise<void>;
15
+ //#endregion
16
+ export { createAuthSystemHook };
17
+ //# sourceMappingURL=notify.d.mts.map