@hachej/boring-workspace 0.1.54 → 0.1.56

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/server.js CHANGED
@@ -11,6 +11,16 @@ function createInMemoryBridge() {
11
11
  pendingCommands.splice(0, pendingCommands.length - MAX_PENDING_COMMANDS);
12
12
  }
13
13
  }
14
+ async function dispatchCommand(cmd) {
15
+ const seq = nextSeq++;
16
+ const annotated = { ...cmd, seq };
17
+ let delivered = false;
18
+ for (const handler of subscribers) {
19
+ if (handler(annotated) !== false) delivered = true;
20
+ }
21
+ if (!delivered) enqueuePending(annotated);
22
+ return { seq, status: "ok" };
23
+ }
14
24
  return {
15
25
  async getState() {
16
26
  return state;
@@ -19,14 +29,10 @@ function createInMemoryBridge() {
19
29
  state = s;
20
30
  },
21
31
  async postCommand(cmd) {
22
- const seq = nextSeq++;
23
- const annotated = { ...cmd, seq };
24
- let delivered = false;
25
- for (const handler of subscribers) {
26
- if (handler(annotated) !== false) delivered = true;
27
- }
28
- if (!delivered) enqueuePending(annotated);
29
- return { seq, status: "ok" };
32
+ return dispatchCommand(cmd);
33
+ },
34
+ async emitUiEffect(cmd) {
35
+ return dispatchCommand(cmd);
30
36
  },
31
37
  subscribeCommands(handler) {
32
38
  subscribers.add(handler);
@@ -394,7 +400,7 @@ async function validateExistingPath(workspaceRoot, relPath) {
394
400
  };
395
401
  }
396
402
  }
397
- function createGetUiStateTool(uiBridge) {
403
+ function createGetUiStateTool(workspaceBridge) {
398
404
  return {
399
405
  name: "get_ui_state",
400
406
  readinessRequirements: ["ui-bridge"],
@@ -420,7 +426,7 @@ function createGetUiStateTool(uiBridge) {
420
426
  },
421
427
  async execute() {
422
428
  try {
423
- const state = await uiBridge.getState();
429
+ const state = await workspaceBridge.getState();
424
430
  return {
425
431
  content: [{ type: "text", text: JSON.stringify(state ?? {}) }],
426
432
  details: state
@@ -450,7 +456,7 @@ function isVerified(kind, params, state) {
450
456
  }
451
457
  return true;
452
458
  }
453
- function createExecUiTool(uiBridge, opts = {}) {
459
+ function createExecUiTool(workspaceBridge, opts = {}) {
454
460
  const { workspaceRoot, resolvePathKind } = opts;
455
461
  const verifyDelayMs = opts.verifyDelayMs ?? 250;
456
462
  const verifyRetries = opts.verifyRetries ?? 20;
@@ -607,7 +613,7 @@ function createExecUiTool(uiBridge, opts = {}) {
607
613
  }
608
614
  try {
609
615
  const command = { kind: effectiveKind, params: cmdParams };
610
- const result = await uiBridge.postCommand(command);
616
+ const result = await workspaceBridge.emitUiEffect(command);
611
617
  if (result.status === "error") {
612
618
  return {
613
619
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -617,11 +623,11 @@ function createExecUiTool(uiBridge, opts = {}) {
617
623
  }
618
624
  if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
619
625
  await new Promise((r) => setTimeout(r, verifyDelayMs));
620
- let uiState = await uiBridge.getState();
626
+ let uiState = await workspaceBridge.getState();
621
627
  for (let i = 0; i < verifyRetries; i++) {
622
628
  if (uiState === null || isVerified(effectiveKind, cmdParams, uiState)) break;
623
629
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
624
- uiState = await uiBridge.getState();
630
+ uiState = await workspaceBridge.getState();
625
631
  }
626
632
  const combined = { ...result, uiState };
627
633
  return {
@@ -640,8 +646,1269 @@ function createExecUiTool(uiBridge, opts = {}) {
640
646
  }
641
647
  };
642
648
  }
643
- function createWorkspaceUiTools(uiBridge, opts = {}) {
644
- return [createGetUiStateTool(uiBridge), createExecUiTool(uiBridge, opts)];
649
+ function createWorkspaceUiTools(workspaceBridge, opts = {}) {
650
+ return [createGetUiStateTool(workspaceBridge), createExecUiTool(workspaceBridge, opts)];
651
+ }
652
+
653
+ // src/shared/workspace-bridge-rpc.ts
654
+ var WORKSPACE_BRIDGE_URL_ENV = "BORING_WORKSPACE_BRIDGE_URL";
655
+ var WORKSPACE_BRIDGE_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN";
656
+ var WORKSPACE_BRIDGE_TOKEN_URL_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN_URL";
657
+ var WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_REFRESH_TOKEN";
658
+ var WORKSPACE_BRIDGE_DISABLED_ENV = "BORING_WORKSPACE_BRIDGE_DISABLED";
659
+ var WorkspaceBridgeErrorCode = /* @__PURE__ */ ((WorkspaceBridgeErrorCode2) => {
660
+ WorkspaceBridgeErrorCode2["OpNotFound"] = "BRIDGE_OP_NOT_FOUND";
661
+ WorkspaceBridgeErrorCode2["DuplicateOp"] = "BRIDGE_DUPLICATE_OP";
662
+ WorkspaceBridgeErrorCode2["CallerNotAllowed"] = "BRIDGE_CALLER_NOT_ALLOWED";
663
+ WorkspaceBridgeErrorCode2["AuthRequired"] = "BRIDGE_AUTH_REQUIRED";
664
+ WorkspaceBridgeErrorCode2["CapabilityDenied"] = "BRIDGE_CAPABILITY_DENIED";
665
+ WorkspaceBridgeErrorCode2["ResourceScopeDenied"] = "BRIDGE_RESOURCE_SCOPE_DENIED";
666
+ WorkspaceBridgeErrorCode2["SchemaInvalid"] = "BRIDGE_SCHEMA_INVALID";
667
+ WorkspaceBridgeErrorCode2["OutputSchemaInvalid"] = "BRIDGE_OUTPUT_SCHEMA_INVALID";
668
+ WorkspaceBridgeErrorCode2["InputTooLarge"] = "BRIDGE_INPUT_TOO_LARGE";
669
+ WorkspaceBridgeErrorCode2["OutputTooLarge"] = "BRIDGE_OUTPUT_TOO_LARGE";
670
+ WorkspaceBridgeErrorCode2["Timeout"] = "BRIDGE_TIMEOUT";
671
+ WorkspaceBridgeErrorCode2["HandlerFailed"] = "BRIDGE_HANDLER_FAILED";
672
+ WorkspaceBridgeErrorCode2["IdempotencyRequired"] = "BRIDGE_IDEMPOTENCY_REQUIRED";
673
+ WorkspaceBridgeErrorCode2["IdempotencyConflict"] = "BRIDGE_IDEMPOTENCY_CONFLICT";
674
+ WorkspaceBridgeErrorCode2["ReplayRejected"] = "BRIDGE_REPLAY_REJECTED";
675
+ WorkspaceBridgeErrorCode2["RateLimited"] = "BRIDGE_RATE_LIMITED";
676
+ WorkspaceBridgeErrorCode2["InvalidToken"] = "BRIDGE_INVALID_TOKEN";
677
+ WorkspaceBridgeErrorCode2["ExpiredToken"] = "BRIDGE_EXPIRED_TOKEN";
678
+ WorkspaceBridgeErrorCode2["InvalidRequest"] = "BRIDGE_INVALID_REQUEST";
679
+ WorkspaceBridgeErrorCode2["UnsupportedRuntime"] = "BRIDGE_UNSUPPORTED_RUNTIME";
680
+ return WorkspaceBridgeErrorCode2;
681
+ })(WorkspaceBridgeErrorCode || {});
682
+ function createWorkspaceBridgeError(code, message, details) {
683
+ return details === void 0 ? { code, message } : { code, message, details };
684
+ }
685
+
686
+ // src/server/workspaceBridge/json.ts
687
+ function stableStringify(value) {
688
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
689
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
690
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
691
+ }
692
+ function measureJsonBytes(value) {
693
+ try {
694
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
695
+ } catch {
696
+ return Number.POSITIVE_INFINITY;
697
+ }
698
+ }
699
+
700
+ // src/server/workspaceBridge/registry.ts
701
+ var RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES = ["workspace-files.v1."];
702
+ var BRIDGE_CALLER_CLASSES = /* @__PURE__ */ new Set(["browser", "runtime", "server"]);
703
+ var BRIDGE_IDEMPOTENCY_POLICIES = /* @__PURE__ */ new Set(["none", "required", "request-id"]);
704
+ function validateWorkspaceBridgeOperationDefinition(definition) {
705
+ if (!definition || typeof definition !== "object") {
706
+ throw invalidDefinition("WorkspaceBridge operation definition must be an object");
707
+ }
708
+ if (typeof definition.op !== "string" || definition.op.trim().length === 0) {
709
+ throw invalidDefinition("WorkspaceBridge operation definition op must be a non-empty string");
710
+ }
711
+ const reservedPrefix = RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES.find((prefix) => definition.op.startsWith(prefix));
712
+ if (reservedPrefix) {
713
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} uses reserved prefix ${reservedPrefix}`);
714
+ }
715
+ if (!Number.isInteger(definition.version) || definition.version < 1) {
716
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} version must be a positive integer`);
717
+ }
718
+ if (typeof definition.owner !== "string" || definition.owner.trim().length === 0) {
719
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} owner must be a non-empty string`);
720
+ }
721
+ if (!Array.isArray(definition.callerClassesAllowed) || definition.callerClassesAllowed.length === 0) {
722
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed must be a non-empty array`);
723
+ }
724
+ for (const callerClass of definition.callerClassesAllowed) {
725
+ if (!BRIDGE_CALLER_CLASSES.has(callerClass)) {
726
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed contains invalid caller class`);
727
+ }
728
+ }
729
+ if (!Array.isArray(definition.requiredCapabilities)) {
730
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must be an array`);
731
+ }
732
+ if (definition.allowCrossWorkspace !== void 0 && typeof definition.allowCrossWorkspace !== "boolean") {
733
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} allowCrossWorkspace must be a boolean when provided`);
734
+ }
735
+ for (const capability of definition.requiredCapabilities) {
736
+ if (typeof capability !== "string" || capability.trim().length === 0) {
737
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must contain non-empty strings`);
738
+ }
739
+ }
740
+ validateSchemaDefinition(definition.op, "inputSchema", definition.inputSchema, true);
741
+ validateSchemaDefinition(definition.op, "outputSchema", definition.outputSchema, false);
742
+ assertPositiveFiniteNumber(definition.op, "timeoutMs", definition.timeoutMs);
743
+ assertPositiveFiniteNumber(definition.op, "maxInputBytes", definition.maxInputBytes);
744
+ assertPositiveFiniteNumber(definition.op, "maxOutputBytes", definition.maxOutputBytes);
745
+ if (!BRIDGE_IDEMPOTENCY_POLICIES.has(definition.idempotencyPolicy)) {
746
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} idempotencyPolicy is invalid`);
747
+ }
748
+ }
749
+ function validateSchemaDefinition(op, field, schema, required) {
750
+ if (schema === void 0) {
751
+ if (required) throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} is required`);
752
+ return;
753
+ }
754
+ if (typeof schema !== "object" && typeof schema !== "function" || schema === null) {
755
+ throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a schema object`);
756
+ }
757
+ if (hasSafeParse(schema)) return;
758
+ validateJsonSchemaDefinition(schema, `${op} ${field}`);
759
+ }
760
+ var SUPPORTED_JSON_SCHEMA_KEYS = /* @__PURE__ */ new Set(["type", "properties", "required", "items", "enum", "const", "additionalProperties", "description", "title"]);
761
+ function validateJsonSchemaDefinition(schema, label) {
762
+ if (!isPlainSchemaObject(schema)) {
763
+ throw invalidDefinition(`WorkspaceBridge operation ${label} must be a schema object`);
764
+ }
765
+ for (const key of Object.keys(schema)) {
766
+ if (!SUPPORTED_JSON_SCHEMA_KEYS.has(key)) {
767
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.${key} is not supported by the bridge schema subset`);
768
+ }
769
+ }
770
+ if (!isSupportedJsonSchemaType(schema.type)) {
771
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.type must be a supported JSON schema type`);
772
+ }
773
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((item) => typeof item !== "string"))) {
774
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.required must be an array of strings`);
775
+ }
776
+ if (schema.required !== void 0 && schema.type !== "object") {
777
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.required is only supported for object schemas`);
778
+ }
779
+ if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
780
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.enum must be an array`);
781
+ }
782
+ if (schema.additionalProperties !== void 0 && schema.additionalProperties !== false && schema.additionalProperties !== true) {
783
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties must be boolean when provided`);
784
+ }
785
+ if (schema.additionalProperties !== void 0 && schema.type !== "object") {
786
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties is only supported for object schemas`);
787
+ }
788
+ if (schema.properties !== void 0) {
789
+ if (schema.type !== "object") throw invalidDefinition(`WorkspaceBridge operation ${label}.properties is only supported for object schemas`);
790
+ if (!isRecord(schema.properties)) throw invalidDefinition(`WorkspaceBridge operation ${label}.properties must be an object`);
791
+ for (const [key, child] of Object.entries(schema.properties)) validateJsonSchemaDefinition(child, `${label}.properties.${key}`);
792
+ }
793
+ if (schema.items !== void 0) {
794
+ if (schema.type !== "array") throw invalidDefinition(`WorkspaceBridge operation ${label}.items is only supported for array schemas`);
795
+ validateJsonSchemaDefinition(schema.items, `${label}.items`);
796
+ }
797
+ }
798
+ function isSupportedJsonSchemaType(type) {
799
+ return type === "object" || type === "array" || type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "null";
800
+ }
801
+ function hasSafeParse(schema) {
802
+ return (typeof schema === "object" || typeof schema === "function") && schema !== null && typeof schema.safeParse === "function";
803
+ }
804
+ function isPlainSchemaObject(value) {
805
+ return isRecord(value);
806
+ }
807
+ function isRecord(value) {
808
+ return !!value && typeof value === "object" && !Array.isArray(value);
809
+ }
810
+ function hasOwn(record, key) {
811
+ return Object.prototype.hasOwnProperty.call(record, key);
812
+ }
813
+ function assertPositiveFiniteNumber(op, field, value) {
814
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
815
+ throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a positive finite number`);
816
+ }
817
+ }
818
+ function invalidDefinition(message) {
819
+ throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, message);
820
+ }
821
+ var WorkspaceBridgeRegistry = class {
822
+ handlers = /* @__PURE__ */ new Map();
823
+ logger;
824
+ ownerWorkspaceId;
825
+ constructor(options = {}) {
826
+ if (options.ownerWorkspaceId !== void 0 && options.ownerWorkspaceId.trim().length === 0) {
827
+ throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge registry ownerWorkspaceId must be non-empty when provided");
828
+ }
829
+ this.logger = options.logger;
830
+ this.ownerWorkspaceId = options.ownerWorkspaceId;
831
+ }
832
+ registerHandler(definition, handler, options = {}) {
833
+ validateWorkspaceBridgeOperationDefinition(definition);
834
+ const existing = this.handlers.get(definition.op);
835
+ if (existing && !options.replace) {
836
+ throw createWorkspaceBridgeError(
837
+ "BRIDGE_DUPLICATE_OP" /* DuplicateOp */,
838
+ `WorkspaceBridge operation is already registered: ${definition.op}`
839
+ );
840
+ }
841
+ this.handlers.set(definition.op, {
842
+ definition,
843
+ handler
844
+ });
845
+ }
846
+ getDefinition(op) {
847
+ return this.handlers.get(op)?.definition;
848
+ }
849
+ listDefinitions() {
850
+ return Array.from(this.handlers.values(), ({ definition }) => definition);
851
+ }
852
+ async call(request, context, options = {}) {
853
+ const requestId = request.requestId ?? context.requestId ?? createRequestId();
854
+ const registered = this.handlers.get(request.op);
855
+ if (!registered) {
856
+ return this.failure(request.op, requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
857
+ }
858
+ const { definition, handler } = registered;
859
+ const logBase = {
860
+ requestId,
861
+ op: request.op,
862
+ callerClass: context.callerClass,
863
+ workspaceId: context.workspaceId,
864
+ sessionId: context.sessionId,
865
+ pluginId: context.pluginId,
866
+ tokenId: context.tokenId,
867
+ capabilities: context.capabilities,
868
+ actor: context.actor
869
+ };
870
+ if (!definition.callerClassesAllowed.includes(context.callerClass)) {
871
+ return this.failure(request.op, requestId, "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */, "Caller class is not allowed for operation", logBase);
872
+ }
873
+ const expectedWorkspaceId = options.expectedWorkspaceId ?? this.ownerWorkspaceId;
874
+ if (expectedWorkspaceId && context.workspaceId !== expectedWorkspaceId && definition.allowCrossWorkspace !== true) {
875
+ return this.failure(request.op, requestId, "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */, "Caller workspace is not authorized for this bridge registry", {
876
+ ...logBase,
877
+ expectedWorkspaceId
878
+ });
879
+ }
880
+ const missingCapability = definition.requiredCapabilities.find((capability) => !context.capabilities.includes(capability));
881
+ if (missingCapability) {
882
+ return this.failure(request.op, requestId, "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Caller is missing a required capability", {
883
+ ...logBase,
884
+ missingCapability
885
+ });
886
+ }
887
+ const inputBytes = measureJsonBytes(request.input);
888
+ if (inputBytes > definition.maxInputBytes) {
889
+ return this.failure(request.op, requestId, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "Bridge input exceeds operation limit", {
890
+ ...logBase,
891
+ inputBytes,
892
+ maxInputBytes: definition.maxInputBytes
893
+ });
894
+ }
895
+ const inputValidation = validateSchema(definition.inputSchema, request.input);
896
+ if (!inputValidation.success) {
897
+ return this.failure(request.op, requestId, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "Bridge input failed schema validation", {
898
+ ...logBase,
899
+ schemaMessage: inputValidation.message
900
+ });
901
+ }
902
+ const controller = new AbortController();
903
+ const abortFromCaller = () => controller.abort(context.signal?.reason);
904
+ if (context.signal?.aborted) abortFromCaller();
905
+ else context.signal?.addEventListener("abort", abortFromCaller, { once: true });
906
+ const timeoutResult = /* @__PURE__ */ Symbol("workspace-bridge-timeout");
907
+ let timeout;
908
+ try {
909
+ this.logger?.debug?.("workspace bridge call started", logBase);
910
+ const handlerPromise = Promise.resolve().then(() => handler({
911
+ input: request.input,
912
+ context: { ...context, requestId },
913
+ definition,
914
+ signal: controller.signal,
915
+ emitUiEffect: context.emitUiEffect
916
+ }));
917
+ const timeoutPromise = new Promise((resolve6) => {
918
+ timeout = setTimeout(() => {
919
+ controller.abort("timeout");
920
+ resolve6(timeoutResult);
921
+ }, definition.timeoutMs);
922
+ });
923
+ const output = await Promise.race([handlerPromise, timeoutPromise]);
924
+ if (output === timeoutResult) {
925
+ return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
926
+ }
927
+ const outputValidation = definition.outputSchema ? validateSchema(definition.outputSchema, output) : { success: true };
928
+ if (!outputValidation.success) {
929
+ return this.failure(request.op, requestId, "BRIDGE_OUTPUT_SCHEMA_INVALID" /* OutputSchemaInvalid */, "Bridge output failed schema validation", {
930
+ ...logBase,
931
+ schemaMessage: outputValidation.message
932
+ });
933
+ }
934
+ const outputBytes = measureJsonBytes(output);
935
+ if (outputBytes > definition.maxOutputBytes) {
936
+ return this.failure(request.op, requestId, "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */, "Bridge output exceeds operation limit", {
937
+ ...logBase,
938
+ outputBytes,
939
+ maxOutputBytes: definition.maxOutputBytes
940
+ });
941
+ }
942
+ this.logger?.info?.("workspace bridge call completed", logBase);
943
+ return { ok: true, op: request.op, requestId, output };
944
+ } catch (err) {
945
+ if (controller.signal.aborted && controller.signal.reason === "timeout") {
946
+ return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
947
+ }
948
+ const bridgeError = isWorkspaceBridgeError(err) ? err : void 0;
949
+ this.logger?.error?.("workspace bridge call failed", {
950
+ ...logBase,
951
+ errorName: err instanceof Error ? err.name : typeof err,
952
+ errorCode: bridgeError?.code
953
+ });
954
+ return this.failure(request.op, requestId, bridgeError?.code ?? "BRIDGE_HANDLER_FAILED" /* HandlerFailed */, bridgeError?.message ?? "Bridge handler failed", logBase);
955
+ } finally {
956
+ if (timeout) clearTimeout(timeout);
957
+ context.signal?.removeEventListener("abort", abortFromCaller);
958
+ }
959
+ }
960
+ failure(op, requestId, code, message, logFields) {
961
+ const error = createWorkspaceBridgeError(code, message);
962
+ this.logger?.warn?.("workspace bridge call rejected", {
963
+ ...logFields,
964
+ op,
965
+ requestId,
966
+ errorCode: code
967
+ });
968
+ return { ok: false, op, requestId, error };
969
+ }
970
+ };
971
+ function createWorkspaceBridgeRegistry(options = {}) {
972
+ return new WorkspaceBridgeRegistry(options);
973
+ }
974
+ var WORKSPACE_BRIDGE_ERROR_CODES = new Set(Object.values(WorkspaceBridgeErrorCode));
975
+ function isWorkspaceBridgeError(err) {
976
+ if (!err || typeof err !== "object") return false;
977
+ const candidate = err;
978
+ return typeof candidate.code === "string" && WORKSPACE_BRIDGE_ERROR_CODES.has(candidate.code) && typeof candidate.message === "string";
979
+ }
980
+ function validateSchema(schema, value) {
981
+ if (schema === void 0) return { success: true };
982
+ if (hasSafeParse(schema)) {
983
+ const result = schema.safeParse(value);
984
+ return result.success ? { success: true } : { success: false, message: result.error?.message };
985
+ }
986
+ if (isPlainSchemaObject(schema)) return validateJsonSchema(schema, value, "$");
987
+ return { success: false, message: "Unsupported schema" };
988
+ }
989
+ function validateJsonSchema(schema, value, path) {
990
+ if (schema.const !== void 0 && !jsonEqual(value, schema.const)) {
991
+ return { success: false, message: `${path}: Expected const value` };
992
+ }
993
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(value, candidate))) {
994
+ return { success: false, message: `${path}: Expected one of enum values` };
995
+ }
996
+ const typeValidation = validateJsonSchemaType(schema.type, value, path);
997
+ if (!typeValidation.success) return typeValidation;
998
+ if (schema.type === "object") {
999
+ if (!isRecord(value)) return { success: false, message: `${path}: Expected object` };
1000
+ const properties = isRecord(schema.properties) ? schema.properties : {};
1001
+ const required = Array.isArray(schema.required) ? schema.required.filter((item) => typeof item === "string") : [];
1002
+ for (const key of required) {
1003
+ if (!hasOwn(value, key)) return { success: false, message: `${path}.${key}: Required property missing` };
1004
+ }
1005
+ if (schema.additionalProperties === false) {
1006
+ for (const key of Object.keys(value)) {
1007
+ if (!hasOwn(properties, key)) return { success: false, message: `${path}.${key}: Additional properties are not allowed` };
1008
+ }
1009
+ }
1010
+ for (const [key, child] of Object.entries(properties)) {
1011
+ if (hasOwn(value, key)) {
1012
+ const childResult = validateJsonSchema(child, value[key], `${path}.${key}`);
1013
+ if (!childResult.success) return childResult;
1014
+ }
1015
+ }
1016
+ }
1017
+ if (schema.type === "array" && schema.items !== void 0) {
1018
+ if (!Array.isArray(value)) return { success: false, message: `${path}: Expected array` };
1019
+ for (let i = 0; i < value.length; i += 1) {
1020
+ const childResult = validateJsonSchema(schema.items, value[i], `${path}[${i}]`);
1021
+ if (!childResult.success) return childResult;
1022
+ }
1023
+ }
1024
+ return { success: true };
1025
+ }
1026
+ function validateJsonSchemaType(type, value, path) {
1027
+ switch (type) {
1028
+ case "object":
1029
+ return isRecord(value) ? { success: true } : { success: false, message: `${path}: Expected object` };
1030
+ case "array":
1031
+ return Array.isArray(value) ? { success: true } : { success: false, message: `${path}: Expected array` };
1032
+ case "string":
1033
+ return typeof value === "string" ? { success: true } : { success: false, message: `${path}: Expected string` };
1034
+ case "number":
1035
+ return typeof value === "number" && Number.isFinite(value) ? { success: true } : { success: false, message: `${path}: Expected number` };
1036
+ case "integer":
1037
+ return Number.isInteger(value) ? { success: true } : { success: false, message: `${path}: Expected integer` };
1038
+ case "boolean":
1039
+ return typeof value === "boolean" ? { success: true } : { success: false, message: `${path}: Expected boolean` };
1040
+ case "null":
1041
+ return value === null ? { success: true } : { success: false, message: `${path}: Expected null` };
1042
+ default:
1043
+ return { success: false, message: `${path}: Unsupported schema type` };
1044
+ }
1045
+ }
1046
+ function jsonEqual(a, b) {
1047
+ return stableStringify(a) === stableStringify(b);
1048
+ }
1049
+ function createRequestId() {
1050
+ return `bridge_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1051
+ }
1052
+
1053
+ // src/server/workspaceBridge/authPolicy.ts
1054
+ function createBrowserBridgeAuthPolicy(options) {
1055
+ return {
1056
+ async resolve(input) {
1057
+ if (input.callerClass !== "browser") {
1058
+ throw createWorkspaceBridgeError(
1059
+ "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
1060
+ "Browser auth policy only accepts browser callers"
1061
+ );
1062
+ }
1063
+ ensureCallerAllowed(input.definition, "browser");
1064
+ ensureBrowserRequestAllowed(input, options);
1065
+ const principal = await options.getPrincipal(input);
1066
+ if (!principal) {
1067
+ throw createWorkspaceBridgeError(
1068
+ "BRIDGE_AUTH_REQUIRED" /* AuthRequired */,
1069
+ "Browser bridge caller is not authenticated"
1070
+ );
1071
+ }
1072
+ const grant = await options.authorizeWorkspace({
1073
+ principal,
1074
+ workspaceId: input.workspaceId,
1075
+ sessionId: input.sessionId,
1076
+ definition: input.definition,
1077
+ request: input.request
1078
+ });
1079
+ if (!grant.allowed) {
1080
+ throw createWorkspaceBridgeError(
1081
+ "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */,
1082
+ "Browser bridge caller is not authorized for workspace"
1083
+ );
1084
+ }
1085
+ const capabilities = grant.capabilities;
1086
+ ensureCapabilities(capabilities, input.requiredCapabilities ?? input.definition.requiredCapabilities);
1087
+ const context = makeContext({
1088
+ callerClass: "browser",
1089
+ workspaceId: input.workspaceId,
1090
+ sessionId: input.sessionId,
1091
+ pluginId: input.pluginId,
1092
+ capabilities,
1093
+ actor: {
1094
+ actorKind: "human",
1095
+ performedBy: {
1096
+ label: principal.email ? `user:${principal.email}` : `user:${principal.userId}`,
1097
+ id: principal.userId
1098
+ }
1099
+ }
1100
+ });
1101
+ return {
1102
+ context,
1103
+ effectiveCapabilities: capabilities,
1104
+ principal,
1105
+ resourceScope: {
1106
+ workspaceId: input.workspaceId,
1107
+ sessionId: input.sessionId,
1108
+ role: grant.role,
1109
+ ...grant.resourceScope
1110
+ }
1111
+ };
1112
+ }
1113
+ };
1114
+ }
1115
+ function createLocalCliBridgeAuthPolicy(options) {
1116
+ return {
1117
+ resolve(input) {
1118
+ if (input.callerClass !== "browser") {
1119
+ throw createWorkspaceBridgeError(
1120
+ "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
1121
+ "Local CLI auth policy only accepts browser callers"
1122
+ );
1123
+ }
1124
+ ensureCallerAllowed(input.definition, "browser");
1125
+ if (input.workspaceId !== options.workspaceId) {
1126
+ throw createWorkspaceBridgeError(
1127
+ "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */,
1128
+ "Local CLI bridge caller is not authorized for workspace"
1129
+ );
1130
+ }
1131
+ const capabilities = options.capabilities ?? input.definition.requiredCapabilities;
1132
+ ensureCapabilities(capabilities, input.requiredCapabilities ?? input.definition.requiredCapabilities);
1133
+ const context = makeContext({
1134
+ callerClass: "browser",
1135
+ workspaceId: input.workspaceId,
1136
+ sessionId: input.sessionId,
1137
+ pluginId: input.pluginId,
1138
+ capabilities,
1139
+ actor: { actorKind: "human", performedBy: { label: "local-cli:user" } }
1140
+ });
1141
+ return {
1142
+ context,
1143
+ effectiveCapabilities: capabilities,
1144
+ principal: { userId: "local-cli" },
1145
+ resourceScope: { workspaceId: input.workspaceId, sessionId: input.sessionId }
1146
+ };
1147
+ }
1148
+ };
1149
+ }
1150
+ function ensureCallerAllowed(definition, callerClass) {
1151
+ if (!definition.callerClassesAllowed.includes(callerClass)) {
1152
+ throw createWorkspaceBridgeError(
1153
+ "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
1154
+ "Bridge caller class is not allowed for operation"
1155
+ );
1156
+ }
1157
+ }
1158
+ function ensureCapabilities(actual, required) {
1159
+ const missing = required.find((capability) => !actual.includes(capability));
1160
+ if (missing) {
1161
+ throw createWorkspaceBridgeError(
1162
+ "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */,
1163
+ "Bridge caller is missing a required capability"
1164
+ );
1165
+ }
1166
+ }
1167
+ function ensureBrowserRequestAllowed(input, options) {
1168
+ if (options.allowedOrigins && options.allowedOrigins.length > 0) {
1169
+ const origin = firstHeader(input.request?.headers, "origin");
1170
+ if (!origin || !options.allowedOrigins.includes(origin)) {
1171
+ throw createWorkspaceBridgeError(
1172
+ "BRIDGE_AUTH_REQUIRED" /* AuthRequired */,
1173
+ "Browser bridge request origin is not allowed"
1174
+ );
1175
+ }
1176
+ }
1177
+ if (options.requireCsrfHeader) {
1178
+ const csrf = firstHeader(input.request?.headers, "x-csrf-token");
1179
+ if (!csrf) {
1180
+ throw createWorkspaceBridgeError(
1181
+ "BRIDGE_AUTH_REQUIRED" /* AuthRequired */,
1182
+ "Browser bridge request is missing CSRF proof"
1183
+ );
1184
+ }
1185
+ }
1186
+ }
1187
+ function firstHeader(headers, name) {
1188
+ if (!headers) return void 0;
1189
+ const direct = headers[name] ?? headers[name.toLowerCase()];
1190
+ return Array.isArray(direct) ? direct[0] : direct;
1191
+ }
1192
+ function makeContext(context) {
1193
+ return {
1194
+ ...context,
1195
+ capabilities: [...context.capabilities],
1196
+ actor: sanitizeActor(context.actor)
1197
+ };
1198
+ }
1199
+ function sanitizeActor(actor) {
1200
+ return {
1201
+ actorKind: actor.actorKind,
1202
+ performedBy: actor.performedBy ? { label: actor.performedBy.label, id: actor.performedBy.id } : void 0,
1203
+ onBehalfOf: actor.onBehalfOf ? { label: actor.onBehalfOf.label, id: actor.onBehalfOf.id } : void 0
1204
+ };
1205
+ }
1206
+
1207
+ // src/server/workspaceBridge/httpRoutes.ts
1208
+ import { z as z3 } from "zod";
1209
+
1210
+ // src/server/workspaceBridge/idempotency.ts
1211
+ import { createHash } from "crypto";
1212
+ var InMemoryWorkspaceBridgeIdempotencyStore = class {
1213
+ records = /* @__PURE__ */ new Map();
1214
+ lastGcMs = 0;
1215
+ async begin(options) {
1216
+ const prepared = prepareIdempotency(options);
1217
+ if ("error" in prepared) return { action: "reject", error: prepared.error };
1218
+ const nowMs = options.nowMs ?? Date.now();
1219
+ if (nowMs - this.lastGcMs >= DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS) {
1220
+ await this.gc(nowMs);
1221
+ this.lastGcMs = nowMs;
1222
+ }
1223
+ const existing = this.records.get(prepared.scopeKey);
1224
+ if (existing && Date.parse(existing.expiresAt) > nowMs) {
1225
+ if (existing.inputHash !== prepared.inputHash) {
1226
+ return { action: "reject", error: replayRejectedError() };
1227
+ }
1228
+ return { action: "replay", record: existing };
1229
+ }
1230
+ this.records.set(prepared.scopeKey, createPendingRecord(prepared.scopeKey, prepared.inputHash, nowMs, options.ttlMs));
1231
+ return { action: "execute", scopeKey: prepared.scopeKey, inputHash: prepared.inputHash };
1232
+ }
1233
+ async complete(options) {
1234
+ const existing = this.records.get(options.scopeKey);
1235
+ if (!existing || existing.inputHash !== options.inputHash) return;
1236
+ const nowMs = options.nowMs ?? Date.now();
1237
+ this.records.set(options.scopeKey, {
1238
+ ...existing,
1239
+ // Only successful responses are completed; failures release the key
1240
+ // (see runWithWorkspaceBridgeIdempotency), so there is no "failed" record.
1241
+ status: "completed",
1242
+ response: options.response,
1243
+ updatedAt: new Date(nowMs).toISOString(),
1244
+ expiresAt: new Date(nowMs + (options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS)).toISOString()
1245
+ });
1246
+ }
1247
+ async release(scopeKey, inputHash) {
1248
+ const existing = this.records.get(scopeKey);
1249
+ if (existing && existing.inputHash === inputHash) this.records.delete(scopeKey);
1250
+ }
1251
+ async gc(nowMs = Date.now()) {
1252
+ let removed = 0;
1253
+ for (const [key, record] of this.records) {
1254
+ if (Date.parse(record.expiresAt) <= nowMs) {
1255
+ this.records.delete(key);
1256
+ removed++;
1257
+ }
1258
+ }
1259
+ return removed;
1260
+ }
1261
+ };
1262
+ async function runWithWorkspaceBridgeIdempotency(store, options, execute) {
1263
+ if (options.definition.idempotencyPolicy === "none") return await execute();
1264
+ if (!store) {
1265
+ return {
1266
+ ok: false,
1267
+ op: options.definition.op,
1268
+ requestId: options.request.requestId,
1269
+ error: createWorkspaceBridgeError(
1270
+ "BRIDGE_UNSUPPORTED_RUNTIME" /* UnsupportedRuntime */,
1271
+ "WorkspaceBridge mutation requires an atomic idempotency store"
1272
+ )
1273
+ };
1274
+ }
1275
+ const begin = await store.begin(options);
1276
+ if (begin.action === "reject") {
1277
+ return { ok: false, op: options.definition.op, requestId: options.request.requestId, error: begin.error };
1278
+ }
1279
+ if (begin.action === "replay") {
1280
+ return begin.record.response ?? {
1281
+ ok: false,
1282
+ op: options.definition.op,
1283
+ requestId: options.request.requestId,
1284
+ error: createWorkspaceBridgeError(
1285
+ "BRIDGE_IDEMPOTENCY_CONFLICT" /* IdempotencyConflict */,
1286
+ "WorkspaceBridge idempotency key is already pending"
1287
+ )
1288
+ };
1289
+ }
1290
+ const response = await execute();
1291
+ if (response.ok) {
1292
+ await store.complete({
1293
+ scopeKey: begin.scopeKey,
1294
+ inputHash: begin.inputHash,
1295
+ response,
1296
+ nowMs: options.nowMs,
1297
+ ttlMs: options.ttlMs
1298
+ });
1299
+ } else if (response.error.code !== "BRIDGE_TIMEOUT" /* Timeout */) {
1300
+ await store.release(begin.scopeKey, begin.inputHash);
1301
+ }
1302
+ return response;
1303
+ }
1304
+ var DEFAULT_IDEMPOTENCY_TTL_MS = 15 * 6e4;
1305
+ var DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS = 6e4;
1306
+ function prepareIdempotency(options) {
1307
+ const key = semanticKey(options);
1308
+ if (!key) {
1309
+ return {
1310
+ error: createWorkspaceBridgeError(
1311
+ "BRIDGE_IDEMPOTENCY_REQUIRED" /* IdempotencyRequired */,
1312
+ "WorkspaceBridge operation requires an idempotency key"
1313
+ )
1314
+ };
1315
+ }
1316
+ return {
1317
+ scopeKey: stableStringify({
1318
+ workspaceId: options.auth.workspaceId,
1319
+ sessionId: options.auth.sessionId,
1320
+ pluginId: options.auth.pluginId,
1321
+ op: options.definition.op,
1322
+ key
1323
+ }),
1324
+ inputHash: hashNormalizedInput(options.request.input)
1325
+ };
1326
+ }
1327
+ function semanticKey(options) {
1328
+ if (options.definition.idempotencyPolicy === "none") return "none";
1329
+ if (options.definition.idempotencyPolicy === "required") return options.request.idempotencyKey;
1330
+ if (options.definition.idempotencyPolicy === "request-id") return options.request.requestId ?? options.request.idempotencyKey;
1331
+ return void 0;
1332
+ }
1333
+ function hashNormalizedInput(input) {
1334
+ return hashString(stableStringify(input));
1335
+ }
1336
+ function createPendingRecord(scopeKey, inputHash, nowMs, ttlMs = DEFAULT_IDEMPOTENCY_TTL_MS) {
1337
+ const now = new Date(nowMs).toISOString();
1338
+ return {
1339
+ scopeKey,
1340
+ inputHash,
1341
+ status: "pending",
1342
+ createdAt: now,
1343
+ updatedAt: now,
1344
+ expiresAt: new Date(nowMs + ttlMs).toISOString()
1345
+ };
1346
+ }
1347
+ function replayRejectedError() {
1348
+ return createWorkspaceBridgeError(
1349
+ "BRIDGE_REPLAY_REJECTED" /* ReplayRejected */,
1350
+ "WorkspaceBridge idempotency key was reused with a different payload"
1351
+ );
1352
+ }
1353
+ function hashString(value) {
1354
+ return createHash("sha256").update(value).digest("hex");
1355
+ }
1356
+
1357
+ // src/server/workspaceBridge/runtimeToken.ts
1358
+ import { createHmac, randomUUID, timingSafeEqual } from "crypto";
1359
+ var WORKSPACE_BRIDGE_TOKEN_AUDIENCE = "workspace-bridge";
1360
+ var WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE = "workspace-bridge-refresh";
1361
+ var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 5 * 6e4;
1362
+ var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS = 60 * 6e4;
1363
+ var MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 15 * 6e4;
1364
+ function mintWorkspaceBridgeRuntimeToken(options) {
1365
+ return mintWorkspaceBridgeToken({
1366
+ ...options,
1367
+ audience: WORKSPACE_BRIDGE_TOKEN_AUDIENCE,
1368
+ ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS
1369
+ });
1370
+ }
1371
+ function mintWorkspaceBridgeRuntimeRefreshToken(options) {
1372
+ return mintWorkspaceBridgeToken({
1373
+ ...options,
1374
+ audience: WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE,
1375
+ // Refresh tokens intentionally outlive short call tokens, but remain
1376
+ // sandbox-bound by workspace/session/runtime/capabilities claims.
1377
+ ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS,
1378
+ tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(options.tokenTtlMs)
1379
+ });
1380
+ }
1381
+ function verifyWorkspaceBridgeRuntimeToken(token, options) {
1382
+ assertUsableSecret(options.secret);
1383
+ const claims = parseAndVerifyToken(token, options.secret);
1384
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
1385
+ ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_TOKEN_AUDIENCE, "Runtime bridge token");
1386
+ const missingCapability = (options.requiredCapabilities ?? []).find(
1387
+ (capability) => !claims.capabilities.includes(capability)
1388
+ );
1389
+ if (missingCapability) {
1390
+ throw bridgeTokenError("BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Runtime bridge token is missing a required capability");
1391
+ }
1392
+ const runtimeClaims = claims;
1393
+ return {
1394
+ claims: runtimeClaims,
1395
+ authContext: runtimeClaimsToBridgeAuthContext(runtimeClaims)
1396
+ };
1397
+ }
1398
+ function verifyWorkspaceBridgeRuntimeRefreshToken(token, options) {
1399
+ assertUsableSecret(options.secret);
1400
+ const claims = parseAndVerifyToken(token, options.secret);
1401
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
1402
+ ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE, "Runtime bridge refresh token");
1403
+ return { claims };
1404
+ }
1405
+ function clampWorkspaceBridgeRuntimeTokenTtlMs(ttlMs) {
1406
+ if (ttlMs === void 0) return void 0;
1407
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return void 0;
1408
+ return Math.min(ttlMs, MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS);
1409
+ }
1410
+ function runtimeClaimsToBridgeAuthContext(claims) {
1411
+ return {
1412
+ callerClass: "runtime",
1413
+ workspaceId: claims.workspaceId,
1414
+ sessionId: claims.sessionId,
1415
+ capabilities: claims.capabilities,
1416
+ tokenId: claims.jti,
1417
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
1418
+ actor: {
1419
+ actorKind: "agent",
1420
+ performedBy: {
1421
+ label: claims.runtimeId ? `runtime:${claims.runtimeId}` : "runtime:agent",
1422
+ id: claims.runtimeId
1423
+ },
1424
+ onBehalfOf: claims.sessionId ? { label: `session:${claims.sessionId}` } : void 0
1425
+ }
1426
+ };
1427
+ }
1428
+ function mintWorkspaceBridgeToken(options) {
1429
+ assertUsableSecret(options.secret);
1430
+ const nowMs = options.nowMs ?? Date.now();
1431
+ const claims = {
1432
+ aud: options.audience,
1433
+ workspaceId: options.workspaceId,
1434
+ sessionId: options.sessionId,
1435
+ runtimeId: options.runtimeId,
1436
+ capabilities: [...options.capabilities],
1437
+ iat: Math.floor(nowMs / 1e3),
1438
+ exp: Math.floor((nowMs + options.ttlMs) / 1e3),
1439
+ jti: options.jti ?? randomUUID(),
1440
+ ...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
1441
+ };
1442
+ return signClaims(claims, options.secret);
1443
+ }
1444
+ function ensureLiveTokenClaims(claims, now, expectedAudience, label) {
1445
+ if (claims.aud !== expectedAudience) {
1446
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} has invalid audience`);
1447
+ }
1448
+ if (claims.exp <= now) {
1449
+ throw bridgeTokenError("BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, `${label} has expired`);
1450
+ }
1451
+ if (claims.iat > now + 60) {
1452
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} is not valid yet`);
1453
+ }
1454
+ }
1455
+ function signClaims(claims, secret) {
1456
+ const header = { alg: "HS256", typ: "JWT" };
1457
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
1458
+ const encodedPayload = base64UrlEncode(JSON.stringify(claims));
1459
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
1460
+ return `${signingInput}.${hmac(signingInput, secret)}`;
1461
+ }
1462
+ function parseAndVerifyToken(token, secret) {
1463
+ const parts = token.split(".");
1464
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
1465
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token is malformed");
1466
+ }
1467
+ const [headerPart, payloadPart, signature] = parts;
1468
+ const signingInput = `${headerPart}.${payloadPart}`;
1469
+ const expected = hmac(signingInput, secret);
1470
+ if (!safeEqual(signature, expected)) {
1471
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token signature is invalid");
1472
+ }
1473
+ let header;
1474
+ let payload;
1475
+ try {
1476
+ header = JSON.parse(base64UrlDecode(headerPart));
1477
+ payload = JSON.parse(base64UrlDecode(payloadPart));
1478
+ } catch {
1479
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token payload is invalid");
1480
+ }
1481
+ if (!header || typeof header !== "object" || header.alg !== "HS256") {
1482
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token algorithm is invalid");
1483
+ }
1484
+ return parseClaims(payload);
1485
+ }
1486
+ function parseClaims(payload) {
1487
+ if (!payload || typeof payload !== "object") {
1488
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
1489
+ }
1490
+ const claims = payload;
1491
+ if (typeof claims.aud !== "string" || typeof claims.workspaceId !== "string" || !Array.isArray(claims.capabilities) || !claims.capabilities.every((capability) => typeof capability === "string") || typeof claims.iat !== "number" || typeof claims.exp !== "number" || typeof claims.jti !== "string") {
1492
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
1493
+ }
1494
+ if (claims.tokenTtlMs !== void 0 && (typeof claims.tokenTtlMs !== "number" || !Number.isFinite(claims.tokenTtlMs) || claims.tokenTtlMs <= 0)) {
1495
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
1496
+ }
1497
+ return {
1498
+ aud: claims.aud,
1499
+ workspaceId: claims.workspaceId,
1500
+ sessionId: optionalString(claims.sessionId),
1501
+ runtimeId: optionalString(claims.runtimeId),
1502
+ capabilities: [...claims.capabilities],
1503
+ iat: claims.iat,
1504
+ exp: claims.exp,
1505
+ jti: claims.jti,
1506
+ ...typeof claims.tokenTtlMs === "number" ? { tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) } : {}
1507
+ };
1508
+ }
1509
+ function bridgeTokenError(code, message) {
1510
+ return createWorkspaceBridgeError(code, message);
1511
+ }
1512
+ function assertUsableSecret(secret) {
1513
+ if (secret.length < 32) {
1514
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token secret is too short");
1515
+ }
1516
+ }
1517
+ function hmac(value, secret) {
1518
+ return createHmac("sha256", secret).update(value).digest("base64url");
1519
+ }
1520
+ function safeEqual(actual, expected) {
1521
+ const actualBuffer = Buffer.from(actual);
1522
+ const expectedBuffer = Buffer.from(expected);
1523
+ return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
1524
+ }
1525
+ function base64UrlEncode(value) {
1526
+ return Buffer.from(value).toString("base64url");
1527
+ }
1528
+ function base64UrlDecode(value) {
1529
+ return Buffer.from(value, "base64url").toString("utf8");
1530
+ }
1531
+ function optionalString(value) {
1532
+ return typeof value === "string" ? value : void 0;
1533
+ }
1534
+
1535
+ // src/server/workspaceBridge/refreshTokenStore.ts
1536
+ var InMemoryWorkspaceBridgeRuntimeRefreshTokenStore = class {
1537
+ revoked = /* @__PURE__ */ new Map();
1538
+ rateLimits = /* @__PURE__ */ new Map();
1539
+ lastGcMs = 0;
1540
+ revoke(jti, expiresAtMs) {
1541
+ this.gc();
1542
+ this.revoked.set(jti, expiresAtMs);
1543
+ this.rateLimits.delete(jti);
1544
+ }
1545
+ recordUse(options) {
1546
+ const nowMs = options.nowMs ?? Date.now();
1547
+ this.gc(nowMs);
1548
+ if (this.revoked.has(options.jti)) return { allowed: false, reason: "revoked" };
1549
+ if (options.maxUses <= 0) return { allowed: false, reason: "rate-limited", retryAfterMs: options.windowMs };
1550
+ const existing = this.rateLimits.get(options.jti);
1551
+ if (!existing || nowMs - existing.windowStartMs >= existing.windowMs || isExpired(existing.expiresAtMs, nowMs)) {
1552
+ this.rateLimits.set(options.jti, {
1553
+ windowStartMs: nowMs,
1554
+ windowMs: options.windowMs,
1555
+ count: 1,
1556
+ expiresAtMs: options.expiresAtMs
1557
+ });
1558
+ return { allowed: true };
1559
+ }
1560
+ if (existing.count >= options.maxUses) {
1561
+ return { allowed: false, reason: "rate-limited", retryAfterMs: Math.max(0, existing.windowStartMs + existing.windowMs - nowMs) };
1562
+ }
1563
+ existing.count += 1;
1564
+ existing.windowMs = options.windowMs;
1565
+ existing.expiresAtMs = options.expiresAtMs ?? existing.expiresAtMs;
1566
+ return { allowed: true };
1567
+ }
1568
+ gc(nowMs = Date.now()) {
1569
+ if (nowMs - this.lastGcMs < 6e4) return 0;
1570
+ this.lastGcMs = nowMs;
1571
+ let removed = 0;
1572
+ for (const [jti, record] of this.rateLimits) {
1573
+ if (isExpired(record.expiresAtMs, nowMs) || nowMs - record.windowStartMs >= record.windowMs) {
1574
+ this.rateLimits.delete(jti);
1575
+ removed += 1;
1576
+ }
1577
+ }
1578
+ for (const [jti, expiresAtMs] of this.revoked) {
1579
+ if (isExpired(expiresAtMs, nowMs)) {
1580
+ this.revoked.delete(jti);
1581
+ removed += 1;
1582
+ }
1583
+ }
1584
+ return removed;
1585
+ }
1586
+ };
1587
+ function isExpired(expiresAtMs, nowMs) {
1588
+ return expiresAtMs !== void 0 && expiresAtMs <= nowMs;
1589
+ }
1590
+
1591
+ // src/server/workspaceBridge/httpRoutes.ts
1592
+ var bridgeCallBodySchema = z3.object({
1593
+ op: z3.string().min(1),
1594
+ input: z3.unknown().default({}),
1595
+ requestId: z3.string().optional(),
1596
+ idempotencyKey: z3.string().optional()
1597
+ });
1598
+ var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES = 30;
1599
+ var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS = 6e4;
1600
+ function workspaceBridgeHttpRoutes(app, opts, done) {
1601
+ const defaultRefreshTokenStore = opts.runtimeRefreshTokenStore ?? new InMemoryWorkspaceBridgeRuntimeRefreshTokenStore();
1602
+ app.post("/api/v1/workspace-bridge/call", async (request, reply) => {
1603
+ reply.header("Cache-Control", "no-store");
1604
+ const contentType = String(request.headers["content-type"] ?? "");
1605
+ if (!contentType.toLowerCase().startsWith("application/json")) {
1606
+ return sendBridgeError(reply, 415, void 0, "BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge transport requires application/json");
1607
+ }
1608
+ const rawBodySize = measureJsonBytes(request.body);
1609
+ if (opts.maxBodyBytes && rawBodySize > opts.maxBodyBytes) {
1610
+ return sendBridgeError(reply, 413, void 0, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "WorkspaceBridge request body is too large");
1611
+ }
1612
+ const parsed = bridgeCallBodySchema.safeParse(request.body);
1613
+ if (!parsed.success) {
1614
+ return sendBridgeError(reply, 400, void 0, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "WorkspaceBridge request body is invalid");
1615
+ }
1616
+ const body = { ...parsed.data, input: parsed.data.input ?? {} };
1617
+ try {
1618
+ const registry = await resolveRegistry(request, body, opts);
1619
+ const definition = registry.getDefinition(body.op);
1620
+ if (!definition) {
1621
+ return sendBridgeError(reply, statusForBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */), body.requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
1622
+ }
1623
+ const authHeader = firstHeader2(request.headers.authorization);
1624
+ const authContext = authHeader?.startsWith("Bearer ") ? resolveRuntimeContext(authHeader.slice("Bearer ".length), opts, definition) : await resolveBrowserContext(request, opts, definition, body);
1625
+ const idempotencyStore = opts.getIdempotencyStore ? await opts.getIdempotencyStore(request, body) : opts.idempotencyStore;
1626
+ const expectedWorkspaceId = opts.getOwnerWorkspaceId ? await opts.getOwnerWorkspaceId(request, body, authContext) : opts.ownerWorkspaceId;
1627
+ const response = await runWithWorkspaceBridgeIdempotency(idempotencyStore, {
1628
+ definition,
1629
+ request: body,
1630
+ auth: authContext
1631
+ }, async () => await registry.call(body, authContext, { expectedWorkspaceId }));
1632
+ return await sendResponse(reply, response);
1633
+ } catch (err) {
1634
+ const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_HANDLER_FAILED" /* HandlerFailed */, "WorkspaceBridge transport failed");
1635
+ return sendBridgeError(reply, statusForBridgeError(bridgeError.code), body.requestId, bridgeError.code, bridgeError.message);
1636
+ }
1637
+ });
1638
+ app.post("/api/v1/workspace-bridge/token", async (request, reply) => {
1639
+ reply.header("Cache-Control", "no-store");
1640
+ const authHeader = firstHeader2(request.headers.authorization);
1641
+ if (!authHeader?.startsWith("Bearer ")) {
1642
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge refresh token is required");
1643
+ }
1644
+ if (!opts.runtimeTokenSecret || !opts.runtimeRefreshTokenSecret) {
1645
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge token refresh is not configured");
1646
+ }
1647
+ try {
1648
+ const nowMs = Date.now();
1649
+ const verified = verifyWorkspaceBridgeRuntimeRefreshToken(authHeader.slice("Bearer ".length), {
1650
+ secret: opts.runtimeRefreshTokenSecret,
1651
+ nowMs
1652
+ });
1653
+ const store = opts.getRuntimeRefreshTokenStore ? await opts.getRuntimeRefreshTokenStore(request, verified.claims) ?? defaultRefreshTokenStore : defaultRefreshTokenStore;
1654
+ const refreshUse = await store.recordUse({
1655
+ jti: verified.claims.jti,
1656
+ nowMs,
1657
+ maxUses: opts.refreshTokenRateLimit?.maxUses ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES,
1658
+ windowMs: opts.refreshTokenRateLimit?.windowMs ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS,
1659
+ expiresAtMs: verified.claims.exp * 1e3
1660
+ });
1661
+ if (!refreshUse.allowed) {
1662
+ if (refreshUse.reason === "revoked") {
1663
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is revoked");
1664
+ }
1665
+ reply.header("Retry-After", Math.ceil(refreshUse.retryAfterMs / 1e3).toString());
1666
+ return sendBridgeError(reply, 429, void 0, "BRIDGE_RATE_LIMITED" /* RateLimited */, "WorkspaceBridge refresh token rate limit exceeded");
1667
+ }
1668
+ const ttlMs = refreshMintTtlMs(verified.claims, Date.now());
1669
+ if (ttlMs === void 0) {
1670
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, "Runtime bridge refresh token has expired");
1671
+ }
1672
+ const token = mintWorkspaceBridgeRuntimeToken({
1673
+ secret: opts.runtimeTokenSecret,
1674
+ workspaceId: verified.claims.workspaceId,
1675
+ sessionId: verified.claims.sessionId,
1676
+ runtimeId: verified.claims.runtimeId,
1677
+ capabilities: verified.claims.capabilities,
1678
+ ttlMs
1679
+ });
1680
+ return reply.code(200).send({ ok: true, token });
1681
+ } catch (err) {
1682
+ const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is invalid");
1683
+ return sendBridgeError(reply, statusForBridgeError(bridgeError.code), void 0, bridgeError.code, bridgeError.message);
1684
+ }
1685
+ });
1686
+ done();
1687
+ }
1688
+ async function resolveRegistry(request, body, opts) {
1689
+ const registry = opts.getRegistry ? await opts.getRegistry(request, body) : opts.registry;
1690
+ if (!registry) {
1691
+ throw createWorkspaceBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge registry is not configured");
1692
+ }
1693
+ return registry;
1694
+ }
1695
+ function resolveRuntimeContext(token, opts, definition) {
1696
+ if (!opts.runtimeTokenSecret) {
1697
+ throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Runtime bridge token auth is not configured");
1698
+ }
1699
+ return verifyWorkspaceBridgeRuntimeToken(token, {
1700
+ secret: opts.runtimeTokenSecret,
1701
+ requiredCapabilities: definition.requiredCapabilities
1702
+ }).authContext;
1703
+ }
1704
+ async function resolveBrowserContext(request, opts, definition, body) {
1705
+ if (!opts.browserAuthPolicy) {
1706
+ throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Browser bridge auth is not configured");
1707
+ }
1708
+ const workspaceId = firstHeader2(request.headers["x-boring-workspace-id"]) ?? "default";
1709
+ const sessionId = firstHeader2(request.headers["x-boring-session-id"]);
1710
+ return (await opts.browserAuthPolicy.resolve({
1711
+ callerClass: "browser",
1712
+ definition,
1713
+ workspaceId,
1714
+ sessionId,
1715
+ request: { headers: request.headers, method: request.method, user: request.user },
1716
+ body
1717
+ })).context;
1718
+ }
1719
+ async function sendResponse(reply, response) {
1720
+ return reply.code(response.ok ? 200 : statusForBridgeError(response.error.code)).send(response);
1721
+ }
1722
+ function refreshMintTtlMs(claims, nowMs) {
1723
+ const requested = clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS;
1724
+ const remaining = claims.exp * 1e3 - nowMs;
1725
+ if (remaining <= 0) return void 0;
1726
+ return Math.min(requested, remaining);
1727
+ }
1728
+ function sendBridgeError(reply, status, requestId, code, message) {
1729
+ reply.header("Cache-Control", "no-store");
1730
+ return reply.code(status).send({ ok: false, requestId, error: { code, message } });
1731
+ }
1732
+ function statusForBridgeError(code) {
1733
+ if (code === "BRIDGE_AUTH_REQUIRED" /* AuthRequired */ || code === "BRIDGE_INVALID_TOKEN" /* InvalidToken */ || code === "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */) return 401;
1734
+ if (code === "BRIDGE_RATE_LIMITED" /* RateLimited */) return 429;
1735
+ if (code === "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */ || code === "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */ || code === "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */) return 403;
1736
+ if (code === "BRIDGE_OP_NOT_FOUND" /* OpNotFound */) return 404;
1737
+ if (code === "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */ || code === "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */) return 413;
1738
+ return 400;
1739
+ }
1740
+ function firstHeader2(value) {
1741
+ return Array.isArray(value) ? value[0] : value;
1742
+ }
1743
+ function isBridgeError(err) {
1744
+ return !!err && typeof err === "object" && "code" in err && "message" in err;
1745
+ }
1746
+
1747
+ // src/server/workspaceBridge/runtimeEnv.ts
1748
+ var BRIDGE_CALL_PATH = "/api/v1/workspace-bridge/call";
1749
+ var BRIDGE_TOKEN_PATH = "/api/v1/workspace-bridge/token";
1750
+ function createWorkspaceBridgeRuntimeEnvContribution(options) {
1751
+ const enabled = options.runtimeEnv?.enabled ?? Boolean(options.runtimeEnv?.bridgeUrl);
1752
+ if (!enabled) return void 0;
1753
+ const bridgeUrl = resolveBridgeCallUrl(options.runtimeEnv?.bridgeUrl);
1754
+ const tokenUrl = resolveBridgeTokenUrl(options.runtimeEnv?.bridgeUrl);
1755
+ const capabilities = options.runtimeEnv?.capabilities;
1756
+ return {
1757
+ id: "workspace-bridge-runtime-env",
1758
+ getEnv: (ctx) => {
1759
+ const runtimePlacement = resolveRuntimePlacement(ctx?.runtimeBundle, options.runtimePlacement);
1760
+ const disabledReason = validateRuntimeBridgeUrl({
1761
+ bridgeUrl,
1762
+ runtimePlacement,
1763
+ allowInsecureHttp: options.runtimeEnv?.allowInsecureHttp,
1764
+ hasRuntimeTokenSecret: Boolean(options.runtimeTokenSecret),
1765
+ hasCapabilities: Array.isArray(capabilities) && capabilities.length > 0
1766
+ });
1767
+ if (disabledReason) {
1768
+ return { [WORKSPACE_BRIDGE_DISABLED_ENV]: disabledReason };
1769
+ }
1770
+ const token = mintWorkspaceBridgeRuntimeToken({
1771
+ secret: options.runtimeTokenSecret,
1772
+ workspaceId: options.workspaceId,
1773
+ sessionId: options.runtimeEnv?.sessionId,
1774
+ runtimeId: options.runtimeMode,
1775
+ capabilities,
1776
+ ttlMs: options.runtimeEnv?.tokenTtlMs
1777
+ });
1778
+ const refreshToken = options.runtimeRefreshTokenSecret && tokenUrl && isRefreshTokenUrlSafe(tokenUrl) ? mintWorkspaceBridgeRuntimeRefreshToken({
1779
+ secret: options.runtimeRefreshTokenSecret,
1780
+ workspaceId: options.workspaceId,
1781
+ sessionId: options.runtimeEnv?.sessionId,
1782
+ runtimeId: options.runtimeMode,
1783
+ capabilities,
1784
+ ttlMs: options.runtimeEnv?.refreshTokenTtlMs,
1785
+ tokenTtlMs: options.runtimeEnv?.tokenTtlMs
1786
+ }) : void 0;
1787
+ return {
1788
+ [WORKSPACE_BRIDGE_URL_ENV]: bridgeUrl,
1789
+ [WORKSPACE_BRIDGE_TOKEN_ENV]: token,
1790
+ ...refreshToken ? {
1791
+ [WORKSPACE_BRIDGE_TOKEN_URL_ENV]: tokenUrl,
1792
+ [WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV]: refreshToken
1793
+ } : {},
1794
+ BORING_WORKSPACE_ID: options.workspaceId,
1795
+ BORING_AGENT_SESSION_ID: options.runtimeEnv?.sessionId ?? options.workspaceId
1796
+ };
1797
+ }
1798
+ };
1799
+ }
1800
+ function resolveBridgeCallUrl(value) {
1801
+ return resolveBridgeUrl(value, BRIDGE_CALL_PATH);
1802
+ }
1803
+ function resolveBridgeTokenUrl(value) {
1804
+ return resolveBridgeUrl(value, BRIDGE_TOKEN_PATH);
1805
+ }
1806
+ function resolveBridgeUrl(value, path) {
1807
+ if (!value?.trim()) return void 0;
1808
+ try {
1809
+ const url = new URL(value);
1810
+ if (url.pathname === "" || url.pathname === "/" || url.pathname === BRIDGE_CALL_PATH || url.pathname === BRIDGE_TOKEN_PATH) {
1811
+ url.pathname = path;
1812
+ }
1813
+ return url.toString();
1814
+ } catch {
1815
+ return void 0;
1816
+ }
1817
+ }
1818
+ function isRefreshTokenUrlSafe(value) {
1819
+ try {
1820
+ const url = new URL(value);
1821
+ return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
1822
+ } catch {
1823
+ return false;
1824
+ }
1825
+ }
1826
+ function isLoopbackHost(hostname) {
1827
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1828
+ }
1829
+ function resolveRuntimePlacement(runtimeBundle, fallback) {
1830
+ if (runtimeBundle?.filesystem?.kind === "remote-workspace" || runtimeBundle?.bash?.kind === "remote") return "remote";
1831
+ return fallback ?? "local";
1832
+ }
1833
+ function validateRuntimeBridgeUrl(options) {
1834
+ if (!options.bridgeUrl) return "bridge-url-missing";
1835
+ if (!options.hasRuntimeTokenSecret) return "runtime-token-secret-missing";
1836
+ if (!options.hasCapabilities) return "runtime-capabilities-missing";
1837
+ let url;
1838
+ try {
1839
+ url = new URL(options.bridgeUrl);
1840
+ } catch {
1841
+ return "bridge-url-invalid";
1842
+ }
1843
+ const isRemote = options.runtimePlacement === "remote";
1844
+ const isLocalhost = isLoopbackHost(url.hostname);
1845
+ if (isRemote && url.protocol !== "https:") return "remote-bridge-url-must-be-https";
1846
+ if (isRemote && isLocalhost) return "remote-bridge-url-must-not-be-localhost";
1847
+ if (url.protocol === "http:" && !options.allowInsecureHttp && !isLocalhost) return "remote-bridge-url-must-be-https";
1848
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "bridge-url-invalid";
1849
+ return void 0;
1850
+ }
1851
+
1852
+ // src/server/workspaceBridge/trustedDomainHandler.ts
1853
+ var DEFAULT_RESERVED_OP_PREFIXES = RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES;
1854
+ var VERSIONED_OP_PATTERN = /^[a-z][a-z0-9-]*\.v[1-9][0-9]*\.[a-z][a-z0-9.-]*$/;
1855
+ function defineTrustedDomainBridgeHandler(options) {
1856
+ validateTrustedDomainMetadata(options);
1857
+ return {
1858
+ definition: {
1859
+ op: options.op,
1860
+ version: options.version,
1861
+ owner: options.owner,
1862
+ callerClassesAllowed: [...options.callerClassesAllowed],
1863
+ requiredCapabilities: [...options.requiredCapabilities],
1864
+ inputSchema: options.inputSchema,
1865
+ ...options.outputSchema ? { outputSchema: options.outputSchema } : {},
1866
+ timeoutMs: options.timeoutMs ?? 5e3,
1867
+ maxInputBytes: options.maxInputBytes ?? 64 * 1024,
1868
+ maxOutputBytes: options.maxOutputBytes,
1869
+ idempotencyPolicy: options.idempotencyPolicy ?? "none"
1870
+ },
1871
+ handler: options.handler
1872
+ };
1873
+ }
1874
+ function validateTrustedDomainMetadata(options) {
1875
+ if (!options || typeof options !== "object") {
1876
+ throw invalid("Trusted domain bridge handler metadata is required");
1877
+ }
1878
+ if (!options.op?.trim()) throw invalid("Trusted domain bridge handler op is required");
1879
+ if (!Number.isInteger(options.version) || options.version < 1) throw invalid("Trusted domain bridge handler version is required");
1880
+ if (!options.owner?.trim()) throw invalid("Trusted domain bridge handler owner is required");
1881
+ if (!Array.isArray(options.callerClassesAllowed) || options.callerClassesAllowed.length === 0) {
1882
+ throw invalid("Trusted domain bridge handler callerClassesAllowed is required");
1883
+ }
1884
+ if (!Array.isArray(options.requiredCapabilities)) throw invalid("Trusted domain bridge handler requiredCapabilities is required");
1885
+ if (options.inputSchema === void 0) throw invalid("Trusted domain bridge handler inputSchema is required");
1886
+ if (typeof options.handler !== "function") throw invalid("Trusted domain bridge handler function is required");
1887
+ if (!Number.isFinite(options.maxOutputBytes) || options.maxOutputBytes <= 0) {
1888
+ throw invalid("Trusted domain bridge handler maxOutputBytes must be positive");
1889
+ }
1890
+ const policy = options.policy ?? {};
1891
+ const requireVersionedOp = policy.requireVersionedOp ?? true;
1892
+ if (requireVersionedOp && !VERSIONED_OP_PATTERN.test(options.op)) {
1893
+ throw invalid("Trusted domain bridge handler op must be versioned as domain.v1.action");
1894
+ }
1895
+ const reservedPrefixes = policy.reservedOpPrefixes ?? DEFAULT_RESERVED_OP_PREFIXES;
1896
+ const reservedPrefix = reservedPrefixes.find((prefix) => options.op.startsWith(prefix));
1897
+ if (reservedPrefix) {
1898
+ throw invalid(`Trusted domain bridge handler op uses reserved prefix ${reservedPrefix}`);
1899
+ }
1900
+ }
1901
+ function invalid(message) {
1902
+ throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, message);
1903
+ }
1904
+
1905
+ // src/server/workspaceBridge/runtimeCore.ts
1906
+ function createWorkspaceBridgeRuntimeCore(options = {}) {
1907
+ const registry = options.registry ?? createWorkspaceBridgeRegistry({ ownerWorkspaceId: options.ownerWorkspaceId });
1908
+ for (const entry of options.handlers ?? []) {
1909
+ registry.registerHandler(entry.definition, entry.handler);
1910
+ }
1911
+ return { registry };
645
1912
  }
646
1913
 
647
1914
  // src/server/plugins/piPackages.ts
@@ -734,6 +2001,26 @@ function validatePluginAssets(pluginId, assets) {
734
2001
  }
735
2002
  }
