@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/{FileTree-CfJoRiTm.js → FileTree-ChkSU4gL.js} +17 -17
- package/dist/{MarkdownEditor-DrPDGy9F.js → MarkdownEditor-BlYGosmH.js} +1 -1
- package/dist/{WorkspaceLoadingState-Bqsn2JEc.js → WorkspaceLoadingState-BYTO4wdt.js} +249 -225
- package/dist/{WorkspaceProvider-CrOT4ab7.js → WorkspaceProvider-Bp1Sm40K.js} +3359 -3237
- 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/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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
644
|
-
return [createGetUiStateTool(
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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-${
|
|
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 =
|
|
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
|
|
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
|
};
|