@ibgib/space-gib 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/Dockerfile +14 -0
  3. package/IMPLEMENTATION.md +484 -0
  4. package/README.md +46 -0
  5. package/dist/client/bootstrap.mjs +58 -0
  6. package/dist/client/bootstrap.mjs.map +7 -0
  7. package/dist/client/chunk-CT47Z5WU.mjs +21 -0
  8. package/dist/client/chunk-CT47Z5WU.mjs.map +7 -0
  9. package/dist/client/chunk-RHEDTRKF.mjs +235 -0
  10. package/dist/client/chunk-RHEDTRKF.mjs.map +7 -0
  11. package/dist/client/index.html +147 -0
  12. package/dist/client/index.mjs +2 -0
  13. package/dist/client/index.mjs.map +7 -0
  14. package/dist/client/script.mjs +2 -0
  15. package/dist/client/script.mjs.map +7 -0
  16. package/dist/client/style.css +605 -0
  17. package/dist/respec-gib.node.mjs +5 -0
  18. package/dist/server/server.mjs +20157 -0
  19. package/dist/server/server.mjs.map +7 -0
  20. package/generate-version-file.js +35 -0
  21. package/package.json +27 -0
  22. package/src/client/AUTO-GENERATED-version.mts +11 -0
  23. package/src/client/README.md +19 -0
  24. package/src/client/api/function-infos.web.mts +38 -0
  25. package/src/client/api/space-gib-api-bridge.mts +85 -0
  26. package/src/client/bootstrap.mts +49 -0
  27. package/src/client/components/keystone-creator/keystone-creator.css +139 -0
  28. package/src/client/components/keystone-creator/keystone-creator.html +26 -0
  29. package/src/client/components/keystone-creator/keystone-creator.mts +229 -0
  30. package/src/client/constants.mts +76 -0
  31. package/src/client/custom.d.ts +11 -0
  32. package/src/client/dev-tools.mts +540 -0
  33. package/src/client/helpers.web.mts +178 -0
  34. package/src/client/index.html +147 -0
  35. package/src/client/index.mts +59 -0
  36. package/src/client/script.mts +13 -0
  37. package/src/client/style.css +605 -0
  38. package/src/client/types.mts +85 -0
  39. package/src/client/ui/shell/space-gib-shell-constants.mts +24 -0
  40. package/src/client/ui/shell/space-gib-shell-service.mts +233 -0
  41. package/src/client/ui/shell/space-gib-shell-types.mts +5 -0
  42. package/src/client/witness/app/space-gib/space-gib-app-v1.mts +160 -0
  43. package/src/client/witness/app/space-gib/space-gib-constants.mts +38 -0
  44. package/src/client/witness/app/space-gib/space-gib-helper.mts +72 -0
  45. package/src/client/witness/app/space-gib/space-gib-types.mts +47 -0
  46. package/src/common/keystone-policies.mts +159 -0
  47. package/src/respec-gib.node.mts +6 -0
  48. package/src/server/README.md +18 -0
  49. package/src/server/bootstrap-helper.mts +141 -0
  50. package/src/server/bootstrap-helper.respec.mts +100 -0
  51. package/src/server/metaspace-nodeindexedspace/metaspace-nodeindexedspace.mts +85 -0
  52. package/src/server/path-constants.mts +89 -0
  53. package/src/server/path-helper.mts +101 -0
  54. package/src/server/path-helper.respec.mts +94 -0
  55. package/src/server/serve-gib/CHANGELOG.md +29 -0
  56. package/src/server/serve-gib/README.md +34 -0
  57. package/src/server/serve-gib/constants.mts +1 -0
  58. package/src/server/serve-gib/handlers/api/debug/ws-echo.handler.mts +104 -0
  59. package/src/server/serve-gib/handlers/api/health.handler.mts +23 -0
  60. package/src/server/serve-gib/handlers/api/health.respec.mts +51 -0
  61. package/src/server/serve-gib/handlers/api/ibgib/ibgib-handler-types.mts +49 -0
  62. package/src/server/serve-gib/handlers/api/ibgib/ibgib.handler.mts +176 -0
  63. package/src/server/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +261 -0
  64. package/src/server/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +146 -0
  65. package/src/server/serve-gib/handlers/api/keystone/keystone-get.handler.mts +198 -0
  66. package/src/server/serve-gib/handlers/api/keystone/keystone-get.respec.mts +107 -0
  67. package/src/server/serve-gib/handlers/api/keystone/keystone-handler-types.mts +29 -0
  68. package/src/server/serve-gib/handlers/api/keystone/keystone-post.handler.mts +70 -0
  69. package/src/server/serve-gib/handlers/api/keystone/keystone-post.respec.mts +130 -0
  70. package/src/server/serve-gib/handlers/error-handler.mts +36 -0
  71. package/src/server/serve-gib/handlers/handler-base.mts +383 -0
  72. package/src/server/serve-gib/handlers/static-handler.mts +82 -0
  73. package/src/server/serve-gib/handlers/ws/sync-upgrade.handler.mts +498 -0
  74. package/src/server/serve-gib/handlers/ws/ws-helper.mts +111 -0
  75. package/src/server/serve-gib/handlers/ws/ws-types.mts +53 -0
  76. package/src/server/serve-gib/serve-gib-helpers.mts +32 -0
  77. package/src/server/serve-gib/serve-gib-v1.mts +172 -0
  78. package/src/server/serve-gib/serve-gib.respec.mts +90 -0
  79. package/src/server/serve-gib/types.mts +102 -0
  80. package/src/server/server-constants.mts +2 -0
  81. package/src/server/server.mts +96 -0
  82. package/tsconfig.json +29 -0
  83. package/tsconfig.server.json +29 -0
  84. package/tsconfig.test.json +27 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * @module serve-gib/handler
