@jskit-ai/workspaces-web 0.1.30 → 0.1.32

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.
@@ -6,6 +6,7 @@ import {
6
6
  WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
7
7
  WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND,
8
8
  WORKSPACE_BOOTSTRAP_STATUS_RESOLVED,
9
+ WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED,
9
10
  createBootstrapPlacementRuntime
10
11
  } from "../src/client/runtime/bootstrapPlacementRuntime.js";
11
12
 
@@ -171,6 +172,17 @@ function createSocketStub() {
171
172
  };
172
173
  }
173
174
 
175
+ function createBootstrapRuntimeStub() {
176
+ const calls = [];
177
+ return {
178
+ async refresh(reason) {
179
+ calls.push(String(reason || ""));
180
+ return null;
181
+ },
182
+ calls
183
+ };
184
+ }
185
+
174
186
  function createAppStub(records = {}) {
175
187
  const registry = new Map();
176
188
  for (const key of Reflect.ownKeys(records)) {
@@ -231,61 +243,84 @@ function createVueAppWithThemeController(themeController) {
231
243
  };
232
244
  }
233
245
 
234
- test("bootstrap placement runtime writes user/workspace/permissions into placement context", async () => {
246
+ function createBootstrapRequest(path = "/w/acme/dashboard", workspaceSlug = "") {
247
+ const normalizedPath = resolvePathFromFullPath(path);
248
+ const normalizedWorkspaceSlug = String(workspaceSlug || "").trim().toLowerCase();
249
+ return Object.freeze({
250
+ path: "/api/bootstrap",
251
+ query: Object.freeze(normalizedWorkspaceSlug ? { workspaceSlug: normalizedWorkspaceSlug } : {}),
252
+ meta: Object.freeze({
253
+ path: normalizedPath,
254
+ workspaceSlug: normalizedWorkspaceSlug
255
+ })
256
+ });
257
+ }
258
+
259
+ function createErrorWithStatus(status, message = "") {
260
+ const error = new Error(message || `HTTP ${status}`);
261
+ error.status = status;
262
+ return error;
263
+ }
264
+
265
+ test("bootstrap placement runtime contributes workspace slug to shared bootstrap request and writes payload into placement context", async () => {
235
266
  const placementRuntime = createPlacementRuntimeStub();
236
267
  const router = createRouterStub("/w/acme/dashboard");
237
- const fetchCalls = [];
238
268
  const runtime = createBootstrapPlacementRuntime({
239
269
  app: createAppStub({
240
270
  ["runtime.web-placement.client"]: placementRuntime,
271
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
241
272
  ["jskit.client.router"]: router
242
- }),
243
- fetchBootstrap: async (workspaceSlug) => {
244
- fetchCalls.push(workspaceSlug);
245
- return {
246
- session: {
247
- authenticated: true,
248
- userId: "7"
249
- },
250
- profile: {
251
- displayName: "Ada Lovelace",
252
- email: "ADA@EXAMPLE.COM",
253
- avatar: {
254
- effectiveUrl: "https://cdn.example.com/ada.png"
255
- }
256
- },
257
- app: {
258
- features: {
259
- workspaceInvites: true
260
- }
261
- },
262
- pendingInvites: [
263
- { id: "1", workspaceId: "1", token: "a" },
264
- { id: "2", workspaceId: "2", token: "b" }
265
- ],
266
- workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
267
- permissions: ["workspace.settings.view"]
268
- };
269
- }
273
+ })
270
274
  });
271
275
 
272
276
  await runtime.initialize();
273
- const context = placementRuntime.getContext();
277
+ const request = runtime.resolveBootstrapRequest();
278
+ assert.deepEqual(request, {
279
+ query: {
280
+ workspaceSlug: "acme"
281
+ },
282
+ meta: {
283
+ path: "/w/acme/dashboard",
284
+ workspaceSlug: "acme"
285
+ }
286
+ });
287
+
288
+ await runtime.applyBootstrapPayload({
289
+ request,
290
+ payload: {
291
+ session: {
292
+ authenticated: true,
293
+ userId: "7"
294
+ },
295
+ profile: {
296
+ displayName: "Ada Lovelace",
297
+ email: "ADA@EXAMPLE.COM",
298
+ avatar: {
299
+ effectiveUrl: "https://cdn.example.com/ada.png"
300
+ }
301
+ },
302
+ app: {
303
+ features: {
304
+ workspaceInvites: true
305
+ }
306
+ },
307
+ pendingInvites: [
308
+ { id: "1", workspaceId: "1", token: "a" },
309
+ { id: "2", workspaceId: "2", token: "b" }
310
+ ],
311
+ workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
312
+ permissions: ["workspace.settings.view"]
313
+ },
314
+ source: "test.bootstrap"
315
+ });
274
316
 
275
- assert.deepEqual(fetchCalls, ["acme"]);
317
+ const context = placementRuntime.getContext();
276
318
  assert.equal(context.workspace?.slug, "acme");
277
319
  assert.equal(Array.isArray(context.workspaces), true);
278
320
  assert.equal(context.workspaces.length, 1);
279
321
  assert.deepEqual(context.permissions, ["workspace.settings.view"]);
280
322
  assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_RESOLVED);
281
323
  assert.equal(context.workspaceBootstrapStatuses?.acme, WORKSPACE_BOOTSTRAP_STATUS_RESOLVED);
282
- assert.deepEqual(context.user, {
283
- id: "7",
284
- displayName: "Ada Lovelace",
285
- name: "Ada Lovelace",
286
- email: "ada@example.com",
287
- avatarUrl: "https://cdn.example.com/ada.png"
288
- });
289
324
  assert.equal(context.workspaceInvitesEnabled, true);
290
325
  assert.equal(context.pendingInvitesCount, 2);
291
326
  });
