@hachej/boring-workspace 0.1.53 → 0.1.55
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/{FileTree-CfJoRiTm.js → FileTree-DGonRKiT.js} +17 -17
- package/dist/{MarkdownEditor-DrPDGy9F.js → MarkdownEditor-CwglOF-8.js} +1 -1
- package/dist/{WorkspaceLoadingState-Bqsn2JEc.js → WorkspaceLoadingState-B2Ctp-b2.js} +249 -225
- package/dist/{WorkspaceProvider-CrOT4ab7.js → WorkspaceProvider-DF90RUdU.js} +3275 -3159
- package/dist/app-front.d.ts +20 -0
- package/dist/app-front.js +539 -527
- package/dist/app-server.d.ts +39 -8
- package/dist/app-server.js +1248 -22
- package/dist/bridge-client.d.ts +68 -0
- package/dist/bridge-client.js +370 -0
- package/dist/plugin.d.ts +3 -1
- package/dist/runtimeEnv-XVFZ1OkV.d.ts +353 -0
- package/dist/server.d.ts +236 -8
- package/dist/server.js +1349 -25
- package/dist/shared.d.ts +2 -1
- package/dist/shared.js +30 -0
- package/dist/testing.js +1 -1
- package/dist/{ui-bridge-LeBuZqfA.d.ts → ui-bridge-BbuUZ5iC.d.ts} +6 -1
- package/dist/workspace-bridge-rpc-A98RA5o6.d.ts +124 -0
- package/dist/workspace.css +35 -0
- package/dist/workspace.d.ts +68 -2
- package/dist/workspace.js +196 -185
- package/docs/PLUGIN_SYSTEM.md +57 -0
- package/package.json +8 -4
- package/dist/createInMemoryBridge-siFWq_R_.d.ts +0 -198
package/dist/app-server.js
CHANGED
|
@@ -649,6 +649,406 @@ function normalizeBoringPluginPiPackages(plugins) {
|
|
|
649
649
|
);
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
// src/shared/workspace-bridge-rpc.ts
|
|
653
|
+
var WORKSPACE_BRIDGE_URL_ENV = "BORING_WORKSPACE_BRIDGE_URL";
|
|
654
|
+
var WORKSPACE_BRIDGE_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN";
|
|
655
|
+
var WORKSPACE_BRIDGE_TOKEN_URL_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN_URL";
|
|
656
|
+
var WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_REFRESH_TOKEN";
|
|
657
|
+
var WORKSPACE_BRIDGE_DISABLED_ENV = "BORING_WORKSPACE_BRIDGE_DISABLED";
|
|
658
|
+
var WorkspaceBridgeErrorCode = /* @__PURE__ */ ((WorkspaceBridgeErrorCode2) => {
|
|
659
|
+
WorkspaceBridgeErrorCode2["OpNotFound"] = "BRIDGE_OP_NOT_FOUND";
|
|
660
|
+
WorkspaceBridgeErrorCode2["DuplicateOp"] = "BRIDGE_DUPLICATE_OP";
|
|
661
|
+
WorkspaceBridgeErrorCode2["CallerNotAllowed"] = "BRIDGE_CALLER_NOT_ALLOWED";
|
|
662
|
+
WorkspaceBridgeErrorCode2["AuthRequired"] = "BRIDGE_AUTH_REQUIRED";
|
|
663
|
+
WorkspaceBridgeErrorCode2["CapabilityDenied"] = "BRIDGE_CAPABILITY_DENIED";
|
|
664
|
+
WorkspaceBridgeErrorCode2["ResourceScopeDenied"] = "BRIDGE_RESOURCE_SCOPE_DENIED";
|
|
665
|
+
WorkspaceBridgeErrorCode2["SchemaInvalid"] = "BRIDGE_SCHEMA_INVALID";
|
|
666
|
+
WorkspaceBridgeErrorCode2["OutputSchemaInvalid"] = "BRIDGE_OUTPUT_SCHEMA_INVALID";
|
|
667
|
+
WorkspaceBridgeErrorCode2["InputTooLarge"] = "BRIDGE_INPUT_TOO_LARGE";
|
|
668
|
+
WorkspaceBridgeErrorCode2["OutputTooLarge"] = "BRIDGE_OUTPUT_TOO_LARGE";
|
|
669
|
+
WorkspaceBridgeErrorCode2["Timeout"] = "BRIDGE_TIMEOUT";
|
|
670
|
+
WorkspaceBridgeErrorCode2["HandlerFailed"] = "BRIDGE_HANDLER_FAILED";
|
|
671
|
+
WorkspaceBridgeErrorCode2["IdempotencyRequired"] = "BRIDGE_IDEMPOTENCY_REQUIRED";
|
|
672
|
+
WorkspaceBridgeErrorCode2["IdempotencyConflict"] = "BRIDGE_IDEMPOTENCY_CONFLICT";
|
|
673
|
+
WorkspaceBridgeErrorCode2["ReplayRejected"] = "BRIDGE_REPLAY_REJECTED";
|
|
674
|
+
WorkspaceBridgeErrorCode2["RateLimited"] = "BRIDGE_RATE_LIMITED";
|
|
675
|
+
WorkspaceBridgeErrorCode2["InvalidToken"] = "BRIDGE_INVALID_TOKEN";
|
|
676
|
+
WorkspaceBridgeErrorCode2["ExpiredToken"] = "BRIDGE_EXPIRED_TOKEN";
|
|
677
|
+
WorkspaceBridgeErrorCode2["InvalidRequest"] = "BRIDGE_INVALID_REQUEST";
|
|
678
|
+
WorkspaceBridgeErrorCode2["UnsupportedRuntime"] = "BRIDGE_UNSUPPORTED_RUNTIME";
|
|
679
|
+
return WorkspaceBridgeErrorCode2;
|
|
680
|
+
})(WorkspaceBridgeErrorCode || {});
|
|
681
|
+
function createWorkspaceBridgeError(code, message, details) {
|
|
682
|
+
return details === void 0 ? { code, message } : { code, message, details };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/server/workspaceBridge/json.ts
|
|
686
|
+
function stableStringify(value) {
|
|
687
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
688
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
689
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
690
|
+
}
|
|
691
|
+
function measureJsonBytes(value) {
|
|
692
|
+
try {
|
|
693
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
694
|
+
} catch {
|
|
695
|
+
return Number.POSITIVE_INFINITY;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/server/workspaceBridge/registry.ts
|
|
700
|
+
var RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES = ["workspace-files.v1."];
|
|
701
|
+
var BRIDGE_CALLER_CLASSES = /* @__PURE__ */ new Set(["browser", "runtime", "server"]);
|
|
702
|
+
var BRIDGE_IDEMPOTENCY_POLICIES = /* @__PURE__ */ new Set(["none", "required", "request-id"]);
|
|
703
|
+
function validateWorkspaceBridgeOperationDefinition(definition) {
|
|
704
|
+
if (!definition || typeof definition !== "object") {
|
|
705
|
+
throw invalidDefinition("WorkspaceBridge operation definition must be an object");
|
|
706
|
+
}
|
|
707
|
+
if (typeof definition.op !== "string" || definition.op.trim().length === 0) {
|
|
708
|
+
throw invalidDefinition("WorkspaceBridge operation definition op must be a non-empty string");
|
|
709
|
+
}
|
|
710
|
+
const reservedPrefix = RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES.find((prefix) => definition.op.startsWith(prefix));
|
|
711
|
+
if (reservedPrefix) {
|
|
712
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} uses reserved prefix ${reservedPrefix}`);
|
|
713
|
+
}
|
|
714
|
+
if (!Number.isInteger(definition.version) || definition.version < 1) {
|
|
715
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} version must be a positive integer`);
|
|
716
|
+
}
|
|
717
|
+
if (typeof definition.owner !== "string" || definition.owner.trim().length === 0) {
|
|
718
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} owner must be a non-empty string`);
|
|
719
|
+
}
|
|
720
|
+
if (!Array.isArray(definition.callerClassesAllowed) || definition.callerClassesAllowed.length === 0) {
|
|
721
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed must be a non-empty array`);
|
|
722
|
+
}
|
|
723
|
+
for (const callerClass of definition.callerClassesAllowed) {
|
|
724
|
+
if (!BRIDGE_CALLER_CLASSES.has(callerClass)) {
|
|
725
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed contains invalid caller class`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!Array.isArray(definition.requiredCapabilities)) {
|
|
729
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must be an array`);
|
|
730
|
+
}
|
|
731
|
+
if (definition.allowCrossWorkspace !== void 0 && typeof definition.allowCrossWorkspace !== "boolean") {
|
|
732
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} allowCrossWorkspace must be a boolean when provided`);
|
|
733
|
+
}
|
|
734
|
+
for (const capability of definition.requiredCapabilities) {
|
|
735
|
+
if (typeof capability !== "string" || capability.trim().length === 0) {
|
|
736
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must contain non-empty strings`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
validateSchemaDefinition(definition.op, "inputSchema", definition.inputSchema, true);
|
|
740
|
+
validateSchemaDefinition(definition.op, "outputSchema", definition.outputSchema, false);
|
|
741
|
+
assertPositiveFiniteNumber(definition.op, "timeoutMs", definition.timeoutMs);
|
|
742
|
+
assertPositiveFiniteNumber(definition.op, "maxInputBytes", definition.maxInputBytes);
|
|
743
|
+
assertPositiveFiniteNumber(definition.op, "maxOutputBytes", definition.maxOutputBytes);
|
|
744
|
+
if (!BRIDGE_IDEMPOTENCY_POLICIES.has(definition.idempotencyPolicy)) {
|
|
745
|
+
throw invalidDefinition(`WorkspaceBridge operation ${definition.op} idempotencyPolicy is invalid`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function validateSchemaDefinition(op, field, schema, required) {
|
|
749
|
+
if (schema === void 0) {
|
|
750
|
+
if (required) throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} is required`);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (typeof schema !== "object" && typeof schema !== "function" || schema === null) {
|
|
754
|
+
throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a schema object`);
|
|
755
|
+
}
|
|
756
|
+
if (hasSafeParse(schema)) return;
|
|
757
|
+
validateJsonSchemaDefinition(schema, `${op} ${field}`);
|
|
758
|
+
}
|
|
759
|
+
var SUPPORTED_JSON_SCHEMA_KEYS = /* @__PURE__ */ new Set(["type", "properties", "required", "items", "enum", "const", "additionalProperties", "description", "title"]);
|
|
760
|
+
function validateJsonSchemaDefinition(schema, label) {
|
|
761
|
+
if (!isPlainSchemaObject(schema)) {
|
|
762
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label} must be a schema object`);
|
|
763
|
+
}
|
|
764
|
+
for (const key of Object.keys(schema)) {
|
|
765
|
+
if (!SUPPORTED_JSON_SCHEMA_KEYS.has(key)) {
|
|
766
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.${key} is not supported by the bridge schema subset`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (!isSupportedJsonSchemaType(schema.type)) {
|
|
770
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.type must be a supported JSON schema type`);
|
|
771
|
+
}
|
|
772
|
+
if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((item) => typeof item !== "string"))) {
|
|
773
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.required must be an array of strings`);
|
|
774
|
+
}
|
|
775
|
+
if (schema.required !== void 0 && schema.type !== "object") {
|
|
776
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.required is only supported for object schemas`);
|
|
777
|
+
}
|
|
778
|
+
if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
|
|
779
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.enum must be an array`);
|
|
780
|
+
}
|
|
781
|
+
if (schema.additionalProperties !== void 0 && schema.additionalProperties !== false && schema.additionalProperties !== true) {
|
|
782
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties must be boolean when provided`);
|
|
783
|
+
}
|
|
784
|
+
if (schema.additionalProperties !== void 0 && schema.type !== "object") {
|
|
785
|
+
throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties is only supported for object schemas`);
|
|
786
|
+
}
|
|
787
|
+
if (schema.properties !== void 0) {
|
|
788
|
+
if (schema.type !== "object") throw invalidDefinition(`WorkspaceBridge operation ${label}.properties is only supported for object schemas`);
|
|
789
|
+
if (!isRecord2(schema.properties)) throw invalidDefinition(`WorkspaceBridge operation ${label}.properties must be an object`);
|
|
790
|
+
for (const [key, child] of Object.entries(schema.properties)) validateJsonSchemaDefinition(child, `${label}.properties.${key}`);
|
|
791
|
+
}
|
|
792
|
+
if (schema.items !== void 0) {
|
|
793
|
+
if (schema.type !== "array") throw invalidDefinition(`WorkspaceBridge operation ${label}.items is only supported for array schemas`);
|
|
794
|
+
validateJsonSchemaDefinition(schema.items, `${label}.items`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function isSupportedJsonSchemaType(type) {
|
|
798
|
+
return type === "object" || type === "array" || type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "null";
|
|
799
|
+
}
|
|
800
|
+
function hasSafeParse(schema) {
|
|
801
|
+
return (typeof schema === "object" || typeof schema === "function") && schema !== null && typeof schema.safeParse === "function";
|
|
802
|
+
}
|
|
803
|
+
function isPlainSchemaObject(value) {
|
|
804
|
+
return isRecord2(value);
|
|
805
|
+
}
|
|
806
|
+
function isRecord2(value) {
|
|
807
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
808
|
+
}
|
|
809
|
+
function hasOwn(record, key) {
|
|
810
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
811
|
+
}
|
|
812
|
+
function assertPositiveFiniteNumber(op, field, value) {
|
|
813
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
814
|
+
throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a positive finite number`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function invalidDefinition(message) {
|
|
818
|
+
throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, message);
|
|
819
|
+
}
|
|
820
|
+
var WorkspaceBridgeRegistry = class {
|
|
821
|
+
handlers = /* @__PURE__ */ new Map();
|
|
822
|
+
logger;
|
|
823
|
+
ownerWorkspaceId;
|
|
824
|
+
constructor(options = {}) {
|
|
825
|
+
if (options.ownerWorkspaceId !== void 0 && options.ownerWorkspaceId.trim().length === 0) {
|
|
826
|
+
throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge registry ownerWorkspaceId must be non-empty when provided");
|
|
827
|
+
}
|
|
828
|
+
this.logger = options.logger;
|
|
829
|
+
this.ownerWorkspaceId = options.ownerWorkspaceId;
|
|
830
|
+
}
|
|
831
|
+
registerHandler(definition, handler, options = {}) {
|
|
832
|
+
validateWorkspaceBridgeOperationDefinition(definition);
|
|
833
|
+
const existing = this.handlers.get(definition.op);
|
|
834
|
+
if (existing && !options.replace) {
|
|
835
|
+
throw createWorkspaceBridgeError(
|
|
836
|
+
"BRIDGE_DUPLICATE_OP" /* DuplicateOp */,
|
|
837
|
+
`WorkspaceBridge operation is already registered: ${definition.op}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
this.handlers.set(definition.op, {
|
|
841
|
+
definition,
|
|
842
|
+
handler
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
getDefinition(op) {
|
|
846
|
+
return this.handlers.get(op)?.definition;
|
|
847
|
+
}
|
|
848
|
+
listDefinitions() {
|
|
849
|
+
return Array.from(this.handlers.values(), ({ definition }) => definition);
|
|
850
|
+
}
|
|
851
|
+
async call(request, context, options = {}) {
|
|
852
|
+
const requestId = request.requestId ?? context.requestId ?? createRequestId();
|
|
853
|
+
const registered = this.handlers.get(request.op);
|
|
854
|
+
if (!registered) {
|
|
855
|
+
return this.failure(request.op, requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
|
|
856
|
+
}
|
|
857
|
+
const { definition, handler } = registered;
|
|
858
|
+
const logBase = {
|
|
859
|
+
requestId,
|
|
860
|
+
op: request.op,
|
|
861
|
+
callerClass: context.callerClass,
|
|
862
|
+
workspaceId: context.workspaceId,
|
|
863
|
+
sessionId: context.sessionId,
|
|
864
|
+
pluginId: context.pluginId,
|
|
865
|
+
tokenId: context.tokenId,
|
|
866
|
+
capabilities: context.capabilities,
|
|
867
|
+
actor: context.actor
|
|
868
|
+
};
|
|
869
|
+
if (!definition.callerClassesAllowed.includes(context.callerClass)) {
|
|
870
|
+
return this.failure(request.op, requestId, "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */, "Caller class is not allowed for operation", logBase);
|
|
871
|
+
}
|
|
872
|
+
const expectedWorkspaceId = options.expectedWorkspaceId ?? this.ownerWorkspaceId;
|
|
873
|
+
if (expectedWorkspaceId && context.workspaceId !== expectedWorkspaceId && definition.allowCrossWorkspace !== true) {
|
|
874
|
+
return this.failure(request.op, requestId, "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */, "Caller workspace is not authorized for this bridge registry", {
|
|
875
|
+
...logBase,
|
|
876
|
+
expectedWorkspaceId
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
const missingCapability = definition.requiredCapabilities.find((capability) => !context.capabilities.includes(capability));
|
|
880
|
+
if (missingCapability) {
|
|
881
|
+
return this.failure(request.op, requestId, "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Caller is missing a required capability", {
|
|
882
|
+
...logBase,
|
|
883
|
+
missingCapability
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
const inputBytes = measureJsonBytes(request.input);
|
|
887
|
+
if (inputBytes > definition.maxInputBytes) {
|
|
888
|
+
return this.failure(request.op, requestId, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "Bridge input exceeds operation limit", {
|
|
889
|
+
...logBase,
|
|
890
|
+
inputBytes,
|
|
891
|
+
maxInputBytes: definition.maxInputBytes
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const inputValidation = validateSchema(definition.inputSchema, request.input);
|
|
895
|
+
if (!inputValidation.success) {
|
|
896
|
+
return this.failure(request.op, requestId, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "Bridge input failed schema validation", {
|
|
897
|
+
...logBase,
|
|
898
|
+
schemaMessage: inputValidation.message
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
const controller = new AbortController();
|
|
902
|
+
const abortFromCaller = () => controller.abort(context.signal?.reason);
|
|
903
|
+
if (context.signal?.aborted) abortFromCaller();
|
|
904
|
+
else context.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
905
|
+
const timeoutResult = /* @__PURE__ */ Symbol("workspace-bridge-timeout");
|
|
906
|
+
let timeout;
|
|
907
|
+
try {
|
|
908
|
+
this.logger?.debug?.("workspace bridge call started", logBase);
|
|
909
|
+
const handlerPromise = Promise.resolve().then(() => handler({
|
|
910
|
+
input: request.input,
|
|
911
|
+
context: { ...context, requestId },
|
|
912
|
+
definition,
|
|
913
|
+
signal: controller.signal,
|
|
914
|
+
emitUiEffect: context.emitUiEffect
|
|
915
|
+
}));
|
|
916
|
+
const timeoutPromise = new Promise((resolve8) => {
|
|
917
|
+
timeout = setTimeout(() => {
|
|
918
|
+
controller.abort("timeout");
|
|
919
|
+
resolve8(timeoutResult);
|
|
920
|
+
}, definition.timeoutMs);
|
|
921
|
+
});
|
|
922
|
+
const output = await Promise.race([handlerPromise, timeoutPromise]);
|
|
923
|
+
if (output === timeoutResult) {
|
|
924
|
+
return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
|
|
925
|
+
}
|
|
926
|
+
const outputValidation = definition.outputSchema ? validateSchema(definition.outputSchema, output) : { success: true };
|
|
927
|
+
if (!outputValidation.success) {
|
|
928
|
+
return this.failure(request.op, requestId, "BRIDGE_OUTPUT_SCHEMA_INVALID" /* OutputSchemaInvalid */, "Bridge output failed schema validation", {
|
|
929
|
+
...logBase,
|
|
930
|
+
schemaMessage: outputValidation.message
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
const outputBytes = measureJsonBytes(output);
|
|
934
|
+
if (outputBytes > definition.maxOutputBytes) {
|
|
935
|
+
return this.failure(request.op, requestId, "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */, "Bridge output exceeds operation limit", {
|
|
936
|
+
...logBase,
|
|
937
|
+
outputBytes,
|
|
938
|
+
maxOutputBytes: definition.maxOutputBytes
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
this.logger?.info?.("workspace bridge call completed", logBase);
|
|
942
|
+
return { ok: true, op: request.op, requestId, output };
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (controller.signal.aborted && controller.signal.reason === "timeout") {
|
|
945
|
+
return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
|
|
946
|
+
}
|
|
947
|
+
const bridgeError = isWorkspaceBridgeError(err) ? err : void 0;
|
|
948
|
+
this.logger?.error?.("workspace bridge call failed", {
|
|
949
|
+
...logBase,
|
|
950
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
951
|
+
errorCode: bridgeError?.code
|
|
952
|
+
});
|
|
953
|
+
return this.failure(request.op, requestId, bridgeError?.code ?? "BRIDGE_HANDLER_FAILED" /* HandlerFailed */, bridgeError?.message ?? "Bridge handler failed", logBase);
|
|
954
|
+
} finally {
|
|
955
|
+
if (timeout) clearTimeout(timeout);
|
|
956
|
+
context.signal?.removeEventListener("abort", abortFromCaller);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
failure(op, requestId, code, message, logFields) {
|
|
960
|
+
const error = createWorkspaceBridgeError(code, message);
|
|
961
|
+
this.logger?.warn?.("workspace bridge call rejected", {
|
|
962
|
+
...logFields,
|
|
963
|
+
op,
|
|
964
|
+
requestId,
|
|
965
|
+
errorCode: code
|
|
966
|
+
});
|
|
967
|
+
return { ok: false, op, requestId, error };
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
function createWorkspaceBridgeRegistry(options = {}) {
|
|
971
|
+
return new WorkspaceBridgeRegistry(options);
|
|
972
|
+
}
|
|
973
|
+
var WORKSPACE_BRIDGE_ERROR_CODES = new Set(Object.values(WorkspaceBridgeErrorCode));
|
|
974
|
+
function isWorkspaceBridgeError(err) {
|
|
975
|
+
if (!err || typeof err !== "object") return false;
|
|
976
|
+
const candidate = err;
|
|
977
|
+
return typeof candidate.code === "string" && WORKSPACE_BRIDGE_ERROR_CODES.has(candidate.code) && typeof candidate.message === "string";
|
|
978
|
+
}
|
|
979
|
+
function validateSchema(schema, value) {
|
|
980
|
+
if (schema === void 0) return { success: true };
|
|
981
|
+
if (hasSafeParse(schema)) {
|
|
982
|
+
const result = schema.safeParse(value);
|
|
983
|
+
return result.success ? { success: true } : { success: false, message: result.error?.message };
|
|
984
|
+
}
|
|
985
|
+
if (isPlainSchemaObject(schema)) return validateJsonSchema(schema, value, "$");
|
|
986
|
+
return { success: false, message: "Unsupported schema" };
|
|
987
|
+
}
|
|
988
|
+
function validateJsonSchema(schema, value, path) {
|
|
989
|
+
if (schema.const !== void 0 && !jsonEqual(value, schema.const)) {
|
|
990
|
+
return { success: false, message: `${path}: Expected const value` };
|
|
991
|
+
}
|
|
992
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(value, candidate))) {
|
|
993
|
+
return { success: false, message: `${path}: Expected one of enum values` };
|
|
994
|
+
}
|
|
995
|
+
const typeValidation = validateJsonSchemaType(schema.type, value, path);
|
|
996
|
+
if (!typeValidation.success) return typeValidation;
|
|
997
|
+
if (schema.type === "object") {
|
|
998
|
+
if (!isRecord2(value)) return { success: false, message: `${path}: Expected object` };
|
|
999
|
+
const properties = isRecord2(schema.properties) ? schema.properties : {};
|
|
1000
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((item) => typeof item === "string") : [];
|
|
1001
|
+
for (const key of required) {
|
|
1002
|
+
if (!hasOwn(value, key)) return { success: false, message: `${path}.${key}: Required property missing` };
|
|
1003
|
+
}
|
|
1004
|
+
if (schema.additionalProperties === false) {
|
|
1005
|
+
for (const key of Object.keys(value)) {
|
|
1006
|
+
if (!hasOwn(properties, key)) return { success: false, message: `${path}.${key}: Additional properties are not allowed` };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
1010
|
+
if (hasOwn(value, key)) {
|
|
1011
|
+
const childResult = validateJsonSchema(child, value[key], `${path}.${key}`);
|
|
1012
|
+
if (!childResult.success) return childResult;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (schema.type === "array" && schema.items !== void 0) {
|
|
1017
|
+
if (!Array.isArray(value)) return { success: false, message: `${path}: Expected array` };
|
|
1018
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1019
|
+
const childResult = validateJsonSchema(schema.items, value[i], `${path}[${i}]`);
|
|
1020
|
+
if (!childResult.success) return childResult;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return { success: true };
|
|
1024
|
+
}
|
|
1025
|
+
function validateJsonSchemaType(type, value, path) {
|
|
1026
|
+
switch (type) {
|
|
1027
|
+
case "object":
|
|
1028
|
+
return isRecord2(value) ? { success: true } : { success: false, message: `${path}: Expected object` };
|
|
1029
|
+
case "array":
|
|
1030
|
+
return Array.isArray(value) ? { success: true } : { success: false, message: `${path}: Expected array` };
|
|
1031
|
+
case "string":
|
|
1032
|
+
return typeof value === "string" ? { success: true } : { success: false, message: `${path}: Expected string` };
|
|
1033
|
+
case "number":
|
|
1034
|
+
return typeof value === "number" && Number.isFinite(value) ? { success: true } : { success: false, message: `${path}: Expected number` };
|
|
1035
|
+
case "integer":
|
|
1036
|
+
return Number.isInteger(value) ? { success: true } : { success: false, message: `${path}: Expected integer` };
|
|
1037
|
+
case "boolean":
|
|
1038
|
+
return typeof value === "boolean" ? { success: true } : { success: false, message: `${path}: Expected boolean` };
|
|
1039
|
+
case "null":
|
|
1040
|
+
return value === null ? { success: true } : { success: false, message: `${path}: Expected null` };
|
|
1041
|
+
default:
|
|
1042
|
+
return { success: false, message: `${path}: Unsupported schema type` };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function jsonEqual(a, b) {
|
|
1046
|
+
return stableStringify(a) === stableStringify(b);
|
|
1047
|
+
}
|
|
1048
|
+
function createRequestId() {
|
|
1049
|
+
return `bridge_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
652
1052
|
// src/server/plugins/piPackages.ts
|
|
653
1053
|
import {
|
|
654
1054
|
compactPiPackages,
|
|
@@ -739,6 +1139,26 @@ function validatePluginAssets(pluginId, assets) {
|
|
|
739
1139
|
}
|
|
740
1140
|
}
|
|
741
1141
|
}
|
|
1142
|
+
function validateWorkspaceBridgeHandlers(pluginId, handlers) {
|
|
1143
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
1144
|
+
const entry = handlers[i];
|
|
1145
|
+
if (!entry || typeof entry !== "object") {
|
|
1146
|
+
fail(pluginId, `workspaceBridgeHandlers[${i}] must be an object`);
|
|
1147
|
+
}
|
|
1148
|
+
if (!entry.definition || typeof entry.definition !== "object") {
|
|
1149
|
+
fail(pluginId, `workspaceBridgeHandlers[${i}].definition must be an object`);
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
validateWorkspaceBridgeOperationDefinition(entry.definition);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
const message = error && typeof error === "object" && "message" in error ? String(error.message) : "invalid WorkspaceBridge operation definition";
|
|
1155
|
+
fail(pluginId, `workspaceBridgeHandlers[${i}].definition invalid: ${message}`);
|
|
1156
|
+
}
|
|
1157
|
+
if (typeof entry.handler !== "function") {
|
|
1158
|
+
fail(pluginId, `workspaceBridgeHandlers[${i}].handler must be a function`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
742
1162
|
function validateProvisioning(pluginId, provisioning) {
|
|
743
1163
|
if (!provisioning || typeof provisioning !== "object") {
|
|
744
1164
|
fail(pluginId, "provisioning must be an object");
|
|
@@ -858,6 +1278,12 @@ function validateServerPlugin(plugin) {
|
|
|
858
1278
|
}
|
|
859
1279
|
validatePluginAssets(plugin.id, plugin.assets);
|
|
860
1280
|
}
|
|
1281
|
+
if (plugin.workspaceBridgeHandlers !== void 0) {
|
|
1282
|
+
if (!Array.isArray(plugin.workspaceBridgeHandlers)) {
|
|
1283
|
+
fail(plugin.id, "workspaceBridgeHandlers must be an array when provided");
|
|
1284
|
+
}
|
|
1285
|
+
validateWorkspaceBridgeHandlers(plugin.id, plugin.workspaceBridgeHandlers);
|
|
1286
|
+
}
|
|
861
1287
|
if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
|
|
862
1288
|
fail(plugin.id, "routes must be a Fastify plugin function when provided");
|
|
863
1289
|
}
|
|
@@ -902,6 +1328,7 @@ function bootstrapServer(options) {
|
|
|
902
1328
|
...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
|
|
903
1329
|
}));
|
|
904
1330
|
const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
|
|
1331
|
+
const workspaceBridgeHandlers = finalPlugins.flatMap((p) => p.workspaceBridgeHandlers ?? []);
|
|
905
1332
|
const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
|
|
906
1333
|
return {
|
|
907
1334
|
registered: finalPlugins.map((p) => p.id),
|
|
@@ -912,6 +1339,7 @@ function bootstrapServer(options) {
|
|
|
912
1339
|
runtimePlugins,
|
|
913
1340
|
provisioningContributions,
|
|
914
1341
|
routeContributions,
|
|
1342
|
+
workspaceBridgeHandlers,
|
|
915
1343
|
preservedUiStateKeys
|
|
916
1344
|
};
|
|
917
1345
|
}
|
|
@@ -1886,6 +2314,18 @@ async function resolveDirServerPlugin(entry, ctx) {
|
|
|
1886
2314
|
function isDirEntry(entry) {
|
|
1887
2315
|
return typeof entry === "object" && entry !== null && "dir" in entry;
|
|
1888
2316
|
}
|
|
2317
|
+
function isTrustedWorkspaceBridgeHandlerEntry(entry) {
|
|
2318
|
+
if (!isDirEntry(entry)) return true;
|
|
2319
|
+
return entry.trust === "internal";
|
|
2320
|
+
}
|
|
2321
|
+
function assertWorkspaceBridgeHandlersTrusted(plugin, entry) {
|
|
2322
|
+
if ((plugin.workspaceBridgeHandlers?.length ?? 0) === 0) return;
|
|
2323
|
+
if (isTrustedWorkspaceBridgeHandlerEntry(entry)) return;
|
|
2324
|
+
const label = isDirEntry(entry) ? resolve5(entry.dir) : plugin.id;
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
`server plugin "${plugin.id}" from ${label} declares workspaceBridgeHandlers, but host bridge handlers are only allowed for pre-built host plugins or directory entries marked trust: "internal"`
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
1889
2329
|
async function resolveOnePluginEntry(entry, ctx) {
|
|
1890
2330
|
if (isDirEntry(entry)) return await resolveDirServerPlugin(entry, ctx);
|
|
1891
2331
|
return entry;
|
|
@@ -1964,6 +2404,16 @@ function createInMemoryBridge() {
|
|
|
1964
2404
|
pendingCommands.splice(0, pendingCommands.length - MAX_PENDING_COMMANDS);
|
|
1965
2405
|
}
|
|
1966
2406
|
}
|
|
2407
|
+
async function dispatchCommand(cmd) {
|
|
2408
|
+
const seq = nextSeq++;
|
|
2409
|
+
const annotated = { ...cmd, seq };
|
|
2410
|
+
let delivered = false;
|
|
2411
|
+
for (const handler of subscribers) {
|
|
2412
|
+
if (handler(annotated) !== false) delivered = true;
|
|
2413
|
+
}
|
|
2414
|
+
if (!delivered) enqueuePending(annotated);
|
|
2415
|
+
return { seq, status: "ok" };
|
|
2416
|
+
}
|
|
1967
2417
|
return {
|
|
1968
2418
|
async getState() {
|
|
1969
2419
|
return state;
|
|
@@ -1972,14 +2422,10 @@ function createInMemoryBridge() {
|
|
|
1972
2422
|
state = s;
|
|
1973
2423
|
},
|
|
1974
2424
|
async postCommand(cmd) {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
if (handler(annotated) !== false) delivered = true;
|
|
1980
|
-
}
|
|
1981
|
-
if (!delivered) enqueuePending(annotated);
|
|
1982
|
-
return { seq, status: "ok" };
|
|
2425
|
+
return dispatchCommand(cmd);
|
|
2426
|
+
},
|
|
2427
|
+
async emitUiEffect(cmd) {
|
|
2428
|
+
return dispatchCommand(cmd);
|
|
1983
2429
|
},
|
|
1984
2430
|
subscribeCommands(handler) {
|
|
1985
2431
|
subscribers.add(handler);
|
|
@@ -2068,7 +2514,7 @@ async function validateExistingPath(workspaceRoot, relPath) {
|
|
|
2068
2514
|
};
|
|
2069
2515
|
}
|
|
2070
2516
|
}
|
|
2071
|
-
function createGetUiStateTool(
|
|
2517
|
+
function createGetUiStateTool(workspaceBridge) {
|
|
2072
2518
|
return {
|
|
2073
2519
|
name: "get_ui_state",
|
|
2074
2520
|
readinessRequirements: ["ui-bridge"],
|
|
@@ -2094,7 +2540,7 @@ function createGetUiStateTool(uiBridge) {
|
|
|
2094
2540
|
},
|
|
2095
2541
|
async execute() {
|
|
2096
2542
|
try {
|
|
2097
|
-
const state = await
|
|
2543
|
+
const state = await workspaceBridge.getState();
|
|
2098
2544
|
return {
|
|
2099
2545
|
content: [{ type: "text", text: JSON.stringify(state ?? {}) }],
|
|
2100
2546
|
details: state
|
|
@@ -2124,7 +2570,7 @@ function isVerified(kind, params, state) {
|
|
|
2124
2570
|
}
|
|
2125
2571
|
return true;
|
|
2126
2572
|
}
|
|
2127
|
-
function createExecUiTool(
|
|
2573
|
+
function createExecUiTool(workspaceBridge, opts = {}) {
|
|
2128
2574
|
const { workspaceRoot, resolvePathKind } = opts;
|
|
2129
2575
|
const verifyDelayMs = opts.verifyDelayMs ?? 250;
|
|
2130
2576
|
const verifyRetries = opts.verifyRetries ?? 20;
|
|
@@ -2281,7 +2727,7 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
2281
2727
|
}
|
|
2282
2728
|
try {
|
|
2283
2729
|
const command = { kind: effectiveKind, params: cmdParams };
|
|
2284
|
-
const result = await
|
|
2730
|
+
const result = await workspaceBridge.emitUiEffect(command);
|
|
2285
2731
|
if (result.status === "error") {
|
|
2286
2732
|
return {
|
|
2287
2733
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
@@ -2291,11 +2737,11 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
2291
2737
|
}
|
|
2292
2738
|
if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
|
|
2293
2739
|
await new Promise((r) => setTimeout(r, verifyDelayMs));
|
|
2294
|
-
let uiState = await
|
|
2740
|
+
let uiState = await workspaceBridge.getState();
|
|
2295
2741
|
for (let i = 0; i < verifyRetries; i++) {
|
|
2296
2742
|
if (uiState === null || isVerified(effectiveKind, cmdParams, uiState)) break;
|
|
2297
2743
|
await new Promise((r) => setTimeout(r, verifyIntervalMs));
|
|
2298
|
-
uiState = await
|
|
2744
|
+
uiState = await workspaceBridge.getState();
|
|
2299
2745
|
}
|
|
2300
2746
|
const combined = { ...result, uiState };
|
|
2301
2747
|
return {
|
|
@@ -2314,8 +2760,8 @@ function createExecUiTool(uiBridge, opts = {}) {
|
|
|
2314
2760
|
}
|
|
2315
2761
|
};
|
|
2316
2762
|
}
|
|
2317
|
-
function createWorkspaceUiTools(
|
|
2318
|
-
return [createGetUiStateTool(
|
|
2763
|
+
function createWorkspaceUiTools(workspaceBridge, opts = {}) {
|
|
2764
|
+
return [createGetUiStateTool(workspaceBridge), createExecUiTool(workspaceBridge, opts)];
|
|
2319
2765
|
}
|
|
2320
2766
|
|
|
2321
2767
|
// src/server/ui-control/http/uiRoutes.ts
|
|
@@ -2611,6 +3057,728 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
|
|
|
2611
3057
|
done();
|
|
2612
3058
|
}
|
|
2613
3059
|
|
|
3060
|
+
// src/server/workspaceBridge/authPolicy.ts
|
|
3061
|
+
function createLocalCliBridgeAuthPolicy(options) {
|
|
3062
|
+
return {
|
|
3063
|
+
resolve(input) {
|
|
3064
|
+
if (input.callerClass !== "browser") {
|
|
3065
|
+
throw createWorkspaceBridgeError(
|
|
3066
|
+
"BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
|
|
3067
|
+
"Local CLI auth policy only accepts browser callers"
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
ensureCallerAllowed(input.definition, "browser");
|
|
3071
|
+
if (input.workspaceId !== options.workspaceId) {
|
|
3072
|
+
throw createWorkspaceBridgeError(
|
|
3073
|
+
"BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */,
|
|
3074
|
+
"Local CLI bridge caller is not authorized for workspace"
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
const capabilities = options.capabilities ?? input.definition.requiredCapabilities;
|
|
3078
|
+
ensureCapabilities(capabilities, input.requiredCapabilities ?? input.definition.requiredCapabilities);
|
|
3079
|
+
const context = makeContext({
|
|
3080
|
+
callerClass: "browser",
|
|
3081
|
+
workspaceId: input.workspaceId,
|
|
3082
|
+
sessionId: input.sessionId,
|
|
3083
|
+
pluginId: input.pluginId,
|
|
3084
|
+
capabilities,
|
|
3085
|
+
actor: { actorKind: "human", performedBy: { label: "local-cli:user" } }
|
|
3086
|
+
});
|
|
3087
|
+
return {
|
|
3088
|
+
context,
|
|
3089
|
+
effectiveCapabilities: capabilities,
|
|
3090
|
+
principal: { userId: "local-cli" },
|
|
3091
|
+
resourceScope: { workspaceId: input.workspaceId, sessionId: input.sessionId }
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
function ensureCallerAllowed(definition, callerClass) {
|
|
3097
|
+
if (!definition.callerClassesAllowed.includes(callerClass)) {
|
|
3098
|
+
throw createWorkspaceBridgeError(
|
|
3099
|
+
"BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
|
|
3100
|
+
"Bridge caller class is not allowed for operation"
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
function ensureCapabilities(actual, required) {
|
|
3105
|
+
const missing = required.find((capability) => !actual.includes(capability));
|
|
3106
|
+
if (missing) {
|
|
3107
|
+
throw createWorkspaceBridgeError(
|
|
3108
|
+
"BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */,
|
|
3109
|
+
"Bridge caller is missing a required capability"
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function makeContext(context) {
|
|
3114
|
+
return {
|
|
3115
|
+
...context,
|
|
3116
|
+
capabilities: [...context.capabilities],
|
|
3117
|
+
actor: sanitizeActor(context.actor)
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function sanitizeActor(actor) {
|
|
3121
|
+
return {
|
|
3122
|
+
actorKind: actor.actorKind,
|
|
3123
|
+
performedBy: actor.performedBy ? { label: actor.performedBy.label, id: actor.performedBy.id } : void 0,
|
|
3124
|
+
onBehalfOf: actor.onBehalfOf ? { label: actor.onBehalfOf.label, id: actor.onBehalfOf.id } : void 0
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/server/workspaceBridge/httpRoutes.ts
|
|
3129
|
+
import { z as z3 } from "zod";
|
|
3130
|
+
|
|
3131
|
+
// src/server/workspaceBridge/idempotency.ts
|
|
3132
|
+
import { createHash as createHash2 } from "crypto";
|
|
3133
|
+
var InMemoryWorkspaceBridgeIdempotencyStore = class {
|
|
3134
|
+
records = /* @__PURE__ */ new Map();
|
|
3135
|
+
lastGcMs = 0;
|
|
3136
|
+
async begin(options) {
|
|
3137
|
+
const prepared = prepareIdempotency(options);
|
|
3138
|
+
if ("error" in prepared) return { action: "reject", error: prepared.error };
|
|
3139
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
3140
|
+
if (nowMs - this.lastGcMs >= DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS) {
|
|
3141
|
+
await this.gc(nowMs);
|
|
3142
|
+
this.lastGcMs = nowMs;
|
|
3143
|
+
}
|
|
3144
|
+
const existing = this.records.get(prepared.scopeKey);
|
|
3145
|
+
if (existing && Date.parse(existing.expiresAt) > nowMs) {
|
|
3146
|
+
if (existing.inputHash !== prepared.inputHash) {
|
|
3147
|
+
return { action: "reject", error: replayRejectedError() };
|
|
3148
|
+
}
|
|
3149
|
+
return { action: "replay", record: existing };
|
|
3150
|
+
}
|
|
3151
|
+
this.records.set(prepared.scopeKey, createPendingRecord(prepared.scopeKey, prepared.inputHash, nowMs, options.ttlMs));
|
|
3152
|
+
return { action: "execute", scopeKey: prepared.scopeKey, inputHash: prepared.inputHash };
|
|
3153
|
+
}
|
|
3154
|
+
async complete(options) {
|
|
3155
|
+
const existing = this.records.get(options.scopeKey);
|
|
3156
|
+
if (!existing || existing.inputHash !== options.inputHash) return;
|
|
3157
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
3158
|
+
this.records.set(options.scopeKey, {
|
|
3159
|
+
...existing,
|
|
3160
|
+
// Only successful responses are completed; failures release the key
|
|
3161
|
+
// (see runWithWorkspaceBridgeIdempotency), so there is no "failed" record.
|
|
3162
|
+
status: "completed",
|
|
3163
|
+
response: options.response,
|
|
3164
|
+
updatedAt: new Date(nowMs).toISOString(),
|
|
3165
|
+
expiresAt: new Date(nowMs + (options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS)).toISOString()
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
async release(scopeKey, inputHash) {
|
|
3169
|
+
const existing = this.records.get(scopeKey);
|
|
3170
|
+
if (existing && existing.inputHash === inputHash) this.records.delete(scopeKey);
|
|
3171
|
+
}
|
|
3172
|
+
async gc(nowMs = Date.now()) {
|
|
3173
|
+
let removed = 0;
|
|
3174
|
+
for (const [key, record] of this.records) {
|
|
3175
|
+
if (Date.parse(record.expiresAt) <= nowMs) {
|
|
3176
|
+
this.records.delete(key);
|
|
3177
|
+
removed++;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return removed;
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
async function runWithWorkspaceBridgeIdempotency(store, options, execute) {
|
|
3184
|
+
if (options.definition.idempotencyPolicy === "none") return await execute();
|
|
3185
|
+
if (!store) {
|
|
3186
|
+
return {
|
|
3187
|
+
ok: false,
|
|
3188
|
+
op: options.definition.op,
|
|
3189
|
+
requestId: options.request.requestId,
|
|
3190
|
+
error: createWorkspaceBridgeError(
|
|
3191
|
+
"BRIDGE_UNSUPPORTED_RUNTIME" /* UnsupportedRuntime */,
|
|
3192
|
+
"WorkspaceBridge mutation requires an atomic idempotency store"
|
|
3193
|
+
)
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
const begin = await store.begin(options);
|
|
3197
|
+
if (begin.action === "reject") {
|
|
3198
|
+
return { ok: false, op: options.definition.op, requestId: options.request.requestId, error: begin.error };
|
|
3199
|
+
}
|
|
3200
|
+
if (begin.action === "replay") {
|
|
3201
|
+
return begin.record.response ?? {
|
|
3202
|
+
ok: false,
|
|
3203
|
+
op: options.definition.op,
|
|
3204
|
+
requestId: options.request.requestId,
|
|
3205
|
+
error: createWorkspaceBridgeError(
|
|
3206
|
+
"BRIDGE_IDEMPOTENCY_CONFLICT" /* IdempotencyConflict */,
|
|
3207
|
+
"WorkspaceBridge idempotency key is already pending"
|
|
3208
|
+
)
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
const response = await execute();
|
|
3212
|
+
if (response.ok) {
|
|
3213
|
+
await store.complete({
|
|
3214
|
+
scopeKey: begin.scopeKey,
|
|
3215
|
+
inputHash: begin.inputHash,
|
|
3216
|
+
response,
|
|
3217
|
+
nowMs: options.nowMs,
|
|
3218
|
+
ttlMs: options.ttlMs
|
|
3219
|
+
});
|
|
3220
|
+
} else if (response.error.code !== "BRIDGE_TIMEOUT" /* Timeout */) {
|
|
3221
|
+
await store.release(begin.scopeKey, begin.inputHash);
|
|
3222
|
+
}
|
|
3223
|
+
return response;
|
|
3224
|
+
}
|
|
3225
|
+
var DEFAULT_IDEMPOTENCY_TTL_MS = 15 * 6e4;
|
|
3226
|
+
var DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS = 6e4;
|
|
3227
|
+
function prepareIdempotency(options) {
|
|
3228
|
+
const key = semanticKey(options);
|
|
3229
|
+
if (!key) {
|
|
3230
|
+
return {
|
|
3231
|
+
error: createWorkspaceBridgeError(
|
|
3232
|
+
"BRIDGE_IDEMPOTENCY_REQUIRED" /* IdempotencyRequired */,
|
|
3233
|
+
"WorkspaceBridge operation requires an idempotency key"
|
|
3234
|
+
)
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
return {
|
|
3238
|
+
scopeKey: stableStringify({
|
|
3239
|
+
workspaceId: options.auth.workspaceId,
|
|
3240
|
+
sessionId: options.auth.sessionId,
|
|
3241
|
+
pluginId: options.auth.pluginId,
|
|
3242
|
+
op: options.definition.op,
|
|
3243
|
+
key
|
|
3244
|
+
}),
|
|
3245
|
+
inputHash: hashNormalizedInput(options.request.input)
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
function semanticKey(options) {
|
|
3249
|
+
if (options.definition.idempotencyPolicy === "none") return "none";
|
|
3250
|
+
if (options.definition.idempotencyPolicy === "required") return options.request.idempotencyKey;
|
|
3251
|
+
if (options.definition.idempotencyPolicy === "request-id") return options.request.requestId ?? options.request.idempotencyKey;
|
|
3252
|
+
return void 0;
|
|
3253
|
+
}
|
|
3254
|
+
function hashNormalizedInput(input) {
|
|
3255
|
+
return hashString(stableStringify(input));
|
|
3256
|
+
}
|
|
3257
|
+
function createPendingRecord(scopeKey, inputHash, nowMs, ttlMs = DEFAULT_IDEMPOTENCY_TTL_MS) {
|
|
3258
|
+
const now = new Date(nowMs).toISOString();
|
|
3259
|
+
return {
|
|
3260
|
+
scopeKey,
|
|
3261
|
+
inputHash,
|
|
3262
|
+
status: "pending",
|
|
3263
|
+
createdAt: now,
|
|
3264
|
+
updatedAt: now,
|
|
3265
|
+
expiresAt: new Date(nowMs + ttlMs).toISOString()
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
function replayRejectedError() {
|
|
3269
|
+
return createWorkspaceBridgeError(
|
|
3270
|
+
"BRIDGE_REPLAY_REJECTED" /* ReplayRejected */,
|
|
3271
|
+
"WorkspaceBridge idempotency key was reused with a different payload"
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
function hashString(value) {
|
|
3275
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// src/server/workspaceBridge/runtimeToken.ts
|
|
3279
|
+
import { createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
3280
|
+
var WORKSPACE_BRIDGE_TOKEN_AUDIENCE = "workspace-bridge";
|
|
3281
|
+
var WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE = "workspace-bridge-refresh";
|
|
3282
|
+
var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 5 * 6e4;
|
|
3283
|
+
var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS = 60 * 6e4;
|
|
3284
|
+
var MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 15 * 6e4;
|
|
3285
|
+
function mintWorkspaceBridgeRuntimeToken(options) {
|
|
3286
|
+
return mintWorkspaceBridgeToken({
|
|
3287
|
+
...options,
|
|
3288
|
+
audience: WORKSPACE_BRIDGE_TOKEN_AUDIENCE,
|
|
3289
|
+
ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
function mintWorkspaceBridgeRuntimeRefreshToken(options) {
|
|
3293
|
+
return mintWorkspaceBridgeToken({
|
|
3294
|
+
...options,
|
|
3295
|
+
audience: WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE,
|
|
3296
|
+
// Refresh tokens intentionally outlive short call tokens, but remain
|
|
3297
|
+
// sandbox-bound by workspace/session/runtime/capabilities claims.
|
|
3298
|
+
ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS,
|
|
3299
|
+
tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(options.tokenTtlMs)
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
function verifyWorkspaceBridgeRuntimeToken(token, options) {
|
|
3303
|
+
assertUsableSecret(options.secret);
|
|
3304
|
+
const claims = parseAndVerifyToken(token, options.secret);
|
|
3305
|
+
const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
|
|
3306
|
+
ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_TOKEN_AUDIENCE, "Runtime bridge token");
|
|
3307
|
+
const missingCapability = (options.requiredCapabilities ?? []).find(
|
|
3308
|
+
(capability) => !claims.capabilities.includes(capability)
|
|
3309
|
+
);
|
|
3310
|
+
if (missingCapability) {
|
|
3311
|
+
throw bridgeTokenError("BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Runtime bridge token is missing a required capability");
|
|
3312
|
+
}
|
|
3313
|
+
const runtimeClaims = claims;
|
|
3314
|
+
return {
|
|
3315
|
+
claims: runtimeClaims,
|
|
3316
|
+
authContext: runtimeClaimsToBridgeAuthContext(runtimeClaims)
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
function verifyWorkspaceBridgeRuntimeRefreshToken(token, options) {
|
|
3320
|
+
assertUsableSecret(options.secret);
|
|
3321
|
+
const claims = parseAndVerifyToken(token, options.secret);
|
|
3322
|
+
const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
|
|
3323
|
+
ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE, "Runtime bridge refresh token");
|
|
3324
|
+
return { claims };
|
|
3325
|
+
}
|
|
3326
|
+
function clampWorkspaceBridgeRuntimeTokenTtlMs(ttlMs) {
|
|
3327
|
+
if (ttlMs === void 0) return void 0;
|
|
3328
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return void 0;
|
|
3329
|
+
return Math.min(ttlMs, MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS);
|
|
3330
|
+
}
|
|
3331
|
+
function runtimeClaimsToBridgeAuthContext(claims) {
|
|
3332
|
+
return {
|
|
3333
|
+
callerClass: "runtime",
|
|
3334
|
+
workspaceId: claims.workspaceId,
|
|
3335
|
+
sessionId: claims.sessionId,
|
|
3336
|
+
capabilities: claims.capabilities,
|
|
3337
|
+
tokenId: claims.jti,
|
|
3338
|
+
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
3339
|
+
actor: {
|
|
3340
|
+
actorKind: "agent",
|
|
3341
|
+
performedBy: {
|
|
3342
|
+
label: claims.runtimeId ? `runtime:${claims.runtimeId}` : "runtime:agent",
|
|
3343
|
+
id: claims.runtimeId
|
|
3344
|
+
},
|
|
3345
|
+
onBehalfOf: claims.sessionId ? { label: `session:${claims.sessionId}` } : void 0
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
function mintWorkspaceBridgeToken(options) {
|
|
3350
|
+
assertUsableSecret(options.secret);
|
|
3351
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
3352
|
+
const claims = {
|
|
3353
|
+
aud: options.audience,
|
|
3354
|
+
workspaceId: options.workspaceId,
|
|
3355
|
+
sessionId: options.sessionId,
|
|
3356
|
+
runtimeId: options.runtimeId,
|
|
3357
|
+
capabilities: [...options.capabilities],
|
|
3358
|
+
iat: Math.floor(nowMs / 1e3),
|
|
3359
|
+
exp: Math.floor((nowMs + options.ttlMs) / 1e3),
|
|
3360
|
+
jti: options.jti ?? randomUUID(),
|
|
3361
|
+
...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
|
|
3362
|
+
};
|
|
3363
|
+
return signClaims(claims, options.secret);
|
|
3364
|
+
}
|
|
3365
|
+
function ensureLiveTokenClaims(claims, now, expectedAudience, label) {
|
|
3366
|
+
if (claims.aud !== expectedAudience) {
|
|
3367
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} has invalid audience`);
|
|
3368
|
+
}
|
|
3369
|
+
if (claims.exp <= now) {
|
|
3370
|
+
throw bridgeTokenError("BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, `${label} has expired`);
|
|
3371
|
+
}
|
|
3372
|
+
if (claims.iat > now + 60) {
|
|
3373
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} is not valid yet`);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function signClaims(claims, secret) {
|
|
3377
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
3378
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
3379
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(claims));
|
|
3380
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
3381
|
+
return `${signingInput}.${hmac(signingInput, secret)}`;
|
|
3382
|
+
}
|
|
3383
|
+
function parseAndVerifyToken(token, secret) {
|
|
3384
|
+
const parts = token.split(".");
|
|
3385
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
3386
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token is malformed");
|
|
3387
|
+
}
|
|
3388
|
+
const [headerPart, payloadPart, signature] = parts;
|
|
3389
|
+
const signingInput = `${headerPart}.${payloadPart}`;
|
|
3390
|
+
const expected = hmac(signingInput, secret);
|
|
3391
|
+
if (!safeEqual(signature, expected)) {
|
|
3392
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token signature is invalid");
|
|
3393
|
+
}
|
|
3394
|
+
let header;
|
|
3395
|
+
let payload;
|
|
3396
|
+
try {
|
|
3397
|
+
header = JSON.parse(base64UrlDecode(headerPart));
|
|
3398
|
+
payload = JSON.parse(base64UrlDecode(payloadPart));
|
|
3399
|
+
} catch {
|
|
3400
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token payload is invalid");
|
|
3401
|
+
}
|
|
3402
|
+
if (!header || typeof header !== "object" || header.alg !== "HS256") {
|
|
3403
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token algorithm is invalid");
|
|
3404
|
+
}
|
|
3405
|
+
return parseClaims(payload);
|
|
3406
|
+
}
|
|
3407
|
+
function parseClaims(payload) {
|
|
3408
|
+
if (!payload || typeof payload !== "object") {
|
|
3409
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
|
|
3410
|
+
}
|
|
3411
|
+
const claims = payload;
|
|
3412
|
+
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") {
|
|
3413
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
|
|
3414
|
+
}
|
|
3415
|
+
if (claims.tokenTtlMs !== void 0 && (typeof claims.tokenTtlMs !== "number" || !Number.isFinite(claims.tokenTtlMs) || claims.tokenTtlMs <= 0)) {
|
|
3416
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
aud: claims.aud,
|
|
3420
|
+
workspaceId: claims.workspaceId,
|
|
3421
|
+
sessionId: optionalString(claims.sessionId),
|
|
3422
|
+
runtimeId: optionalString(claims.runtimeId),
|
|
3423
|
+
capabilities: [...claims.capabilities],
|
|
3424
|
+
iat: claims.iat,
|
|
3425
|
+
exp: claims.exp,
|
|
3426
|
+
jti: claims.jti,
|
|
3427
|
+
...typeof claims.tokenTtlMs === "number" ? { tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) } : {}
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
function bridgeTokenError(code, message) {
|
|
3431
|
+
return createWorkspaceBridgeError(code, message);
|
|
3432
|
+
}
|
|
3433
|
+
function assertUsableSecret(secret) {
|
|
3434
|
+
if (secret.length < 32) {
|
|
3435
|
+
throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token secret is too short");
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
function hmac(value, secret) {
|
|
3439
|
+
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
3440
|
+
}
|
|
3441
|
+
function safeEqual(actual, expected) {
|
|
3442
|
+
const actualBuffer = Buffer.from(actual);
|
|
3443
|
+
const expectedBuffer = Buffer.from(expected);
|
|
3444
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
3445
|
+
}
|
|
3446
|
+
function base64UrlEncode(value) {
|
|
3447
|
+
return Buffer.from(value).toString("base64url");
|
|
3448
|
+
}
|
|
3449
|
+
function base64UrlDecode(value) {
|
|
3450
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
3451
|
+
}
|
|
3452
|
+
function optionalString(value) {
|
|
3453
|
+
return typeof value === "string" ? value : void 0;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/server/workspaceBridge/refreshTokenStore.ts
|
|
3457
|
+
var InMemoryWorkspaceBridgeRuntimeRefreshTokenStore = class {
|
|
3458
|
+
revoked = /* @__PURE__ */ new Map();
|
|
3459
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
3460
|
+
lastGcMs = 0;
|
|
3461
|
+
revoke(jti, expiresAtMs) {
|
|
3462
|
+
this.gc();
|
|
3463
|
+
this.revoked.set(jti, expiresAtMs);
|
|
3464
|
+
this.rateLimits.delete(jti);
|
|
3465
|
+
}
|
|
3466
|
+
recordUse(options) {
|
|
3467
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
3468
|
+
this.gc(nowMs);
|
|
3469
|
+
if (this.revoked.has(options.jti)) return { allowed: false, reason: "revoked" };
|
|
3470
|
+
if (options.maxUses <= 0) return { allowed: false, reason: "rate-limited", retryAfterMs: options.windowMs };
|
|
3471
|
+
const existing = this.rateLimits.get(options.jti);
|
|
3472
|
+
if (!existing || nowMs - existing.windowStartMs >= existing.windowMs || isExpired(existing.expiresAtMs, nowMs)) {
|
|
3473
|
+
this.rateLimits.set(options.jti, {
|
|
3474
|
+
windowStartMs: nowMs,
|
|
3475
|
+
windowMs: options.windowMs,
|
|
3476
|
+
count: 1,
|
|
3477
|
+
expiresAtMs: options.expiresAtMs
|
|
3478
|
+
});
|
|
3479
|
+
return { allowed: true };
|
|
3480
|
+
}
|
|
3481
|
+
if (existing.count >= options.maxUses) {
|
|
3482
|
+
return { allowed: false, reason: "rate-limited", retryAfterMs: Math.max(0, existing.windowStartMs + existing.windowMs - nowMs) };
|
|
3483
|
+
}
|
|
3484
|
+
existing.count += 1;
|
|
3485
|
+
existing.windowMs = options.windowMs;
|
|
3486
|
+
existing.expiresAtMs = options.expiresAtMs ?? existing.expiresAtMs;
|
|
3487
|
+
return { allowed: true };
|
|
3488
|
+
}
|
|
3489
|
+
gc(nowMs = Date.now()) {
|
|
3490
|
+
if (nowMs - this.lastGcMs < 6e4) return 0;
|
|
3491
|
+
this.lastGcMs = nowMs;
|
|
3492
|
+
let removed = 0;
|
|
3493
|
+
for (const [jti, record] of this.rateLimits) {
|
|
3494
|
+
if (isExpired(record.expiresAtMs, nowMs) || nowMs - record.windowStartMs >= record.windowMs) {
|
|
3495
|
+
this.rateLimits.delete(jti);
|
|
3496
|
+
removed += 1;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
for (const [jti, expiresAtMs] of this.revoked) {
|
|
3500
|
+
if (isExpired(expiresAtMs, nowMs)) {
|
|
3501
|
+
this.revoked.delete(jti);
|
|
3502
|
+
removed += 1;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return removed;
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
function isExpired(expiresAtMs, nowMs) {
|
|
3509
|
+
return expiresAtMs !== void 0 && expiresAtMs <= nowMs;
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// src/server/workspaceBridge/httpRoutes.ts
|
|
3513
|
+
var bridgeCallBodySchema = z3.object({
|
|
3514
|
+
op: z3.string().min(1),
|
|
3515
|
+
input: z3.unknown().default({}),
|
|
3516
|
+
requestId: z3.string().optional(),
|
|
3517
|
+
idempotencyKey: z3.string().optional()
|
|
3518
|
+
});
|
|
3519
|
+
var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES = 30;
|
|
3520
|
+
var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3521
|
+
function workspaceBridgeHttpRoutes(app, opts, done) {
|
|
3522
|
+
const defaultRefreshTokenStore = opts.runtimeRefreshTokenStore ?? new InMemoryWorkspaceBridgeRuntimeRefreshTokenStore();
|
|
3523
|
+
app.post("/api/v1/workspace-bridge/call", async (request, reply) => {
|
|
3524
|
+
reply.header("Cache-Control", "no-store");
|
|
3525
|
+
const contentType = String(request.headers["content-type"] ?? "");
|
|
3526
|
+
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
3527
|
+
return sendBridgeError(reply, 415, void 0, "BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge transport requires application/json");
|
|
3528
|
+
}
|
|
3529
|
+
const rawBodySize = measureJsonBytes(request.body);
|
|
3530
|
+
if (opts.maxBodyBytes && rawBodySize > opts.maxBodyBytes) {
|
|
3531
|
+
return sendBridgeError(reply, 413, void 0, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "WorkspaceBridge request body is too large");
|
|
3532
|
+
}
|
|
3533
|
+
const parsed = bridgeCallBodySchema.safeParse(request.body);
|
|
3534
|
+
if (!parsed.success) {
|
|
3535
|
+
return sendBridgeError(reply, 400, void 0, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "WorkspaceBridge request body is invalid");
|
|
3536
|
+
}
|
|
3537
|
+
const body = { ...parsed.data, input: parsed.data.input ?? {} };
|
|
3538
|
+
try {
|
|
3539
|
+
const registry = await resolveRegistry(request, body, opts);
|
|
3540
|
+
const definition = registry.getDefinition(body.op);
|
|
3541
|
+
if (!definition) {
|
|
3542
|
+
return sendBridgeError(reply, statusForBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */), body.requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
|
|
3543
|
+
}
|
|
3544
|
+
const authHeader = firstHeader(request.headers.authorization);
|
|
3545
|
+
const authContext = authHeader?.startsWith("Bearer ") ? resolveRuntimeContext(authHeader.slice("Bearer ".length), opts, definition) : await resolveBrowserContext(request, opts, definition, body);
|
|
3546
|
+
const idempotencyStore = opts.getIdempotencyStore ? await opts.getIdempotencyStore(request, body) : opts.idempotencyStore;
|
|
3547
|
+
const expectedWorkspaceId = opts.getOwnerWorkspaceId ? await opts.getOwnerWorkspaceId(request, body, authContext) : opts.ownerWorkspaceId;
|
|
3548
|
+
const response = await runWithWorkspaceBridgeIdempotency(idempotencyStore, {
|
|
3549
|
+
definition,
|
|
3550
|
+
request: body,
|
|
3551
|
+
auth: authContext
|
|
3552
|
+
}, async () => await registry.call(body, authContext, { expectedWorkspaceId }));
|
|
3553
|
+
return await sendResponse(reply, response);
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_HANDLER_FAILED" /* HandlerFailed */, "WorkspaceBridge transport failed");
|
|
3556
|
+
return sendBridgeError(reply, statusForBridgeError(bridgeError.code), body.requestId, bridgeError.code, bridgeError.message);
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
3559
|
+
app.post("/api/v1/workspace-bridge/token", async (request, reply) => {
|
|
3560
|
+
reply.header("Cache-Control", "no-store");
|
|
3561
|
+
const authHeader = firstHeader(request.headers.authorization);
|
|
3562
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
3563
|
+
return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge refresh token is required");
|
|
3564
|
+
}
|
|
3565
|
+
if (!opts.runtimeTokenSecret || !opts.runtimeRefreshTokenSecret) {
|
|
3566
|
+
return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge token refresh is not configured");
|
|
3567
|
+
}
|
|
3568
|
+
try {
|
|
3569
|
+
const nowMs = Date.now();
|
|
3570
|
+
const verified = verifyWorkspaceBridgeRuntimeRefreshToken(authHeader.slice("Bearer ".length), {
|
|
3571
|
+
secret: opts.runtimeRefreshTokenSecret,
|
|
3572
|
+
nowMs
|
|
3573
|
+
});
|
|
3574
|
+
const store = opts.getRuntimeRefreshTokenStore ? await opts.getRuntimeRefreshTokenStore(request, verified.claims) ?? defaultRefreshTokenStore : defaultRefreshTokenStore;
|
|
3575
|
+
const refreshUse = await store.recordUse({
|
|
3576
|
+
jti: verified.claims.jti,
|
|
3577
|
+
nowMs,
|
|
3578
|
+
maxUses: opts.refreshTokenRateLimit?.maxUses ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES,
|
|
3579
|
+
windowMs: opts.refreshTokenRateLimit?.windowMs ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS,
|
|
3580
|
+
expiresAtMs: verified.claims.exp * 1e3
|
|
3581
|
+
});
|
|
3582
|
+
if (!refreshUse.allowed) {
|
|
3583
|
+
if (refreshUse.reason === "revoked") {
|
|
3584
|
+
return sendBridgeError(reply, 401, void 0, "BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is revoked");
|
|
3585
|
+
}
|
|
3586
|
+
reply.header("Retry-After", Math.ceil(refreshUse.retryAfterMs / 1e3).toString());
|
|
3587
|
+
return sendBridgeError(reply, 429, void 0, "BRIDGE_RATE_LIMITED" /* RateLimited */, "WorkspaceBridge refresh token rate limit exceeded");
|
|
3588
|
+
}
|
|
3589
|
+
const ttlMs = refreshMintTtlMs(verified.claims, Date.now());
|
|
3590
|
+
if (ttlMs === void 0) {
|
|
3591
|
+
return sendBridgeError(reply, 401, void 0, "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, "Runtime bridge refresh token has expired");
|
|
3592
|
+
}
|
|
3593
|
+
const token = mintWorkspaceBridgeRuntimeToken({
|
|
3594
|
+
secret: opts.runtimeTokenSecret,
|
|
3595
|
+
workspaceId: verified.claims.workspaceId,
|
|
3596
|
+
sessionId: verified.claims.sessionId,
|
|
3597
|
+
runtimeId: verified.claims.runtimeId,
|
|
3598
|
+
capabilities: verified.claims.capabilities,
|
|
3599
|
+
ttlMs
|
|
3600
|
+
});
|
|
3601
|
+
return reply.code(200).send({ ok: true, token });
|
|
3602
|
+
} catch (err) {
|
|
3603
|
+
const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is invalid");
|
|
3604
|
+
return sendBridgeError(reply, statusForBridgeError(bridgeError.code), void 0, bridgeError.code, bridgeError.message);
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
done();
|
|
3608
|
+
}
|
|
3609
|
+
async function resolveRegistry(request, body, opts) {
|
|
3610
|
+
const registry = opts.getRegistry ? await opts.getRegistry(request, body) : opts.registry;
|
|
3611
|
+
if (!registry) {
|
|
3612
|
+
throw createWorkspaceBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge registry is not configured");
|
|
3613
|
+
}
|
|
3614
|
+
return registry;
|
|
3615
|
+
}
|
|
3616
|
+
function resolveRuntimeContext(token, opts, definition) {
|
|
3617
|
+
if (!opts.runtimeTokenSecret) {
|
|
3618
|
+
throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Runtime bridge token auth is not configured");
|
|
3619
|
+
}
|
|
3620
|
+
return verifyWorkspaceBridgeRuntimeToken(token, {
|
|
3621
|
+
secret: opts.runtimeTokenSecret,
|
|
3622
|
+
requiredCapabilities: definition.requiredCapabilities
|
|
3623
|
+
}).authContext;
|
|
3624
|
+
}
|
|
3625
|
+
async function resolveBrowserContext(request, opts, definition, body) {
|
|
3626
|
+
if (!opts.browserAuthPolicy) {
|
|
3627
|
+
throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Browser bridge auth is not configured");
|
|
3628
|
+
}
|
|
3629
|
+
const workspaceId = firstHeader(request.headers["x-boring-workspace-id"]) ?? "default";
|
|
3630
|
+
const sessionId = firstHeader(request.headers["x-boring-session-id"]);
|
|
3631
|
+
return (await opts.browserAuthPolicy.resolve({
|
|
3632
|
+
callerClass: "browser",
|
|
3633
|
+
definition,
|
|
3634
|
+
workspaceId,
|
|
3635
|
+
sessionId,
|
|
3636
|
+
request: { headers: request.headers, method: request.method, user: request.user },
|
|
3637
|
+
body
|
|
3638
|
+
})).context;
|
|
3639
|
+
}
|
|
3640
|
+
async function sendResponse(reply, response) {
|
|
3641
|
+
return reply.code(response.ok ? 200 : statusForBridgeError(response.error.code)).send(response);
|
|
3642
|
+
}
|
|
3643
|
+
function refreshMintTtlMs(claims, nowMs) {
|
|
3644
|
+
const requested = clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS;
|
|
3645
|
+
const remaining = claims.exp * 1e3 - nowMs;
|
|
3646
|
+
if (remaining <= 0) return void 0;
|
|
3647
|
+
return Math.min(requested, remaining);
|
|
3648
|
+
}
|
|
3649
|
+
function sendBridgeError(reply, status, requestId, code, message) {
|
|
3650
|
+
reply.header("Cache-Control", "no-store");
|
|
3651
|
+
return reply.code(status).send({ ok: false, requestId, error: { code, message } });
|
|
3652
|
+
}
|
|
3653
|
+
function statusForBridgeError(code) {
|
|
3654
|
+
if (code === "BRIDGE_AUTH_REQUIRED" /* AuthRequired */ || code === "BRIDGE_INVALID_TOKEN" /* InvalidToken */ || code === "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */) return 401;
|
|
3655
|
+
if (code === "BRIDGE_RATE_LIMITED" /* RateLimited */) return 429;
|
|
3656
|
+
if (code === "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */ || code === "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */ || code === "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */) return 403;
|
|
3657
|
+
if (code === "BRIDGE_OP_NOT_FOUND" /* OpNotFound */) return 404;
|
|
3658
|
+
if (code === "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */ || code === "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */) return 413;
|
|
3659
|
+
return 400;
|
|
3660
|
+
}
|
|
3661
|
+
function firstHeader(value) {
|
|
3662
|
+
return Array.isArray(value) ? value[0] : value;
|
|
3663
|
+
}
|
|
3664
|
+
function isBridgeError(err) {
|
|
3665
|
+
return !!err && typeof err === "object" && "code" in err && "message" in err;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// src/server/workspaceBridge/runtimeEnv.ts
|
|
3669
|
+
var BRIDGE_CALL_PATH = "/api/v1/workspace-bridge/call";
|
|
3670
|
+
var BRIDGE_TOKEN_PATH = "/api/v1/workspace-bridge/token";
|
|
3671
|
+
function createWorkspaceBridgeRuntimeEnvContribution(options) {
|
|
3672
|
+
const enabled = options.runtimeEnv?.enabled ?? Boolean(options.runtimeEnv?.bridgeUrl);
|
|
3673
|
+
if (!enabled) return void 0;
|
|
3674
|
+
const bridgeUrl = resolveBridgeCallUrl(options.runtimeEnv?.bridgeUrl);
|
|
3675
|
+
const tokenUrl = resolveBridgeTokenUrl(options.runtimeEnv?.bridgeUrl);
|
|
3676
|
+
const capabilities = options.runtimeEnv?.capabilities;
|
|
3677
|
+
return {
|
|
3678
|
+
id: "workspace-bridge-runtime-env",
|
|
3679
|
+
getEnv: (ctx) => {
|
|
3680
|
+
const runtimePlacement = resolveRuntimePlacement(ctx?.runtimeBundle, options.runtimePlacement);
|
|
3681
|
+
const disabledReason = validateRuntimeBridgeUrl({
|
|
3682
|
+
bridgeUrl,
|
|
3683
|
+
runtimePlacement,
|
|
3684
|
+
allowInsecureHttp: options.runtimeEnv?.allowInsecureHttp,
|
|
3685
|
+
hasRuntimeTokenSecret: Boolean(options.runtimeTokenSecret),
|
|
3686
|
+
hasCapabilities: Array.isArray(capabilities) && capabilities.length > 0
|
|
3687
|
+
});
|
|
3688
|
+
if (disabledReason) {
|
|
3689
|
+
return { [WORKSPACE_BRIDGE_DISABLED_ENV]: disabledReason };
|
|
3690
|
+
}
|
|
3691
|
+
const token = mintWorkspaceBridgeRuntimeToken({
|
|
3692
|
+
secret: options.runtimeTokenSecret,
|
|
3693
|
+
workspaceId: options.workspaceId,
|
|
3694
|
+
sessionId: options.runtimeEnv?.sessionId,
|
|
3695
|
+
runtimeId: options.runtimeMode,
|
|
3696
|
+
capabilities,
|
|
3697
|
+
ttlMs: options.runtimeEnv?.tokenTtlMs
|
|
3698
|
+
});
|
|
3699
|
+
const refreshToken = options.runtimeRefreshTokenSecret && tokenUrl && isRefreshTokenUrlSafe(tokenUrl) ? mintWorkspaceBridgeRuntimeRefreshToken({
|
|
3700
|
+
secret: options.runtimeRefreshTokenSecret,
|
|
3701
|
+
workspaceId: options.workspaceId,
|
|
3702
|
+
sessionId: options.runtimeEnv?.sessionId,
|
|
3703
|
+
runtimeId: options.runtimeMode,
|
|
3704
|
+
capabilities,
|
|
3705
|
+
ttlMs: options.runtimeEnv?.refreshTokenTtlMs,
|
|
3706
|
+
tokenTtlMs: options.runtimeEnv?.tokenTtlMs
|
|
3707
|
+
}) : void 0;
|
|
3708
|
+
return {
|
|
3709
|
+
[WORKSPACE_BRIDGE_URL_ENV]: bridgeUrl,
|
|
3710
|
+
[WORKSPACE_BRIDGE_TOKEN_ENV]: token,
|
|
3711
|
+
...refreshToken ? {
|
|
3712
|
+
[WORKSPACE_BRIDGE_TOKEN_URL_ENV]: tokenUrl,
|
|
3713
|
+
[WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV]: refreshToken
|
|
3714
|
+
} : {},
|
|
3715
|
+
BORING_WORKSPACE_ID: options.workspaceId,
|
|
3716
|
+
BORING_AGENT_SESSION_ID: options.runtimeEnv?.sessionId ?? options.workspaceId
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
function resolveBridgeCallUrl(value) {
|
|
3722
|
+
return resolveBridgeUrl(value, BRIDGE_CALL_PATH);
|
|
3723
|
+
}
|
|
3724
|
+
function resolveBridgeTokenUrl(value) {
|
|
3725
|
+
return resolveBridgeUrl(value, BRIDGE_TOKEN_PATH);
|
|
3726
|
+
}
|
|
3727
|
+
function resolveBridgeUrl(value, path) {
|
|
3728
|
+
if (!value?.trim()) return void 0;
|
|
3729
|
+
try {
|
|
3730
|
+
const url = new URL(value);
|
|
3731
|
+
if (url.pathname === "" || url.pathname === "/" || url.pathname === BRIDGE_CALL_PATH || url.pathname === BRIDGE_TOKEN_PATH) {
|
|
3732
|
+
url.pathname = path;
|
|
3733
|
+
}
|
|
3734
|
+
return url.toString();
|
|
3735
|
+
} catch {
|
|
3736
|
+
return void 0;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
function isRefreshTokenUrlSafe(value) {
|
|
3740
|
+
try {
|
|
3741
|
+
const url = new URL(value);
|
|
3742
|
+
return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
|
|
3743
|
+
} catch {
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
function isLoopbackHost(hostname) {
|
|
3748
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
3749
|
+
}
|
|
3750
|
+
function resolveRuntimePlacement(runtimeBundle, fallback) {
|
|
3751
|
+
if (runtimeBundle?.filesystem?.kind === "remote-workspace" || runtimeBundle?.bash?.kind === "remote") return "remote";
|
|
3752
|
+
return fallback ?? "local";
|
|
3753
|
+
}
|
|
3754
|
+
function validateRuntimeBridgeUrl(options) {
|
|
3755
|
+
if (!options.bridgeUrl) return "bridge-url-missing";
|
|
3756
|
+
if (!options.hasRuntimeTokenSecret) return "runtime-token-secret-missing";
|
|
3757
|
+
if (!options.hasCapabilities) return "runtime-capabilities-missing";
|
|
3758
|
+
let url;
|
|
3759
|
+
try {
|
|
3760
|
+
url = new URL(options.bridgeUrl);
|
|
3761
|
+
} catch {
|
|
3762
|
+
return "bridge-url-invalid";
|
|
3763
|
+
}
|
|
3764
|
+
const isRemote = options.runtimePlacement === "remote";
|
|
3765
|
+
const isLocalhost = isLoopbackHost(url.hostname);
|
|
3766
|
+
if (isRemote && url.protocol !== "https:") return "remote-bridge-url-must-be-https";
|
|
3767
|
+
if (isRemote && isLocalhost) return "remote-bridge-url-must-not-be-localhost";
|
|
3768
|
+
if (url.protocol === "http:" && !options.allowInsecureHttp && !isLocalhost) return "remote-bridge-url-must-be-https";
|
|
3769
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "bridge-url-invalid";
|
|
3770
|
+
return void 0;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/server/workspaceBridge/runtimeCore.ts
|
|
3774
|
+
function createWorkspaceBridgeRuntimeCore(options = {}) {
|
|
3775
|
+
const registry = options.registry ?? createWorkspaceBridgeRegistry({ ownerWorkspaceId: options.ownerWorkspaceId });
|
|
3776
|
+
for (const entry of options.handlers ?? []) {
|
|
3777
|
+
registry.registerHandler(entry.definition, entry.handler);
|
|
3778
|
+
}
|
|
3779
|
+
return { registry };
|
|
3780
|
+
}
|
|
3781
|
+
|
|
2614
3782
|
// src/app/server/createWorkspaceAgentServer.ts
|
|
2615
3783
|
var __dirname = dirname7(fileURLToPath(import.meta.url));
|
|
2616
3784
|
var require4 = createRequire4(import.meta.url);
|
|
@@ -2758,6 +3926,7 @@ function collectWorkspaceAgentServerPlugins(opts = {}) {
|
|
|
2758
3926
|
...result.runtimePlugins
|
|
2759
3927
|
],
|
|
2760
3928
|
routeContributions: result.routeContributions,
|
|
3929
|
+
workspaceBridgeHandlers: result.workspaceBridgeHandlers,
|
|
2761
3930
|
preservedUiStateKeys: result.preservedUiStateKeys,
|
|
2762
3931
|
agentOptions: {
|
|
2763
3932
|
extraTools: result.agentTools,
|
|
@@ -2919,6 +4088,23 @@ function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
|
|
|
2919
4088
|
return emptyPackageJsonPiSnapshot();
|
|
2920
4089
|
}
|
|
2921
4090
|
}
|
|
4091
|
+
function resolveWorkspaceBridgeBrowserAuthPolicy(opts, registry) {
|
|
4092
|
+
if (opts.workspaceBridge?.browserAuthPolicy) return opts.workspaceBridge.browserAuthPolicy;
|
|
4093
|
+
if (opts.workspaceBridge?.allowInsecureLocalCliBrowserAuth !== true) return void 0;
|
|
4094
|
+
emitLocalCliBridgeAuthWarning();
|
|
4095
|
+
return createLocalCliBridgeAuthPolicy({
|
|
4096
|
+
workspaceId: "default",
|
|
4097
|
+
capabilities: registry.listDefinitions().flatMap((definition) => [...definition.requiredCapabilities])
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
function emitLocalCliBridgeAuthWarning() {
|
|
4101
|
+
const message = "createWorkspaceAgentServer is using createLocalCliBridgeAuthPolicy for WorkspaceBridge browser calls. This policy is unauthenticated, grants registered bridge capabilities to a fixed local-cli principal, and is intended only for local/dev CLI usage. Provide workspaceBridge.browserAuthPolicy before exposing this server.";
|
|
4102
|
+
if (typeof process.emitWarning === "function") {
|
|
4103
|
+
process.emitWarning(message, { code: "BORING_WORKSPACE_BRIDGE_INSECURE_AUTH" });
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
console.warn(message);
|
|
4107
|
+
}
|
|
2922
4108
|
async function createWorkspaceAgentServer(opts = {}) {
|
|
2923
4109
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
2924
4110
|
const bridge = createInMemoryBridge();
|
|
@@ -2937,13 +4123,17 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2937
4123
|
defaultPluginPackages: opts.defaultPluginPackages,
|
|
2938
4124
|
anchorDir: opts.appRoot
|
|
2939
4125
|
});
|
|
2940
|
-
const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: true })).filter((entry) => hasDirServerPlugin(entry));
|
|
4126
|
+
const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: true, trust: "internal" })).filter((entry) => hasDirServerPlugin(entry));
|
|
2941
4127
|
const allPluginEntries = [
|
|
2942
4128
|
...defaultPluginDirEntries,
|
|
2943
4129
|
...opts.plugins ?? []
|
|
2944
4130
|
];
|
|
2945
4131
|
const resolvedPlugins = await Promise.all(
|
|
2946
|
-
allPluginEntries.map((entry) =>
|
|
4132
|
+
allPluginEntries.map(async (entry) => {
|
|
4133
|
+
const plugin = await resolveOnePluginEntry(entry, ctx);
|
|
4134
|
+
assertWorkspaceBridgeHandlersTrusted(plugin, entry);
|
|
4135
|
+
return plugin;
|
|
4136
|
+
})
|
|
2947
4137
|
);
|
|
2948
4138
|
const pluginAuthoringEnabled = externalPluginsEnabled && (opts.installPluginAuthoring ?? workspaceFsCapability === "strong") && !(opts.excludeDefaults ?? []).includes("boring-ui-plugin-cli-package");
|
|
2949
4139
|
const pluginCollection = collectWorkspaceAgentServerPlugins({
|
|
@@ -2951,6 +4141,14 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
2951
4141
|
plugins: resolvedPlugins,
|
|
2952
4142
|
installPluginAuthoring: pluginAuthoringEnabled
|
|
2953
4143
|
});
|
|
4144
|
+
const { registry: workspaceBridgeRegistry } = createWorkspaceBridgeRuntimeCore({
|
|
4145
|
+
registry: opts.workspaceBridge?.registry,
|
|
4146
|
+
ownerWorkspaceId: "default",
|
|
4147
|
+
handlers: [
|
|
4148
|
+
...opts.workspaceBridge?.handlers ?? [],
|
|
4149
|
+
...pluginCollection.workspaceBridgeHandlers ?? []
|
|
4150
|
+
]
|
|
4151
|
+
});
|
|
2954
4152
|
const workspacePackagePiPackage = pluginAuthoringEnabled ? createBoringPiPackageSource(workspaceRoot) : void 0;
|
|
2955
4153
|
const baseStaticPiSkillPaths = [
|
|
2956
4154
|
...pluginAuthoringEnabled ? resolveBoringPiSkillPaths(workspaceRoot) : [],
|
|
@@ -3027,11 +4225,28 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
3027
4225
|
const rebuildPlugins = async () => {
|
|
3028
4226
|
return rebuildServerPlugins({ entries: allPluginEntries, ctx });
|
|
3029
4227
|
};
|
|
4228
|
+
const callerRuntimeProvisioner = opts.runtimeProvisioner;
|
|
4229
|
+
const boringUiCliCommandAvailable = opts.provisionWorkspace !== false && pluginCollection.provisioningContributions.some(
|
|
4230
|
+
(entry) => entry.id === "boring-ui-cli-package"
|
|
4231
|
+
);
|
|
4232
|
+
const workspaceBridgeRuntimeEnvContribution = createWorkspaceBridgeRuntimeEnvContribution({
|
|
4233
|
+
workspaceId: "default",
|
|
4234
|
+
runtimeMode: resolvedMode,
|
|
4235
|
+
registry: workspaceBridgeRegistry,
|
|
4236
|
+
runtimeTokenSecret: opts.workspaceBridge?.runtimeTokenSecret,
|
|
4237
|
+
runtimeRefreshTokenSecret: opts.workspaceBridge?.runtimeRefreshTokenSecret,
|
|
4238
|
+
runtimeEnv: opts.workspaceBridge?.runtimeEnv,
|
|
4239
|
+
runtimePlacement: workspaceFsCapability === "strong" ? "local" : "remote"
|
|
4240
|
+
});
|
|
3030
4241
|
const app = await createAgentApp({
|
|
3031
4242
|
...opts,
|
|
3032
4243
|
mode: resolvedMode,
|
|
3033
4244
|
workspaceRoot,
|
|
3034
4245
|
externalPlugins: externalPluginsEnabled,
|
|
4246
|
+
runtimeEnvContributions: [
|
|
4247
|
+
...opts.runtimeEnvContributions ?? [],
|
|
4248
|
+
...workspaceBridgeRuntimeEnvContribution ? [workspaceBridgeRuntimeEnvContribution] : []
|
|
4249
|
+
],
|
|
3035
4250
|
extraTools: [
|
|
3036
4251
|
...opts.extraTools ?? [],
|
|
3037
4252
|
...uiTools,
|
|
@@ -3114,6 +4329,19 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
3114
4329
|
});
|
|
3115
4330
|
}
|
|
3116
4331
|
await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
|
|
4332
|
+
await app.register(workspaceBridgeHttpRoutes, {
|
|
4333
|
+
registry: workspaceBridgeRegistry,
|
|
4334
|
+
runtimeTokenSecret: opts.workspaceBridge?.runtimeTokenSecret,
|
|
4335
|
+
runtimeRefreshTokenSecret: opts.workspaceBridge?.runtimeRefreshTokenSecret,
|
|
4336
|
+
ownerWorkspaceId: "default",
|
|
4337
|
+
idempotencyStore: new InMemoryWorkspaceBridgeIdempotencyStore(),
|
|
4338
|
+
browserAuthPolicy: resolveWorkspaceBridgeBrowserAuthPolicy(opts, workspaceBridgeRegistry)
|
|
4339
|
+
});
|
|
4340
|
+
const internals = app;
|
|
4341
|
+
internals.__boringWorkspaceBridgeRegistry = workspaceBridgeRegistry;
|
|
4342
|
+
internals.__boringRebuildPlugins = rebuildPlugins;
|
|
4343
|
+
internals.__boringAssetManager = boringAssetManager;
|
|
4344
|
+
internals.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
|
|
3117
4345
|
await app.register(boringPluginRoutes, {
|
|
3118
4346
|
manager: boringAssetManager
|
|
3119
4347
|
});
|
|
@@ -3121,18 +4349,16 @@ async function createWorkspaceAgentServer(opts = {}) {
|
|
|
3121
4349
|
for (const { routes } of pluginCollection.routeContributions) {
|
|
3122
4350
|
await app.register(routes);
|
|
3123
4351
|
}
|
|
3124
|
-
;
|
|
3125
|
-
app.__boringRebuildPlugins = rebuildPlugins;
|
|
3126
|
-
app.__boringAssetManager = boringAssetManager;
|
|
3127
|
-
app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
|
|
3128
4352
|
return app;
|
|
3129
4353
|
}
|
|
3130
4354
|
export {
|
|
3131
4355
|
PLUGIN_AUTHORING_PROVISIONING_IDS,
|
|
4356
|
+
assertWorkspaceBridgeHandlersTrusted,
|
|
3132
4357
|
buildWorkspaceContextPrompt,
|
|
3133
4358
|
collectWorkspaceAgentServerPlugins,
|
|
3134
4359
|
createWorkspaceAgentServer,
|
|
3135
4360
|
hasDirServerPlugin,
|
|
4361
|
+
isTrustedWorkspaceBridgeHandlerEntry,
|
|
3136
4362
|
omitPluginAuthoringProvisioning,
|
|
3137
4363
|
provisionWorkspaceAgentServer,
|
|
3138
4364
|
readWorkspacePluginPackagePiSnapshot,
|