736
2003
  }
2004
+ function validateWorkspaceBridgeHandlers(pluginId, handlers) {
2005
+ for (let i = 0; i < handlers.length; i++) {
2006
+ const entry = handlers[i];
2007
+ if (!entry || typeof entry !== "object") {
2008
+ fail(pluginId, `workspaceBridgeHandlers[${i}] must be an object`);
2009
+ }
2010
+ if (!entry.definition || typeof entry.definition !== "object") {
2011
+ fail(pluginId, `workspaceBridgeHandlers[${i}].definition must be an object`);
2012
+ }
2013
+ try {
2014
+ validateWorkspaceBridgeOperationDefinition(entry.definition);
2015
+ } catch (error) {
2016
+ const message = error && typeof error === "object" && "message" in error ? String(error.message) : "invalid WorkspaceBridge operation definition";
2017
+ fail(pluginId, `workspaceBridgeHandlers[${i}].definition invalid: ${message}`);
2018
+ }
2019
+ if (typeof entry.handler !== "function") {
2020
+ fail(pluginId, `workspaceBridgeHandlers[${i}].handler must be a function`);
2021
+ }
2022
+ }
2023
+ }
737
2024
  function validateProvisioning(pluginId, provisioning) {
738
2025
  if (!provisioning || typeof provisioning !== "object") {
739
2026
  fail(pluginId, "provisioning must be an object");
@@ -853,6 +2140,12 @@ function validateServerPlugin(plugin) {
853
2140
  }
854
2141
  validatePluginAssets(plugin.id, plugin.assets);
855
2142
  }
2143
+ if (plugin.workspaceBridgeHandlers !== void 0) {
2144
+ if (!Array.isArray(plugin.workspaceBridgeHandlers)) {
2145
+ fail(plugin.id, "workspaceBridgeHandlers must be an array when provided");
2146
+ }
2147
+ validateWorkspaceBridgeHandlers(plugin.id, plugin.workspaceBridgeHandlers);
2148
+ }
856
2149
  if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
857
2150
  fail(plugin.id, "routes must be a Fastify plugin function when provided");
858
2151
  }
