@axiom-lattice/gateway 4.1.5 → 4.1.7

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.
package/dist/index.mjs CHANGED
@@ -32,7 +32,7 @@ import staticPlugin from "@fastify/static";
32
32
  import path3 from "path";
33
33
 
34
34
  // src/routes/index.ts
35
- import { randomUUID as randomUUID18 } from "crypto";
35
+ import { randomUUID as randomUUID19 } from "crypto";
36
36
  import {
37
37
  descriptorDataValue as descriptorDataValue3,
38
38
  snapshotExactRecord as snapshotExactRecord13,
@@ -5221,6 +5221,113 @@ import { getStoreLattice as getStoreLattice6 } from "@axiom-lattice/core";
5221
5221
  import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
5222
5222
  import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
5223
5223
  import { v4 as uuidv42 } from "uuid";
5224
+
5225
+ // src/services/collaboration/RoomCoordinatorProvisioner.ts
5226
+ var ROOM_COORDINATOR_KEY = "room-coordinator";
5227
+ var RoomCoordinatorProvisioner = class {
5228
+ /** Creates a provisioner over the tenant-scoped assistant store operations. */
5229
+ constructor(deps) {
5230
+ this.deps = deps;
5231
+ }
5232
+ /**
5233
+ * Ensures the Room Coordinator assistant exists, reusing an existing record
5234
+ * and reconciling concurrent create races by rereading the winner.
5235
+ *
5236
+ * @param tenantId Tenant to provision the coordinator for.
5237
+ * @returns The persisted Room Coordinator assistant.
5238
+ */
5239
+ async ensure(tenantId) {
5240
+ const existing = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
5241
+ if (existing) return existing;
5242
+ try {
5243
+ return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, {
5244
+ name: "Room Coordinator",
5245
+ description: "Coordinates project room conversations and organizes room work.",
5246
+ graphDefinition: {}
5247
+ });
5248
+ } catch (error) {
5249
+ const winner = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
5250
+ if (winner) return winner;
5251
+ throw error;
5252
+ }
5253
+ }
5254
+ };
5255
+
5256
+ // src/services/collaboration/WorkspacePublicRoomService.ts
5257
+ import { createHash } from "crypto";
5258
+ import { randomUUID as randomUUID5 } from "crypto";
5259
+ var WORKSPACE_PUBLIC_ROOM_PROJECT_PREFIX = "workspace_public_";
5260
+ var PUBLIC_ROOM_PROJECT_NAME = "General";
5261
+ var PUBLIC_ROOM_PROJECT_DESCRIPTION = "Public room for everyone in the workspace";
5262
+ function workspacePublicRoomProjectId(tenantId, workspaceId) {
5263
+ const hash = createHash("sha256").update([tenantId, workspaceId].join(":")).digest("hex");
5264
+ return `${WORKSPACE_PUBLIC_ROOM_PROJECT_PREFIX}${hash}`;
5265
+ }
5266
+ var WorkspacePublicRoomService = class {
5267
+ /** Creates the service from explicitly injected stores and provisioners. */
5268
+ constructor(deps) {
5269
+ this.deps = deps;
5270
+ this.idFactory = deps.idFactory ?? randomUUID5;
5271
+ }
5272
+ /**
5273
+ * Returns the workspace's public room project and room, creating them on first call.
5274
+ *
5275
+ * @param tenantId Tenant owning the workspace.
5276
+ * @param workspaceId Workspace hosting the public room.
5277
+ * @returns The ensured project and room.
5278
+ */
5279
+ async ensure(tenantId, workspaceId) {
5280
+ const projectId = workspacePublicRoomProjectId(tenantId, workspaceId);
5281
+ const project = await this.ensureProject(tenantId, workspaceId, projectId);
5282
+ const room = await this.deps.ensureMainRoom({
5283
+ id: this.idFactory(),
5284
+ tenantId,
5285
+ workspaceId,
5286
+ projectId,
5287
+ name: project.name
5288
+ });
5289
+ await this.ensureCoordinatorBot(tenantId, workspaceId, projectId, room.id);
5290
+ return { project, room };
5291
+ }
5292
+ async ensureProject(tenantId, workspaceId, projectId) {
5293
+ const existing = await this.deps.getProjectById(tenantId, projectId);
5294
+ if (existing && existing.kind === "public") return existing;
5295
+ try {
5296
+ return await this.deps.createProject(tenantId, workspaceId, projectId, {
5297
+ name: PUBLIC_ROOM_PROJECT_NAME,
5298
+ description: PUBLIC_ROOM_PROJECT_DESCRIPTION,
5299
+ kind: "public"
5300
+ });
5301
+ } catch (error) {
5302
+ const winner = await this.deps.getProjectById(tenantId, projectId);
5303
+ if (winner && winner.kind === "public") return winner;
5304
+ throw Object.assign(new Error("Workspace public room project could not be ensured"), { cause: error });
5305
+ }
5306
+ }
5307
+ async ensureCoordinatorBot(tenantId, workspaceId, projectId, roomId) {
5308
+ const coordinator = await this.deps.ensureCoordinator(tenantId);
5309
+ const existing = await this.deps.findBotByAssistant(tenantId, projectId, coordinator.id);
5310
+ if (existing && existing.status !== "removed") return;
5311
+ const saved = await this.deps.saveBot({
5312
+ id: this.idFactory(),
5313
+ tenantId,
5314
+ workspaceId,
5315
+ projectId,
5316
+ roomId,
5317
+ assistantId: coordinator.id,
5318
+ role: "coordinator",
5319
+ title: coordinator.name,
5320
+ mentionName: ROOM_COORDINATOR_KEY,
5321
+ status: "active",
5322
+ roomThreadId: this.idFactory()
5323
+ });
5324
+ if (saved.kind === "coordinator_conflict" || saved.kind === "mention_conflict") {
5325
+ throw new Error(`Workspace public room coordinator bot could not be ensured: ${saved.kind}`);
5326
+ }
5327
+ }
5328
+ };
5329
+
5330
+ // src/controllers/workspace.ts
5224
5331
  var MAX_ZIP_ENTRIES = 1e4;
5225
5332
  var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
5226
5333
  var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
@@ -5355,6 +5462,21 @@ var WorkspaceController = class {
5355
5462
  this.projectCollaboration = projectCollaboration;
5356
5463
  this.workspaceStore = getStoreLattice6("default", "workspace").store;
5357
5464
  this.projectStore = getStoreLattice6("default", "project").store;
5465
+ const roomStore = getStoreLattice6("default", "projectRoom").store;
5466
+ const botMembershipStore = getStoreLattice6("default", "projectBotMembership").store;
5467
+ const assistantStore = getStoreLattice6("default", "assistant").store;
5468
+ const coordinatorProvisioner = new RoomCoordinatorProvisioner({
5469
+ getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
5470
+ createAssistant: assistantStore.createAssistant.bind(assistantStore)
5471
+ });
5472
+ this.publicRoom = new WorkspacePublicRoomService({
5473
+ getProjectById: this.projectStore.getProjectById.bind(this.projectStore),
5474
+ createProject: this.projectStore.createProject.bind(this.projectStore),
5475
+ ensureMainRoom: roomStore.ensureMainRoom.bind(roomStore),
5476
+ findBotByAssistant: botMembershipStore.findByAssistant.bind(botMembershipStore),
5477
+ saveBot: botMembershipStore.save.bind(botMembershipStore),
5478
+ ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
5479
+ });
5358
5480
  }
5359
5481
  getTenantId(request) {
5360
5482
  const userTenantId = request.user?.tenantId;
@@ -5428,6 +5550,7 @@ var WorkspaceController = class {
5428
5550
  const { workspaceId } = request.params;
5429
5551
  const kindFilter = request.query.kind;
5430
5552
  await this.ensureTrainingProject(tenantId, workspaceId);
5553
+ await this.publicRoom.ensure(tenantId, workspaceId);
5431
5554
  const projects = kindFilter ? await this.projectStore.getProjectsByWorkspace(tenantId, workspaceId, {
5432
5555
  kind: kindFilter
5433
5556
  }) : await this.projectStore.getProjectsByWorkspace(tenantId, workspaceId);
@@ -5471,6 +5594,12 @@ var WorkspaceController = class {
5471
5594
  message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
5472
5595
  });
5473
5596
  }
5597
+ if (data.kind === "public") {
5598
+ return reply.status(409).send({
5599
+ success: false,
5600
+ error: "The public room project is managed automatically"
5601
+ });
5602
+ }
5474
5603
  if (user?.id && this.projectCollaboration) {
5475
5604
  try {
5476
5605
  const authorized = await this.projectCollaboration.authorizeCreator({ tenantId, userId: user.id });
@@ -5546,6 +5675,21 @@ var WorkspaceController = class {
5546
5675
  message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
5547
5676
  });
5548
5677
  }
5678
+ if (updates.kind !== void 0) {
5679
+ const existingProject = await this.projectStore.getProjectById(tenantId, projectId);
5680
+ if (existingProject && updates.kind === "public") {
5681
+ return reply.status(409).send({
5682
+ success: false,
5683
+ error: "kind public is reserved for the workspace public room"
5684
+ });
5685
+ }
5686
+ if (existingProject?.kind === "public") {
5687
+ return reply.status(409).send({
5688
+ success: false,
5689
+ error: "The public room project classification cannot be changed"
5690
+ });
5691
+ }
5692
+ }
5549
5693
  const project = await this.projectStore.updateProject(
5550
5694
  tenantId,
5551
5695
  projectId,
@@ -5576,6 +5720,12 @@ var WorkspaceController = class {
5576
5720
  });
5577
5721
  }
5578
5722
  }
5723
+ if (project.kind === "public") {
5724
+ return reply.status(409).send({
5725
+ success: false,
5726
+ error: "The public room project cannot be deleted"
5727
+ });
5728
+ }
5579
5729
  const deleted = await this.projectStore.deleteProject(tenantId, projectId);
5580
5730
  if (!deleted) {
5581
5731
  return reply.status(404).send({ success: false, error: "Project not found" });
@@ -6274,14 +6424,202 @@ function registerWorkspaceRoutes(app2, projectCollaboration) {
6274
6424
  "/api/filesystem/browse",
6275
6425
  controller.browseFilesystem.bind(controller)
6276
6426
  );
6427
+ return controller;
6428
+ }
6429
+
6430
+ // src/services/collaboration/ProjectAccessService.ts
6431
+ import { randomUUID as randomUUID6 } from "crypto";
6432
+ var ProjectCollaborationError = class extends Error {
6433
+ /** Creates a project collaboration error. */
6434
+ constructor(code, statusCode, message) {
6435
+ super(message);
6436
+ this.code = code;
6437
+ this.statusCode = statusCode;
6438
+ this.name = "ProjectCollaborationError";
6439
+ }
6440
+ };
6441
+ var allowedRoles = {
6442
+ read: /* @__PURE__ */ new Set(["owner", "admin", "member", "viewer"]),
6443
+ send: /* @__PURE__ */ new Set(["owner", "admin", "member"]),
6444
+ manage: /* @__PURE__ */ new Set(["owner", "admin"])
6445
+ };
6446
+ var membershipConflictNames = /* @__PURE__ */ new Set([
6447
+ "DuplicateProjectMembershipError",
6448
+ "ProjectMembershipIdConflictError"
6449
+ ]);
6450
+ function isProjectMembershipConflict(error) {
6451
+ return error instanceof Error && membershipConflictNames.has(error.name);
6277
6452
  }