@@ -294,35 +329,39 @@ test("bootstrap placement runtime resolves workspace slug from pathname when sur
294
329
  const placementRuntime = createPlacementRuntimeStub();
295
330
  placementRuntime.setContext({}, { replace: true, source: "test.clear" });
296
331
  const router = createRouterStub("/w/acme/admin");
297
- const fetchCalls = [];
298
332
  const runtime = createBootstrapPlacementRuntime({
299
333
  app: createAppStub({
300
334
  ["runtime.web-placement.client"]: placementRuntime,
335
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
301
336
  ["jskit.client.router"]: router
302
- }),
303
- fetchBootstrap: async (workspaceSlug) => {
304
- fetchCalls.push(workspaceSlug);
305
- return {
306
- session: {
307
- authenticated: true,
308
- userId: "1"
309
- },
310
- profile: {
311
- displayName: "User",
312
- email: "user@example.com",
313
- avatar: {
314
- effectiveUrl: ""
315
- }
316
- },
317
- workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
318
- permissions: ["workspace.settings.view"]
319
- };
320
- }
337
+ })
321
338
  });
322
339
 
323
340
  await runtime.initialize();
341
+ const request = runtime.resolveBootstrapRequest();
342
+ assert.deepEqual(request.query, {
343
+ workspaceSlug: "acme"
344
+ });
345
+
346
+ await runtime.applyBootstrapPayload({
347
+ request,
348
+ payload: {
349
+ session: {
350
+ authenticated: true,
351
+ userId: "1"
352
+ },
353
+ profile: {
354
+ displayName: "User",
355
+ email: "user@example.com",
356
+ avatar: {
357
+ effectiveUrl: ""
358
+ }
359
+ },
360
+ workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
361
+ permissions: ["workspace.settings.view"]
362
+ }
363
+ });
324
364
 
325
- assert.deepEqual(fetchCalls, ["acme"]);
326
365
  assert.deepEqual(placementRuntime.getContext().permissions, ["workspace.settings.view"]);
327
366
  });
328
367
 
@@ -342,28 +381,31 @@ test("bootstrap placement runtime does not mutate placement auth context", async
342
381
  const runtime = createBootstrapPlacementRuntime({
343
382
  app: createAppStub({
344
383
  ["runtime.web-placement.client"]: placementRuntime,
384
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
345
385
  ["jskit.client.router"]: router
346
- }),
347
- fetchBootstrap: async () => {
348
- return {
349
- session: {
350
- authenticated: true,
351
- userId: "9"
352
- },
353
- profile: {
354
- displayName: "User",
355
- email: "user@example.com",
356
- avatar: {
357
- effectiveUrl: ""
358
- }
359
- },
360
- workspaces: [{ id: "1", slug: "acme", name: "Workspace" }],
361
- permissions: []
362
- };
363
- }
386
+ })
364
387
  });
365
388
 
366
389
  await runtime.initialize();
390
+ await runtime.applyBootstrapPayload({
391
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
392
+ payload: {
393
+ session: {
394
+ authenticated: true,
395
+ userId: "9"
396
+ },
397
+ profile: {
398
+ displayName: "User",
399
+ email: "user@example.com",
400
+ avatar: {
401
+ effectiveUrl: ""
402
+ }
403
+ },
404
+ workspaces: [{ id: "1", slug: "acme", name: "Workspace" }],
405
+ permissions: []
406
+ }
407
+ });
408
+
367
409
  assert.deepEqual(placementRuntime.getContext().auth, {
368
410
  authenticated: true,
369
411
  oauthDefaultProvider: "github",
@@ -371,235 +413,130 @@ test("bootstrap placement runtime does not mutate placement auth context", async
371
413
  });
372
414
  });
373
415
 