@@ -915,6 +2208,7 @@ function bootstrapServer(options) {
915
2208
  ...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
916
2209
  }));
917
2210
  const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
2211
+ const workspaceBridgeHandlers = finalPlugins.flatMap((p) => p.workspaceBridgeHandlers ?? []);
918
2212
  const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
919
2213
  return {
920
2214
  registered: finalPlugins.map((p) => p.id),
@@ -925,6 +2219,7 @@ function bootstrapServer(options) {
925
2219
  runtimePlugins,
926
2220
  provisioningContributions,
927
2221
  routeContributions,
2222
+ workspaceBridgeHandlers,
928
2223
  preservedUiStateKeys
929
2224
  };
930
2225
  }
@@ -1037,7 +2332,7 @@ function buildBoringSystemPrompt(opts) {
1037
2332
  }
1038
2333
 
1039
2334
  // src/server/agentPlugins/manager.ts
1040
- import { createHash } from "crypto";
2335
+ import { createHash as createHash2 } from "crypto";
1041
2336
  import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
1042
2337
  import { dirname as dirname6, isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve5 } from "path";
1043
2338
 
@@ -1053,7 +2348,7 @@ function isSafePluginRelativePath(value) {
1053
2348
  function issue(code, field, message) {
1054
2349
  return { code, field, message };
1055
2350
  }
1056
- function isRecord(value) {
2351
+ function isRecord2(value) {
1057
2352
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1058
2353
  }
1059
2354
  function validateStringArray(issues, value, field, pathLike) {
@@ -1076,7 +2371,7 @@ function validateStringArray(issues, value, field, pathLike) {
1076
2371
  var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
1077
2372
  function validateBoringField(issues, boring) {
1078
2373
  if (boring === void 0) return void 0;
1079
- if (!isRecord(boring)) {
2374
+ if (!isRecord2(boring)) {
1080
2375
  issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
1081
2376
  return void 0;
1082
2377
  }
@@ -1136,7 +2431,7 @@ function validatePiPackages2(issues, value) {
1136
2431
  }
1137
2432
  return;
1138
2433
  }
1139
- if (!isRecord(entry)) {
2434
+ if (!isRecord2(entry)) {
1140
2435
  issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
1141
2436
  return;
1142
2437
  }
@@ -1149,7 +2444,7 @@ function validatePiPackages2(issues, value) {
1149
2444
  }
1150
2445
  function validatePiField(issues, pi) {
1151
2446
  if (pi === void 0) return void 0;
1152
- if (!isRecord(pi)) {
2447
+ if (!isRecord2(pi)) {
1153
2448
  issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
1154
2449
  return void 0;
1155
2450
  }
@@ -1163,7 +2458,7 @@ function validatePiField(issues, pi) {
1163
2458
  }
1164
2459
  function validateBoringPluginManifest(raw) {
1165
2460
  const issues = [];
1166
- if (!isRecord(raw)) {
2461
+ if (!isRecord2(raw)) {
1167
2462
  return {
1168
2463
  valid: false,
1169
2464
  issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
@@ -1551,11 +2846,11 @@ function skillPathForPiLoader(path) {
1551
2846
  return existsSync4(join5(path, "SKILL.md")) ? dirname6(path) : path;
1552
2847
  }
1553
2848
  function preflightErrorId(pluginDir) {
1554
- return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
2849
+ return `preflight-${createHash2("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
1555
2850
  }
1556
2851
  function directorySignature(root) {
1557
2852
  if (!root || !existsSync4(root)) return "missing";
1558
- const hash = createHash("sha256");
2853
+ const hash = createHash2("sha256");
1559
2854
  const visited = /* @__PURE__ */ new Set();
1560
2855
  let rootReal;
1561
2856
  try {
@@ -1618,7 +2913,7 @@ function frontSignatureRoot(plugin) {
1618
2913
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot : dirname6(plugin.frontPath);
1619
2914
  }
1620
2915
  function pluginSignature(plugin) {
1621
- return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(JSON.stringify(plugin.source)).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join5(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname6(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
2916
+ return createHash2("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(JSON.stringify(plugin.source)).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join5(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname6(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
1622
2917
  }
1623
2918
  function computeRequiresRestart(previous, next) {
1624
2919
  if (!previous) return [];
@@ -2463,29 +3758,58 @@ async function runtimeBackendGateway(app, opts) {
2463
3758
  }
2464
3759
  export {
2465
3760
  BoringPluginAssetManager,
3761
+ DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS,
3762
+ DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS,
3763
+ InMemoryWorkspaceBridgeIdempotencyStore,
3764
+ InMemoryWorkspaceBridgeRuntimeRefreshTokenStore,
3765
+ MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS,
2466
3766
  RuntimeBackendError,
2467
3767
  RuntimeBackendRegistry,
3768
+ WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE,
3769
+ WORKSPACE_BRIDGE_TOKEN_AUDIENCE,
3770
+ WorkspaceBridgeErrorCode,
3771
+ WorkspaceBridgeRegistry,
2468
3772
  aggregatePluginPrompts,
2469
3773
  bootstrapServer,
2470
3774
  boringPluginRoutes,
2471
3775
  buildBoringSystemPrompt,
3776
+ clampWorkspaceBridgeRuntimeTokenTtlMs,
2472
3777
  collectRestartWarnings,
3778
+ createBrowserBridgeAuthPolicy,
2473
3779
  createExecUiTool,
2474
3780
  createGetUiStateTool,
2475
3781
  createInMemoryBridge,
3782
+ createLocalCliBridgeAuthPolicy,
3783
+ createWorkspaceBridgeError,
3784
+ createWorkspaceBridgeRegistry,
3785
+ createWorkspaceBridgeRuntimeCore,
3786
+ createWorkspaceBridgeRuntimeEnvContribution,
2476
3787
  createWorkspaceUiTools,
2477
3788
  definePluginAsset,
2478
3789
  defineRuntimeServerPlugin,
2479
3790
  defineServerPlugin,
3791
+ defineTrustedDomainBridgeHandler,
3792
+ hashNormalizedInput,
3793
+ mintWorkspaceBridgeRuntimeRefreshToken,
3794
+ mintWorkspaceBridgeRuntimeToken,
2480
3795
  pluginFileSignature,
2481
3796
  preflightBoringPlugins,
2482
3797
  readBoringPlugins,
2483
3798
  readPluginSignatureCache,
3799
+ resolveBridgeCallUrl,
3800
+ resolveBridgeTokenUrl,
2484
3801
  resolvePluginAssetPath,
3802
+ runWithWorkspaceBridgeIdempotency,
2485
3803
  runtimeBackendGateway,
3804
+ runtimeClaimsToBridgeAuthContext,
2486
3805
  scanBoringPlugins,
3806
+ stableStringify,
2487
3807
  uiRoutes,
2488
3808
  validateRuntimeServerPlugin,
2489
3809
  validateServerPlugin,
3810
+ validateWorkspaceBridgeOperationDefinition,
3811
+ verifyWorkspaceBridgeRuntimeRefreshToken,
3812
+ verifyWorkspaceBridgeRuntimeToken,
3813
+ workspaceBridgeHttpRoutes,
2490
3814
  writePluginSignatureCache
2491
3815
  };