6453
+ var ProjectAccessService = class {
6454
+ /** Creates a project access service from explicitly injected stores. */
6455
+ constructor(deps) {
6456
+ this.deps = deps;
6457
+ this.idFactory = deps.idFactory ?? randomUUID6;
6458
+ }
6459
+ /** Returns the project and active membership when the user has the requested capability. */
6460
+ async requireProjectAccess(input) {
6461
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
6462
+ if (!project) {
6463
+ throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
6464
+ }
6465
+ if (project.kind === "public") {
6466
+ return this.requirePublicProjectAccess(project, input);
6467
+ }
6468
+ const membership = await this.deps.membershipStore.findByUser(
6469
+ input.tenantId,
6470
+ input.projectId,
6471
+ input.userId
6472
+ );
6473
+ if (membership?.status === "active") {
6474
+ this.requireCapability(membership, input.capability);
6475
+ return { project, membership };
6476
+ }
6477
+ const memberships = await this.deps.membershipStore.list(input.tenantId, input.projectId);
6478
+ if (memberships.length !== 0) this.accessDenied();
6479
+ if (project.kind === "personal") this.accessDenied();
6480
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
6481
+ if (tenantLink?.role !== "owner" && tenantLink?.role !== "admin") this.accessDenied();
6482
+ let claim;
6483
+ try {
6484
+ claim = await this.deps.membershipStore.createInitialOwner({
6485
+ id: this.idFactory(),
6486
+ tenantId: input.tenantId,
6487
+ projectId: input.projectId,
6488
+ userId: input.userId
6489
+ });
6490
+ } catch (error) {
6491
+ if (isProjectMembershipConflict(error)) {
6492
+ throw new ProjectCollaborationError(
6493
+ "PROJECT_MEMBERSHIP_CONFLICT",
6494
+ 409,
6495
+ "Project membership could not be initialized"
6496
+ );
6497
+ }
6498
+ throw error;
6499
+ }
6500
+ if (claim.kind === "created" || claim.kind === "existing") {
6501
+ return { project, membership: claim.membership };
6502
+ }
6503
+ const racedMembership = await this.deps.membershipStore.findByUser(
6504
+ input.tenantId,
6505
+ input.projectId,
6506
+ input.userId
6507
+ );
6508
+ if (racedMembership?.status !== "active") this.accessDenied();
6509
+ this.requireCapability(racedMembership, input.capability);
6510
+ return { project, membership: racedMembership };
6511
+ }
6512
+ /** Authorizes an already-existing membership, or any tenant member for public rooms; never mutates persistence. */
6513
+ async requireExistingProjectAccess(input) {
6514
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
6515
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
6516
+ if (project.kind === "public") {
6517
+ return this.requireExistingPublicProjectAccess(project, input);
6518
+ }
6519
+ const membership = await this.deps.membershipStore.findByUser(
6520
+ input.tenantId,
6521
+ input.projectId,
6522
+ input.userId
6523
+ );
6524
+ if (!membership || membership.tenantId !== input.tenantId || membership.projectId !== input.projectId || membership.userId !== input.userId || membership.status !== "active") this.accessDenied();
6525
+ this.requireCapability(membership, input.capability);
6526
+ return { project, membership };
6527
+ }
6528
+ /** Authorizes roster visibility: read for public rooms, manage elsewhere. */
6529
+ async requireRosterAccess(input) {
6530
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
6531
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
6532
+ return this.requireProjectAccess({
6533
+ ...input,
6534
+ capability: project.kind === "public" ? "read" : "manage"
6535
+ });
6536
+ }
6537
+ async requirePublicProjectAccess(project, input) {
6538
+ const membership = await this.deps.membershipStore.findByUser(
6539
+ input.tenantId,
6540
+ input.projectId,
6541
+ input.userId
6542
+ );
6543
+ if (membership?.status === "active") {
6544
+ this.requireCapability(membership, input.capability);
6545
+ return { project, membership };
6546
+ }
6547
+ const materialized = await this.materializePublicMembership(input);
6548
+ this.requireCapability(materialized, input.capability);
6549
+ return { project, membership: materialized };
6550
+ }
6551
+ async requireExistingPublicProjectAccess(project, input) {
6552
+ const membership = await this.deps.membershipStore.findByUser(
6553
+ input.tenantId,
6554
+ input.projectId,
6555
+ input.userId
6556
+ );
6557
+ if (membership?.status === "active") {
6558
+ this.requireCapability(membership, input.capability);
6559
+ return { project, membership };
6560
+ }
6561
+ if (membership) this.accessDenied();
6562
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
6563
+ if (!tenantLink) this.accessDenied();
6564
+ const role = tenantLink.role === "owner" || tenantLink.role === "admin" ? "admin" : "member";
6565
+ const synthetic = {
6566
+ id: `public:${input.userId}`,
6567
+ tenantId: input.tenantId,
6568
+ projectId: input.projectId,
6569
+ userId: input.userId,
6570
+ role,
6571
+ status: "active",
6572
+ joinedAt: project.createdAt,
6573
+ updatedAt: project.updatedAt
6574
+ };
6575
+ this.requireCapability(synthetic, input.capability);
6576
+ return { project, membership: synthetic };
6577
+ }
6578
+ async materializePublicMembership(input) {
6579
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
6580
+ if (!tenantLink) this.accessDenied();
6581
+ const existing = await this.deps.membershipStore.findByUser(
6582
+ input.tenantId,
6583
+ input.projectId,
6584
+ input.userId
6585
+ );
6586
+ if (existing) this.accessDenied();
6587
+ const role = tenantLink.role === "owner" || tenantLink.role === "admin" ? "admin" : "member";
6588
+ try {
6589
+ return await this.deps.membershipStore.create({
6590
+ id: this.idFactory(),
6591
+ tenantId: input.tenantId,
6592
+ projectId: input.projectId,
6593
+ userId: input.userId,
6594
+ role,
6595
+ status: "active"
6596
+ });
6597
+ } catch (error) {
6598
+ if (isProjectMembershipConflict(error)) {
6599
+ const raced = await this.deps.membershipStore.findByUser(
6600
+ input.tenantId,
6601
+ input.projectId,
6602
+ input.userId
6603
+ );
6604
+ if (raced?.status === "active") return raced;
6605
+ }
6606
+ throw error;
6607
+ }
6608
+ }
6609
+ requireCapability(membership, capability2) {
6610
+ if (!allowedRoles[capability2].has(membership.role)) this.accessDenied();
6611
+ }
6612
+ accessDenied() {
6613
+ throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
6614
+ }
6615
+ };
6278
6616
 
6279
6617
  // src/controllers/database-configs.ts
6280
6618
  import {
6281
6619
  getStoreLattice as getStoreLattice7,
6282
6620
  sqlDatabaseManager
6283
6621
  } from "@axiom-lattice/core";
