@furystack/rest-service 13.0.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +47 -28
  3. package/esm/actions/get-current-user.spec.js +10 -6
  4. package/esm/actions/get-current-user.spec.js.map +1 -1
  5. package/esm/actions/is-authenticated.spec.js +10 -6
  6. package/esm/actions/is-authenticated.spec.js.map +1 -1
  7. package/esm/actions/logout-action.spec.js +11 -9
  8. package/esm/actions/logout-action.spec.js.map +1 -1
  9. package/esm/actions/logout.js +1 -1
  10. package/esm/actions/logout.js.map +1 -1
  11. package/esm/actions/password-login-action.js +1 -1
  12. package/esm/actions/password-login-action.js.map +1 -1
  13. package/esm/actions/password-login-action.spec.js +23 -28
  14. package/esm/actions/password-login-action.spec.js.map +1 -1
  15. package/esm/authenticate.js +1 -1
  16. package/esm/authenticate.js.map +1 -1
  17. package/esm/authenticate.spec.js +41 -33
  18. package/esm/authenticate.spec.js.map +1 -1
  19. package/esm/authorize.spec.js +34 -29
  20. package/esm/authorize.spec.js.map +1 -1
  21. package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
  22. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  23. package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
  24. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  25. package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
  26. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  27. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
  28. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  29. package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
  30. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  31. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
  32. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  33. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
  34. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  35. package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
  36. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  37. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
  38. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  39. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
  40. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
  41. package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
  42. package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
  43. package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
  44. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
  45. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
  46. package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
  47. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  48. package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
  49. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  50. package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
  51. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  52. package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
  53. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  54. package/esm/endpoint-generators/create-post-endpoint.js +8 -13
  55. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  56. package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
  57. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  58. package/esm/endpoint-generators/utils.d.ts +17 -2
  59. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  60. package/esm/endpoint-generators/utils.js +33 -20
  61. package/esm/endpoint-generators/utils.js.map +1 -1
  62. package/esm/get-schema-from-api.d.ts +1 -1
  63. package/esm/get-schema-from-api.d.ts.map +1 -1
  64. package/esm/get-schema-from-api.spec.js.map +1 -1
  65. package/esm/helpers.d.ts +53 -87
  66. package/esm/helpers.d.ts.map +1 -1
  67. package/esm/helpers.js +91 -92
  68. package/esm/helpers.js.map +1 -1
  69. package/esm/helpers.spec.d.ts +2 -1
  70. package/esm/helpers.spec.d.ts.map +1 -1
  71. package/esm/helpers.spec.js +41 -38
  72. package/esm/helpers.spec.js.map +1 -1
  73. package/esm/http-authentication-settings.d.ts +35 -12
  74. package/esm/http-authentication-settings.d.ts.map +1 -1
  75. package/esm/http-authentication-settings.js +21 -29
  76. package/esm/http-authentication-settings.js.map +1 -1
  77. package/esm/http-server-pool.d.ts +64 -0
  78. package/esm/http-server-pool.d.ts.map +1 -0
  79. package/esm/http-server-pool.js +95 -0
  80. package/esm/http-server-pool.js.map +1 -0
  81. package/esm/http-server-pool.spec.d.ts +2 -0
  82. package/esm/http-server-pool.spec.d.ts.map +1 -0
  83. package/esm/http-server-pool.spec.js +193 -0
  84. package/esm/http-server-pool.spec.js.map +1 -0
  85. package/esm/http-user-context.d.ts +31 -44
  86. package/esm/http-user-context.d.ts.map +1 -1
  87. package/esm/http-user-context.js +76 -95
  88. package/esm/http-user-context.js.map +1 -1
  89. package/esm/http-user-context.spec.d.ts +8 -2
  90. package/esm/http-user-context.spec.d.ts.map +1 -1
  91. package/esm/http-user-context.spec.js +189 -337
  92. package/esm/http-user-context.spec.js.map +1 -1
  93. package/esm/index.d.ts +6 -4
  94. package/esm/index.d.ts.map +1 -1
  95. package/esm/index.js +6 -4
  96. package/esm/index.js.map +1 -1
  97. package/esm/login-response-strategy.d.ts +11 -10
  98. package/esm/login-response-strategy.d.ts.map +1 -1
  99. package/esm/login-response-strategy.js +7 -6
  100. package/esm/login-response-strategy.js.map +1 -1
  101. package/esm/login-response-strategy.spec.js +25 -29
  102. package/esm/login-response-strategy.spec.js.map +1 -1
  103. package/esm/proxy-manager.spec.js +152 -176
  104. package/esm/proxy-manager.spec.js.map +1 -1
  105. package/esm/proxy-runtime.d.ts +27 -0
  106. package/esm/proxy-runtime.d.ts.map +1 -0
  107. package/esm/proxy-runtime.js +45 -0
  108. package/esm/proxy-runtime.js.map +1 -0
  109. package/esm/proxy-runtime.spec.d.ts +2 -0
  110. package/esm/proxy-runtime.spec.d.ts.map +1 -0
  111. package/esm/proxy-runtime.spec.js +39 -0
  112. package/esm/proxy-runtime.spec.js.map +1 -0
  113. package/esm/rest-api-runtime.d.ts +76 -0
  114. package/esm/rest-api-runtime.d.ts.map +1 -0
  115. package/esm/rest-api-runtime.js +131 -0
  116. package/esm/rest-api-runtime.js.map +1 -0
  117. package/esm/rest-api-runtime.spec.d.ts +2 -0
  118. package/esm/rest-api-runtime.spec.d.ts.map +1 -0
  119. package/esm/rest-api-runtime.spec.js +58 -0
  120. package/esm/rest-api-runtime.spec.js.map +1 -0
  121. package/esm/rest-service.integration.spec.d.ts +1 -1
  122. package/esm/rest-service.integration.spec.d.ts.map +1 -1
  123. package/esm/rest-service.integration.spec.js +57 -80
  124. package/esm/rest-service.integration.spec.js.map +1 -1
  125. package/esm/rest.integration.test.js +26 -84
  126. package/esm/rest.integration.test.js.map +1 -1
  127. package/esm/server-telemetry.d.ts +57 -0
  128. package/esm/server-telemetry.d.ts.map +1 -0
  129. package/esm/server-telemetry.js +26 -0
  130. package/esm/server-telemetry.js.map +1 -0
  131. package/esm/server-telemetry.spec.d.ts +2 -0
  132. package/esm/server-telemetry.spec.d.ts.map +1 -0
  133. package/esm/server-telemetry.spec.js +52 -0
  134. package/esm/server-telemetry.spec.js.map +1 -0
  135. package/esm/static-server-manager.spec.js +60 -125
  136. package/esm/static-server-manager.spec.js.map +1 -1
  137. package/esm/static-site-runtime.d.ts +21 -0
  138. package/esm/static-site-runtime.d.ts.map +1 -0
  139. package/esm/static-site-runtime.js +48 -0
  140. package/esm/static-site-runtime.js.map +1 -0
  141. package/esm/static-site-runtime.spec.d.ts +2 -0
  142. package/esm/static-site-runtime.spec.d.ts.map +1 -0
  143. package/esm/static-site-runtime.spec.js +30 -0
  144. package/esm/static-site-runtime.spec.js.map +1 -0
  145. package/esm/user-store.d.ts +39 -0
  146. package/esm/user-store.d.ts.map +1 -0
  147. package/esm/user-store.js +62 -0
  148. package/esm/user-store.js.map +1 -0
  149. package/esm/user-store.spec.d.ts +2 -0
  150. package/esm/user-store.spec.d.ts.map +1 -0
  151. package/esm/user-store.spec.js +72 -0
  152. package/esm/user-store.spec.js.map +1 -0
  153. package/esm/validate.integration.spec.js +26 -28
  154. package/esm/validate.integration.spec.js.map +1 -1
  155. package/package.json +8 -8
  156. package/src/actions/get-current-user.spec.ts +11 -10
  157. package/src/actions/is-authenticated.spec.ts +12 -7
  158. package/src/actions/logout-action.spec.ts +12 -12
  159. package/src/actions/logout.ts +1 -1
  160. package/src/actions/password-login-action.spec.ts +30 -30
  161. package/src/actions/password-login-action.ts +1 -1
  162. package/src/authenticate.spec.ts +45 -51
  163. package/src/authenticate.ts +1 -1
  164. package/src/authorize.spec.ts +37 -39
  165. package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
  166. package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
  167. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
  168. package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
  169. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
  170. package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
  171. package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
  172. package/src/endpoint-generators/create-get-schema-action.ts +1 -1
  173. package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
  174. package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
  175. package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
  176. package/src/endpoint-generators/create-post-endpoint.ts +13 -23
  177. package/src/endpoint-generators/utils.ts +36 -28
  178. package/src/get-schema-from-api.spec.ts +1 -1
  179. package/src/get-schema-from-api.ts +1 -1
  180. package/src/helpers.spec.ts +51 -41
  181. package/src/helpers.ts +140 -109
  182. package/src/http-authentication-settings.ts +51 -25
  183. package/src/http-server-pool.spec.ts +220 -0
  184. package/src/http-server-pool.ts +169 -0
  185. package/src/http-user-context.spec.ts +203 -383
  186. package/src/http-user-context.ts +114 -91
  187. package/src/index.ts +6 -4
  188. package/src/login-response-strategy.spec.ts +32 -31
  189. package/src/login-response-strategy.ts +13 -12
  190. package/src/proxy-manager.spec.ts +152 -182
  191. package/src/proxy-runtime.spec.ts +50 -0
  192. package/src/proxy-runtime.ts +75 -0
  193. package/src/rest-api-runtime.spec.ts +77 -0
  194. package/src/rest-api-runtime.ts +204 -0
  195. package/src/rest-service.integration.spec.ts +65 -85
  196. package/src/rest.integration.test.ts +27 -87
  197. package/src/server-telemetry.spec.ts +56 -0
  198. package/src/server-telemetry.ts +51 -0
  199. package/src/static-server-manager.spec.ts +63 -151
  200. package/src/static-site-runtime.spec.ts +39 -0
  201. package/src/static-site-runtime.ts +75 -0
  202. package/src/user-store.spec.ts +86 -0
  203. package/src/user-store.ts +68 -0
  204. package/src/validate.integration.spec.ts +39 -35
  205. package/esm/api-manager.d.ts +0 -104
  206. package/esm/api-manager.d.ts.map +0 -1
  207. package/esm/api-manager.js +0 -179
  208. package/esm/api-manager.js.map +0 -1
  209. package/esm/proxy-manager.d.ts +0 -53
  210. package/esm/proxy-manager.d.ts.map +0 -1
  211. package/esm/proxy-manager.js +0 -84
  212. package/esm/proxy-manager.js.map +0 -1
  213. package/esm/server-manager.d.ts +0 -48
  214. package/esm/server-manager.d.ts.map +0 -1
  215. package/esm/server-manager.js +0 -100
  216. package/esm/server-manager.js.map +0 -1
  217. package/esm/static-server-manager.d.ts +0 -19
  218. package/esm/static-server-manager.d.ts.map +0 -1
  219. package/esm/static-server-manager.js +0 -65
  220. package/esm/static-server-manager.js.map +0 -1
  221. package/src/api-manager.ts +0 -304
  222. package/src/proxy-manager.ts +0 -102
  223. package/src/server-manager.ts +0 -135
  224. package/src/static-server-manager.ts +0 -90