374
- test("bootstrap placement runtime refetches on route changes and users.bootstrap.changed events", async () => {
416
+ test("bootstrap placement runtime delegates route and realtime refreshes to the shared bootstrap runtime", async () => {
375
417
  const placementRuntime = createPlacementRuntimeStub();
376
418
  const router = createRouterStub("/w/acme/dashboard");
377
419
  const socket = createSocketStub();
378
- const fetchCalls = [];
420
+ const bootstrapRuntime = createBootstrapRuntimeStub();
379
421
  const runtime = createBootstrapPlacementRuntime({
380
422
  app: createAppStub({
381
423
  ["runtime.web-placement.client"]: placementRuntime,
424
+ ["runtime.web-bootstrap.client"]: bootstrapRuntime,
382
425
  ["jskit.client.router"]: router,
383
426
  ["runtime.realtime.client.socket"]: socket
384
- }),
385
- fetchBootstrap: async (workspaceSlug) => {
386
- fetchCalls.push(workspaceSlug);
387
- return {
388
- session: {
389
- authenticated: true,
390
- userId: "1"
391
- },
392
- profile: {
393
- displayName: "User",
394
- email: "user@example.com",
395
- avatar: {
396
- effectiveUrl: ""
397
- }
398
- },
399
- workspaces: [{ id: "1", slug: workspaceSlug || "acme", name: "Workspace" }],
400
- permissions: []
401
- };
402
- }
427
+ })
403
428
  });
404
429
 
405
430
  await runtime.initialize();
406
- assert.deepEqual(fetchCalls, ["acme"]);
431
+ assert.deepEqual(bootstrapRuntime.calls, []);
407
432
 
408
433
  router.currentRoute.value.path = "/w/acme/customers";
409
434
  router.currentRoute.value.fullPath = "/w/acme/customers";
410
435
  router.emitAfterEach();
411
436
  await flushTasks();
412
- assert.deepEqual(fetchCalls, ["acme"]);
437
+ assert.deepEqual(bootstrapRuntime.calls, []);
413
438
 
414
439
  router.currentRoute.value.path = "/w/zen/dashboard";
415
440
  router.currentRoute.value.fullPath = "/w/zen/dashboard";
416
441
  router.emitAfterEach();
417
442
  await flushTasks();
418
- assert.deepEqual(fetchCalls, ["acme", "zen"]);
443
+ assert.deepEqual(bootstrapRuntime.calls, ["route"]);
419
444
 
420
445
  socket.emit("users.bootstrap.changed", {});
421
446
  await flushTasks();
422
- assert.deepEqual(fetchCalls, ["acme", "zen", "zen"]);
447
+ assert.deepEqual(bootstrapRuntime.calls, ["route", "realtime"]);
423
448
  });
424
449
 