6284
- import { randomUUID as randomUUID5 } from "crypto";
6622
+ import { randomUUID as randomUUID7 } from "crypto";
6285
6623
  function getTenantId9(request) {
6286
6624
  const userTenantId = request.user?.tenantId;
6287
6625
  if (userTenantId) {
@@ -6359,7 +6697,7 @@ async function createDatabaseConfig(request, reply) {
6359
6697
  message: "Database configuration with this key already exists"
6360
6698
  };
6361
6699
  }
6362
- const id3 = body.id || randomUUID5();
6700
+ const id3 = body.id || randomUUID7();
6363
6701
  const config = await store.createConfig(tenantId, id3, body);
6364
6702
  try {
6365
6703
  sqlDatabaseManager.registerDatabase(tenantId, config.key, config.config);
@@ -6564,7 +6902,7 @@ import {
6564
6902
  metricsServerManager as metricsServerManager2,
6565
6903
  SemanticMetricsClient as SemanticMetricsClient2
6566
6904
  } from "@axiom-lattice/core";
6567
- import { randomUUID as randomUUID6 } from "crypto";
6905
+ import { randomUUID as randomUUID8 } from "crypto";
6568
6906
  function getTenantId10(request) {
6569
6907
  const userTenantId = request.user?.tenantId;
6570
6908
  if (userTenantId) {
@@ -6645,7 +6983,7 @@ async function createMetricsServerConfig(request, reply) {
6645
6983
  message: "selectedDataSources is required for semantic metrics servers"
6646
6984
  };
6647
6985
  }
6648
- const id3 = body.id || randomUUID6();
6986
+ const id3 = body.id || randomUUID8();
6649
6987
  const configData = {
6650
6988
  key: body.key,
6651
6989
  name: body.name,
@@ -9152,7 +9490,7 @@ function registerChannelRoutes(app2, dependencies) {
9152
9490
  }
9153
9491
 
9154
9492
  // src/controllers/channel-installations.ts
9155
- import { randomUUID as randomUUID7 } from "crypto";
9493
+ import { randomUUID as randomUUID9 } from "crypto";
9156
9494
  import {
9157
9495
  descriptorDataValue
9158
9496
  } from "@axiom-lattice/protocols";
@@ -9356,7 +9694,7 @@ async function createChannelInstallation(request, reply) {
9356
9694
  };
9357
9695
  }
9358
9696
  const store = await getInstallationStore();
9359
- const installationId = randomUUID7();
9697
+ const installationId = randomUUID9();
9360
9698
  const createInput = {
9361
9699
  channel: body.channel,
9362
9700
  name: body.name,
@@ -10316,7 +10654,7 @@ async function getLocalA2ATemplates(_request, reply) {
10316
10654
  }
10317
10655
 
10318
10656
  // src/routes/a2a-test-connection.ts
10319
- import { randomUUID as randomUUID8 } from "crypto";
10657
+ import { randomUUID as randomUUID10 } from "crypto";
10320
10658
  function describeTestConnectionError(error, agentCardUrl) {
10321
10659
  if (!(error instanceof Error)) {
10322
10660
  return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
@@ -10356,7 +10694,7 @@ async function ensureLocalRuntimeForConnectionTest(localRuntime, agentCardUrl) {
10356
10694
  return { error: portValidation.error };
10357
10695
  }
10358
10696
  const state = await localA2AService.startProvider(
10359
- `test-${randomUUID8()}`,
10697
+ `test-${randomUUID10()}`,
10360
10698
  testRuntime
10361
10699
  );
10362
10700
  if (state.status === "running") return { runtimeId: state.runtimeId };
@@ -10445,106 +10783,6 @@ import {
10445
10783
  import { snapshotExactRecord as snapshotExactRecord3 } from "@axiom-lattice/protocols";
10446
10784
  import { ProjectTaskOwnerPolicyError, projectTaskAdmissionNotifier } from "@axiom-lattice/core";
10447
10785
 
10448
- // src/services/collaboration/ProjectAccessService.ts
10449
- import { randomUUID as randomUUID9 } from "crypto";
10450
- var ProjectCollaborationError = class extends Error {
10451
- /** Creates a project collaboration error. */
10452
- constructor(code, statusCode, message) {
10453
- super(message);
10454
- this.code = code;
10455
- this.statusCode = statusCode;
10456
- this.name = "ProjectCollaborationError";
10457
- }
10458
- };
10459
- var allowedRoles = {
10460
- read: /* @__PURE__ */ new Set(["owner", "admin", "member", "viewer"]),
10461
- send: /* @__PURE__ */ new Set(["owner", "admin", "member"]),
10462
- manage: /* @__PURE__ */ new Set(["owner", "admin"])
10463
- };
10464
- var membershipConflictNames = /* @__PURE__ */ new Set([
10465
- "DuplicateProjectMembershipError",
10466
- "ProjectMembershipIdConflictError"
10467
- ]);
10468
- function isProjectMembershipConflict(error) {
10469
- return error instanceof Error && membershipConflictNames.has(error.name);
10470
- }
10471
- var ProjectAccessService = class {
10472
- /** Creates a project access service from explicitly injected stores. */
10473
- constructor(deps) {
10474
- this.deps = deps;
10475
- this.idFactory = deps.idFactory ?? randomUUID9;
10476
- }
10477
- /** Returns the project and active membership when the user has the requested capability. */
10478
- async requireProjectAccess(input) {
10479
- const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
10480
- if (!project) {
10481
- throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
10482
- }
10483
- const membership = await this.deps.membershipStore.findByUser(
10484
- input.tenantId,
10485
- input.projectId,
10486
- input.userId
10487
- );
10488
- if (membership?.status === "active") {
10489
- this.requireCapability(membership, input.capability);
10490
- return { project, membership };
10491
- }
10492
- const memberships = await this.deps.membershipStore.list(input.tenantId, input.projectId);
10493
- if (memberships.length !== 0) this.accessDenied();
10494
- if (project.kind === "personal") this.accessDenied();
10495
- const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
10496
- if (tenantLink?.role !== "owner" && tenantLink?.role !== "admin") this.accessDenied();
10497
- let claim;
10498
- try {
10499
- claim = await this.deps.membershipStore.createInitialOwner({
10500
- id: this.idFactory(),
10501
- tenantId: input.tenantId,
10502
- projectId: input.projectId,
10503
- userId: input.userId
10504
- });
10505
- } catch (error) {
10506
- if (isProjectMembershipConflict(error)) {
10507
- throw new ProjectCollaborationError(
10508
- "PROJECT_MEMBERSHIP_CONFLICT",
10509
- 409,
10510
- "Project membership could not be initialized"
10511
- );
10512
- }
10513
- throw error;
10514
- }
10515
- if (claim.kind === "created" || claim.kind === "existing") {
10516
- return { project, membership: claim.membership };
10517
- }
10518
- const racedMembership = await this.deps.membershipStore.findByUser(
10519
- input.tenantId,
10520
- input.projectId,
10521
- input.userId
10522
- );
10523
- if (racedMembership?.status !== "active") this.accessDenied();
10524
- this.requireCapability(racedMembership, input.capability);
10525
- return { project, membership: racedMembership };
10526
- }
10527
- /** Authorizes only an already-existing active membership and never mutates persistence. */
10528
- async requireExistingProjectAccess(input) {
10529
- const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
10530
- if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
10531
- const membership = await this.deps.membershipStore.findByUser(
10532
- input.tenantId,
10533
- input.projectId,
10534
- input.userId
10535
- );
10536
- if (!membership || membership.tenantId !== input.tenantId || membership.projectId !== input.projectId || membership.userId !== input.userId || membership.status !== "active") this.accessDenied();
10537
- this.requireCapability(membership, input.capability);
10538
- return { project, membership };
10539
- }
10540
- requireCapability(membership, capability2) {
10541
- if (!allowedRoles[capability2].has(membership.role)) this.accessDenied();
10542
- }
10543
- accessDenied() {
10544
- throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
10545
- }
10546
- };
10547
-
10548
10786
  // src/services/collaboration/ProjectTaskThreadService.ts
10549
10787
  import { isProjectLifecycleTask } from "@axiom-lattice/core";
10550
10788
  import {
@@ -11083,9 +11321,6 @@ function governedUpdate(task, body) {
11083
11321
  if (has("ownerType") && body.ownerType !== task.ownerType || has("ownerId") && body.ownerId !== task.ownerId) {
11084
11322
  return "TASK_OWNERSHIP_IMMUTABLE";
11085
11323
  }
11086
- if (task.ownerType !== "agent") return void 0;
11087
- if (has("parentId") && body.parentId !== task.parentId || has("dependencies") || has("requireReview") || has("result") || has("failureReason") || has("status")) return "TASK_LIFECYCLE_TRANSITION_REQUIRED";
11088
- if (has("context") || has("description")) return "COMPLETION_CONTEXT_MUTATION";
11089
11324
  return void 0;
11090
11325
  }
11091
11326
  function lifecycleStatus(code) {
@@ -11526,7 +11761,8 @@ var AgentWebAppService = class {
11526
11761
  integration: current.integration,
11527
11762
  scope: { ...current.scope, ...patch.scope },
11528
11763
  features: { ...current.features, ...patch.features },
11529
- appearance: { ...current.appearance, ...patch.appearance }
11764
+ appearance: { ...current.appearance, ...patch.appearance },
11765
+ identity: patch.identity ?? current.identity
11530
11766
  };
11531
11767
  await this.validateAssistant(context, current.assistantId);
11532
11768
  const validated = await this.validateConfig(context.tenantId, effective);
@@ -11535,7 +11771,8 @@ var AgentWebAppService = class {
11535
11771
  ...Object.prototype.hasOwnProperty.call(patch, "description") ? { description: validated.description } : {},
11536
11772
  ...patch.scope ? { scope: validated.scope } : {},
11537
11773
  ...patch.features ? { features: validated.features } : {},
11538
- ...patch.appearance ? { appearance: validated.appearance } : {}
11774
+ ...patch.appearance ? { appearance: validated.appearance } : {},
11775
+ ...patch.identity !== void 0 ? { identity: patch.identity } : {}
11539
11776
  };
11540
11777
  return this.writeSnapshot(context, current, update);
11541
11778
  }
@@ -11648,13 +11885,28 @@ var agentWebAppFeaturesSchema = z4.object({
11648
11885
  threadManagement: z4.boolean(),
11649
11886
  attachments: z4.boolean(),
11650
11887
  hitl: z4.boolean(),
11651
- genUI: z4.boolean()
11888
+ genUI: z4.boolean(),
11889
+ files: z4.boolean().optional()
11652
11890
  }).strict();
11653
11891
  var agentWebAppAppearanceSchema = z4.object({
11654
11892
  title: z4.string().max(120).optional(),
11655
11893
  welcomeMessage: z4.string().max(1e3).optional(),
11656
11894
  primaryColor: z4.string().regex(/^#[0-9A-Fa-f]{6}$/).optional()
11657
11895
  }).strict();
11896
+ var agentWebAppIssuerSchema = z4.object({
11897
+ iss: z4.string().min(1).max(255),
11898
+ aud: z4.string().min(1).max(255),
11899
+ alg: z4.enum(["HS256", "RS256"]),
11900
+ secret: z4.string().min(1).max(512).optional(),
11901
+ jwksUrl: z4.string().url().max(2048).optional()
11902
+ }).strict().refine(
11903
+ (value) => value.alg === "HS256" ? value.secret !== void 0 : value.jwksUrl !== void 0,
11904
+ { message: "HS256 requires secret; RS256 requires jwksUrl" }
11905
+ );
11906
+ var agentWebAppIdentitySchema = z4.object({
11907
+ policy: z4.enum(["unverified", "verified", "both"]),
11908
+ issuers: z4.array(agentWebAppIssuerSchema).min(1).max(10)
11909
+ }).strict();
11658
11910
  var createAgentWebAppSchema = z4.object({
11659
11911
  assistantId: externalId,
11660
11912
  name: z4.string().min(1).max(120),
@@ -11662,14 +11914,16 @@ var createAgentWebAppSchema = z4.object({
11662
11914
  integration: z4.object({ type: z4.literal("react_sdk") }).strict(),
11663
11915
  scope: agentWebAppScopeSchema,
11664
11916
  features: agentWebAppFeaturesSchema,
11665
- appearance: agentWebAppAppearanceSchema
11917
+ appearance: agentWebAppAppearanceSchema,
11918
+ identity: agentWebAppIdentitySchema.optional()
11666
11919
  }).strict();
11667
11920
  var updateAgentWebAppSchema = z4.object({
11668
11921
  name: z4.string().min(1).max(120).optional(),
11669
11922
  description: z4.string().max(1e3).optional(),
11670
11923
  scope: agentWebAppScopeSchema.partial().strict().optional(),
11671
11924
  features: agentWebAppFeaturesSchema.partial().strict().optional(),
11672
- appearance: agentWebAppAppearanceSchema.partial().strict().optional()
11925
+ appearance: agentWebAppAppearanceSchema.partial().strict().optional(),
11926
+ identity: agentWebAppIdentitySchema.optional()
11673
11927
  }).strict().refine((value) => Object.keys(value).length > 0, "Patch must not be empty");
11674
11928
  var listAgentWebAppsQuerySchema = z4.object({
11675
11929
  assistantId: externalId.optional()
@@ -11756,10 +12010,11 @@ function invalidRequest(reply, statusCode) {
11756
12010
  }
11757
12011
 
11758
12012
  // src/routes/agent-web-app-runtime.ts
12013
+ import { createHmac as createHmac3 } from "crypto";
11759
12014
  import { z as z5, ZodError as ZodError2 } from "zod";
11760
12015
 
11761
12016
  // src/services/agent-web-app/AgentWebAppRuntimeService.ts
11762
- import { createHash, randomUUID as randomUUID10 } from "crypto";
12017
+ import { createHash as createHash2, randomUUID as randomUUID11 } from "crypto";
11763
12018
  import { MessageChunkTypes as MessageChunkTypes3, parseAgentWebAppGenUIBlock } from "@axiom-lattice/protocols";
11764
12019
  function isRecord2(value) {
11765
12020
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -11776,7 +12031,16 @@ var AgentWebAppRuntimeService = class {
11776
12031
  constructor(deps) {
11777
12032
  this.deps = deps;
11778
12033
  }
11779
- async bootstrap(webAppId, userId, selection) {
12034
+ /** Identity policy configuration for the web app, or null when the app is missing/unconfigured. */
12035
+ async getIdentityConfig(webAppId) {
12036
+ try {
12037
+ const runtime = await this.resolveRuntime(webAppId, {});
12038
+ return runtime.webApp.identity ?? null;
12039
+ } catch {
12040
+ return null;
12041
+ }
12042
+ }
12043
+ async bootstrap(webAppId, userId, selection, identity) {
11780
12044
  const runtime = await this.resolveRuntime(webAppId, selection);
11781
12045
  const assistant = await this.deps.assistantResolver.resolve(
11782
12046
  runtime.webApp.tenantId,
@@ -11809,7 +12073,7 @@ var AgentWebAppRuntimeService = class {
11809
12073
  defaultModelKey: runtime.webApp.scope.defaultModelKey,
11810
12074
  features: runtime.webApp.features,
11811
12075
  appearance: runtime.webApp.appearance,
11812
- identityAssurance: "unverified"
12076
+ identityAssurance: identity?.assurance ?? "unverified"
11813
12077
  },
11814
12078
  projects,
11815
12079
  models
@@ -11830,6 +12094,14 @@ var AgentWebAppRuntimeService = class {
11830
12094
  this.requireFeature(runtime.webApp.features.threadManagement);
11831
12095
  return this.createOwnedThread(runtime, userId, input.label);
11832
12096
  }
12097
+ /** Fetch one owned thread. */
12098
+ async getThread(webAppId, userId, threadId, selection) {
12099
+ const runtime = await this.resolveRuntime(webAppId, selection);
12100
+ await this.requireOwnedThread(runtime, userId, threadId);
12101
+ const thread = await this.deps.threadStore.getThreadById(runtime.webApp.tenantId, threadId);
12102
+ if (!thread) this.threadNotFound();
12103
+ return toRuntimeThread(thread);
12104
+ }
11833
12105
  /** Update a thread label; omitted or blank labels preserve the existing label. */
11834
12106
  async updateThread(webAppId, userId, threadId, input) {
11835
12107
  const runtime = await this.resolveRuntime(webAppId, input);
@@ -11864,6 +12136,60 @@ var AgentWebAppRuntimeService = class {
11864
12136
  return [{ id: message.id, role: message.role, ...content && (typeof content === "string" || content.length > 0) ? { content } : {} }];
11865
12137
  });
11866
12138
  }
12139
+ async getState(webAppId, userId, threadId, selection) {
12140
+ const runtime = await this.resolveRuntime(webAppId, selection);
12141
+ await this.requireOwnedThread(runtime, userId, threadId);
12142
+ const agent = this.resolveAgent(runtime, threadId);
12143
+ const state = await agent.getCurrentState();
12144
+ const pendingMessages = await agent.getPendingMessages();
12145
+ const normalized = pendingMessages.map((message) => ({
12146
+ ...message,
12147
+ content: typeof message.content === "object" && message.content !== null ? "message" in message.content ? message.content.message : JSON.stringify(message.content) : String(message.content ?? "")
12148
+ }));
12149
+ return { ...state && typeof state === "object" ? state : {}, pendingMessages: normalized };
12150
+ }
12151
+ /** Raw stored messages (Console memory contract, tool/interrupt preserved). */
12152
+ async getConsoleMessages(webAppId, userId, threadId, selection) {
12153
+ const runtime = await this.resolveRuntime(webAppId, selection);
12154
+ await this.requireOwnedThread(runtime, userId, threadId);
12155
+ return this.deps.messageReader.getCurrentMessages({
12156
+ tenantId: runtime.webApp.tenantId,
12157
+ assistantId: runtime.webApp.assistantId,
12158
+ threadId,
12159
+ workspaceId: runtime.project.workspaceId,
12160
+ projectId: runtime.project.id
12161
+ });
12162
+ }
12163
+ /** Background enqueue only (ADR-026): returns the message id; delivery happens over resume_stream. */
12164
+ async enqueueMessage(webAppId, userId, threadId, content, selection) {
12165
+ const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, selection);
12166
+ const agent = this.resolveAgent(runtime, threadId);
12167
+ const execution = await this.execute(agent, runtime, { input: { message: content } });
12168
+ return { messageId: execution.messageId };
12169
+ }
12170
+ async removePendingMessage(webAppId, userId, threadId, messageId, selection) {
12171
+ const runtime = await this.resolveRuntime(webAppId, selection);
12172
+ await this.requireOwnedThread(runtime, userId, threadId);
12173
+ const agent = this.resolveAgent(runtime, threadId);
12174
+ if (!await agent.removePendingMessage(messageId)) this.threadNotFound();
12175
+ }
12176
+ /** Thread-lifetime SSE resume from the last known client message id. */
12177
+ async resumeStream(webAppId, userId, threadId, messageId, selection) {
12178
+ const runtime = await this.resolveRuntime(webAppId, selection);
12179
+ await this.requireOwnedThread(runtime, userId, threadId);
12180
+ const agent = this.resolveAgent(runtime, threadId);
12181
+ return { stream: agent.chunkStream(messageId, []) };
12182
+ }
12183
+ async resolveFileScope(webAppId, selection) {
12184
+ const runtime = await this.resolveRuntime(webAppId, selection);
12185
+ return {
12186
+ tenantId: runtime.webApp.tenantId,
12187
+ workspaceId: runtime.project.workspaceId,
12188
+ projectId: runtime.project.id,
12189
+ assistantId: runtime.webApp.assistantId,
12190
+ enabled: runtime.webApp.features.files === true
12191
+ };
12192
+ }
11867
12193
  async streamMessage(webAppId, userId, threadId, input) {
11868
12194
  const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, input);
11869
12195
  const refs = this.resolveAttachmentRefs(runtime, userId, threadId, input.attachmentRefs ?? []);
@@ -11882,10 +12208,10 @@ ${formatFileRefsSection(refs)}` : input.content;
11882
12208
  const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, selection);
11883
12209
  this.requireNamedFeature(runtime.webApp.features.attachments, "Attachments are disabled");
11884
12210
  if (!this.deps.saveProjectFile || !this.deps.attachmentRefs) this.fail("INTERNAL_ERROR", 500, "Attachment service is unavailable");
11885
- const userDigest = createHash("sha256").update(userId, "utf8").digest("hex");
11886
- const contentDigest = createHash("sha256").update(file.bytes).digest("hex");
12211
+ const userDigest = createHash2("sha256").update(userId, "utf8").digest("hex");
12212
+ const contentDigest = createHash2("sha256").update(file.bytes).digest("hex");
11887
12213
  const displayName = file.name;
11888
- const physicalName = `${randomUUID10()}-${sanitizePhysicalFilename(file.name)}`;
12214
+ const physicalName = `${randomUUID11()}-${sanitizePhysicalFilename(file.name)}`;
11889
12215
  const saved = await this.deps.saveProjectFile({
11890
12216
  tenantId: runtime.webApp.tenantId,
11891
12217
  workspaceId: runtime.project.workspaceId,
@@ -12037,7 +12363,7 @@ ${formatFileRefsSection(refs)}` : input.content;
12037
12363
  const thread = await this.deps.threadStore.createThread(
12038
12364
  runtime.webApp.tenantId,
12039
12365
  runtime.webApp.assistantId,
12040
- randomUUID10(),
12366
+ randomUUID11(),
12041
12367
  { metadata: this.metadata(runtime, userId, normalizeLabel(rawLabel)) }
12042
12368
  );
12043
12369
  return toRuntimeThread(thread);
@@ -12089,7 +12415,7 @@ function implicitThreadId(runtime, userId) {
12089
12415
  userId,
12090
12416
  runtime.project.id
12091
12417
  ]);
12092
- const bytes = createHash("sha256").update(canonicalTuple, "utf8").digest().subarray(0, 16);
12418
+ const bytes = createHash2("sha256").update(canonicalTuple, "utf8").digest().subarray(0, 16);
12093
12419
  bytes[6] = bytes[6] & 15 | 128;
12094
12420
  bytes[8] = bytes[8] & 63 | 128;
12095
12421
  const hex = bytes.toString("hex");
@@ -12108,96 +12434,102 @@ function toRuntimeThread(thread) {
12108
12434
  };
12109
12435
  }
12110
12436
 
12111
- // src/services/agent-web-app/AgentWebAppStreamProjector.ts
12112
- import {
12113
- MessageChunkTypes as MessageChunkTypes4,
12114
- parseAgentWebAppGenUIBlock as parseAgentWebAppGenUIBlock2
12115
- } from "@axiom-lattice/protocols";
12116
- function createAgentWebAppStreamProjectionContext(options = {}) {
12117
- return { startedToolIds: /* @__PURE__ */ new Set(), allowInterrupts: options.allowInterrupts ?? true, allowGenUI: options.allowGenUI ?? false };
12118
- }
12119
- function projectAgentWebAppChunk(chunk, context) {
12120
- if (chunk.type === MessageChunkTypes4.AI) {
12121
- const events = [];
12122
- const startedToolIds = context?.startedToolIds ?? /* @__PURE__ */ new Set();
12123
- if (typeof chunk.data.content === "string" && chunk.data.content) events.push({ type: "message.delta", text: chunk.data.content });
12124
- if (context?.allowGenUI && Array.isArray(chunk.data.content)) {
12125
- for (const value of chunk.data.content) {
12126
- const block = parseAgentWebAppGenUIBlock2(value);
12127
- if (block) events.push({ type: "genui.render", block });
12128
- }
12129
- }
12130
- for (const call of chunk.data.tool_call_chunks ?? []) {
12131
- if (call.id && call.name && !startedToolIds.has(call.id)) {
12132
- startedToolIds.add(call.id);
12133
- events.push({ type: "tool.started", id: call.id, name: call.name });
12134
- }
12135
- }
12136
- for (const call of chunk.data.tool_calls ?? []) {
12137
- if (!startedToolIds.has(call.id)) {
12138
- startedToolIds.add(call.id);
12139
- events.push({ type: "tool.started", id: call.id, name: call.name });
12140
- }
12141
- }
12142
- return events;
12143
- }
12144
- if (chunk.type === MessageChunkTypes4.TOOL && chunk.data.tool_call_id) {
12145
- return [{ type: "tool.completed", id: chunk.data.tool_call_id }];
12146
- }
12147
- if (chunk.type === MessageChunkTypes4.INTERRUPT) {
12148
- if (context && !context.allowInterrupts) return [{ type: "stream.completed" }];
12149
- const interrupt = publicInterrupt(chunk);
12150
- return interrupt ? [{ type: "interrupt.created", interrupt }, { type: "stream.completed" }] : [{ type: "stream.completed" }];
12151
- }
12152
- if (chunk.type === MessageChunkTypes4.MESSAGE_COMPLETED) {
12153
- return [
12154
- { type: "message.completed", messageId: chunk.data.id },
12155
- { type: "stream.completed" }
12156
- ];
12157
- }
12158
- if (chunk.type === MessageChunkTypes4.MESSAGE_FAILED) {
12159
- return [
12160
- { type: "error", error: { code: "STREAM_FAILED", message: "Stream failed", retryable: true } },
12161
- { type: "stream.completed" }
12162
- ];
12437
+ // src/services/agent-web-app/AgentWebAppTokenVerifier.ts
12438
+ import { createHmac as createHmac2, createPublicKey, timingSafeEqual as timingSafeEqual2, verify as cryptoVerify } from "crypto";
12439
+ var WebAppTokenError = class extends Error {
12440
+ constructor(message) {
12441
+ super(message);
12442
+ this.name = "WebAppTokenError";
12163
12443
  }
12164
- return [];
12165
- }
12166
- function publicInterrupt(chunk) {
12167
- const value = parseInterruptContent(chunk.data.content);
12168
- if (!value) return void 0;
12169
- const topLevelId = chunk.id;
12170
- const id3 = typeof topLevelId === "string" ? topLevelId : chunk.data.id;
12171
- if (!id3) return void 0;
12172
- return {
12173
- id: id3,
12174
- type: typeof value.type === "string" ? value.type : "input",
12175
- prompt: typeof value.prompt === "string" ? value.prompt : typeof value.message === "string" ? value.message : "Input required",
12176
- ...isRecord3(value.data) ? { data: value.data } : {}
12177
- };
12178
- }
12179
- function parseInterruptContent(content) {
12180
- if (!content) return {};
12181
- if (isRecord3(content)) return content;
12182
- if (typeof content !== "string") return void 0;
12444
+ };
12445
+ var MAX_TOKEN_BYTES = 8 * 1024;
12446
+ var CLOCK_SKEW_SECONDS = 60;
12447
+ var JWKS_TTL_MS = 10 * 60 * 1e3;
12448
+ var jwksCache = /* @__PURE__ */ new Map();
12449
+ function decodeSegment(segment) {
12450
+ const json = Buffer.from(segment, "base64url").toString("utf8");
12451
+ let value;
12183
12452
  try {
12184
- const parsed = JSON.parse(content);
12185
- return isRecord3(parsed) ? parsed : { prompt: content };
12453
+ value = JSON.parse(json);
12186
12454
  } catch {
12187
- return { prompt: content };
12455
+ throw new WebAppTokenError("Malformed token segment");
12188
12456
  }
12457
+ if (typeof value !== "object" || value === null) throw new WebAppTokenError("Malformed token segment");
12458
+ return value;
12189
12459
  }
12190
- function isRecord3(value) {
12191
- return typeof value === "object" && value !== null && !Array.isArray(value);
12460
+ async function resolveKey(issuer, kid, fetchImpl) {
12461
+ const cacheKey = issuer.jwksUrl ?? "";
12462
+ const load = async () => {
12463
+ const response = await fetchImpl(issuer.jwksUrl ?? "");
12464
+ if (!response.ok) throw new WebAppTokenError("JWKS fetch failed");
12465
+ const document = await response.json();
12466
+ const keys2 = document.keys;
12467
+ const entry2 = { keys: Array.isArray(keys2) ? keys2 : [], fetchedAt: Date.now() };
12468
+ jwksCache.set(cacheKey, entry2);
12469
+ return entry2;
12470
+ };
12471
+ const cached = jwksCache.get(cacheKey);
12472
+ let entry = cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS ? cached : await load();
12473
+ let jwk = entry.keys.find((key) => kid === void 0 ? true : key.kid === kid);
12474
+ if (!jwk && kid !== void 0) {
12475
+ entry = await load();
12476
+ jwk = entry.keys.find((key) => key.kid === kid);
12477
+ }
12478
+ if (!jwk) throw new WebAppTokenError("No matching JWKS key");
12479
+ return createPublicKey({ key: jwk, format: "jwk" });
12480
+ }
12481
+ async function verifyWebAppToken(token, issuer, options = {}) {
12482
+ const nowMs = options.now ? options.now() : Date.now();
12483
+ const now = Math.floor(nowMs / 1e3);
12484
+ if (token.length === 0 || Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new WebAppTokenError("Token size invalid");
12485
+ const parts = token.split(".");
12486
+ if (parts.length !== 3) throw new WebAppTokenError("Token structure invalid");
12487
+ const [rawHeader, rawPayload, rawSignature] = parts;
12488
+ const header2 = decodeSegment(rawHeader);
12489
+ const payload = decodeSegment(rawPayload);
12490
+ if (header2.alg !== issuer.alg) throw new WebAppTokenError("Token algorithm mismatch");
12491
+ if (payload.iss !== issuer.iss) throw new WebAppTokenError("Token issuer mismatch");
12492
+ if (payload.aud !== issuer.aud) throw new WebAppTokenError("Token audience mismatch");
12493
+ const exp = typeof payload.exp === "number" ? payload.exp : void 0;
12494
+ if (exp === void 0 || exp + CLOCK_SKEW_SECONDS < now) throw new WebAppTokenError("Token expired");
12495
+ const nbf = typeof payload.nbf === "number" ? payload.nbf : void 0;
12496
+ if (nbf !== void 0 && nbf - CLOCK_SKEW_SECONDS > now) throw new WebAppTokenError("Token not yet valid");
12497
+ const signature = Buffer.from(rawSignature, "base64url");
12498
+ const data = Buffer.from(`${rawHeader}.${rawPayload}`);
12499
+ if (issuer.alg === "HS256") {
12500
+ if (typeof issuer.secret !== "string" || issuer.secret.length === 0) throw new WebAppTokenError("Issuer secret missing");
12501
+ const expected = createHmac2("sha256", issuer.secret).update(data).digest();
12502
+ if (signature.length !== expected.length || !timingSafeEqual2(signature, expected)) {
12503
+ throw new WebAppTokenError("Token signature invalid");
12504
+ }
12505
+ } else {
12506
+ if (typeof issuer.jwksUrl !== "string") throw new WebAppTokenError("Issuer JWKS URL missing");
12507
+ const key = await resolveKey(issuer, typeof header2.kid === "string" ? header2.kid : void 0, options.fetchImpl ?? fetch);
12508
+ let valid = false;
12509
+ try {
12510
+ valid = cryptoVerify("RSA-SHA256", data, key, signature);
12511
+ } catch {
12512
+ valid = false;
12513
+ }
12514
+ if (!valid) throw new WebAppTokenError("Token signature invalid");
12515
+ }
12516
+ const sub = payload.sub;
12517
+ if (typeof sub !== "string" || sub.length === 0 || sub.length > 128 || !/^[A-Za-z0-9._:@-]+$/.test(sub)) {
12518
+ throw new WebAppTokenError("Token subject invalid");
12519
+ }
12520
+ return sub;
12192
12521
  }
12193
12522
 
12194
12523
  // src/routes/agent-web-app-runtime.ts
12524
+ import { createAgentWebAppStreamProjectionContext, projectAgentWebAppChunk } from "@axiom-lattice/protocols";
12195
12525
  var idSchema = z5.string().min(1).max(128);
12196
12526
  var paramsSchema = z5.object({ webAppId: idSchema }).strict();
12197
12527
  var threadParamsSchema = z5.object({ webAppId: idSchema, threadId: idSchema }).strict();
12198
12528
  var interruptParamsSchema = z5.object({ webAppId: idSchema, threadId: idSchema, interruptId: idSchema }).strict();
12529
+ var pendingMessageParamsSchema = z5.object({ webAppId: idSchema, threadId: idSchema, messageId: idSchema }).strict();
12199
12530
  var bootstrapQuerySchema = z5.object({ projectId: idSchema.optional(), modelKey: idSchema.optional() }).strict();
12200
12531
  var projectQuerySchema = z5.object({ projectId: idSchema.optional() }).strict();
12532
+ var projectQueryLenientSchema = projectQuerySchema.passthrough();
12201
12533
  var threadBodySchema = z5.object({ projectId: idSchema.optional(), label: z5.string().max(128).optional() }).strict();
12202
12534
  var streamBodySchema = z5.object({
12203
12535
  content: z5.string().trim().min(1).max(32768),
@@ -12216,8 +12548,27 @@ var resumeBodySchema = z5.object({
12216
12548
  ])
12217
12549
  }).strict();
12218
12550
  var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
12551
+ var stateQuerySchema = z5.object({
12552
+ threadId: idSchema,
12553
+ projectId: idSchema.optional(),
12554
+ modelKey: idSchema.optional()
12555
+ }).strict();
12556
+ var resumeSchema = z5.object({
12557
+ messageId: idSchema,
12558
+ projectId: idSchema.optional(),
12559
+ modelKey: idSchema.optional()
12560
+ }).strict();
12561
+ var devTokenSchema = z5.object({
12562
+ iss: z5.string().min(1).max(255).optional(),
12563
+ sub: z5.string().regex(/^[A-Za-z0-9._:@-]+$/).max(128).optional(),
12564
+ hours: z5.number().gt(0).max(720).optional()
12565
+ }).strict();
12566
+ var fileQuerySchema = z5.object({
12567
+ projectId: idSchema.optional(),
12568
+ modelKey: idSchema.optional()
12569
+ }).passthrough();
12219
12570
  function createExternalHeaderIdentityResolver() {
12220
- return { resolve(request) {
12571
+ return { async resolve(request) {
12221
12572
  const raw = request.headers["x-axiom-external-user-id"];
12222
12573
  if (raw === void 0) throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "External user id is required");
12223
12574
  if (Array.isArray(raw) || typeof raw !== "string") throw new AgentWebAppRuntimeError("INVALID_USER_ID", 400, "External user id is invalid");
@@ -12226,47 +12577,109 @@ function createExternalHeaderIdentityResolver() {
12226
12577
  return { userId, assurance: "unverified" };
12227
12578
  } };
12228
12579
  }
12580
+ function createWebAppIdentityResolver(deps) {
12581
+ const headerResolver = createExternalHeaderIdentityResolver();
12582
+ return {
12583
+ async resolve(request, webAppId) {
12584
+ let config;
12585
+ try {
12586
+ config = await deps.getIdentityConfig(webAppId);
12587
+ } catch {
12588
+ config = void 0;
12589
+ }
12590
+ const policy = config?.policy ?? "unverified";
12591
+ const authHeader = request.headers.authorization;
12592
+ const bearer = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : void 0;
12593
+ if (policy !== "unverified" && bearer !== void 0) {
12594
+ const issuers = config?.issuers ?? [];
12595
+ if (issuers.length === 0) throw new AgentWebAppRuntimeError("INVALID_REQUEST", 401, "No trusted issuer configured");
12596
+ let lastError = new AgentWebAppRuntimeError("INVALID_REQUEST", 401, "Invalid identity token");
12597
+ for (const issuer of issuers) {
12598
+ try {
12599
+ const sub = await verifyWebAppToken(bearer, issuer);
12600
+ return { userId: sub, assurance: "verified" };
12601
+ } catch (error) {
12602
+ if (error instanceof WebAppTokenError) {
12603
+ lastError = error;
12604
+ continue;
12605
+ }
12606
+ throw error;
12607
+ }
12608
+ }
12609
+ const message = lastError instanceof WebAppTokenError ? `Invalid identity token: ${lastError.message}` : "Invalid identity token";
12610
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 401, message);
12611
+ }
12612
+ if (policy === "verified") throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "A verified identity token is required");
12613
+ return headerResolver.resolve(request, webAppId);
12614
+ }
12615
+ };
12616
+ }
12229
12617
  function registerAgentWebAppRuntimeRoutes(app2, deps) {
12230
- const identities = deps.identityResolver ?? createExternalHeaderIdentityResolver();
12618
+ const identities = deps.identityResolver ?? createWebAppIdentityResolver({ getIdentityConfig: (webAppId) => deps.service.getIdentityConfig(webAppId) });
12231
12619
  app2.register(async (routes) => {
12232
12620
  routes.setErrorHandler((error, _request, reply) => normalizeError2(reply, error));
12233
12621
  routes.get("/api/web-apps/:webAppId/runtime/bootstrap", async (request, reply) => handle2(reply, async () => {
12234
12622
  const { webAppId } = paramsSchema.parse(request.params);
12235
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12236
- return { data: await deps.service.bootstrap(webAppId, identity.userId, withPreview(bootstrapQuerySchema.parse(request.query), identity.preview)) };
12623
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12624
+ return { data: await deps.service.bootstrap(webAppId, identity.userId, withPreview(bootstrapQuerySchema.parse(request.query), identity.preview), { assurance: identity.assurance ?? "unverified" }) };
12237
12625
  }));
12238
12626
  routes.get("/api/web-apps/:webAppId/runtime/threads", async (request, reply) => handle2(reply, async () => {
12239
12627
  const { webAppId } = paramsSchema.parse(request.params);
12240
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12628
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12241
12629
  const records = await deps.service.listThreads(webAppId, identity.userId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
12242
12630
  return { data: { records, total: records.length } };
12243
12631
  }));
12244
12632
  routes.post("/api/web-apps/:webAppId/runtime/threads", async (request, reply) => handle2(reply, async () => {
12245
12633
  const { webAppId } = paramsSchema.parse(request.params);
12246
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12634
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12247
12635
  return { statusCode: 201, data: await deps.service.createThread(webAppId, identity.userId, withPreview(threadBodySchema.parse(request.body), identity.preview)) };
12248
12636
  }));
12249
12637
  routes.patch("/api/web-apps/:webAppId/runtime/threads/:threadId", async (request, reply) => handle2(reply, async () => {
12250
12638
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12251
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12639
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12252
12640
  return { data: await deps.service.updateThread(webAppId, identity.userId, threadId, withPreview(threadBodySchema.parse(request.body), identity.preview)) };
12253
12641
  }));
12254
12642
  routes.delete("/api/web-apps/:webAppId/runtime/threads/:threadId", async (request, reply) => handle2(reply, async () => {
12255
12643
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12256
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12644
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12257
12645
  await deps.service.deleteThread(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
12258
12646
  return { data: { id: threadId } };
12259
12647
  }));
12260
12648
  routes.get("/api/web-apps/:webAppId/runtime/threads/:threadId/messages", async (request, reply) => handle2(reply, async () => {
12261
12649
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12262
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12650
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12263
12651
  const messages = await deps.service.getMessages(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
12264
12652
  return { data: { messages } };
12265
12653
  }));
12654
+ routes.get("/api/web-apps/:webAppId/runtime/state", async (request, reply) => handle2(reply, async () => {
12655
+ const { webAppId } = paramsSchema.parse(request.params);
12656
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12657
+ const { threadId, projectId, modelKey: modelKey2 } = stateQuerySchema.parse(request.query);
12658
+ const state = await deps.service.getState(webAppId, identity.userId, threadId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview));
12659
+ return { data: state };
12660
+ }));
12661
+ routes.delete("/api/web-apps/:webAppId/runtime/threads/:threadId/pending-messages/:messageId", async (request, reply) => handle2(reply, async () => {
12662
+ const { webAppId, threadId, messageId } = pendingMessageParamsSchema.parse(request.params);
12663
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12664
+ await deps.service.removePendingMessage(webAppId, identity.userId, threadId, messageId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
12665
+ return { data: { removed: true } };
12666
+ }));
12266
12667
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/messages/stream", async (request, reply) => {
12267
12668
  try {
12268
12669
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12269
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12670
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12671
+ const input = streamBodySchema.parse(request.body);
12672
+ const result = await deps.service.streamMessage(webAppId, identity.userId, threadId, withPreview(input, identity.preview));
12673
+ beginSse(reply);
12674
+ await pipeNativeWebAppStream(reply.raw, result.stream);
12675
+ } catch (error) {
12676
+ return normalizeError2(reply, error);
12677
+ }
12678
+ });
12679
+ routes.post("/api/web-apps/:webAppId/runtime/v1/threads/:threadId/messages/stream", async (request, reply) => {
12680
+ try {
12681
+ const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12682
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12270
12683
  const input = streamBodySchema.parse(request.body);
12271
12684
  const result = await deps.service.streamMessage(webAppId, identity.userId, threadId, withPreview(input, identity.preview));
12272
12685
  beginSse(reply);
@@ -12275,15 +12688,27 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
12275
12688
  return normalizeError2(reply, error);
12276
12689
  }
12277
12690
  });
12691
+ routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/resume", async (request, reply) => {
12692
+ try {
12693
+ const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12694
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12695
+ const { messageId, projectId, modelKey: modelKey2 } = resumeSchema.parse(request.body);
12696
+ const result = await deps.service.resumeStream(webAppId, identity.userId, threadId, messageId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview));
12697
+ beginSse(reply);
12698
+ await pipeNativeWebAppStream(reply.raw, result.stream);
12699
+ } catch (error) {
12700
+ return normalizeError2(reply, error);
12701
+ }
12702
+ });
12278
12703
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/abort", async (request, reply) => handle2(reply, async () => {
12279
12704
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12280
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12705
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12281
12706
  await deps.service.abort(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
12282
12707
  return { data: { aborted: true } };
12283
12708
  }));
12284
12709
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/attachments", async (request, reply) => handle2(reply, async () => {
12285
12710
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
12286
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12711
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12287
12712
  if (!request.isMultipart()) throw new ZodError2([]);
12288
12713
  const part = await request.file({ limits: { files: 1, fileSize: MAX_ATTACHMENT_BYTES } });
12289
12714
  if (!part || !part.filename || part.filename.length > 255) throw new ZodError2([]);
@@ -12299,7 +12724,7 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
12299
12724
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/interrupts/:interruptId/resume", async (request, reply) => {
12300
12725
  try {
12301
12726
  const { webAppId, threadId, interruptId } = interruptParamsSchema.parse(request.params);
12302
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
12727
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12303
12728
  const { projectId, modelKey: modelKey2, message, response } = resumeBodySchema.parse(request.body);
12304
12729
  const result = await deps.service.resumeInterrupt(webAppId, identity.userId, threadId, interruptId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview), { message, response });
12305
12730
  beginSse(reply);
@@ -12308,13 +12733,257 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
12308
12733
  return normalizeError2(reply, error);
12309
12734
  }
12310
12735
  });
12736
+ routes.post("/api/web-apps/:webAppId/dev/token", async (request, reply) => handle2(reply, async () => {
12737
+ if (process.env.WEB_APP_DEV_TOKEN_ENABLED !== "true") {
12738
+ throw new AgentWebAppRuntimeError("FEATURE_DISABLED", 403, "Dev token issuance is disabled (set WEB_APP_DEV_TOKEN_ENABLED=true)");
12739
+ }
12740
+ const { webAppId } = paramsSchema.parse(request.params);
12741
+ const { iss, sub, hours } = devTokenSchema.parse(request.body ?? {});
12742
+ const config = await deps.service.getIdentityConfig(webAppId);
12743
+ const issuer = (config?.issuers ?? []).find((entry) => entry.alg === "HS256" && (iss === void 0 || entry.iss === iss));
12744
+ if (!issuer || typeof issuer.secret !== "string") {
12745
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "Web app has no HS256 issuer configured");
12746
+ }
12747
+ const now = Math.floor(Date.now() / 1e3);
12748
+ const expiresIn = (hours ?? 24) * 3600;
12749
+ const subject = sub ?? "dev-user";
12750
+ const b64url = (input) => Buffer.from(input).toString("base64url");
12751
+ const body = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${b64url(JSON.stringify({ sub: subject, iss: issuer.iss, aud: issuer.aud, iat: now, exp: now + expiresIn }))}`;
12752
+ const token = `${body}.${createHmac3("sha256", issuer.secret).update(body).digest("base64url")}`;
12753
+ return { data: { token, tokenType: "Bearer", expiresIn, subject } };
12754
+ }));
12755
+ if (deps.taskController) {
12756
+ const tasks = deps.taskController;
12757
+ const withSubject = (handler) => async (request, reply) => {
12758
+ const { webAppId } = paramsSchema.parse(request.params);
12759
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12760
+ const proxy = Object.create(request);
12761
+ proxy.headers = { ...request.headers, "x-user-id": identity.userId };
12762
+ return handler(proxy, reply);
12763
+ };
12764
+ routes.get("/api/web-apps/:webAppId/runtime/tasks", withSubject((request, reply) => tasks.list(request, reply)));
12765
+ routes.post("/api/web-apps/:webAppId/runtime/tasks", withSubject((request, reply) => tasks.create(request, reply)));
12766
+ routes.get("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.get(request, reply)));
12767
+ routes.put("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.update(request, reply)));
12768
+ routes.patch("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.update(request, reply)));
12769
+ routes.delete("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.remove(request, reply)));
12770
+ routes.patch("/api/web-apps/:webAppId/runtime/tasks/:taskId/complete", withSubject((request, reply) => tasks.complete(request, reply)));
12771
+ routes.get("/api/web-apps/:webAppId/runtime/tasks/:taskId/work-items", withSubject((request, reply) => tasks.workItems(request, reply)));
12772
+ }
12773
+ if (deps.workspaceController) {
12774
+ const workspace = deps.workspaceController;
12775
+ const fileScope = async (webAppId, selection) => {
12776
+ const scope = await deps.service.resolveFileScope(webAppId, selection);
12777
+ if (!scope.enabled) throw new AgentWebAppRuntimeError("FEATURE_DISABLED", 403, "Files are disabled for this web app");
12778
+ return scope;
12779
+ };
12780
+ const scopedRequest = (request, scope, params, query) => {
12781
+ const proxy = Object.create(request);
12782
+ proxy.params = params;
12783
+ proxy.query = query;
12784
+ proxy.headers = { ...request.headers, "x-tenant-id": scope.tenantId };
12785
+ return proxy;
12786
+ };
12787
+ const unwrap = (result) => ({ data: result.data });
12788
+ routes.get("/api/web-apps/:webAppId/runtime/files/listpath", async (request) => {
12789
+ const { webAppId } = paramsSchema.parse(request.params);
12790
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12791
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12792
+ const query = { ...request.query, assistantId: scope.assistantId };
12793
+ const result = await workspace.listPath(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query));
12794
+ return unwrap(result);
12795
+ });
12796
+ routes.get("/api/web-apps/:webAppId/runtime/files/readfile", async (request) => {
12797
+ const { webAppId } = paramsSchema.parse(request.params);
12798
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12799
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12800
+ const query = { ...request.query, assistantId: scope.assistantId };
12801
+ const result = await workspace.readFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query));
12802
+ return unwrap(result);
12803
+ });
12804
+ routes.post("/api/web-apps/:webAppId/runtime/files/uploadfile", async (request, reply) => {
12805
+ const { webAppId } = paramsSchema.parse(request.params);
12806
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12807
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12808
+ const scoped = scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, { assistantId: scope.assistantId });
12809
+ await workspace.uploadFile(scoped, reply);
12810
+ return reply;
12811
+ });
12812
+ routes.get("/api/web-apps/:webAppId/runtime/files/viewfile", async (request, reply) => {
12813
+ const { webAppId } = paramsSchema.parse(request.params);
12814
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12815
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12816
+ const query = { ...request.query, assistantId: scope.assistantId };
12817
+ await workspace.viewFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
12818
+ return reply;
12819
+ });
12820
+ routes.get("/api/web-apps/:webAppId/runtime/files/downloadfile", async (request, reply) => {
12821
+ const { webAppId } = paramsSchema.parse(request.params);
12822
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12823
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12824
+ const query = { ...request.query, assistantId: scope.assistantId };
12825
+ await workspace.downloadFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
12826
+ return reply;
12827
+ });
12828
+ routes.get("/api/web-apps/:webAppId/runtime/files/downloadfolder", async (request, reply) => {
12829
+ const { webAppId } = paramsSchema.parse(request.params);
12830
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12831
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12832
+ const query = { ...request.query, assistantId: scope.assistantId };
12833
+ await workspace.downloadFolder(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
12834
+ return reply;
12835
+ });
12836
+ const genuiSandboxParams = z5.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema }).strict();
12837
+ const delegateGenuiFile = (method) => async (request, reply) => {
12838
+ const { webAppId } = genuiSandboxParams.parse(request.params);
12839
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12840
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
12841
+ const query = { ...request.query, assistantId: scope.assistantId };
12842
+ await method(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
12843
+ return reply;
12844
+ };
12845
+ routes.get("/api/web-apps/:webAppId/runtime/assistants/:assistantId/threads/:threadId/sandbox/downloadfile", delegateGenuiFile(workspace.downloadFile.bind(workspace)));
12846
+ routes.get("/api/web-apps/:webAppId/runtime/assistants/:assistantId/threads/:threadId/sandbox/viewfile", delegateGenuiFile(workspace.viewFile.bind(workspace)));
12847
+ }
12848
+ const mirrorThreadParams = z5.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema }).strict();
12849
+ const mirrorBody = z5.object({ metadata: z5.object({ label: z5.string().max(300).optional() }).strict().optional(), projectId: idSchema.optional(), modelKey: idSchema.optional() }).strict();
12850
+ const toConsoleThread = (thread, assistantId) => ({
12851
+ id: thread.id,
12852
+ assistantId,
12853
+ metadata: thread.label ? { label: thread.label } : {},
12854
+ createdAt: thread.createdAt,
12855
+ updatedAt: thread.updatedAt
12856
+ });
12857
+ const mirrorAssistantParams = z5.object({ webAppId: idSchema, assistantId: idSchema }).strict();
12858
+ const mirrorPendingParams = z5.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema, messageId: idSchema }).strict();
12859
+ const registerMirror = (prefix) => {
12860
+ routes.get(`${prefix}/assistants/:assistantId/threads`, async (request, reply) => handle2(reply, async () => {
12861
+ const { webAppId, assistantId } = mirrorAssistantParams.parse(request.params);
12862
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12863
+ const threads = await deps.service.listThreads(webAppId, identity.userId, projectQueryLenientSchema.parse(request.query));
12864
+ return { data: { records: threads.map((thread) => toConsoleThread(thread, assistantId)) } };
12865
+ }));
12866
+ routes.post(`${prefix}/assistants/:assistantId/threads`, async (request, reply) => handle2(reply, async () => {
12867
+ const { webAppId, assistantId } = mirrorAssistantParams.parse(request.params);
12868
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12869
+ const body = mirrorBody.parse(request.body ?? {});
12870
+ const created = await deps.service.createThread(webAppId, identity.userId, { label: body.metadata?.label, ...body.projectId ? { projectId: body.projectId } : {} });
12871
+ return { statusCode: 201, data: { id: created.id } };
12872
+ }));
12873
+ routes.get(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
12874
+ const { webAppId, assistantId, threadId } = mirrorThreadParams.parse(request.params);
12875
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12876
+ const thread = await deps.service.getThread(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
12877
+ return { data: toConsoleThread(thread, assistantId) };
12878
+ }));
12879
+ routes.put(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
12880
+ const { webAppId, assistantId, threadId } = mirrorThreadParams.parse(request.params);
12881
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12882
+ const body = mirrorBody.parse(request.body ?? {});
12883
+ const updated = await deps.service.updateThread(webAppId, identity.userId, threadId, { label: body.metadata?.label, ...body.projectId ? { projectId: body.projectId } : {} });
12884
+ return { data: toConsoleThread(updated, assistantId) };
12885
+ }));
12886
+ routes.delete(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
12887
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
12888
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12889
+ await deps.service.deleteThread(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
12890
+ return { data: { success: true, message: "Thread deleted" } };
12891
+ }));
12892
+ routes.post(`${prefix}/assistants/:assistantId/threads/:threadId/sandbox/uploadfile`, async (request, reply) => handle2(reply, async () => {
12893
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
12894
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12895
+ if (!request.isMultipart()) throw new ZodError2([]);
12896
+ const part = await request.file({ limits: { files: 1, fileSize: MAX_ATTACHMENT_BYTES } });
12897
+ if (!part || !part.filename || part.filename.length > 255) throw new ZodError2([]);
12898
+ const bytes = await part.toBuffer();
12899
+ if (bytes.length === 0 || part.file.truncated) throw new ZodError2([]);
12900
+ const uploaded = await deps.service.uploadAttachment(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query), {
12901
+ name: part.filename,
12902
+ mimeType: part.mimetype || "application/octet-stream",
12903
+ bytes
12904
+ });
12905
+ return { statusCode: 201, data: { id: uploaded.ref, path: uploaded.ref, name: uploaded.name, mimeType: uploaded.mimeType, size: uploaded.size } };
12906
+ }));
12907
+ routes.get(`${prefix}/assistants/:assistantId/:threadId/memory`, async (request) => {
12908
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
12909
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12910
+ const messages = await deps.service.getConsoleMessages(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
12911
+ return messages;
12912
+ });
12913
+ routes.get(`${prefix}/assistants/:assistantId/:threadId/state`, async (request) => {
12914
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
12915
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12916
+ return deps.service.getState(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
12917
+ });
12918
+ routes.delete(`${prefix}/assistants/:assistantId/threads/:threadId/pending-messages/:messageId`, async (request, reply) => handle2(reply, async () => {
12919
+ const { webAppId, threadId, messageId } = mirrorPendingParams.parse(request.params);
12920
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12921
+ await deps.service.removePendingMessage(webAppId, identity.userId, threadId, messageId, projectQueryLenientSchema.parse(request.query));
12922
+ return { data: { removed: true } };
12923
+ }));
12924
+ routes.post(`${prefix}/assistants/:assistantId/threads/:threadId/abort`, async (request, reply) => handle2(reply, async () => {
12925
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
12926
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12927
+ await deps.service.abort(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
12928
+ return { data: { aborted: true } };
12929
+ }));
12930
+ const runsBodySchema = z5.object({
12931
+ thread_id: idSchema.optional(),
12932
+ assistant_id: idSchema.optional(),
12933
+ messages: z5.array(z5.object({ content: z5.string().min(1).max(32768) }).passthrough()).min(1).max(20),
12934
+ background: z5.boolean().optional(),
12935
+ streaming: z5.boolean().optional(),
12936
+ projectId: idSchema.optional(),
12937
+ modelKey: idSchema.optional()
12938
+ }).passthrough();
12939
+ routes.post(`${prefix}/runs`, async (request, reply) => {
12940
+ try {
12941
+ const { webAppId } = paramsSchema.parse(request.params);
12942
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12943
+ const body = runsBodySchema.parse(request.body ?? {});
12944
+ if (!body.background || body.streaming) {
12945
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "Mirror runs supports only background enqueue (stream via resume_stream)");
12946
+ }
12947
+ const threadId = body.thread_id ?? "";
12948
+ if (!threadId) throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "thread_id is required");
12949
+ const content = body.messages.map((message) => message.content).join("\n");
12950
+ const { messageId } = await deps.service.enqueueMessage(webAppId, identity.userId, threadId, content, { projectId: body.projectId, modelKey: body.modelKey });
12951
+ return reply.code(202).send({ success: true, messageId, queued: true });
12952
+ } catch (error) {
12953
+ return normalizeError2(reply, error);
12954
+ }
12955
+ });
12956
+ const resumeStreamSchema2 = z5.object({
12957
+ assistant_id: idSchema.optional(),
12958
+ thread_id: idSchema,
12959
+ message_id: idSchema,
12960
+ known_content: z5.string().optional(),
12961
+ poll_interval: z5.number().optional(),
12962
+ projectId: idSchema.optional(),
12963
+ modelKey: idSchema.optional()
12964
+ }).strict();
12965
+ routes.post(`${prefix}/resume_stream`, async (request, reply) => {
12966
+ try {
12967
+ const { webAppId } = paramsSchema.parse(request.params);
12968
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
12969
+ const body = resumeStreamSchema2.parse(request.body ?? {});
12970
+ const result = await deps.service.resumeStream(webAppId, identity.userId, body.thread_id, body.message_id, { projectId: body.projectId, modelKey: body.modelKey });
12971
+ beginSse(reply);
12972
+ await pipeNativeWebAppStream(reply.raw, result.stream);
12973
+ } catch (error) {
12974
+ return normalizeError2(reply, error);
12975
+ }
12976
+ });
12977
+ };
12978
+ registerMirror("/api/web-apps/:webAppId/runtime");
12979
+ registerMirror("/api/web-apps/:webAppId/runtime/api");
12311
12980
  });
12312
12981
  }
12313
- function resolveRuntimeIdentity(request, webAppId, identities) {
12314
- if (request.headers["x-axiom-web-app-preview"] !== "true") return identities.resolve(request);
12982
+ async function resolveRuntimeIdentity(request, webAppId, identities) {
12983
+ if (request.headers["x-axiom-web-app-preview"] !== "true") return identities.resolve(request, webAppId);
12315
12984
  const user = request.user;
12316
12985
  if (!user?.id || !user.tenantId) throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "Authenticated Console preview is required");
12317
- return { userId: `console-preview:${user.id}:${webAppId}`, preview: { tenantId: user.tenantId, userId: user.id } };
12986
+ return { userId: `console-preview:${user.id}:${webAppId}`, assurance: "unverified", preview: { tenantId: user.tenantId, userId: user.id } };
12318
12987
  }
12319
12988
  function withPreview(selection, preview) {
12320
12989
  return { ...selection, ...preview ? { preview } : {} };
@@ -12329,13 +12998,22 @@ async function pipeAgentWebAppStream(raw, stream, options = {}) {
12329
12998
  ]
12330
12999
  });
12331
13000
  }
13001
+ async function pipeNativeWebAppStream(raw, stream) {
13002
+ await pipeAsyncIterableToSse(raw, stream, {
13003
+ onError: () => [{
13004
+ type: "error",
13005
+ data: { id: "", content: "Stream failed" }
13006
+ }]
13007
+ });
13008
+ }
12332
13009
  function beginSse(reply) {
12333
13010
  reply.hijack();
12334
13011
  reply.raw.writeHead(200, {
12335
13012
  "Content-Type": "text/event-stream; charset=utf-8",
12336
13013
  "Cache-Control": "no-cache, no-transform",
12337
13014
  Connection: "keep-alive",
12338
- "X-Accel-Buffering": "no"
13015
+ "X-Accel-Buffering": "no",
13016
+ "Access-Control-Allow-Origin": "*"
12339
13017
  });
12340
13018
  }
12341
13019
  async function handle2(reply, operation) {
@@ -12357,7 +13035,7 @@ function isMultipartRequestError(error) {
12357
13035
  }
12358
13036
 
12359
13037
  // src/services/agent-web-app/AgentWebAppAttachmentRef.ts
12360
- import { createCipheriv, createDecipheriv, createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
13038
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
12361
13039
  var TTL_MS = 15 * 6e4;
12362
13040
  function resolveAgentWebAppAttachmentSecret(consoleSecret) {
12363
13041
  return process.env.WEB_APP_ATTACHMENT_SECRET || process.env.AUTH_TOKEN_SECRET || process.env.JWT_SECRET || consoleSecret;
@@ -12366,7 +13044,7 @@ var AgentWebAppAttachmentRefService = class {
12366
13044
  constructor(secret, now = Date.now) {
12367
13045
  this.now = now;
12368
13046
  if (!secret) throw new Error("Attachment signing secret is required");
12369
- this.key = createHash2("sha256").update(secret, "utf8").digest();
13047
+ this.key = createHash3("sha256").update(secret, "utf8").digest();
12370
13048
  }
12371
13049
  sign(input) {
12372
13050
  const payload = {
@@ -12428,7 +13106,7 @@ function decodeCanonical(value, expectedBytes) {
12428
13106
  return decoded;
12429
13107
  }
12430
13108
  function digestUser(userId) {
12431
- return createHash2("sha256").update(userId, "utf8").digest("hex");
13109
+ return createHash3("sha256").update(userId, "utf8").digest("hex");
12432
13110
  }
12433
13111
  function isAttachmentPayload(value) {
12434
13112
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -12788,12 +13466,12 @@ function registerCapabilityBundleRoutes(app2, controller) {
12788
13466
  }
12789
13467
 
12790
13468
  // src/services/collaboration/ProjectRoomService.ts
12791
- import { randomUUID as randomUUID11 } from "crypto";
13469
+ import { randomUUID as randomUUID12 } from "crypto";
12792
13470
  var ProjectRoomService = class {
12793
13471
  /** Creates a project room service from explicitly injected collaborators. */
12794
13472
  constructor(deps) {
12795
13473
  this.deps = deps;
12796
- this.idFactory = deps.idFactory ?? randomUUID11;
13474
+ this.idFactory = deps.idFactory ?? randomUUID12;
12797
13475
  }
12798
13476
  /** Authorizes read access and atomically ensures the project's main room. */
12799
13477
  async ensureMainRoom(input) {
@@ -12812,7 +13490,7 @@ var ProjectRoomService = class {
12812
13490
  };
12813
13491
 
12814
13492
  // src/services/collaboration/ProjectMembershipService.ts
12815
- import { randomUUID as randomUUID12 } from "crypto";
13493
+ import { randomUUID as randomUUID13 } from "crypto";
12816
13494
  var ProjectMembershipServiceError = class extends Error {
12817
13495
  constructor(code, statusCode, message) {
12818
13496
  super(message);
@@ -12828,11 +13506,13 @@ function isMembershipConflict(error) {
12828
13506
  var ProjectMembershipService = class {
12829
13507
  constructor(deps) {
12830
13508
  this.deps = deps;
12831
- this.idFactory = deps.idFactory ?? randomUUID12;
13509
+ this.idFactory = deps.idFactory ?? randomUUID13;
12832
13510
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
12833
13511
  }
12834
13512
  async list(actor2) {
12835
- await this.authorize(actor2);
13513
+ await this.deps.accessService.requireRosterAccess(actor2);
13514
+ const membership = await this.deps.membershipStore.findByUser(actor2.tenantId, actor2.projectId, actor2.userId);
13515
+ if (!membership || membership.status !== "active") this.accessDenied();
12836
13516
  return this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
12837
13517
  }
12838
13518
  async add(actor2, input) {
@@ -12952,7 +13632,7 @@ var ProjectMembershipService = class {
12952
13632
  };
12953
13633
 
12954
13634
  // src/services/collaboration/ProjectRosterService.ts
12955
- import { randomUUID as randomUUID13 } from "crypto";
13635
+ import { randomUUID as randomUUID14 } from "crypto";
12956
13636
  var ProjectRosterServiceError = class extends Error {
12957
13637
  /** Creates a stable roster service error. */
12958
13638
  constructor(code, statusCode, message) {
@@ -12969,7 +13649,7 @@ var ProjectRosterService = class {
12969
13649
  /** Creates a roster service from explicitly injected collaborators. */
12970
13650
  constructor(deps) {
12971
13651
  this.deps = deps;
12972
- this.idFactory = deps.idFactory ?? randomUUID13;
13652
+ this.idFactory = deps.idFactory ?? randomUUID14;
12973
13653
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
12974
13654
  }
12975
13655
  /** Lists all bot memberships visible to a project reader. */
@@ -13189,7 +13869,7 @@ var ProjectRosterService = class {
13189
13869
  };
13190
13870
 
13191
13871
  // src/services/collaboration/RoomMessageService.ts
13192
- import { createHash as createHash3, randomUUID as randomUUID14 } from "crypto";
13872
+ import { createHash as createHash4, randomUUID as randomUUID15 } from "crypto";
13193
13873
 
13194
13874
  // src/services/collaboration/ProjectBotMembershipSnapshot.ts
13195
13875
  import { snapshotExactRecord as snapshotExactRecord4 } from "@axiom-lattice/protocols";
@@ -13278,7 +13958,7 @@ var RoomMessageService = class {
13278
13958
  constructor(deps, options = {}) {
13279
13959
  this.deps = deps;
13280
13960
  this.idempotentOutcomes = /* @__PURE__ */ new Map();
13281
- this.idFactory = deps.idFactory ?? randomUUID14;
13961
+ this.idFactory = deps.idFactory ?? randomUUID15;
13282
13962
  const limit = options.idempotentOutcomeLimit ?? 1e3;
13283
13963
  if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("idempotentOutcomeLimit must be a positive safe integer");
13284
13964
  this.idempotentOutcomeLimit = limit;
@@ -13459,7 +14139,7 @@ var RoomMessageService = class {
13459
14139
  let idempotencyKey;
13460
14140
  if (value.requestIdempotencyKey !== void 0) {
13461
14141
  if (typeof value.requestIdempotencyKey !== "string" || !/^[\x21-\x7e]{1,200}$/.test(value.requestIdempotencyKey.trim())) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid idempotency key");
13462
- idempotencyKey = `room-human:${actor2.userId}:${createHash3("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
14142
+ idempotencyKey = `room-human:${actor2.userId}:${createHash4("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
13463
14143
  }
13464
14144
  return { text, mentions, botIds, ...value.replyToMessageId === void 0 ? {} : { replyToMessageId: value.replyToMessageId }, ...idempotencyKey === void 0 ? {} : { idempotencyKey } };
13465
14145
  }
@@ -13516,7 +14196,7 @@ function cloneRoomMessage(message) {
13516
14196
  };
13517
14197
  }
13518
14198
  function fingerprintRequest(input) {
13519
- return createHash3("sha256").update(JSON.stringify({
14199
+ return createHash4("sha256").update(JSON.stringify({
13520
14200
  text: input.text,
13521
14201
  mentions: input.mentions,
13522
14202
  replyToMessageId: input.replyToMessageId ?? null
@@ -13685,45 +14365,12 @@ async function execute(reply, operation) {
13685
14365
  }
13686
14366
 
13687
14367
  // src/routes/cospace.ts
13688
- import { randomUUID as randomUUID15 } from "crypto";
14368
+ import { randomUUID as randomUUID16 } from "crypto";
13689
14369
  import { ZodError as ZodError5 } from "zod";
13690
14370
  import {
13691
14371
  generateDirectHumanProjectId,
13692
14372
  generateDirectBotProjectId
13693
14373
  } from "@axiom-lattice/core";
13694
-
13695
- // src/services/collaboration/RoomCoordinatorProvisioner.ts
13696
- var ROOM_COORDINATOR_KEY = "room-coordinator";
13697
- var RoomCoordinatorProvisioner = class {
13698
- /** Creates a provisioner over the tenant-scoped assistant store operations. */
13699
- constructor(deps) {
13700
- this.deps = deps;
13701
- }
13702
- /**
13703
- * Ensures the Room Coordinator assistant exists, reusing an existing record
13704
- * and reconciling concurrent create races by rereading the winner.
13705
- *
13706
- * @param tenantId Tenant to provision the coordinator for.
13707
- * @returns The persisted Room Coordinator assistant.
13708
- */
13709
- async ensure(tenantId) {
13710
- const existing = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
13711
- if (existing) return existing;
13712
- try {
13713
- return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, {
13714
- name: "Room Coordinator",
13715
- description: "Coordinates project room conversations and organizes room work.",
13716
- graphDefinition: {}
13717
- });
13718
- } catch (error) {
13719
- const winner = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
13720
- if (winner) return winner;
13721
- throw error;
13722
- }
13723
- }
13724
- };
13725
-
13726
- // src/routes/cospace.ts
13727
14374
  var CoSpaceAuthError = class extends Error {
13728
14375
  constructor() {
13729
14376
  super(...arguments);
@@ -13739,7 +14386,7 @@ function getActor2(request) {
13739
14386
  return { tenantId: user.tenantId, userId: user.id };
13740
14387
  }
13741
14388
  function registerCoSpaceRoutes(app2, deps) {
13742
- const idFactory = deps.idFactory ?? (() => randomUUID15());
14389
+ const idFactory = deps.idFactory ?? (() => randomUUID16());
13743
14390
  app2.get(
13744
14391
  "/api/workspaces/:workspaceId/rooms",
13745
14392
  async (request, reply) => {
@@ -13764,6 +14411,7 @@ function registerCoSpaceRoutes(app2, deps) {
13764
14411
  );
13765
14412
  const accessibleProjects = [];
13766
14413
  for (const project of allProjects) {
14414
+ if (project.kind === "public") continue;
13767
14415
  const membership = await deps.membershipStore.findByUser(
13768
14416
  actor2.tenantId,
13769
14417
  project.id,
@@ -13817,6 +14465,34 @@ function registerCoSpaceRoutes(app2, deps) {
13817
14465
  const bTime = b.lastMessage?.createdAt?.getTime() ?? 0;
13818
14466
  return bTime - aTime;
13819
14467
  });
14468
+ const publicRoom = await deps.publicRoom.ensure(actor2.tenantId, params.workspaceId);
14469
+ if (publicRoom) {
14470
+ const publicMessages = await deps.messageStore.list({
14471
+ tenantId: actor2.tenantId,
14472
+ roomId: publicRoom.room.id,
14473
+ limit: 1
14474
+ });
14475
+ const publicMemberships = await deps.membershipStore.list(
14476
+ actor2.tenantId,
14477
+ publicRoom.project.id
14478
+ );
14479
+ const publicParticipantNames = [];
14480
+ for (const m of publicMemberships) {
14481
+ if (m.status !== "active") continue;
14482
+ const user = await deps.userStore.getUserById(m.userId);
14483
+ if (user) {
14484
+ publicParticipantNames.push(user.name);
14485
+ }
14486
+ }
14487
+ rooms.unshift({
14488
+ project: publicRoom.project,
14489
+ room: publicRoom.room,
14490
+ lastMessage: publicMessages[0],
14491
+ participantCount: publicMemberships.filter((m) => m.status === "active").length,
14492
+ participantNames: publicParticipantNames,
14493
+ isPublic: true
14494
+ });
14495
+ }
13820
14496
  return { data: { rooms } };
13821
14497
  });
13822
14498
  }
@@ -14573,7 +15249,7 @@ function exactValues(value, required, optional = []) {
14573
15249
  }
14574
15250
 
14575
15251
  // src/services/collaboration/CoordinatorWakeNotifier.ts
14576
- import { randomUUID as randomUUID16 } from "crypto";
15252
+ import { randomUUID as randomUUID17 } from "crypto";
14577
15253
  import { isProjectLifecycleTask as isProjectLifecycleTask2 } from "@axiom-lattice/core";
14578
15254
  import {
14579
15255
  snapshotExactRecord as snapshotExactRecord6
@@ -14590,7 +15266,7 @@ var CoordinatorWakeNotifier = class {
14590
15266
  this.accepted = /* @__PURE__ */ new Set();
14591
15267
  this.maxPending = deps.maxPending ?? 1024;
14592
15268
  if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) throw new RangeError("maxPending must be a positive safe integer");
14593
- this.idFactory = deps.idFactory ?? randomUUID16;
15269
+ this.idFactory = deps.idFactory ?? randomUUID17;
14594
15270
  }
14595
15271
  /** Admits one notification without awaiting any store or dispatch operation. */
14596
15272
  handle(value) {
@@ -15460,7 +16136,7 @@ function validRecoveryRow(row, task, thread) {
15460
16136
  }
15461
16137
 
15462
16138
  // src/services/collaboration/ProjectTaskObserver.ts
15463
- import { createHash as createHash4 } from "crypto";
16139
+ import { createHash as createHash5 } from "crypto";
15464
16140
  import {
15465
16141
  isProjectLifecycleTask as isProjectLifecycleTask4,
15466
16142
  parseExternalDependency as parseExternalDependency2
@@ -16000,7 +16676,7 @@ var ProjectTaskObserver = class {
16000
16676
  action: "activity",
16001
16677
  summary: "Project task recovery warning",
16002
16678
  detail: { type: "project_task_recovery_warning", phase, message },
16003
- eventKey: `project-task-recovery-warning:${task.id}:${phase}:${createHash4("sha256").update(message).digest("hex")}`
16679
+ eventKey: `project-task-recovery-warning:${task.id}:${phase}:${createHash5("sha256").update(message).digest("hex")}`
16004
16680
  });
16005
16681
  }
16006
16682
  async safeWarning(task, phase, error) {
@@ -16067,7 +16743,7 @@ var ProjectTaskObserver = class {
16067
16743
  }
16068
16744
  }
16069
16745
  async persistOverflowWarning(event) {
16070
- const scopeDigest = createHash4("sha256").update(JSON.stringify([event.tenantId, event.workspaceId, event.projectId])).digest("hex");
16746
+ const scopeDigest = createHash5("sha256").update(JSON.stringify([event.tenantId, event.workspaceId, event.projectId])).digest("hex");
16071
16747
  try {
16072
16748
  await this.deps.workItems.createIfAbsentByEventKey({
16073
16749
  tenantId: event.tenantId,
@@ -16168,7 +16844,7 @@ function combineSignals(left, right) {
16168
16844
  }
16169
16845
 
16170
16846
  // src/services/collaboration/RoomProjectionService.ts
16171
- import { createHash as createHash5 } from "crypto";
16847
+ import { createHash as createHash6 } from "crypto";
16172
16848
  import { isProjectLifecycleTask as isProjectLifecycleTask5 } from "@axiom-lattice/core";
16173
16849
  import {
16174
16850
  PROJECT_TASK_LIFECYCLE_ACTIONS,
@@ -16341,7 +17017,7 @@ function safeRepairIdentity(value, scope) {
16341
17017
  return taskId && eventKey && tenantId === scope.tenantId && workspaceId === scope.workspaceId && projectId === scope.projectId ? { taskId, eventKey } : void 0;
16342
17018
  }
16343
17019
  function tupleKey(prefix, values) {
16344
- const digest = createHash5("sha256").update(JSON.stringify(values)).digest("hex");
17020
+ const digest = createHash6("sha256").update(JSON.stringify(values)).digest("hex");
16345
17021
  return `${prefix}:${digest}`;
16346
17022
  }
16347
17023
  function parseProjectionInput(taskValue, itemValue) {
@@ -16369,7 +17045,7 @@ function snapshotProjectableWorkItem(value) {
16369
17045
  if (!row || !createdAt || typeof row.id !== "string" || !row.id || typeof row.taskId !== "string" || !row.taskId || typeof row.tenantId !== "string" || !row.tenantId || typeof row.workspaceId !== "string" || !row.workspaceId || typeof row.projectId !== "string" || !row.projectId || typeof row.actor !== "string" || !row.actor || typeof row.eventKey !== "string" || !row.eventKey || !PROJECT_TASK_LIFECYCLE_ACTIONS.includes(row.action) || row.threadId !== void 0 && typeof row.threadId !== "string" || row.summary !== void 0 && typeof row.summary !== "string" || row.attempt !== void 0 && (typeof row.attempt !== "number" || !Number.isFinite(row.attempt))) return void 0;
16370
17046
  try {
16371
17047
  const detail = row.detail === void 0 ? void 0 : snapshotJsonValue(row.detail);
16372
- if (detail !== void 0 && !isRecord4(detail)) return void 0;
17048
+ if (detail !== void 0 && !isRecord3(detail)) return void 0;
16373
17049
  return {
16374
17050
  id: row.id,
16375
17051
  taskId: row.taskId,
@@ -16516,7 +17192,7 @@ function snapshotLifecycleCursor(value) {
16516
17192
  const createdAt = cloneExactDate(row?.createdAt);
16517
17193
  return row && typeof row.id === "string" && row.id.length > 0 && createdAt ? { id: row.id, createdAt } : void 0;
16518
17194
  }
16519
- function isRecord4(value) {
17195
+ function isRecord3(value) {
16520
17196
  return typeof value === "object" && value !== null && !Array.isArray(value);
16521
17197
  }
16522
17198
 
@@ -16524,7 +17200,7 @@ function isRecord4(value) {
16524
17200
  import { taskLifecycleNotifier } from "@axiom-lattice/core";
16525
17201
 
16526
17202
  // src/services/collaboration/ProjectRoomEventBroker.ts
16527
- import { randomUUID as randomUUID17 } from "crypto";
17203
+ import { randomUUID as randomUUID18 } from "crypto";
16528
17204
  import {
16529
17205
  parseProjectRoomEventId,
16530
17206
  ProjectRoomBrokerCapacityError,
@@ -16553,7 +17229,7 @@ var ProjectRoomEventBroker = class {
16553
17229
  this.subscriberCount = 0;
16554
17230
  this.usageClock = 0;
16555
17231
  this.closed = false;
16556
- this.epoch = options.epoch ?? randomUUID17();
17232
+ this.epoch = options.epoch ?? randomUUID18();
16557
17233
  if (!UUID_PATTERN.test(this.epoch)) {
16558
17234
  throw new TypeError("Project Room event epoch must be a lowercase canonical UUID");
16559
17235
  }
@@ -16871,7 +17547,7 @@ function snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
16871
17547
  }
16872
17548
 
16873
17549
  // src/services/collaboration/ProjectRoomRealtimePublisher.ts
16874
- import { createHash as createHash6 } from "crypto";
17550
+ import { createHash as createHash7 } from "crypto";
16875
17551
  import {
16876
17552
  snapshotExactRecord as snapshotExactRecord10,
16877
17553
  snapshotProjectRoomBotMembershipRealtime,
@@ -16952,7 +17628,7 @@ var BrokerProjectRoomRealtimePublisher = class {
16952
17628
  try {
16953
17629
  event = build();
16954
17630
  if (!event) throw new Error("Invalid Project Room realtime projection");
16955
- const dedupeKey = createHash6("sha256").update(JSON.stringify([type, event.scope.tenantId, event.scope.projectId, event.scope.roomId, event.sourceKey])).digest("hex");
17631
+ const dedupeKey = createHash7("sha256").update(JSON.stringify([type, event.scope.tenantId, event.scope.projectId, event.scope.roomId, event.sourceKey])).digest("hex");
16956
17632
  if (this.sourceKeys.has(dedupeKey)) return;
16957
17633
  this.deps.broker.publish(event.scope, event.draft);
16958
17634
  this.sourceKeys.set(dedupeKey, true);
@@ -17891,12 +18567,24 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
17891
18567
  });
17892
18568
  }
17893
18569
  if (projectTaskComposition) app2.addHook("onClose", async () => projectTaskComposition.lease.release());
17894
- const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: randomUUID18 });
18570
+ const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: randomUUID19 });
17895
18571
  const taskLifecycle = projectTaskComposition?.lease.runtime.lifecycle ?? createTaskLifecycleService({ taskStore, workItemStore: taskWorkItemStore });
17896
18572
  const reassignmentAuthorizer = createProjectTaskReassignmentAuthorizer(
17897
18573
  new ProjectTaskOwnerPolicy(projectBotMembershipStore),
17898
18574
  (input) => validateReassignmentThread(threadStore, input)
17899
18575
  );
18576
+ const coordinatorProvisioner = new RoomCoordinatorProvisioner({
18577
+ getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
18578
+ createAssistant: assistantStore.createAssistant.bind(assistantStore)
18579
+ });
18580
+ const publicRoomService = new WorkspacePublicRoomService({
18581
+ getProjectById: projectStore.getProjectById.bind(projectStore),
18582
+ createProject: projectStore.createProject.bind(projectStore),
18583
+ ensureMainRoom: projectRoomStore.ensureMainRoom.bind(projectRoomStore),
18584
+ findBotByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
18585
+ saveBot: projectBotMembershipStore.save.bind(projectBotMembershipStore),
18586
+ ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
18587
+ });
17900
18588
  registerProjectRoomRoutes(app2, { roomService, membershipService, messageService });
17901
18589
  registerProjectRosterRoutes(app2, { service: rosterService });
17902
18590
  registerCoSpaceRoutes(app2, {
@@ -17935,14 +18623,14 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
17935
18623
  findByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
17936
18624
  save: projectBotMembershipStore.save.bind(projectBotMembershipStore)
17937
18625
  },
17938
- coordinatorProvisioner: new RoomCoordinatorProvisioner({
17939
- getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
17940
- createAssistant: assistantStore.createAssistant.bind(assistantStore)
17941
- }),
18626
+ coordinatorProvisioner,
17942
18627
  rosterService: {
17943
18628
  addBot: async (actor2, input) => {
17944
18629
  await rosterService.add(actor2, input);
17945
18630
  }
18631
+ },
18632
+ publicRoom: {
18633
+ ensure: (tenantId, workspaceId) => publicRoomService.ensure(tenantId, workspaceId)
17946
18634
  }
17947
18635
  });
17948
18636
  registerCapabilityBundleRoutes(app2, new CapabilityBundleController({
@@ -17962,6 +18650,46 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
17962
18650
  })
17963
18651
  });
17964
18652
  registerAgentWebAppRuntimeRoutes(app2, {
18653
+ workspaceController: new WorkspaceController(createProjectCollaborationInitializer({
18654
+ membershipStore: projectMembershipStore,
18655
+ roomStore: projectRoomStore,
18656
+ userTenantLinkStore
18657
+ })),
18658
+ taskController: createTaskController({
18659
+ access: {
18660
+ requireProjectAccess: async (input) => {
18661
+ const apps = await webAppStore.list(input.tenantId);
18662
+ const allowed = apps.some((app3) => app3.status === "active" && app3.scope.allowedProjectIds.includes(input.projectId));
18663
+ if (!allowed) throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project is not exposed through this web app");
18664
+ const project = await projectStore.getProjectById(input.tenantId, input.projectId);
18665
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
18666
+ return {
18667
+ project,
18668
+ membership: {
18669
+ id: `webapp_guest_${input.projectId}_${input.userId}`,
18670
+ tenantId: input.tenantId,
18671
+ projectId: input.projectId,
18672
+ userId: input.userId,
18673
+ role: "member",
18674
+ status: "active",
18675
+ joinedAt: /* @__PURE__ */ new Date(),
18676
+ updatedAt: /* @__PURE__ */ new Date()
18677
+ }
18678
+ };
18679
+ }
18680
+ },
18681
+ tasks: taskStore,
18682
+ workItems: taskWorkItemStore,
18683
+ bots: projectBotMembershipStore,
18684
+ rooms: projectRoomStore,
18685
+ threadService: taskThreadService,
18686
+ reassignmentAuthorizer,
18687
+ lifecycle: taskLifecycle,
18688
+ projectMutationMutex: projectMutationMutex2,
18689
+ executionMutex: projectTaskComposition?.mutex ?? {
18690
+ runExclusive: (_scope, operation) => operation()
18691
+ }
18692
+ }),
17965
18693
  service: new AgentWebAppRuntimeService({
17966
18694
  webAppStore,
17967
18695
  threadStore: getStoreLattice16("default", "thread").store,
@@ -17969,7 +18697,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
17969
18697
  assistantResolver: {
17970
18698
  resolve: async (tenantId, assistantId) => {
17971
18699
  const stored = await assistantStore.getAssistantById(tenantId, assistantId);
17972
- if (stored) return stored;
18700
+ if (stored) return { id: stored.id, name: stored.name, description: stored.description };
17973
18701
  await agentLatticeManager4.initializeStoredAssistantsForTenant(tenantId);
17974
18702
  const config = agentLatticeManager4.getAgentConfigWithTenant(tenantId, assistantId);
17975
18703
  return config ? { id: assistantId, name: config.name, description: config.description } : null;
@@ -18383,7 +19111,7 @@ function tryCreateProjectTaskRuntime(input) {
18383
19111
  function createRuntime(input, stores, log2) {
18384
19112
  const controller = new AbortController();
18385
19113
  const mutex = new ProjectTaskExecutionMutex();
18386
- const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID18 });
19114
+ const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID19 });
18387
19115
  const lifecycle = createTaskLifecycleService({ taskStore: input.taskStore, workItemStore: input.taskWorkItemStore });
18388
19116
  const projection = new RoomProjectionService({
18389
19117
  messages: input.projectRoomMessageStore,
@@ -18392,7 +19120,7 @@ function createRuntime(input, stores, log2) {
18392
19120
  warnings: input.taskWorkItemStore,
18393
19121
  log: log2,
18394
19122
  realtime: input.realtime,
18395
- idFactory: randomUUID18
19123
+ idFactory: randomUUID19
18396
19124
  });
18397
19125
  const agentFor = (task, thread) => agentInstanceManager6.getAgent({
18398
19126
  tenant_id: task.tenantId,
@@ -19217,7 +19945,7 @@ var collectionRegistration = {
19217
19945
 
19218
19946
  // src/export_registrations/channel-installation.registration.ts
19219
19947
  import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
19220
- import { randomUUID as randomUUID19 } from "crypto";
19948
+ import { randomUUID as randomUUID20 } from "crypto";
19221
19949
  var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19222
19950
  var PUBLIC_CHANNELS = /* @__PURE__ */ new Set(["lark", "email", "slack", "wechat"]);
19223
19951
  function isPublicChannel(channel) {
@@ -19376,7 +20104,7 @@ var channelInstallationRegistration = {
19376
20104
  }
19377
20105
  id3 = sourceId;
19378
20106
  } else {
19379
- id3 = randomUUID19();
20107
+ id3 = randomUUID20();
19380
20108
  }
19381
20109
  await store.createInstallation(tenantId, id3, createInput);
19382
20110
  return { newId: id3 };
@@ -20264,7 +20992,7 @@ import {
20264
20992
  snapshotExactArray as snapshotExactArray3,
20265
20993
  snapshotExactRecord as snapshotExactRecord14
20266
20994
  } from "@axiom-lattice/protocols";
20267
- import { randomUUID as randomUUID20 } from "crypto";
20995
+ import { randomUUID as randomUUID21 } from "crypto";
20268
20996
  var BindingNotFoundError = class extends Error {
20269
20997
  constructor(message) {
20270
20998
  super(message);
@@ -20579,7 +21307,7 @@ var MessageRouter = class {
20579
21307
  }
20580
21308
  if (!threadId) {
20581
21309
  const threadStore = getStoreLattice26("default", "thread").store;
20582
- const newThreadId = randomUUID20();
21310
+ const newThreadId = randomUUID21();
20583
21311
  console.log({
20584
21312
  event: "dispatch:thread:create",
20585
21313
  agentId,
@@ -20625,7 +21353,7 @@ var MessageRouter = class {
20625
21353
  workspace_id: binding?.workspaceId || "",
20626
21354
  project_id: binding?.projectId || ""
20627
21355
  });
20628
- const inputMessageId = randomUUID20();
21356
+ const inputMessageId = randomUUID21();
20629
21357
  const replyTarget = internalRoom ? correlatedRoomReplyTarget(message.replyTarget, inputMessageId) : message.replyTarget;
20630
21358
  const replyAdapter = replyTarget ? this.adapterRegistry.get(replyTarget.adapterChannel) : void 0;
20631
21359
  if (options.trustedMessageOptions && (!this.trustedDispatcher || !replyTarget || !replyAdapter)) {
@@ -21082,7 +21810,7 @@ var start = async (config) => {
21082
21810
  });
21083
21811
  try {
21084
21812
  const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-DK5X6VIL.mjs");
21085
- const { registerA2AStandardRoutes } = await import("./a2a-standard-G3J5ZHQ7.mjs");
21813
+ const { registerA2AStandardRoutes } = await import("./a2a-standard-WC2BSOFV.mjs");
21086
21814
  const a2a = await import("./a2a-7N3WDAOP.mjs");
21087
21815
  const a2aKeyStore = getStoreLattice27("default", "a2aApiKey").store;
21088
21816
  const assistantStore = getStoreLattice27("default", "assistant").store;