@@ -1,224 +1,137 @@
1
- import { Injector } from '@furystack/inject'
2
- import { sleepAsync, usingAsync } from '@furystack/utils'
3
- import { ServerManager } from './server-manager.js'
4
- import { StaticServerManager } from './static-server-manager.js'
5
- import { describe, it, expect, vi } from 'vitest'
6
1
  import { getPort } from '@furystack/core/port-generator'
2
+ import { createInjector } from '@furystack/inject'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { useStaticFiles } from './helpers.js'
6
+ import { HttpServerPoolToken } from './http-server-pool.js'
7
+ import { buildStaticSiteServerApi } from './static-site-runtime.js'
7
8
 
8
- describe('StaticServerManager', () => {
9
+ describe('useStaticFiles / buildStaticSiteServerApi', () => {
9
10
  describe('Top level routing', () => {
10
- it('Should return a 404 without fallback', async () => {
11
- await usingAsync(new Injector(), async (injector) => {
12
- const staticServerManager = injector.getInstance(StaticServerManager)
11
+ it('responds 404 without a fallback', async () => {
12
+ await usingAsync(createInjector(), async (i) => {
13
13
  const port = getPort()
14
- await staticServerManager.addStaticSite({
15
- baseUrl: '/',
16
- path: '.',
17
- port,
18
- })
14
+ await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
19
15
 
20
16
  const result = await fetch(`http://127.0.0.1:${port}/not-found.html`)
21
17
  expect(result.ok).toBe(false)
22
18
  expect(result.status).toBe(404)
23
- expect(result?.headers.get('content-type')).toBe('text/plain')
24
- const body = await result.text()
25
- expect(body).toBe('Not found')
19
+ expect(result.headers.get('content-type')).toBe('text/plain')
20
+ expect(await result.text()).toBe('Not found')
26
21
  })
27
22
  })
28
23
 
29
- it('Should return a fallback', async () => {
30
- await usingAsync(new Injector(), async (injector) => {
31
- const staticServerManager = injector.getInstance(StaticServerManager)
24
+ it('falls back to the configured file and returns its content-type', async () => {
25
+ await usingAsync(createInjector(), async (i) => {
32
26
  const port = getPort()
33
-
34
- await staticServerManager.addStaticSite({
27
+ await useStaticFiles({
28
+ injector: i,
35
29
  baseUrl: '/',
36
30
  path: '.',
37
31
  fallback: 'package.json',
38
32
  port,
39
- headers: {
40
- 'custom-header': 'custom-value',
41
- },
33
+ headers: { 'custom-header': 'custom-value' },
42
34
  })
43
35
 
44
36
  const result = await fetch(`http://localhost:${port}/not-found.html`)
45
-
46
37
  expect(result.headers.get('content-type')).toBe('application/json')
47
38
  expect(result.headers.get('custom-header')).toBe('custom-value')
48
39
  })
49
40
  })
50
41
 
51
- it('Should return a fallback for root files', async () => {
52
- await usingAsync(new Injector(), async (injector) => {
53
- const staticServerManager = injector.getInstance(StaticServerManager)
42
+ it('falls back for the root url when no index exists', async () => {
43
+ await usingAsync(createInjector(), async (i) => {
54
44
  const port = getPort()
55
-
56
- await staticServerManager.addStaticSite({
45
+ await useStaticFiles({
46
+ injector: i,
57
47
  baseUrl: '/',
58
48
  path: '.',
59
49
  fallback: 'package.json',
60
50
  port,
61
- headers: {
62
- 'custom-header': 'custom-value',
63
- },
51
+ headers: { 'custom-header': 'custom-value' },
64
52
  })
65
53
 
66
54
  const result = await fetch(`http://localhost:${port}`)
67
-
68
55
  expect(result.headers.get('content-type')).toBe('application/json')
69
56
  expect(result.headers.get('custom-header')).toBe('custom-value')
70
57
  })
71
58
  })
72
59
 
73
- it('Should return a defined file from a root directory', async () => {
74
- await usingAsync(new Injector(), async (injector) => {
75
- const staticServerManager = injector.getInstance(StaticServerManager)
60
+ it('serves an existing file from the root directory', async () => {
61
+ await usingAsync(createInjector(), async (i) => {
76
62
  const port = getPort()
77
-
78
- await staticServerManager.addStaticSite({
79
- baseUrl: '/',
80
- path: '.',
81
- port,
82
- headers: {
83
- 'custom-header': 'custom-value',
84
- },
85
- })
86
-
63
+ await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
87
64
  const result = await fetch(`http://localhost:${port}/README.md`)
88
-
89
65
  expect(result.headers.get('content-type')).toBe('text/markdown')
90
- expect(result.headers.get('custom-header')).toBe('custom-value')
91
66
  })
92
67
  })
93
68
 
94
- it('Should return a defined file from a subdirectory', async () => {
95
- await usingAsync(new Injector(), async (injector) => {
96
- const staticServerManager = injector.getInstance(StaticServerManager)
69
+ it('serves files from subdirectories', async () => {
70
+ await usingAsync(createInjector(), async (i) => {
97
71
  const port = getPort()
98
-
99
- await staticServerManager.addStaticSite({
100
- baseUrl: '/',
101
- path: '.',
102
- port,
103
- })
104
-
72
+ await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
105
73
  const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
106
-
107
74
  expect(result.headers.get('content-type')).toBe('text/markdown')
108
75
  })
109
76
  })
110
77
  })
111
78
 
112
79
  describe('Non-top level routing', () => {
113
- it('Should not handle a request when the path is not matching', async () => {
114
- await usingAsync(new Injector(), async (injector) => {
115
- const staticServerManager = injector.getInstance(StaticServerManager)
80
+ it('leaves requests outside the base url to the next api in the chain', async () => {
81
+ await usingAsync(createInjector(), async (i) => {
116
82
  const port = getPort()
83
+ const serverApi = await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
117
84
 
118
- await staticServerManager.addStaticSite({
119
- baseUrl: '/bundle',
120
- path: '.',
121
- port,
122
- })
123
-
124
- const server = [...injector.getInstance(ServerManager).servers.values()][0]
125
-
126
- server.apis[0].onRequest = vi.fn()
85
+ const record = await i.get(HttpServerPoolToken).acquire({ port })
86
+ const onRequest = vi.fn()
87
+ // Swap in a spy to observe non-matching requests never reach the handler.
88
+ serverApi.onRequest = onRequest
89
+ record.apis[0] = serverApi
127
90
 
128
91
  fetch(`http://localhost:${port}/bundleToAnotherFolder/not-found.html`).catch(() => {
129
- /** should fall, ignore */
92
+ // Expected to fail: the server has no matching API and drops the socket.
130
93
  })
131
-
132
94
  await sleepAsync(100)
133
-
134
- expect(server.apis[0].onRequest).not.toHaveBeenCalled()
95
+ expect(onRequest).not.toHaveBeenCalled()
135
96
  })
136
97
  })
137
98
 
138
- it('Should return a 404 without fallback', async () => {
139
- await usingAsync(new Injector(), async (injector) => {
140
- const staticServerManager = injector.getInstance(StaticServerManager)
99
+ it('returns 404 under a sub-base url when the file is missing and no fallback is set', async () => {
100
+ await usingAsync(createInjector(), async (i) => {
141
101
  const port = getPort()
142
-
143
- await staticServerManager.addStaticSite({
144
- baseUrl: '/bundle',
145
- path: '.',
146
- port,
147
- })
102
+ await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
148
103
 
149
104
  const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
150
105
  expect(result.ok).toBe(false)
151
106
  expect(result.status).toBe(404)
152
107
  expect(result.headers.get('content-type')).toBe('text/plain')
153
- const body = await result.text()
154
- expect(body).toBe('Not found')
108
+ expect(await result.text()).toBe('Not found')
155
109
  })
156
110
  })
157
111
 
158
- it('Should return a fallback', async () => {
159
- await usingAsync(new Injector(), async (injector) => {
160
- const staticServerManager = injector.getInstance(StaticServerManager)
112
+ it('serves the configured fallback under a sub-base url', async () => {
113
+ await usingAsync(createInjector(), async (i) => {
161
114
  const port = getPort()
162
-
163
- await staticServerManager.addStaticSite({
164
- baseUrl: '/bundle',
165
- path: '.',
166
- fallback: 'package.json',
167
- port,
168
- })
115
+ await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', fallback: 'package.json', port })
169
116
 
170
117
  const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
171
-
172
118
  expect(result.headers.get('content-type')).toBe('application/json')
173
119
  })
174
120
  })
175
-
176
- it('Should return a defined file from a root directory', async () => {
177
- await usingAsync(new Injector(), async (injector) => {
178
- const staticServerManager = injector.getInstance(StaticServerManager)
179
- const port = getPort()
180
-
181
- await staticServerManager.addStaticSite({
182
- baseUrl: '/',
183
- path: '.',
184
- port,
185
- })
186
-
187
- const result = await fetch(`http://localhost:${port}/README.md`)
188
-
189
- expect(result.headers.get('content-type')).toBe('text/markdown')
190
- })
191
- })
192
-
193
- it('Should return a defined file from a subdirectory', async () => {
194
- await usingAsync(new Injector(), async (injector) => {
195
- const staticServerManager = injector.getInstance(StaticServerManager)
196
- const port = getPort()
197
-
198
- await staticServerManager.addStaticSite({
199
- baseUrl: '/',
200
- path: '.',
201
- port,
202
- })
203
-
204
- const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
205
-
206
- expect(result.headers.get('content-type')).toBe('text/markdown')
207
- })
208
- })
209
121
  })
210
122
 
211
123
  describe('shouldExec', () => {
212
- it('Should NOT match for requests with not defined method', () => {
213
- const staticServerManager = new StaticServerManager()
214
- expect(staticServerManager.shouldExec('/')({ req: { url: '/' } })).toBe(false)
124
+ const api = (baseUrl: string) => buildStaticSiteServerApi({ baseUrl, path: '.', port: 0 })
125
+
126
+ it('does not match when the request method is missing', () => {
127
+ expect(api('/').shouldExec({ req: { url: '/' } as never, res: {} as never })).toBe(false)
215
128
  })
216
129
 
217
- it('Should NOT match for non-GET requests', () => {
218
- const staticServerManager = new StaticServerManager()
219
- expect(staticServerManager.shouldExec('/')({ req: { method: 'POST', url: '/' } })).toBe(false)
130
+ it('does not match non-GET methods', () => {
131
+ expect(api('/').shouldExec({ req: { method: 'POST', url: '/' } as never, res: {} as never })).toBe(false)
220
132
  })
221
- ;[
133
+
134
+ const positiveCases: Array<[string, string]> = [
222
135
  ['/', '/'],
223
136
  ['/', '/index.html'],
224
137
  ['/', '/subdir'],
@@ -228,22 +141,21 @@ describe('StaticServerManager', () => {
228
141
  ['/subdir', '/subdir/'],
229
142
  ['/subdir', '/subdir/file.js'],
230
143
  ['/subdir', '/subdir/s2/file.js'],
231
- ].forEach(([root, url]) =>
232
- it(`Should match for '${root}' root and '${url}' url`, () => {
233
- const staticServerManager = new StaticServerManager()
234
- const shouldExec = staticServerManager.shouldExec(root)({ req: { method: 'GET', url } })
235
- expect(shouldExec).toBe(true)
144
+ ]
145
+ positiveCases.forEach(([baseUrl, url]) =>
146
+ it(`matches GET '${url}' against base '${baseUrl}'`, () => {
147
+ expect(api(baseUrl).shouldExec({ req: { method: 'GET', url } as never, res: {} as never })).toBe(true)
236
148
  }),
237
149
  )
238
150
 
239
- it('Should not exec different paths for non-top level root directory', () => {
240
- const staticServerManager = new StaticServerManager()
241
- expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/' } })).toBe(false)
242
- expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/other/index.html' } })).toBe(
151
+ it('does not match unrelated urls under a non-root base', () => {
152
+ const subdir = api('/subdir')
153
+ expect(subdir.shouldExec({ req: { method: 'GET', url: '/' } as never, res: {} as never })).toBe(false)
154
+ expect(subdir.shouldExec({ req: { method: 'GET', url: '/other/index.html' } as never, res: {} as never })).toBe(
243
155
  false,
244
156
  )
245
- expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/subdir2' } })).toBe(false)
246
- expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/subdir2/index.html' } })).toBe(
157
+ expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2' } as never, res: {} as never })).toBe(false)
158
+ expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2/index.html' } as never, res: {} as never })).toBe(
247
159
  false,
248
160
  )
249
161
  })
@@ -0,0 +1,39 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { buildStaticSiteServerApi } from './static-site-runtime.js'
4
+
5
+ const makeRequest = (url: string | undefined, method: string | undefined = 'GET'): IncomingMessage =>
6
+ ({ url, method, headers: {} }) as unknown as IncomingMessage
7
+
8
+ // Full onRequest / sendFile behaviour (streaming, fallback, 404) is covered
9
+ // end-to-end via `static-server-manager.spec.ts` which drives the real HTTP
10
+ // pool. These tests cover the pure-function `shouldExec` gate which is
11
+ // exercised by the server pool before any request is handed to the API.
12
+ describe('buildStaticSiteServerApi / shouldExec', () => {
13
+ const api = buildStaticSiteServerApi({ baseUrl: '/static', path: '/tmp', port: 0 })
14
+
15
+ it('matches GET requests under the baseUrl', () => {
16
+ expect(api.shouldExec({ req: makeRequest('/static/index.html'), res: {} as ServerResponse })).toBe(true)
17
+ })
18
+
19
+ it('is case-insensitive about the method (GET only)', () => {
20
+ expect(api.shouldExec({ req: makeRequest('/static/a.html', 'get'), res: {} as ServerResponse })).toBe(true)
21
+ })
22
+
23
+ it('rejects non-GET methods', () => {
24
+ expect(api.shouldExec({ req: makeRequest('/static/index.html', 'POST'), res: {} as ServerResponse })).toBe(false)
25
+ expect(api.shouldExec({ req: makeRequest('/static/index.html', 'DELETE'), res: {} as ServerResponse })).toBe(false)
26
+ })
27
+
28
+ it('rejects URLs that do not match the baseUrl', () => {
29
+ expect(api.shouldExec({ req: makeRequest('/other/index.html'), res: {} as ServerResponse })).toBe(false)
30
+ })
31
+
32
+ it('rejects requests without a URL', () => {
33
+ expect(api.shouldExec({ req: makeRequest(undefined), res: {} as ServerResponse })).toBe(false)
34
+ })
35
+
36
+ it('never exposes an onUpgrade hook (static sites do not handle upgrades)', () => {
37
+ expect(api.onUpgrade).toBeUndefined()
38
+ })
39
+ })
@@ -0,0 +1,75 @@
1
+ import { PathHelper } from '@furystack/utils'
2
+ import { createReadStream } from 'fs'
3
+ import { access, stat } from 'fs/promises'
4
+ import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
5
+ import { join, normalize, sep } from 'path'
6
+ import type { ServerApi } from './http-server-pool.js'
7
+ import { getMimeForFile } from './mime-types.js'
8
+
9
+ /**
10
+ * Options accepted by {@link buildStaticSiteServerApi} and
11
+ * {@link useStaticFiles}.
12
+ */
13
+ export interface StaticServerOptions {
14
+ baseUrl: string
15
+ path: string
16
+ hostName?: string
17
+ port: number
18
+ fallback?: string
19
+ headers?: OutgoingHttpHeaders
20
+ }
21
+
22
+ const sendFile = async ({
23
+ fullPath,
24
+ headers,
25
+ res,
26
+ }: {
27
+ fullPath: string
28
+ res: ServerResponse
29
+ headers?: OutgoingHttpHeaders
30
+ }): Promise<void> => {
31
+ const { size } = await stat(fullPath)
32
+ await access(fullPath)
33
+ const head = {
34
+ ...headers,
35
+ 'Content-Length': size,
36
+ 'Content-Type': getMimeForFile(fullPath),
37
+ }
38
+ Object.entries(head).forEach(([key, value]) => res.setHeader(key, value))
39
+ await new Promise<void>((resolve, reject) =>
40
+ createReadStream(fullPath, { autoClose: true }).once('finish', resolve).once('error', reject).pipe(res),
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Builds a {@link ServerApi} that serves files under `options.path` for every
46
+ * GET request whose URL matches `options.baseUrl`. Falls back to
47
+ * `options.fallback` (if set) for missing files, otherwise responds with 404.
48
+ */
49
+ export const buildStaticSiteServerApi = (options: StaticServerOptions): ServerApi => {
50
+ const onRequest = async ({ req, res }: { req: IncomingMessage; res: ServerResponse }): Promise<void> => {
51
+ const extractedPath = PathHelper.extractPath(req.url as string, options.baseUrl)
52
+ const filePath = (extractedPath || '/').replaceAll('/', sep)
53
+ const fullPath = normalize(join(options.path, filePath))
54
+ try {
55
+ await sendFile({ fullPath, res, headers: options.headers })
56
+ } catch {
57
+ if (options.fallback) {
58
+ await sendFile({ fullPath: normalize(join(options.path, options.fallback)), res, headers: options.headers })
59
+ return
60
+ }
61
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
62
+ res.end('Not found')
63
+ }
64
+ }
65
+
66
+ return {
67
+ shouldExec: ({ req }) =>
68
+ req.url !== undefined &&
69
+ req.method?.toUpperCase() === 'GET' &&
70
+ PathHelper.matchesBaseUrl(req.url, options.baseUrl)
71
+ ? true
72
+ : false,
73
+ onRequest,
74
+ }
75
+ }
@@ -0,0 +1,86 @@
1
+ import { InMemoryStore, User } from '@furystack/core'
2
+ import { createInjector } from '@furystack/inject'
3
+ import { usingAsync } from '@furystack/utils'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { DefaultSession } from './models/default-session.js'
6
+ import {
7
+ HttpAuthenticationStoreNotConfiguredError,
8
+ SessionDataSet,
9
+ SessionStore,
10
+ UserDataSet,
11
+ UserStore,
12
+ } from './user-store.js'
13
+
14
+ describe('UserStore', () => {
15
+ it('exposes the User model and username primary key on the token', () => {
16
+ expect(UserStore.model).toBe(User)
17
+ expect(UserStore.primaryKey).toBe('username')
18
+ expect(UserStore.lifetime).toBe('singleton')
19
+ })
20
+
21
+ it('throws HttpAuthenticationStoreNotConfiguredError when resolved without an override', async () => {
22
+ await usingAsync(createInjector(), async (injector) => {
23
+ expect(() => injector.get(UserStore)).toThrow(HttpAuthenticationStoreNotConfiguredError)
24
+ expect(() => injector.get(UserStore)).toThrow(/UserStore.*not been configured/)
25
+ })
26
+ })
27
+
28
+ it('returns the bound implementation after rebinding the token', async () => {
29
+ await usingAsync(createInjector(), async (injector) => {
30
+ const impl = new InMemoryStore({ model: User, primaryKey: 'username' })
31
+ injector.bind(UserStore, () => impl)
32
+ expect(injector.get(UserStore)).toBe(impl)
33
+ })
34
+ })
35
+ })
36
+
37
+ describe('SessionStore', () => {
38
+ it('exposes the DefaultSession model and sessionId primary key on the token', () => {
39
+ expect(SessionStore.model).toBe(DefaultSession)
40
+ expect(SessionStore.primaryKey).toBe('sessionId')
41
+ expect(SessionStore.lifetime).toBe('singleton')
42
+ })
43
+
44
+ it('throws HttpAuthenticationStoreNotConfiguredError when resolved without an override', async () => {
45
+ await usingAsync(createInjector(), async (injector) => {
46
+ expect(() => injector.get(SessionStore)).toThrow(HttpAuthenticationStoreNotConfiguredError)
47
+ expect(() => injector.get(SessionStore)).toThrow(/SessionStore.*not been configured/)
48
+ })
49
+ })
50
+
51
+ it('returns the bound implementation after rebinding the token', async () => {
52
+ await usingAsync(createInjector(), async (injector) => {
53
+ const impl = new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' })
54
+ injector.bind(SessionStore, () => impl)
55
+ expect(injector.get(SessionStore)).toBe(impl)
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('UserDataSet / SessionDataSet', () => {
61
+ it('mirror the backing store metadata onto the data-set token', () => {
62
+ expect(UserDataSet.model).toBe(User)
63
+ expect(UserDataSet.primaryKey).toBe('username')
64
+ expect(SessionDataSet.model).toBe(DefaultSession)
65
+ expect(SessionDataSet.primaryKey).toBe('sessionId')
66
+ })
67
+
68
+ it('resolves through the bound store implementation', async () => {
69
+ await usingAsync(createInjector(), async (injector) => {
70
+ injector.bind(UserStore, () => new InMemoryStore({ model: User, primaryKey: 'username' }))
71
+ const dataSet = injector.get(UserDataSet)
72
+ await dataSet.add(injector, { username: 'alice', roles: [] })
73
+ const result = await dataSet.get(injector, 'alice')
74
+ expect(result?.username).toBe('alice')
75
+ })
76
+ })
77
+ })
78
+
79
+ describe('HttpAuthenticationStoreNotConfiguredError', () => {
80
+ it('embeds the store name in the message and sets the error name', () => {
81
+ const error = new HttpAuthenticationStoreNotConfiguredError('SomeStore')
82
+ expect(error.name).toBe('HttpAuthenticationStoreNotConfiguredError')
83
+ expect(error.message).toContain("'SomeStore'")
84
+ expect(error).toBeInstanceOf(Error)
85
+ })
86
+ })
@@ -0,0 +1,68 @@
1
+ import { defineStore, type StoreToken, User } from '@furystack/core'
2
+ import { defineDataSet, type DataSetToken } from '@furystack/repository'
3
+ import { DefaultSession } from './models/default-session.js'
4
+
5
+ /**
6
+ * Error thrown by the default factories of {@link UserStore} and
7
+ * {@link SessionStore} when they are resolved without a caller-provided
8
+ * override. Users and sessions must outlive a process restart, so the
9
+ * rest-service refuses to silently fall back to an in-memory store.
10
+ */
11
+ export class HttpAuthenticationStoreNotConfiguredError extends Error {
12
+ constructor(storeName: string) {
13
+ super(
14
+ `HTTP authentication store '${storeName}' has not been configured. Bind a persistent PhysicalStore before resolving the HttpUserContext.`,
15
+ )
16
+ this.name = 'HttpAuthenticationStoreNotConfiguredError'
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Store token for persisted {@link User} records. Applications must bind
22
+ * this token to a persistent implementation before resolving
23
+ * {@link HttpUserContext} (or any service that derives from it).
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * injector.bind(UserStore, () => myPersistentUserStore)
28
+ * ```
29
+ */
30
+ export const UserStore: StoreToken<User, 'username'> = defineStore({
31
+ name: 'furystack/rest-service/UserStore',
32
+ model: User,
33
+ primaryKey: 'username',
34
+ factory: () => {
35
+ throw new HttpAuthenticationStoreNotConfiguredError('UserStore')
36
+ },
37
+ })
38
+
39
+ /**
40
+ * Store token for persisted {@link DefaultSession} records. Shares the
41
+ * throw-by-default contract with {@link UserStore}.
42
+ */
43
+ export const SessionStore: StoreToken<DefaultSession, 'sessionId'> = defineStore({
44
+ name: 'furystack/rest-service/SessionStore',
45
+ model: DefaultSession,
46
+ primaryKey: 'sessionId',
47
+ factory: () => {
48
+ throw new HttpAuthenticationStoreNotConfiguredError('SessionStore')
49
+ },
50
+ })
51
+
52
+ /**
53
+ * Data set token over {@link UserStore}. Consumers of the authentication
54
+ * subsystem (cookie login, JWT authentication plugins, `HttpUserContext`)
55
+ * route all user I/O through this dataset.
56
+ */
57
+ export const UserDataSet: DataSetToken<User, 'username'> = defineDataSet({
58
+ name: 'furystack/rest-service/UserDataSet',
59
+ store: UserStore,
60
+ })
61
+
62
+ /**
63
+ * Data set token over {@link SessionStore}.
64
+ */
65
+ export const SessionDataSet: DataSetToken<DefaultSession, 'sessionId'> = defineDataSet({
66
+ name: 'furystack/rest-service/SessionDataSet',
67
+ store: SessionStore,
68
+ })