@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,498 @@
1
+ /**
2
+ * @module handlers/ws/sync-upgrade.handler
3
+ *
4
+ * Handles WebSocket upgrades for the sync protocol.
5
+ * Implements a challenge/response handshake using Keystone identity.
6
+ */
7
+
8
+ import { IncomingMessage } from 'node:http';
9
+ import { Socket } from 'node:net';
10
+
11
+ import { extractErrorMsg, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
12
+ import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
13
+ import { getIbGibAddr, getIbAndGib } from '@ibgib/ts-gib/dist/helper.mjs';
14
+ import { validateIbGibAddr } from '@ibgib/ts-gib/dist/V1/validate-helper.mjs';
15
+ import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
16
+ import { KeystoneIbGib_V1 } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
17
+ import { parseKeystoneIb } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
18
+ import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
19
+ import { IbGibSpaceResultIbGib, IbGibSpaceResultData, IbGibSpaceResultRel8ns } from '@ibgib/core-gib/dist/witness/space/space-types.mjs';
20
+
21
+ import { GLOBAL_LOG_A_LOT } from '../../constants.mjs';
22
+ import {
23
+ encodeTextFrame, decodeTextFrame, encodeCloseFrame, performHandshake
24
+ } from './ws-helper.mjs';
25
+ import { API_PATH_REGEXES } from '../../../path-constants.mjs';
26
+ import { ServeGibHandlerWithMetaspaceBase } from '../handler-base.mjs';
27
+ import { ParamsWithDomain, RequestContext, ResponseResult, ServeGibHttpMethod } from '../../types.mjs';
28
+ import { SESSION_KEYSTONE_POLICY, checkHandshakeSolution } from '../../../../common/keystone-policies.mjs';
29
+ import { AuthChallengeMessage, AuthFailMessage, AuthInitMessage, AuthOkMessage, AuthProofMessage, HandshakeMessage } from './ws-types.mjs';
30
+
31
+ const logalot = GLOBAL_LOG_A_LOT || true;
32
+
33
+ /**
34
+ * Handles WebSocket upgrades for sync by validating against Keystone identity.
35
+ *
36
+ * Reuses ServeGibHandlerWithMetaspaceBase to ensure consistent metaspace
37
+ * initialization from the request context.
38
+ */
39
+ export interface SyncUpgradeQueryParams {
40
+ sAddr: string;
41
+ solution: string;
42
+ }
43
+
44
+ export interface SyncUpgradeRequestContext extends RequestContext<ParamsWithDomain, SyncUpgradeQueryParams> {
45
+ sAddr_tjp?: IbGibAddr;
46
+ demandedIds?: string[];
47
+ }
48
+
49
+ export class SyncUpgradeHandler extends ServeGibHandlerWithMetaspaceBase<ParamsWithDomain, SyncUpgradeQueryParams> {
50
+ protected override lc: string = `[${SyncUpgradeHandler.name}]`;
51
+ protected override method: ServeGibHttpMethod = 'GET';
52
+ protected override regex = API_PATH_REGEXES.SYNC_WS;
53
+
54
+ /**
55
+ * Entry point for WebSocket upgrades.
56
+ * Orchestrates the upgrade, handshake, and keystone authorization.
57
+ * @returns true if the upgrade was handled (even if it failed auth), false if path mismatch.
58
+ */
59
+ async handleUpgrade(reqCtx: RequestContext<ParamsWithDomain>, socket: Socket, head: Buffer): Promise<boolean> {
60
+ const lc_fn = `${this.lc}[handleUpgrade]`;
61
+ try {
62
+ if (logalot) { console.log(`${lc_fn} starting... (I: d772c676239f1f0a823c14a8063d4826)`); }
63
+
64
+ // 1. Initial validation
65
+ if (!this.canHandleRoute(reqCtx)) {
66
+ if (logalot) { console.log(`${lc_fn} path/method mismatch. (I: 234198c0f649dfdf0e2d21f86b3ff826)`); }
67
+ return false;
68
+ }
69
+
70
+ const ctx = reqCtx as SyncUpgradeRequestContext;
71
+
72
+ // Populate params (e.g. domain info) from the request context
73
+ ctx.params = await this.parseParams(ctx);
74
+ ctx.queryParams = await this.parseQueryParams(ctx);
75
+
76
+ // 2. Initialize Metaspace Context (bootstrapDomainMetaspace)
77
+ await this.initConcreteContext(ctx);
78
+
79
+ // 2b. Perform upfront picket-fence pre-filter validation
80
+ await this.validateUpfrontHandshake(ctx);
81
+
82
+ // 3. RFC 6455 Handshake
83
+ const ok = performHandshake(ctx as any as IncomingMessage, socket);
84
+ if (!ok) { return true; }
85
+
86
+ // 4. Setup Auth Challenge
87
+ const challengeUuid = await getUUID();
88
+
89
+ socket.write(encodeTextFrame(JSON.stringify({
90
+ type: 'auth-challenge-init',
91
+ challengeUuid
92
+ })));
93
+
94
+ socket.on('data', async (data: Buffer) => {
95
+ await this.handleUpgrade_socketDataHandler({
96
+ reqCtx: ctx,
97
+ challengeUuid,
98
+ socket,
99
+ data,
100
+ })
101
+ });
102
+
103
+ return true;
104
+ } catch (error) {
105
+ console.error(`${lc_fn} fatal: ${extractErrorMsg(error)}`);
106
+ socket.destroy();
107
+ return true;
108
+ }
109
+ }
110
+
111
+ protected async handleUpgrade_socketDataHandler({
112
+ reqCtx,
113
+ challengeUuid,
114
+ socket,
115
+ data,
116
+ }: {
117
+ reqCtx: SyncUpgradeRequestContext,
118
+ challengeUuid: string,
119
+ socket: Socket,
120
+ data: Buffer,
121
+ }): Promise<any> {
122
+ const lc = `[${this.handleUpgrade_socketDataHandler.name}]`;
123
+ if (logalot) { console.log(`${lc} starting... (I: 3ba9a857899f0479030e323b755fe826)`); }
124
+ try {
125
+ debugger; //
126
+ const text = decodeTextFrame(data);
127
+ if (text === null) {
128
+ socket.write(encodeCloseFrame());
129
+ socket.end();
130
+ return; /* <<<< returns early */
131
+ }
132
+
133
+ const msg = JSON.parse(text) as HandshakeMessage;
134
+ switch (msg.type) {
135
+ case 'auth-init':
136
+ await this.handleUpgrade_socketDataHandler_authInit({
137
+ reqCtx,
138
+ challengeUuid,
139
+ socket,
140
+ msg,
141
+ })
142
+ break;
143
+
144
+
145
+ case 'auth-proof':
146
+ await this.handleUpgrade_socketDataHandler_authProof({
147
+ reqCtx,
148
+ challengeUuid,
149
+ socket,
150
+ msg,
151
+ });
152
+ break;
153
+
154
+ default:
155
+ if (logalot) { console.log(`${lc} unexpected message type: ${msg.type}`); }
156
+ throw new Error(`unknown msg.type ${msg.type} (E: 8143084554d848f48801fcd5dc5fc826)`);
157
+ }
158
+ } catch (error) {
159
+ console.error(`${lc} ${extractErrorMsg(error)}`);
160
+ socket.write(encodeTextFrame(JSON.stringify({
161
+ type: 'auth-fail',
162
+ message: extractErrorMsg(error)
163
+ } as AuthFailMessage)));
164
+ } finally {
165
+ if (logalot) { console.log(`${lc} complete.`); }
166
+ }
167
+ }
168
+
169
+ protected async handleUpgrade_socketDataHandler_authInit({
170
+ reqCtx,
171
+ challengeUuid,
172
+ socket,
173
+ msg,
174
+ }: {
175
+ reqCtx: SyncUpgradeRequestContext,
176
+ challengeUuid: string,
177
+ socket: Socket,
178
+ msg: AuthInitMessage,
179
+ }): Promise<any> {
180
+ const lc = `${this.lc}[${this.handleUpgrade_socketDataHandler_authInit.name}]`;
181
+ try {
182
+ if (logalot) { console.log(`${lc} starting... (I: 9ef8c8b892f82c7f98e34168f89c4826)`); }
183
+
184
+ const { sAddr } = msg;
185
+ if (logalot) { console.log(`${lc} auth-init for ${sAddr}`); }
186
+
187
+ // Load authorized S frame from context's metaspace
188
+ const metaspace = reqCtx.metaspace!;
189
+ const space = await metaspace.getLocalUserSpace({ lock: false });
190
+ if (!space) { throw new Error("No space."); }
191
+
192
+ let authorizedS: KeystoneIbGib_V1;
193
+ try {
194
+ authorizedS = await this.getSessionKeystone({ metaspace, space, sAddr });
195
+ if (logalot) { console.log(`${lc} loaded authorizedS:`, JSON.stringify(authorizedS).slice(0, 200) + '...'); }
196
+ } catch (error) {
197
+ console.warn(`${lc} session keystone not found: ${extractErrorMsg(error)}`);
198
+ socket.write(encodeTextFrame(JSON.stringify({
199
+ type: 'auth-fail',
200
+ message: 'Session keystone not found on server. Did you complete Step 4?'
201
+ } as AuthFailMessage)));
202
+ return; /* <<<< returns early */
203
+ }
204
+
205
+ // Identify handshake pool and select demanded IDs
206
+ const handshakePool = (authorizedS.data?.challengePools ?? []).find(p => p.id === SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.ID);
207
+ if (!handshakePool) {
208
+ socket.write(encodeTextFrame(JSON.stringify({
209
+ type: 'auth-fail',
210
+ message: `Session keystone missing "${SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.ID}" pool`
211
+ } as AuthFailMessage)));
212
+ return; /* <<<< returns early */
213
+ }
214
+
215
+ const challengeIds = Object.keys(handshakePool.challenges);
216
+ const demandedIds = challengeIds.sort(() => 0.5 - Math.random()).slice(0, SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.SERVER_DEMAND_COUNT);
217
+ reqCtx.demandedIds = demandedIds;
218
+
219
+ socket.write(encodeTextFrame(JSON.stringify({
220
+ type: 'auth-challenge',
221
+ challengeUuid,
222
+ demandedIds
223
+ } as AuthChallengeMessage)));
224
+
225
+ } catch (error) {
226
+ console.error(`${lc} ${extractErrorMsg(error)}`);
227
+ throw error;
228
+ } finally {
229
+ if (logalot) { console.log(`${lc} complete.`); }
230
+ }
231
+ }
232
+
233
+
234
+ /**
235
+ * At this point, the session keystone (S^Stjp) according to the server only
236
+ * has the first frame (tjp) persisted. But the client has successfully
237
+ * initiated the handshake, so it provided the one manual challenge.
238
+ *
239
+ * We (the server) then sent the client some challenge ids and said to the
240
+ * client "Hey, solve the keystone with these challenges included."
241
+ *
242
+ * So now, the client has sent the next evolution of the session keystone,
243
+ * S^S1.Stjp, with those challenge ids included.
244
+ */
245
+ protected async handleUpgrade_socketDataHandler_authProof({
246
+ reqCtx,
247
+ challengeUuid,
248
+ socket,
249
+ msg,
250
+ }: {
251
+ reqCtx: SyncUpgradeRequestContext,
252
+ challengeUuid: string,
253
+ socket: Socket,
254
+ msg: AuthProofMessage,
255
+ }): Promise<any> {
256
+ const lc = `${this.lc}[${this.handleUpgrade_socketDataHandler_authProof.name}]`;
257
+ try {
258
+ if (logalot) { console.log(`${lc} starting... (I: b0c185793602f4e4d862785e906cdd26)`); }
259
+ const { proofFrame } = msg;
260
+ if (logalot) { console.log(`${lc} verifying auth-proof...`); }
261
+
262
+ const sAddr_proofFrame = getIbGibAddr({ ibGib: proofFrame });
263
+
264
+ // need to get the tip of session keystone
265
+ const metaspace = reqCtx.metaspace!;
266
+ const space = await metaspace.getLocalUserSpace({ lock: false });
267
+ if (!space) { throw new Error(`(UNEXPECTED) no default local user space? (E: 1443da451007921b2da9d2c8b15a4826)`); }
268
+ const sAddr_tjp = reqCtx.sAddr_tjp;
269
+ if (!sAddr_tjp) {
270
+ throw new Error("Missing upfront verified session keystone TJP address (E: cb1998fa7c4a11f5ee87b001a4e126a1)");
271
+ }
272
+ /**
273
+ * this is the latest/tip addr for session keystone that the server
274
+ * is currently aware of.
275
+ *
276
+ * NOTE: We do NOT (!!!) trust the incoming session keystone to
277
+ * drive what the client thinks it the latest addr. Not only is this
278
+ * in the control of a would-be attacker, the client could just be
279
+ * out of date with a race condition.
280
+ */
281
+ const sAddr_latest = await metaspace.getLatestAddr({ tjpAddr: sAddr_tjp, space });
282
+ if (!sAddr_latest) {
283
+ socket.write(encodeTextFrame(JSON.stringify({
284
+ type: 'auth-fail',
285
+ message: 'Authorized session keystone tip not found'
286
+ } as AuthFailMessage)));
287
+ return; /* <<<< returns early */
288
+ }
289
+ const sKeystone_latest = await this.getSessionKeystone({
290
+ sAddr: sAddr_latest,
291
+ metaspace,
292
+ space,
293
+ });
294
+
295
+ const keystoneService = new KeystoneService_V1();
296
+ const validationErrors = await keystoneService.validate({
297
+ prevIbGib: sKeystone_latest,
298
+ currentIbGib: proofFrame,
299
+ });
300
+
301
+ if (validationErrors && validationErrors.length > 0) {
302
+ socket.write(encodeTextFrame(JSON.stringify({
303
+ type: 'auth-fail',
304
+ message: 'Proof validation failed',
305
+ errors: validationErrors
306
+ } as AuthFailMessage)));
307
+ return; /* <<<< returns early */
308
+ }
309
+
310
+ const proofs = proofFrame.data.proofs ?? [];
311
+ const proof = proofs.find(p => p.claim?.verb === SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.VERB);
312
+ if (!proof || proof.claim?.target !== challengeUuid) {
313
+ socket.write(encodeTextFrame(JSON.stringify({
314
+ type: 'auth-fail',
315
+ message: 'Proof target mismatch or missing handshake claim'
316
+ } as AuthFailMessage)));
317
+ return; /* <<<< returns early */
318
+ }
319
+
320
+ // Ensure that all of our demanded challenge ids are present in the proof solutions
321
+ const demandedIds = reqCtx.demandedIds;
322
+ if (!demandedIds || demandedIds.length === 0) {
323
+ socket.write(encodeTextFrame(JSON.stringify({
324
+ type: 'auth-fail',
325
+ message: 'Server missing active handshake challenge state'
326
+ } as AuthFailMessage)));
327
+ return; /* <<<< returns early */
328
+ }
329
+
330
+ const solvedIds = new Set(proof.solutions?.map(s => s.challengeId) ?? []);
331
+ const missingIds = demandedIds.filter(id => !solvedIds.has(id));
332
+ if (missingIds.length > 0) {
333
+ socket.write(encodeTextFrame(JSON.stringify({
334
+ type: 'auth-fail',
335
+ message: `Proof is missing demanded solutions: ${missingIds.join(', ')}`
336
+ } as AuthFailMessage)));
337
+ return; /* <<<< returns early */
338
+ }
339
+
340
+ // Persist the newly validated evolved session keystone tip to the server space
341
+ await metaspace.put({ ibGibs: [proofFrame], space });
342
+
343
+ if (logalot) { console.log(`${lc} auth successful for ${sAddr_proofFrame} (I: 6d02c649f55c81cfe8f11988f44bca26)`); }
344
+ socket.write(encodeTextFrame(JSON.stringify({ type: 'auth-ok' } as AuthOkMessage)));
345
+ } catch (error) {
346
+ console.error(`${lc} ${extractErrorMsg(error)}`);
347
+ throw error;
348
+ } finally {
349
+ if (logalot) { console.log(`${lc} complete.`); }
350
+ }
351
+ }
352
+
353
+ protected async getSessionKeystone({
354
+ metaspace,
355
+ space,
356
+ sAddr,
357
+ }: {
358
+ metaspace: NonNullable<RequestContext['metaspace']>,
359
+ space: any,
360
+ sAddr: string,
361
+ }): Promise<KeystoneIbGib_V1> {
362
+ const lc = `${this.lc}[${this.getSessionKeystone.name}]`;
363
+ try {
364
+ if (logalot) { console.log(`${lc} starting... (I: cb9af0e14d3345bfbc0be35728de7c26)`); }
365
+
366
+ const resGet = await metaspace.get({ addrs: [sAddr], space });
367
+
368
+ // Validate result
369
+ if (resGet.success && resGet.ibGibs && resGet.ibGibs.length === 1) {
370
+ return resGet.ibGibs[0] as KeystoneIbGib_V1;
371
+ } else {
372
+ // Robust error handling using rawResultIbGib
373
+ const resIbGib = resGet.rawResultIbGib as IbGibSpaceResultIbGib<IbGib_V1, IbGibSpaceResultData, IbGibSpaceResultRel8ns>;
374
+ const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown';
375
+ throw new Error(`couldn't find all addrs. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg}. space.ib: ${space.ib} (E: f3c87e2b19794358a9e701cd59b8eb26)`);
376
+ }
377
+ } catch (error) {
378
+ console.error(`${lc} ${extractErrorMsg(error)}`);
379
+ throw error;
380
+ } finally {
381
+ if (logalot) { console.log(`${lc} complete.`); }
382
+ }
383
+ }
384
+
385
+ protected async validateUpfrontHandshake(reqCtx: SyncUpgradeRequestContext): Promise<void> {
386
+ const lc = `${this.lc}[${this.validateUpfrontHandshake.name}]`;
387
+ const sAddr = reqCtx.queryParams?.sAddr;
388
+ const solution = reqCtx.queryParams?.solution;
389
+
390
+ if (!sAddr || !solution) {
391
+ throw new Error(`Missing upfront handshake parameters (sAddr, solution) (E: 8a4c9a724ddf59998199ec87a9ee126)`);
392
+ }
393
+
394
+ const metaspace = reqCtx.metaspace!;
395
+ const space = await metaspace.getLocalUserSpace({ lock: false });
396
+ if (!space) { throw new Error("No space."); }
397
+
398
+ const authorizedS = await this.getSessionKeystone({ metaspace, space, sAddr });
399
+ const isValid = await checkHandshakeSolution(authorizedS, solution);
400
+ if (!isValid) {
401
+ throw new Error(`Upfront handshake solution verification failed (E: 804c98a724ddf59998199ec87a9ee127)`);
402
+ }
403
+
404
+ // Store secure TJP address of the verified session keystone in reqCtx
405
+ const past = authorizedS.rel8ns?.past;
406
+ const tjpAddr = (past && past.length > 0) ? past[0] : getIbGibAddr({ ibGib: authorizedS });
407
+ reqCtx.sAddr_tjp = tjpAddr;
408
+
409
+ if (logalot) { console.log(`${lc} Upfront picket-fence verification succeeded for ${sAddr}!`); }
410
+ }
411
+
412
+ protected override async parseParamsImpl(reqCtx: RequestContext<ParamsWithDomain>): Promise<ParamsWithDomain | undefined> {
413
+ const lc = `${this.lc}[${this.parseParamsImpl.name}]`;
414
+ try {
415
+ const match = reqCtx.pathname.match(this.regex);
416
+ if (!match) return undefined;
417
+ const domainIb = decodeURIComponent(match[1]);
418
+ const domainGib = decodeURIComponent(match[2]);
419
+ const domainAddr = getIbGibAddr({ ib: domainIb, gib: domainGib });
420
+
421
+ return {
422
+ domainInfo: this.getDomainInfo({ domainAddr })
423
+ };
424
+ } catch (error) {
425
+ console.error(`${lc} ${extractErrorMsg(error)}`);
426
+ throw error;
427
+ }
428
+ }
429
+
430
+ protected override async parseQueryParams(
431
+ reqCtx: RequestContext<ParamsWithDomain, SyncUpgradeQueryParams>
432
+ ): Promise<SyncUpgradeQueryParams | undefined> {
433
+ const lc = `${this.lc}[${this.parseQueryParams.name}]`;
434
+ try {
435
+ if (logalot) { console.log(`${lc} starting... (I: 3ed5ba8d47af31f538fbdb484cfdb826)`); }
436
+
437
+ const sAddrRaw = reqCtx.url.searchParams.get('sAddr');
438
+ const sAddr = sAddrRaw ? decodeURIComponent(sAddrRaw) : undefined;
439
+ const solution = reqCtx.url.searchParams.get('solution') ?? undefined;
440
+
441
+ const queryParams: Partial<SyncUpgradeQueryParams> = {};
442
+ if (sAddr !== undefined) { queryParams.sAddr = sAddr; }
443
+ if (solution !== undefined) { queryParams.solution = solution; }
444
+
445
+ if (Object.keys(queryParams).length > 0) {
446
+ const validationErrors = await this.validateQueryParams({ queryParams });
447
+ if (validationErrors.length === 0) {
448
+ return queryParams as SyncUpgradeQueryParams;
449
+ } else {
450
+ throw new Error(`invalid query params. validationErrors: ${validationErrors.join(', ')} (E: 9355f8a9643803f52f2241f1c7f39826)`);
451
+ }
452
+ } else {
453
+ return undefined;
454
+ }
455
+ } catch (error) {
456
+ console.error(`${lc} ${extractErrorMsg(error)}`);
457
+ throw error;
458
+ } finally {
459
+ if (logalot) { console.log(`${lc} complete.`); }
460
+ }
461
+ }
462
+
463
+ protected override async validateQueryParams({ queryParams }: { queryParams: any }): Promise<string[]> {
464
+ const errors: string[] = [];
465
+ if (!queryParams) {
466
+ return ["Missing query parameters: sAddr, solution"];
467
+ }
468
+
469
+ if (!queryParams.sAddr) {
470
+ errors.push("Missing sAddr");
471
+ } else {
472
+ try {
473
+ const decodedSAddr = queryParams.sAddr;
474
+
475
+ // use validateIbGibAddr to ensure that it's a valid address.
476
+ const addrErrors = validateIbGibAddr({ addr: decodedSAddr }) ?? [];
477
+ if (addrErrors.length > 0) {
478
+ errors.push(`Invalid sAddr: ${addrErrors.join(', ')}`);
479
+ } else {
480
+ // get the ib and ensure that it is a keystone ib using parseKeystoneIb (will throw if invalid)
481
+ const { ib } = getIbAndGib({ ibGibAddr: decodedSAddr });
482
+ parseKeystoneIb({ ib });
483
+ }
484
+ } catch (error) {
485
+ errors.push(`Invalid sAddr: ${extractErrorMsg(error)}`);
486
+ }
487
+ }
488
+
489
+ if (!queryParams.solution) {
490
+ errors.push("Missing solution");
491
+ }
492
+ return errors;
493
+ }
494
+
495
+ protected override async handleRouteImpl(reqCtx: RequestContext<ParamsWithDomain>): Promise<ResponseResult | undefined> {
496
+ return this.error(400, 'WS endpoint requires upgrade');
497
+ }
498
+ }
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { Socket } from 'node:net';
4
+
5
+ import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
6
+
7
+ const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
8
+
9
+ /** Encodes a UTF-8 string as a minimal unmasked WebSocket text frame. */
10
+ export function encodeTextFrame(text: string): Buffer {
11
+ const payload = Buffer.from(text, 'utf-8');
12
+ const payloadLen = payload.length;
13
+
14
+ let header: Buffer;
15
+ if (payloadLen < 126) {
16
+ header = Buffer.alloc(2);
17
+ header[0] = 0x81; // FIN + opcode 1 (text)
18
+ header[1] = payloadLen;
19
+ } else if (payloadLen < 65536) {
20
+ header = Buffer.alloc(4);
21
+ header[0] = 0x81;
22
+ header[1] = 126;
23
+ header.writeUInt16BE(payloadLen, 2);
24
+ } else {
25
+ header = Buffer.alloc(10);
26
+ header[0] = 0x81;
27
+ header[1] = 127;
28
+ header.writeBigUInt64BE(BigInt(payloadLen), 2);
29
+ }
30
+
31
+ return Buffer.concat([header, payload]);
32
+ }
33
+
34
+ /** Decodes a masked client WebSocket text frame. Returns null for non-text or incomplete frames. */
35
+ export function decodeTextFrame(data: Buffer): string | null {
36
+ try {
37
+ if (data.length < 2) { return null; }
38
+
39
+ const opcode = data[0] & 0x0f;
40
+ if (opcode === 0x8) { return null; } // Close
41
+ if (opcode !== 0x1) { return null; } // Text only
42
+
43
+ const masked = (data[1] & 0x80) !== 0;
44
+ let payloadStart = 2;
45
+ let payloadLen = data[1] & 0x7f;
46
+
47
+ if (payloadLen === 126) {
48
+ payloadLen = data.readUInt16BE(2);
49
+ payloadStart = 4;
50
+ } else if (payloadLen === 127) {
51
+ payloadLen = Number(data.readBigUInt64BE(2));
52
+ payloadStart = 10;
53
+ }
54
+
55
+ if (masked) {
56
+ const maskStart = payloadStart;
57
+ payloadStart += 4;
58
+ const mask = data.slice(maskStart, maskStart + 4);
59
+ const payload = data.slice(payloadStart, payloadStart + payloadLen);
60
+ for (let i = 0; i < payload.length; i++) {
61
+ payload[i] ^= mask[i % 4];
62
+ }
63
+ return payload.toString('utf-8');
64
+ }
65
+
66
+ return data.slice(payloadStart, payloadStart + payloadLen).toString('utf-8');
67
+ } catch (error) {
68
+ console.error(`[ws-helper] decode error: ${extractErrorMsg(error)}`);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** Encodes a WebSocket close frame (opcode 0x8) with optional status code. */
74
+ export function encodeCloseFrame(code = 1000): Buffer {
75
+ const frame = Buffer.alloc(4);
76
+ frame[0] = 0x88;
77
+ frame[1] = 2;
78
+ frame.writeUInt16BE(code, 2);
79
+ return frame;
80
+ }
81
+
82
+ /** Performs the RFC 6455 opening handshake. */
83
+ export function performHandshake(req: IncomingMessage, socket: Socket): boolean {
84
+ try {
85
+ const key = req.headers['sec-websocket-key'];
86
+ if (!key) {
87
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\nMissing Sec-WebSocket-Key');
88
+ socket.destroy();
89
+ return false;
90
+ }
91
+
92
+ const acceptKey = createHash('sha1')
93
+ .update(key + WS_MAGIC)
94
+ .digest('base64');
95
+
96
+ const response = [
97
+ 'HTTP/1.1 101 Switching Protocols',
98
+ 'Upgrade: websocket',
99
+ 'Connection: Upgrade',
100
+ `Sec-WebSocket-Accept: ${acceptKey}`,
101
+ '\r\n',
102
+ ].join('\r\n');
103
+
104
+ socket.write(response);
105
+ return true;
106
+ } catch (error) {
107
+ console.error(`[ws-helper] handshake error: ${extractErrorMsg(error)}`);
108
+ socket.destroy();
109
+ return false;
110
+ }
111
+ }
@@ -0,0 +1,53 @@
1
+ import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
2
+
3
+ export type HandshakeMessageType =
4
+ 'auth-challenge-init' |
5
+ 'auth-init' |
6
+ 'auth-challenge' |
7
+ 'auth-proof' |
8
+ 'auth-ok' |
9
+ 'auth-fail';
10
+
11
+ export interface HandshakeMessageBase {
12
+ type: HandshakeMessageType;
13
+ }
14
+
15
+ export interface AuthChallengeInitMessage extends HandshakeMessageBase {
16
+ type: 'auth-challenge-init';
17
+ challengeUuid: string;
18
+ }
19
+
20
+ export interface AuthInitMessage extends HandshakeMessageBase {
21
+ type: 'auth-init';
22
+ sAddr: string;
23
+ }
24
+
25
+ export interface AuthChallengeMessage extends HandshakeMessageBase {
26
+ type: 'auth-challenge';
27
+ challengeUuid: string;
28
+ demandedIds: string[];
29
+ }
30
+
31
+ export interface AuthProofMessage extends HandshakeMessageBase {
32
+ type: 'auth-proof';
33
+ proofFrame: KeystoneIbGib_V1;
34
+ }
35
+
36
+ export interface AuthOkMessage extends HandshakeMessageBase {
37
+ type: 'auth-ok';
38
+ }
39
+
40
+ export interface AuthFailMessage extends HandshakeMessageBase {
41
+ type: 'auth-fail';
42
+ message: string;
43
+ errors?: string[];
44
+ }
45
+
46
+ export type HandshakeMessage =
47
+ AuthChallengeInitMessage |
48
+ AuthInitMessage |
49
+ AuthChallengeMessage |
50
+ AuthProofMessage |
51
+ AuthOkMessage |
52
+ AuthFailMessage;
53
+
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @module serve-gib/serve-gib-helpers
3
+ *
4
+ * General purpose helper functions for the serve-gib framework.
5
+ */
6
+
7
+ /**
8
+ * Validates and coerces a boolean query parameter in place on the queryParams object.
9
+ *
10
+ * @param queryParams The parsed query parameters object
11
+ * @param key The specific key to validate and coerce
12
+ * @param errors Array to push error messages into if validation fails
13
+ */
14
+ export function validateAndCoerceBooleanQueryParam(
15
+ queryParams: any,
16
+ key: string,
17
+ errors: string[]
18
+ ): void {
19
+ const rawVal = queryParams[key];
20
+ if (typeof rawVal === 'string' &&
21
+ (rawVal.toLowerCase() === 'true' || rawVal.toLowerCase() === 'false')
22
+ ) {
23
+ queryParams[key] = rawVal.toLowerCase() === 'true';
24
+ } else if (typeof rawVal === 'boolean') {
25
+ // do nothing, it's already a boolean
26
+ } else if (rawVal !== undefined) {
27
+ errors.push(`invalid value for "${key}". typeof should either be string or boolean. (E: c28ef87a2d4b4a6faef9a2c8db05b637)`);
28
+ } else {
29
+ // It's undefined, which means it wasn't provided in the URL.
30
+ // We do not error here because default parameters apply later or it may be optional.
31
+ }
32
+ }