3
+ *
4
+ * Base class and utilities for serve-gib handlers.
5
+ */
6
+
7
+ import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
8
+ import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
9
+ import { getIbAndGib } from '@ibgib/ts-gib/dist/helper.mjs';
10
+ import { getGibInfo } from '@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs';
11
+
12
+ import { GLOBAL_LOG_A_LOT } from '../constants.mjs';
13
+ import { DomainInfo, ParamsWithDomain, RequestContext, ResponseResult, ServeGibHandler } from '../types.mjs';
14
+ import { ServeGibHttpMethod } from '../types.mjs';
15
+ import { bootstrapDomainMetaspace, getDomainRootPath } from '../../bootstrap-helper.mjs';
16
+
17
+ const logalot = GLOBAL_LOG_A_LOT;
18
+
19
+ /**
20
+ * Abstract base class for serve-gib handlers.
21
+ * Provides a "pit of success" plumbing for routing, error handling, and parameter parsing.
22
+ *
23
+ * @template TParams Type for the parsed query parameters.
24
+ * @template TQueryParams Type for the parsed query parameters.
25
+ */
26
+ export abstract class ServeGibHandlerBase<TParams = any, TQueryParams = any>
27
+ implements ServeGibHandler<TParams, TQueryParams> {
28
+ protected lc: string = `[${ServeGibHandlerBase.name}]`;
29
+
30
+ /** The HTTP method this handler supports, or 'ALL' for catch-all. */
31
+ protected abstract method: ServeGibHttpMethod;
32
+
33
+ /** Regex for quick route matching via the default `canHandleRoute`. */
34
+ protected abstract regex: RegExp;
35
+
36
+ /**
37
+ * The main entry point processing incoming requests.
38
+ * Implements standard plumbing for matching, error handling, and param parsing.
39
+ */
40
+ async handleRoute(reqCtx: RequestContext<TParams, TQueryParams>): Promise<ResponseResult | undefined> {
41
+ const lc = `${this.lc}[${this.handleRoute.name}]`;
42
+ try {
43
+ if (logalot) { console.log(`${lc} starting... (I: ad1a523a7bbd222db2e6ffab54f59c26)`); }
44
+
45
+ // First check if we can handle it
46
+ if (!this.canHandleRoute(reqCtx)) {
47
+ if (logalot) { console.log(`${lc} cannot handle route. returning undefined. (I: 359bd4a9d44acb2408e3ad9570cfa526)`); }
48
+ return undefined; /* <<<< returns early */
49
+ }
50
+
51
+ // prepare handling params, e.g., keystone/domain addr(s), ibgib
52
+ // addr(s), not sure what else
53
+ reqCtx.params = await this.parseParams(reqCtx);
54
+
55
+ // prepare queryParams
56
+ reqCtx.queryParams = await this.parseQueryParams(reqCtx);
57
+
58
+ // hook to initialize contextual things like metaspace
59
+ await this.initConcreteContext(reqCtx);
60
+
61
+ // Delegate to implementation
62
+ return await this.handleRouteImpl(reqCtx);
63
+
64
+ } catch (error) {
65
+ const emsg = `${lc} Error handling route: ${extractErrorMsg(error)}`;
66
+ console.error(emsg);
67
+ // Standard fallback error. Individual handlers can override with more specific catches in handleRouteImpl.
68
+ return this.error(500, 'Internal Server Error', {
69
+ errorMsg: emsg,
70
+ method: this.method,
71
+ regex: this.regex?.source,
72
+ rawUrl: reqCtx?.rawUrl,
73
+ });
74
+ } finally {
75
+ if (logalot) { console.log(`${lc} complete.`); }
76
+ }
77
+ }
78
+ /**
79
+ * Concrete implementation of the handler logic.
80
+ *
81
+ * This is where the "meat" of the code goes, as there is some
82
+ * pre-processing in the parent wrapper.
83
+ */
84
+ protected abstract handleRouteImpl(reqCtx: RequestContext<TParams, TQueryParams>): Promise<ResponseResult | undefined>;
85
+
86
+ /**
87
+ * Extracts per-handler/route params from a request context.
88
+ *
89
+ * @see {@link RequestContext.params}
90
+ */
91
+ parseParams(reqCtx: RequestContext<TParams, TQueryParams>): Promise<TParams | undefined> {
92
+ const lc = `${this.lc}[${this.parseParams.name}]`;
93
+ try {
94
+ if (logalot) { console.log(`${lc} starting... (I: c08d98768f449317b8e4b56925d3b826)`); }
95
+ return this.parseParamsImpl(reqCtx);
96
+ } catch (error) {
97
+ console.error(`${lc} ${extractErrorMsg(error)}`);
98
+ throw error;
99
+ } finally {
100
+ if (logalot) { console.log(`${lc} complete.`); }
101
+ }
102
+ }
103
+ /**
104
+ * Concrete implementation for extracting params from a path.
105
+ *
106
+ * @see {@link RequestContext.params}
107
+ */
108
+ protected async parseParamsImpl(reqCtx: RequestContext<TParams, TQueryParams>): Promise<TParams | undefined> {
109
+ const lc = `${this.lc}[${this.parseParamsImpl.name}]`;
110
+ try {
111
+ if (logalot) { console.log(`${lc} starting... (I: 849e491b436860d6788b76c898b0a826)`); }
112
+
113
+ if (logalot) { console.log(`${lc} default implementation in base class is a no-op (I: 88d128a11e1c66030839dc582486e826)`); }
114
+
115
+ return undefined;
116
+ } catch (error) {
117
+ console.error(`${lc} ${extractErrorMsg(error)}`);
118
+ throw error;
119
+ } finally {
120
+ if (logalot) { console.log(`${lc} complete.`); }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Hook to initialize concrete context, e.g., initializing a metaspace for the
126
+ * specific domain. The base implementation is a no-op.
127
+ */
128
+ protected async initConcreteContext(reqCtx: RequestContext<TParams, TQueryParams>): Promise<void> {
129
+ const lc = `${this.lc}[${this.initConcreteContext.name}]`;
130
+ try {
131
+ if (logalot) { console.log(`${lc} starting...`); }
132
+
133
+ if (logalot) { console.log(`${lc} default implementation in base class is a no-op`); }
134
+ } catch (error) {
135
+ console.error(`${lc} ${extractErrorMsg(error)}`);
136
+ throw error;
137
+ } finally {
138
+ if (logalot) { console.log(`${lc} complete.`); }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Determines if this handler should attempt to handle the given request.
144
+ *
145
+ * Default implementation checks {@link method} and {@link regex} matching
146
+ * against the pathname. Override for more advanced logic for edge cases.
147
+ */
148
+ protected canHandleRoute(reqCtx: RequestContext<TParams, TQueryParams>): boolean {
149
+ const lc = `${this.lc}[${this.canHandleRoute.name}]`;
150
+ try {
151
+ if (logalot) { console.log(`${lc} starting... (I: d790f637074feee63c4bfcb158f47826)`); }
152
+
153
+ const isMethodMatch = this.method === 'ALL' || reqCtx.method === this.method;
154
+ const isRegexMatch = this.regex.test(reqCtx.pathname);
155
+ return isMethodMatch && isRegexMatch;
156
+ } catch (error) {
157
+ console.error(`${lc} ${extractErrorMsg(error)}`);
158
+ throw error;
159
+ } finally {
160
+ if (logalot) { console.log(`${lc} complete.`); }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Parses query parameters from the URL.
166
+ * Defaults to a basic key-value mapping of the URL search params.
167
+ */
168
+ protected async parseQueryParams(reqCtx: RequestContext<TParams, TQueryParams>): Promise<TQueryParams | undefined> {
169
+ const lc = `${this.lc}[${this.parseQueryParams.name}]`;
170
+ try {
171
+ if (logalot) { console.log(`${lc} starting... (I: b5d985a4a58b8f3edff9f5ab500e5c26)`); }
172
+
173
+ const queryParams: any = {};
174
+ reqCtx.url.searchParams.forEach((value, key) => {
175
+ queryParams[key] = value;
176
+ });
177
+
178
+ if (Object.keys(queryParams).length > 0) {
179
+ // query params exist, validate and return if valid, throw if not
180
+ const validationErrors = await this.validateQueryParams({ queryParams });
181
+ if (validationErrors.length === 0) {
182
+ return queryParams as TQueryParams;
183
+ } else {
184
+ throw new Error(`invalid query params. validationErrors: ${validationErrors.join(', ')} (E: 9355f8a9643803f52f2241f1c7f39826)`);
185
+ }
186
+ } else {
187
+ // no query params given
188
+ return undefined;
189
+ }
190
+ } catch (error) {
191
+ console.error(`${lc} ${extractErrorMsg(error)}`);
192
+ throw error;
193
+ } finally {
194
+ if (logalot) { console.log(`${lc} complete.`); }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * base class implementation is a no-op. override this in descendant class
200
+ * who expect the possibility of query params
201
+ * @returns array of validation errors, empty if valid
202
+ */
203
+ protected async validateQueryParams({ queryParams }: { queryParams: any }): Promise<string[]> {
204
+ const lc = `${this.lc}[${this.validateQueryParams.name}]`;
205
+ try {
206
+ if (logalot) { console.log(`${lc} starting... (I: 33b448e223c8682bd8b8b055cab86f26)`); }
207
+
208
+ if (logalot) { console.log(`${lc} base class implementation is a no-op. returning empty array (i.e., valid) (I: 393e8db53d5a9e6ca8eb69ae25810826)`); }
209
+
210
+ return [];
211
+ } catch (error) {
212
+ console.error(`${lc} ${extractErrorMsg(error)}`);
213
+ throw error;
214
+ } finally {
215
+ if (logalot) { console.log(`${lc} complete.`); }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Standard JSON success response.
221
+ */
222
+ protected ok(body: any, status: number = 200): ResponseResult {
223
+ return { status, body, isJson: true };
224
+ }
225
+
226
+ /**
227
+ * Standard JSON error response.
228
+ */
229
+ protected error(status: number, message: string, details?: any): ResponseResult {
230
+ return { status, body: { error: message, details }, isJson: true };
231
+ }
232
+
233
+ /**
234
+ * 404 Not Found response.
235
+ */
236
+ protected notFound(message: string = 'Not found'): ResponseResult {
237
+ return this.error(404, message);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Base class for handlers that have to first initialize a metaspace in order to
243
+ * process the incoming request.
244
+ *
245
+ * ## notes
246
+ *
247
+ * Some handlers, like those who do error handling or serving static files, do
248
+ * not need to initialize a metaspace or do any get/put of any ibgibs. So those
249
+ * don't need to descend from this class.
250
+ *
251
+ * But if the handler is one that has to put/get an ibgib, then a metaspace will
252
+ * need to be init. Those handlers should descend from this class.
253
+ */
254
+ export abstract class ServeGibHandlerWithMetaspaceBase<TParams extends ParamsWithDomain = ParamsWithDomain, TQueryParams = any>
255
+ extends ServeGibHandlerBase<TParams, TQueryParams> {
256
+ protected override lc: string = `[${ServeGibHandlerWithMetaspaceBase.name}]`;
257
+
258
+ protected override async initConcreteContext(reqCtx: RequestContext<TParams, TQueryParams>): Promise<void> {
259
+ const lc = `${this.lc}[${this.initConcreteContext.name}]`;
260
+ try {
261
+ if (logalot) { console.log(`${lc} starting...`); }
262
+
263
+ await this.initAndPopulateDomainContext(reqCtx);
264
+ } catch (error) {
265
+ console.error(`${lc} ${extractErrorMsg(error)}`);
266
+ throw error;
267
+ } finally {
268
+ if (logalot) { console.log(`${lc} complete.`); }
269
+ }
270
+ }
271
+
272
+ protected override parseParamsImpl(reqCtx: RequestContext<TParams, TQueryParams>): Promise<TParams | undefined> {
273
+ const lc = `${this.lc}[${this.parseParamsImpl.name}]`;
274
+ try {
275
+ if (logalot) { console.log(`${lc} starting... (I: f3ae6e662fe8d28db8c230e80a700826)`); }
276
+
277
+ throw new Error(`(UNEXPECTED) not implemented. this function must be implemented in handlers that initialize metaspace. in that concrete handler class, you must parse out the raw path and build up the params object. That params object must at least include domain info, because that drives the multitenancy. (E: bd6a584b8f988f9ed81c77eedd36b826)`);
278
+ } catch (error) {
279
+ console.error(`${lc} ${extractErrorMsg(error)}`);
280
+ throw error;
281
+ } finally {
282
+ if (logalot) { console.log(`${lc} complete.`); }
283
+ }
284
+ }
285
+
286
+ protected override validateQueryParams({ queryParams }: { queryParams: any; }): Promise<string[]> {
287
+ const lc = `${this.lc}[${this.validateQueryParams.name}]`;
288
+ try {
289
+ if (logalot) { console.log(`${lc} starting... (I: 14bb4659f9587d18786105c8e4966826)`); }
290
+
291
+ throw new Error(`(UNEXPECTED) not implemented. this function must be implemented in handlers that initialize metaspace. in that concrete handler class, you must provide a function that validates query params expectations, even if you expect no query params. (E: 804c98a724ddf59998199ec87a9ee126)`);
292
+
293
+ } catch (error) {
294
+ console.error(`${lc} ${extractErrorMsg(error)}`);
295
+ throw error;
296
+ } finally {
297
+ if (logalot) { console.log(`${lc} complete.`); }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * helper that should extract the "domain" (driven by the correctly scoped
303
+ * keystone) and set related props on the context.
304
+ */
305
+ protected async initAndPopulateDomainContext(reqCtx: RequestContext<TParams, TQueryParams>): Promise<void> {
306
+ const lc = `${this.lc}[${this.initAndPopulateDomainContext.name}]`;
307
+ try {
308
+ if (logalot) { console.log(`${lc} starting...`); }
309
+
310
+ if (!reqCtx.params) { throw new Error(`invalid RequestContext. params is falsy but we expected params.domainInfo at the minimum. (E: 433c15116e2e821768847d23f7853a26)`); }
311
+ if (!reqCtx.params.domainInfo) { throw new Error(`invalid RequestContext. reqCtx.params is truthy but params.domainInfo is falsy. at this point, we should have parsed domain info from reqCtx.params (E: ae40882910aa6f45a810e268fd126f26)`); }
312
+
313
+ const { domainInfo } = reqCtx.params as TParams
314
+ const { addr: domainAddr, } = domainInfo;
315
+ if (!domainAddr) {
316
+ throw new Error(`(UNEXPECTED) domainAddr falsy? reqCtx.params does not contain domainIb and domainGib for a handler extending ServeGibHandlerWithMetaspaceBase. If extending this metaspace base, we are expected to have a domain addr with ib and gib. (E: 06bae6908ba8f200e8c50b28ed8df626)`);
317
+ }
318
+
319
+ // since we have access to reqCtx.dataDir, not sure if we need rootPath in DomainInfo?
320
+ reqCtx.metaspace = await bootstrapDomainMetaspace(domainAddr, reqCtx.dataDir);
321
+ } catch (error) {
322
+ console.error(`${lc} ${extractErrorMsg(error)}`);
323
+ throw error;
324
+ } finally {
325
+ if (logalot) { console.log(`${lc} complete.`); }
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Helper to build the domain info from a {@link domainAddr}.
331
+ *
332
+ * ## usage
333
+ *
334
+ * Use this helper function once your concrete handler has parsed the
335
+ * domainAddr from the request url.
336
+ *
337
+ * For example, say the incoming path is /api/ibgib/:domainAddr/:ibgibAddr (GET)
338
+ * Then the concrete handler for this path would pull out domainAddr, call
339
+ * this function to build the domain info, and then put that domain info on
340
+ * the `reqCtx`.
341
+ */
342
+ protected getDomainInfo({
343
+ domainAddr,
344
+ // dataDir,
345
+ }: {
346
+ /**
347
+ * addr extracted from incoming url by concrete handler
348
+ */
349
+ domainAddr: IbGibAddr,
350
+ // /**
351
+ // * root dir of multitenancy on server. get from reqCtx.dataDir
352
+ // */
353
+ // dataDir: string,
354
+ }): DomainInfo {
355
+ const lc = `${this.lc}[${this.getDomainInfo.name}]`;
356
+ try {
357
+ if (logalot) { console.log(`${lc} starting... (I: 2d8bc8f40fd8ae3c98b84f6aaf6e0f26)`); }
358
+
359
+ if (!domainAddr) { throw new Error(`(UNEXPECTED) domainAddr falsy? (E: 7009f98fead9712cb5d4c8985162c826)`); }
360
+
361
+ const domainGibInfo = getGibInfo({ ibGibAddr: domainAddr });
362
+ const { tjpGib, punctiliarHash } = domainGibInfo;
363
+ const { ib: domainIb, gib: domainGib } = getIbAndGib({ ibGibAddr: domainAddr });
364
+ const finalDomainGib = tjpGib || punctiliarHash;
365
+ if (!finalDomainGib) { throw new Error(`(UNEXPECTED) finalDomainGib falsy? we have a domainAddr (${domainAddr}) so we should have a domain gib. (E: de1e18078678f3b4887ebfae3c893626)`); }
366
+
367
+ const domainInfo: DomainInfo = {
368
+ addr: domainAddr,
369
+ gib: domainGib,
370
+ ib: domainIb,
371
+ gibInfo: domainGibInfo,
372
+ // rootPath: getDomainRootPath(finalDomainGib, dataDir),
373
+ }
374
+
375
+ return domainInfo;
376
+ } catch (error) {
377
+ console.error(`${lc} ${extractErrorMsg(error)}`);
378
+ throw error;
379
+ } finally {
380
+ if (logalot) { console.log(`${lc} complete.`); }
381
+ }
382
+ }
383
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @module serve-gib/handlers/static-handler
3
+ *
4
+ * Handler for serving static client assets and SPA fallback.
5
+ */
6
+
7
+ import { readFile } from 'node:fs/promises';
8
+ import { existsSync, statSync } from 'node:fs';
9
+ import { join, extname } from 'node:path';
10
+
11
+ import { ServeGibHandlerBase } from './handler-base.mjs';
12
+ import { RequestContext, ResponseResult, ServeGibHttpMethod } from '../types.mjs';
13
+ import { isValidStaticPath } from '../../path-helper.mjs';
14
+ import { GLOBAL_LOG_A_LOT } from '../constants.mjs';
15
+
16
+ const logalot = GLOBAL_LOG_A_LOT;
17
+
18
+ export class StaticFileHandler extends ServeGibHandlerBase {
19
+ protected override lc: string = `[${StaticFileHandler.name}]`;
20
+ protected override method: ServeGibHttpMethod = 'GET';
21
+ protected override regex = /.*/;
22
+
23
+ private MIME_TYPES: Record<string, string> = {
24
+ '.html': 'text/html; charset=utf-8',
25
+ '.css': 'text/css; charset=utf-8',
26
+ '.js': 'text/javascript; charset=utf-8',
27
+ '.mjs': 'text/javascript; charset=utf-8',
28
+ '.json': 'application/json; charset=utf-8',
29
+ '.png': 'image/png',
30
+ '.jpg': 'image/jpeg',
31
+ '.jpeg': 'image/jpeg',
32
+ '.svg': 'image/svg+xml',
33
+ '.ico': 'image/x-icon',
34
+ '.woff': 'font/woff',
35
+ '.woff2': 'font/woff2',
36
+ '.ttf': 'font/ttf',
37
+ '.webp': 'image/webp',
38
+ };
39
+
40
+ constructor(private clientDir: string) { super(); }
41
+
42
+ protected async handleRouteImpl(reqCtx: RequestContext): Promise<ResponseResult | undefined> {
43
+ const lc = `${this.lc}[${this.handleRouteImpl.name}]`;
44
+ try {
45
+ if (logalot) { console.log(`${lc} starting...`); }
46
+
47
+ // Hardened path validation
48
+ const { pathname } = reqCtx;
49
+ if (!isValidStaticPath(pathname)) { return undefined; }
50
+
51
+ let filePath = join(this.clientDir, pathname);
52
+
53
+ // Directory → index.html
54
+ if (existsSync(filePath) && statSync(filePath).isDirectory()) {
55
+ filePath = join(filePath, 'index.html');
56
+ }
57
+
58
+ // File not found → SPA fallback
59
+ if (!existsSync(filePath)) {
60
+ filePath = join(this.clientDir, 'index.html');
61
+ }
62
+
63
+ // Final check just in case the fallback doesn't exist
64
+ if (!existsSync(filePath)) { return undefined; }
65
+
66
+ const content = await readFile(filePath);
67
+ const ext = extname(filePath).toLowerCase();
68
+ const contentType = this.MIME_TYPES[ext] ?? 'application/octet-stream';
69
+
70
+ return {
71
+ status: 200,
72
+ headers: { 'Content-Type': contentType },
73
+ body: content
74
+ };
75
+ } catch (error: any) {
76
+ console.error(`${lc} ${error.message}`);
77
+ throw error;
78
+ } finally {
79
+ if (logalot) { console.log(`${lc} complete.`); }
80
+ }
81
+ }
82
+ }