425
- test("bootstrap placement runtime refetches when auth context changes", async () => {
450
+ test("bootstrap placement runtime applies theme changes from bootstrap payloads", async () => {
426
451
  const placementRuntime = createPlacementRuntimeStub();
427
452
  const router = createRouterStub("/w/acme/dashboard");
428
- const fetchCalls = [];
453
+ const themeController = createVuetifyThemeController("light");
429
454
  const runtime = createBootstrapPlacementRuntime({
430
455
  app: createAppStub({
431
456
  ["runtime.web-placement.client"]: placementRuntime,
432
- ["jskit.client.router"]: router
433
- }),
434
- fetchBootstrap: async (workspaceSlug) => {
435
- fetchCalls.push(workspaceSlug);
436
- return {
437
- session: {
438
- authenticated: true,
439
- userId: "1"
440
- },
441
- profile: {
442
- displayName: "User",
443
- email: "user@example.com",
444
- avatar: {
445
- effectiveUrl: ""
446
- }
447
- },
448
- workspaces: [{ id: "1", slug: workspaceSlug || "acme", name: "Workspace" }],
449
- permissions: []
450
- };
451
- }
457
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
458
+ ["jskit.client.router"]: router,
459
+ ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
460
+ })
452
461
  });
453
462
 
454
463
  await runtime.initialize();
455
- assert.deepEqual(fetchCalls, ["acme"]);
456
-
457
- placementRuntime.setContext(
458
- {
459
- auth: {
464
+ const request = createBootstrapRequest("/w/acme/dashboard", "acme");
465
+ await runtime.applyBootstrapPayload({
466
+ request,
467
+ payload: {
468
+ session: {
460
469
  authenticated: true,
461
- oauthDefaultProvider: "",
462
- oauthProviders: []
463
- }
464
- },
465
- {
466
- source: "test.auth"
467
- }
468
- );
469
- await flushTasks();
470
- await flushTasks();
471
- assert.deepEqual(fetchCalls, ["acme", "acme"]);
472
- });
473
-
474
- test("bootstrap placement runtime applies persisted theme preference for unauthenticated bootstrap payloads", async () => {
475
- const placementRuntime = createPlacementRuntimeStub();
476
- const router = createRouterStub("/auth/login");
477
- const themeController = createVuetifyThemeController("dark");
478
- const storage = new Map();
479
- storage.set("jskit.themePreference", "dark");
480
- const originalWindow = globalThis.window;
481
- globalThis.window = {
482
- localStorage: {
483
- getItem(key) {
484
- return storage.get(key) || null;
470
+ userId: "1"
485
471
  },
486
- setItem(key, value) {
487
- storage.set(key, value);
488
- }
489
- }
490
- };
491
- const runtime = createBootstrapPlacementRuntime({
492
- app: createAppStub({
493
- ["runtime.web-placement.client"]: placementRuntime,
494
- ["jskit.client.router"]: router,
495
- ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
496
- }),
497
- fetchBootstrap: async () => {
498
- return {
499
- session: {
500
- authenticated: false
501
- },
502
- workspaces: [],
503
- permissions: []
504
- };
472
+ userSettings: {
473
+ theme: "dark"
474
+ },
475
+ workspaces: [{ id: "1", slug: "acme", name: "Workspace" }],
476
+ permissions: []
505
477
  }
506
478
  });
479
+ assert.equal(themeController.global.name.value, "workspace-dark");
507
480
 
508
- try {
509
- await runtime.initialize();
510
- assert.equal(themeController.global.name.value, "dark");
511
- } finally {
512
- if (typeof originalWindow === "undefined") {
513
- delete globalThis.window;
514
- } else {
515
- globalThis.window = originalWindow;
481
+ await runtime.applyBootstrapPayload({
482
+ request,
483
+ payload: {
484
+ session: {
485
+ authenticated: true,
486
+ userId: "1"
487
+ },
488
+ userSettings: {
489
+ theme: "light"
490
+ },
491
+ workspaces: [{ id: "1", slug: "acme", name: "Workspace" }],
492
+ permissions: []
516
493
  }
517
- }
494
+ });
495
+ assert.equal(themeController.global.name.value, "workspace-light");
518
496
  });
519
497
 
520
- test("bootstrap placement runtime reapplies theme when bootstrap payload changes", async () => {
498
+ test("bootstrap placement runtime applies workspace palette and clears it when leaving workspace routes", async () => {
521
499
  const placementRuntime = createPlacementRuntimeStub();
522
500
  const router = createRouterStub("/w/acme/dashboard");
523
- const socket = createSocketStub();
524
501
  const themeController = createVuetifyThemeController("light");
525
- let fetchCount = 0;
526
502
  const runtime = createBootstrapPlacementRuntime({
527
503
  app: createAppStub({
528
504
  ["runtime.web-placement.client"]: placementRuntime,
505
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
529
506
  ["jskit.client.router"]: router,
530
- ["runtime.realtime.client.socket"]: socket,
531
507
  ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
532
- }),
533
- fetchBootstrap: async (workspaceSlug) => {
534
- fetchCount += 1;
535
- return {
536
- session: {
537
- authenticated: true,
538
- userId: "1"
539
- },
540
- profile: {
541
- displayName: "User",
542
- email: "user@example.com",
543
- avatar: {
544
- effectiveUrl: ""
545
- }
546
- },
547
- userSettings: {
548
- theme: fetchCount === 1 ? "dark" : "light"
549
- },
550
- workspaces: [{ id: "1", slug: workspaceSlug || "acme", name: "Workspace" }],
551
- permissions: []
552
- };
553
- }
508
+ })
554
509
  });
555
510
 
556
511
  await runtime.initialize();
557
- assert.equal(themeController.global.name.value, "workspace-dark");
558
-
559
- socket.emit("users.bootstrap.changed", {});
560
- await flushTasks();
561
- assert.equal(themeController.global.name.value, "workspace-light");
562
- });
563
-
564
- test("bootstrap placement runtime applies workspace palette via Vuetify workspace themes and clears it off workspace routes", async () => {
565
- const placementRuntime = createPlacementRuntimeStub();
566
- const router = createRouterStub("/w/acme/dashboard");
567
- const themeController = createVuetifyThemeController("light");
568
- const runtime = createBootstrapPlacementRuntime({
569
- app: createAppStub({
570
- ["runtime.web-placement.client"]: placementRuntime,
571
- ["jskit.client.router"]: router,
572
- ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
573
- }),
574
- fetchBootstrap: async (workspaceSlug) => {
575
- return {
576
- session: {
577
- authenticated: true,
578
- userId: "1"
579
- },
580
- workspaceSettings: {
581
- lightPrimaryColor: "#CC3344",
582
- lightSecondaryColor: "#884455",
583
- lightSurfaceColor: "#F4F4F4",
584
- lightSurfaceVariantColor: "#444444",
585
- darkPrimaryColor: "#BB2233",
586
- darkSecondaryColor: "#557799",
587
- darkSurfaceColor: "#202020",
588
- darkSurfaceVariantColor: "#A0A0A0"
589
- },
590
- workspaces: [
591
- {
592
- id: "1",
593
- slug: "acme",
594
- name: "Acme Workspace"
595
- }
596
- ],
597
- permissions: []
598
- };
512
+ await runtime.applyBootstrapPayload({
513
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
514
+ payload: {
515
+ session: {
516
+ authenticated: true,
517
+ userId: "1"
518
+ },
519
+ workspaceSettings: {
520
+ lightPrimaryColor: "#CC3344",
521
+ lightSecondaryColor: "#884455",
522
+ lightSurfaceColor: "#F4F4F4",
523
+ lightSurfaceVariantColor: "#444444",
524
+ darkPrimaryColor: "#BB2233",
525
+ darkSecondaryColor: "#557799",
526
+ darkSurfaceColor: "#202020",
527
+ darkSurfaceVariantColor: "#A0A0A0"
528
+ },
529
+ workspaces: [
530
+ {
531
+ id: "1",
532
+ slug: "acme",
533
+ name: "Acme Workspace"
534
+ }
535
+ ],
536
+ permissions: []
599
537
  }
600
538
  });
601
539
 
602
- await runtime.initialize();
603
540
  const palette = resolveWorkspaceThemePalette({
604
541
  lightPrimaryColor: "#CC3344",
605
542
  lightSecondaryColor: "#884455",
@@ -621,7 +558,6 @@ test("bootstrap placement runtime applies workspace palette via Vuetify workspac
621
558
  router.currentRoute.value.fullPath = "/home";
622
559
  router.emitAfterEach();
623
560
  await flushTasks();
624
- await flushTasks();
625
561
 
626
562
  assert.equal(themeController.global.name.value, "light");
627
563
  });
@@ -632,24 +568,26 @@ test("bootstrap placement runtime marks workspace slug as not_found and clears w
632
568
  {
633
569
  workspace: { id: "1", slug: "acme", name: "Acme Workspace" },
634
570
  workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
635
- permissions: ["workspace.settings.view"]
571
+ permissions: ["workspace.settings.view"],
572
+ surfaceAccess: {
573
+ consoleowner: true
574
+ }
636
575
  },
637
576
  { source: "test.seed" }
638
577
  );
639
-
640
578
  const runtime = createBootstrapPlacementRuntime({
641
579
  app: createAppStub({
642
580
  ["runtime.web-placement.client"]: placementRuntime,
581
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
643
582
  ["jskit.client.router"]: createRouterStub("/w/acme/dashboard")
644
- }),
645
- fetchBootstrap: async () => {
646
- const error = new Error("Workspace not found.");
647
- error.status = 404;
648
- throw error;
649
- }
583
+ })
650
584
  });
651
585
 
652
586
  await runtime.initialize();
587
+ await runtime.handleBootstrapError({
588
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
589
+ error: createErrorWithStatus(404, "Workspace not found.")
590
+ });
653
591
 
654
592
  const context = placementRuntime.getContext();
655
593
  assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
@@ -657,56 +595,57 @@ test("bootstrap placement runtime marks workspace slug as not_found and clears w
657
595
  assert.equal(context.workspace, null);
658
596
  assert.deepEqual(context.workspaces, []);
659
597
  assert.deepEqual(context.permissions, []);
660
- assert.equal(context.user, null);
661
598
  assert.equal(context.pendingInvitesCount, 0);
662
599
  assert.equal(context.workspaceInvitesEnabled, false);
600
+ assert.deepEqual(context.surfaceAccess, {
601
+ consoleowner: true
602
+ });
663
603
  });
664
604
 
665
- test("bootstrap placement runtime updates status per workspace slug across route changes", async () => {
605
+ test("bootstrap placement runtime tracks status per workspace slug across route changes", async () => {
666
606
  const placementRuntime = createPlacementRuntimeStub();
667
607
  const router = createRouterStub("/w/acme/dashboard");
608
+ const bootstrapRuntime = createBootstrapRuntimeStub();
668
609
  const runtime = createBootstrapPlacementRuntime({
669
610
  app: createAppStub({
670
611
  ["runtime.web-placement.client"]: placementRuntime,
612
+ ["runtime.web-bootstrap.client"]: bootstrapRuntime,
671
613
  ["jskit.client.router"]: router
672
- }),
673
- fetchBootstrap: async (workspaceSlug) => {
674
- if (workspaceSlug === "zen") {
675
- const error = new Error("Workspace not found.");
676
- error.status = 404;
677
- throw error;
678
- }
614
+ })
615
+ });
679
616
 
680
- return {
681
- session: {
682
- authenticated: true,
683
- userId: "1"
684
- },
685
- profile: {
686
- displayName: "User",
687
- email: "user@example.com",
688
- avatar: {
689
- effectiveUrl: ""
690
- }
691
- },
692
- workspaces: [{ id: "1", slug: workspaceSlug || "acme", name: "Workspace" }],
693
- permissions: []
694
- };
617
+ await runtime.initialize();
618
+ await runtime.applyBootstrapPayload({
619
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
620
+ payload: {
621
+ session: {
622
+ authenticated: true,
623
+ userId: "1"
624
+ },
625
+ workspaces: [{ id: "1", slug: "acme", name: "Workspace" }],
626
+ permissions: []
695
627
  }
696
628
  });
697
629
 
698
- await runtime.initialize();
699
630
  assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_RESOLVED);
700
631
 
701
632
  router.currentRoute.value.path = "/w/zen/dashboard";
702
633
  router.currentRoute.value.fullPath = "/w/zen/dashboard";
703
634
  router.emitAfterEach();
704
635
  await flushTasks();
636
+ assert.deepEqual(bootstrapRuntime.calls, ["route"]);
637
+
638
+ await runtime.handleBootstrapError({
639
+ request: createBootstrapRequest("/w/zen", "zen"),
640
+ error: createErrorWithStatus(404, "Workspace not found.")
641
+ });
705
642
 
706
643
  const context = placementRuntime.getContext();
644
+ assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_RESOLVED);
707
645
  assert.equal(runtime.getWorkspaceBootstrapStatus("zen"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
646
+ assert.equal(context.workspaceBootstrapStatuses?.acme, WORKSPACE_BOOTSTRAP_STATUS_RESOLVED);
708
647
  assert.equal(context.workspaceBootstrapStatuses?.zen, WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
709
- assert.equal(context.workspace, null);
648
+ assert.deepEqual(router.replaceCalls, ["/w/zen"]);
710
649
  });
711
650
 
712
651
  test("bootstrap placement runtime uses requestedWorkspace status and keeps global workspace list on inaccessible slug", async () => {
@@ -715,32 +654,34 @@ test("bootstrap placement runtime uses requestedWorkspace status and keeps globa
715
654
  const runtime = createBootstrapPlacementRuntime({
716
655
  app: createAppStub({
717
656
  ["runtime.web-placement.client"]: placementRuntime,
657
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
718
658
  ["jskit.client.router"]: router
719
- }),
720
- fetchBootstrap: async () => {
721
- return {
722
- session: {
723
- authenticated: true,
724
- userId: "4"
725
- },
726
- profile: {
727
- displayName: "Chiara",
728
- email: "chiara@example.com",
729
- avatar: {
730
- effectiveUrl: ""
731
- }
732
- },
733
- workspaces: [{ id: "3", slug: "chiaramobily", name: "Chiara Workspace" }],
734
- requestedWorkspace: {
735
- slug: "tonymobily",
736
- status: "forbidden"
737
- },
738
- permissions: []
739
- };
740
- }
659
+ })
741
660
  });
742
661
 
743
662
  await runtime.initialize();
663
+ await runtime.applyBootstrapPayload({
664
+ request: createBootstrapRequest("/w/tonymobily", "tonymobily"),
665
+ payload: {
666
+ session: {
667
+ authenticated: true,
668
+ userId: "4"
669
+ },
670
+ profile: {
671
+ displayName: "Chiara",
672
+ email: "chiara@example.com",
673
+ avatar: {
674
+ effectiveUrl: ""
675
+ }
676
+ },
677
+ workspaces: [{ id: "3", slug: "chiaramobily", name: "Chiara Workspace" }],
678
+ requestedWorkspace: {
679
+ slug: "tonymobily",
680
+ status: "forbidden"
681
+ },
682
+ permissions: []
683
+ }
684
+ });
744
685
 
745
686
  const context = placementRuntime.getContext();
746
687
  assert.equal(runtime.getWorkspaceBootstrapStatus("tonymobily"), WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN);
@@ -757,32 +698,34 @@ test("bootstrap placement runtime uses requestedWorkspace=not_found without forc
757
698
  const runtime = createBootstrapPlacementRuntime({
758
699
  app: createAppStub({
759
700
  ["runtime.web-placement.client"]: placementRuntime,
701
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
760
702
  ["jskit.client.router"]: router
761
- }),
762
- fetchBootstrap: async () => {
763
- return {
764
- session: {
765
- authenticated: true,
766
- userId: "1"
767
- },
768
- profile: {
769
- displayName: "User",
770
- email: "user@example.com",
771
- avatar: {
772
- effectiveUrl: ""
773
- }
774
- },
775
- workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
776
- requestedWorkspace: {
777
- slug: "missing",
778
- status: "not_found"
779
- },
780
- permissions: []
781
- };
782
- }
703
+ })
783
704
  });
784
705
 
785
706
  await runtime.initialize();
707
+ await runtime.applyBootstrapPayload({
708
+ request: createBootstrapRequest("/w/missing", "missing"),
709
+ payload: {
710
+ session: {
711
+ authenticated: true,
712
+ userId: "1"
713
+ },
714
+ profile: {
715
+ displayName: "User",
716
+ email: "user@example.com",
717
+ avatar: {
718
+ effectiveUrl: ""
719
+ }
720
+ },
721
+ workspaces: [{ id: "1", slug: "acme", name: "Acme Workspace" }],
722
+ requestedWorkspace: {
723
+ slug: "missing",
724
+ status: "not_found"
725
+ },
726
+ permissions: []
727
+ }
728
+ });
786
729
 
787
730
  const context = placementRuntime.getContext();
788
731
  assert.equal(runtime.getWorkspaceBootstrapStatus("missing"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
@@ -805,21 +748,24 @@ test("bootstrap placement runtime guard wrapper preserves delegated deny outcome
805
748
  const runtime = createBootstrapPlacementRuntime({
806
749
  app: createAppStub({
807
750
  ["runtime.web-placement.client"]: placementRuntime,
751
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
808
752
  ["jskit.client.router"]: router
809
- }),
810
- fetchBootstrap: async () => {
811
- return {
812
- session: {
813
- authenticated: true,
814
- userId: "1"
815
- },
816
- workspaces: [{ id: "1", slug: "acme", name: "Acme" }],
817
- permissions: []
818
- };
819
- }
753
+ })
820
754
  });
821
755
 
822
756
  await runtime.initialize();
757
+ await runtime.applyBootstrapPayload({
758
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
759
+ payload: {
760
+ session: {
761
+ authenticated: true,
762
+ userId: "1"
763
+ },
764
+ workspaces: [{ id: "1", slug: "acme", name: "Acme" }],
765
+ permissions: []
766
+ }
767
+ });
768
+
823
769
  const evaluator = globalThis[SHELL_GUARD_EVALUATOR_KEY];
824
770
  const outcome = evaluator({
825
771
  guard: {
@@ -840,30 +786,25 @@ test("bootstrap placement runtime guard wrapper preserves delegated deny outcome
840
786
  assert.deepEqual(outcome, delegatedOutcome);
841
787
  });
842
788
 
843
- test("bootstrap placement runtime guard wrapper blocks forbidden workspace routes", async () => {
789
+ test("bootstrap placement runtime guard wrapper blocks forbidden workspace routes and redirects nested workspace paths", async () => {
844
790
  const placementRuntime = createPlacementRuntimeStub();
845
- const router = createRouterStub("/w/acme/dashboard");
846
- globalThis[SHELL_GUARD_EVALUATOR_KEY] = () => ({ allow: true, redirectTo: "", reason: "" });
847
-
791
+ const router = createRouterStub("/w/acme/admin/workspace/settings?tab=general");
848
792
  const runtime = createBootstrapPlacementRuntime({
849
793
  app: createAppStub({
850
794
  ["runtime.web-placement.client"]: placementRuntime,
795
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
851
796
  ["jskit.client.router"]: router
852
- }),
853
- fetchBootstrap: async () => {
854
- return {
855
- session: {
856
- authenticated: true,
857
- userId: "1"
858
- },
859
- workspaces: [],
860
- permissions: []
861
- };
862
- }
797
+ })
863
798
  });
864
799
 
865
800
  await runtime.initialize();
801
+ await runtime.handleBootstrapError({
802
+ request: createBootstrapRequest("/w/acme/admin/workspace/settings?tab=general", "acme"),
803
+ error: createErrorWithStatus(403, "Forbidden")
804
+ });
805
+
866
806
  assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN);
807
+ assert.deepEqual(router.replaceCalls, ["/w/acme/admin"]);
867
808
 
868
809
  const evaluator = globalThis[SHELL_GUARD_EVALUATOR_KEY];
869
810
  const outcome = evaluator({
@@ -889,21 +830,23 @@ test("bootstrap placement runtime guard wrapper blocks forbidden workspace route
889
830
 
890
831
  test("bootstrap placement runtime guard wrapper redirects nested not_found routes to workspace surface root", async () => {
891
832
  const placementRuntime = createPlacementRuntimeStub();
892
- const router = createRouterStub("/w/acme/dashboard");
833
+ const router = createRouterStub("/w/acme/projects");
893
834
  const runtime = createBootstrapPlacementRuntime({
894
835
  app: createAppStub({
895
836
  ["runtime.web-placement.client"]: placementRuntime,
837
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
896
838
  ["jskit.client.router"]: router
897
- }),
898
- fetchBootstrap: async () => {
899
- const error = new Error("Not found");
900
- error.status = 404;
901
- throw error;
902
- }
839
+ })
903
840
  });
904
841
 
905
842
  await runtime.initialize();
843
+ await runtime.handleBootstrapError({
844
+ request: createBootstrapRequest("/w/acme/projects", "acme"),
845
+ error: createErrorWithStatus(404, "Not found")
846
+ });
847
+
906
848
  assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
849
+ assert.deepEqual(router.replaceCalls, ["/w/acme"]);
907
850
 
908
851
  const evaluator = globalThis[SHELL_GUARD_EVALUATOR_KEY];
909
852
  const nestedOutcome = evaluator({
@@ -951,46 +894,30 @@ test("bootstrap placement runtime redirects admin nested route to admin root whe
951
894
  const runtime = createBootstrapPlacementRuntime({
952
895
  app: createAppStub({
953
896
  ["runtime.web-placement.client"]: placementRuntime,
897
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
954
898
  ["jskit.client.router"]: router
955
- }),
956
- fetchBootstrap: async () => {
957
- const error = new Error("Not found");
958
- error.status = 404;
959
- throw error;
960
- }
899
+ })
961
900
  });
962
901
 
963
902
  await runtime.initialize();
964
- assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
965
- assert.deepEqual(router.replaceCalls, ["/w/acme/admin"]);
966
- });
967
-
968
- test("bootstrap placement runtime redirects forbidden workspace route to workspace surface root", async () => {
969
- const placementRuntime = createPlacementRuntimeStub();
970
- const router = createRouterStub("/w/acme/admin/workspace/settings?tab=general");
971
- const runtime = createBootstrapPlacementRuntime({
972
- app: createAppStub({
973
- ["runtime.web-placement.client"]: placementRuntime,
974
- ["jskit.client.router"]: router
975
- }),
976
- fetchBootstrap: async () => {
977
- const error = new Error("Forbidden");
978
- error.status = 403;
979
- throw error;
980
- }
903
+ await runtime.handleBootstrapError({
904
+ request: createBootstrapRequest("/w/acme/admin/workspace/settings", "acme"),
905
+ error: createErrorWithStatus(404, "Not found")
981
906
  });
982
907
 
983
- await runtime.initialize();
984
- assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN);
908
+ assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND);
985
909
  assert.deepEqual(router.replaceCalls, ["/w/acme/admin"]);
986
910
  });
987
911
 
988
- test("bootstrap placement runtime enforces surface access policies after bootstrap refresh", async () => {
912
+ test("bootstrap placement runtime enforces surface access policies after bootstrap payloads", async () => {
989
913
  const placementRuntime = createPlacementRuntimeStub();
990
914
  placementRuntime.setContext({
991
915
  auth: {
992
916
  authenticated: true
993
917
  },
918
+ surfaceAccess: {
919
+ opsowner: false
920
+ },
994
921
  surfaceAccessPolicies: {
995
922
  public: {},
996
923
  ops_owner: {
@@ -1026,70 +953,43 @@ test("bootstrap placement runtime enforces surface access policies after bootstr
1026
953
  const runtime = createBootstrapPlacementRuntime({
1027
954
  app: createAppStub({
1028
955
  ["runtime.web-placement.client"]: placementRuntime,
956
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
1029
957
  ["jskit.client.router"]: router
1030
- }),
1031
- fetchBootstrap: async () => {
1032
- return {
1033
- session: {
1034
- authenticated: true,
1035
- userId: "1"
1036
- },
1037
- workspaces: [],
1038
- permissions: [],
1039
- surfaceAccess: {
1040
- opsowner: false
1041
- }
1042
- };
1043
- }
958
+ })
1044
959
  });
1045
960
 
1046
961
  await runtime.initialize();
962
+ await runtime.applyBootstrapPayload({
963
+ request: createBootstrapRequest("/ops"),
964
+ payload: {
965
+ session: {
966
+ authenticated: true,
967
+ userId: "1"
968
+ },
969
+ workspaces: [],
970
+ permissions: []
971
+ }
972
+ });
973
+
1047
974
  assert.deepEqual(router.replaceCalls, ["/home"]);
1048
975
  });
1049
976
 
1050
- test("bootstrap placement runtime captures guard evaluator assignments after initialization", async () => {
977
+ test("bootstrap placement runtime handles unauthenticated errors and marks workspace status", async () => {
1051
978
  const placementRuntime = createPlacementRuntimeStub();
1052
- const router = createRouterStub("/w/acme/dashboard");
1053
979
  const runtime = createBootstrapPlacementRuntime({
1054
980
  app: createAppStub({
1055
981
  ["runtime.web-placement.client"]: placementRuntime,
1056
- ["jskit.client.router"]: router
1057
- }),
1058
- fetchBootstrap: async () => {
1059
- return {
1060
- session: {
1061
- authenticated: true,
1062
- userId: "1"
1063
- },
1064
- workspaces: [{ id: "1", slug: "acme", name: "Acme" }],
1065
- permissions: []
1066
- };
1067
- }
982
+ ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
983
+ ["jskit.client.router"]: createRouterStub("/w/acme/dashboard")
984
+ })
1068
985
  });
1069
986
 
1070
987
  await runtime.initialize();
1071
- const delegatedOutcome = {
1072
- allow: false,
1073
- redirectTo: "/auth/login",
1074
- reason: "auth-required"
1075
- };
1076
- globalThis[SHELL_GUARD_EVALUATOR_KEY] = () => delegatedOutcome;
1077
-
1078
- const evaluator = globalThis[SHELL_GUARD_EVALUATOR_KEY];
1079
- const outcome = evaluator({
1080
- guard: {
1081
- policy: "authenticated"
1082
- },
1083
- context: {
1084
- to: {
1085
- path: "/w/acme/dashboard",
1086
- fullPath: "/w/acme/dashboard"
1087
- },
1088
- location: {
1089
- pathname: "/w/acme/dashboard",
1090
- search: ""
1091
- }
1092
- }
988
+ await runtime.handleBootstrapError({
989
+ request: createBootstrapRequest("/w/acme/dashboard", "acme"),
990
+ error: createErrorWithStatus(401, "Unauthenticated")
1093
991
  });
1094
- assert.deepEqual(outcome, delegatedOutcome);
992
+
993
+ assert.equal(runtime.getWorkspaceBootstrapStatus("acme"), WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED);
994
+ assert.equal(placementRuntime.getContext().workspace, null);
1095
995
  });