@floegence/redevplugin-ui 0.7.27 → 1.0.0
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/capability-client.d.ts +17 -19
- package/dist/capability-client.js +59 -62
- package/dist/contracts.gen.d.ts +26 -119
- package/dist/contracts.gen.js +26 -135
- package/dist/error-codes.gen.d.ts +3 -3
- package/dist/error-codes.gen.js +9 -12
- package/dist/http.js +1 -1
- package/dist/openapi.gen.d.ts +263 -829
- package/dist/platform.d.ts +21 -27
- package/dist/platform.js +23 -92
- package/dist/plugin.d.ts +2 -2
- package/dist/surface.d.ts +42 -45
- package/dist/surface.js +148 -423
- package/dist/trusted-parent.d.ts +1 -1
- package/package.json +3 -3
package/dist/surface.js
CHANGED
|
@@ -11,26 +11,7 @@ export const pluginSurfaceContextSchemaVersion = "redevplugin.surface_context.v1
|
|
|
11
11
|
const opaquePluginBridgeGlobalKey = "__redevpluginWorkerBridge";
|
|
12
12
|
const maxPendingPluginBridgeRequests = 256;
|
|
13
13
|
const maxPluginBridgeMessageBytes = opaqueSurfaceRenderLimits.max_message_bytes;
|
|
14
|
-
const maxRetainedPluginStreamHandles = 128;
|
|
15
14
|
const maxPluginJSONStructuralNodes = 32 * 1024;
|
|
16
|
-
const streamCredentialInvalidatingErrorCodes = new Set([
|
|
17
|
-
"PLUGIN_BRIDGE_DISPOSED",
|
|
18
|
-
"PLUGIN_BRIDGE_HANDSHAKE_FAILED",
|
|
19
|
-
"PLUGIN_BRIDGE_HANDSHAKE_REQUIRED",
|
|
20
|
-
"PLUGIN_BRIDGE_TIMEOUT",
|
|
21
|
-
"PLUGIN_CONTRACT_MISMATCH",
|
|
22
|
-
"PLUGIN_GATEWAY_TOKEN_CHANNEL_MISMATCH",
|
|
23
|
-
"PLUGIN_GATEWAY_TOKEN_INVALID",
|
|
24
|
-
"PLUGIN_GATEWAY_TOKEN_REPLAYED",
|
|
25
|
-
"PLUGIN_GRANT_INVALID",
|
|
26
|
-
"PLUGIN_LEASE_INVALID",
|
|
27
|
-
"PLUGIN_LEASE_REPLAYED",
|
|
28
|
-
"PLUGIN_MANAGEMENT_REVISION_MISMATCH",
|
|
29
|
-
"PLUGIN_STREAM_CANCELLED",
|
|
30
|
-
"PLUGIN_STREAM_TICKET_INVALID",
|
|
31
|
-
"PLUGIN_TOKEN_EXPIRED",
|
|
32
|
-
"PLUGIN_TOKEN_REPLAY",
|
|
33
|
-
]);
|
|
34
15
|
const maxOpaqueSurfaceLazyAssets = 128;
|
|
35
16
|
const maxOpaqueSurfaceLazyBytes = 32 * 1024 * 1024;
|
|
36
17
|
const maxConcurrentAssetReads = 4;
|
|
@@ -62,8 +43,6 @@ export class PluginBridgeClient {
|
|
|
62
43
|
#pendingRender;
|
|
63
44
|
#renderLoop;
|
|
64
45
|
#controlEditRevisions = new Map();
|
|
65
|
-
#pendingStreamDeliveries = new Map();
|
|
66
|
-
#streamReadTails = new Map();
|
|
67
46
|
#onMessage = (event) => {
|
|
68
47
|
void this.#handleMessage(event);
|
|
69
48
|
};
|
|
@@ -111,81 +90,45 @@ export class PluginBridgeClient {
|
|
|
111
90
|
request,
|
|
112
91
|
}, { mutation, signal: options.signal });
|
|
113
92
|
}
|
|
114
|
-
|
|
93
|
+
executionEvents(executionID, afterCursor, options = {}) {
|
|
115
94
|
this.#assertActive();
|
|
116
|
-
if (!validOpaqueHandle(
|
|
117
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin
|
|
95
|
+
if (!validOpaqueHandle(executionID, "execution") || !Number.isSafeInteger(afterCursor) || afterCursor < 0) {
|
|
96
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin execution event cursor is invalid");
|
|
118
97
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (options.signal?.aborted)
|
|
123
|
-
throw streamReadAbortedError();
|
|
124
|
-
return this.#readStream(streamHandle, options.signal);
|
|
125
|
-
});
|
|
126
|
-
this.#streamReadTails.set(streamHandle, read);
|
|
127
|
-
void read.finally(() => {
|
|
128
|
-
if (this.#streamReadTails.get(streamHandle) === read)
|
|
129
|
-
this.#streamReadTails.delete(streamHandle);
|
|
130
|
-
}).catch(() => undefined);
|
|
131
|
-
return abortableStreamRead(read, options.signal);
|
|
132
|
-
}
|
|
133
|
-
async #readStream(streamHandle, signal) {
|
|
134
|
-
const pending = this.#pendingStreamDeliveries.get(streamHandle);
|
|
135
|
-
if (pending) {
|
|
136
|
-
await this.#acknowledgeStream(streamHandle, pending.deliveryID, signal);
|
|
137
|
-
this.#pendingStreamDeliveries.delete(streamHandle);
|
|
138
|
-
return pending.result;
|
|
139
|
-
}
|
|
140
|
-
const id = this.#requestID("stream");
|
|
141
|
-
const privateResult = await this.#request(id, {
|
|
142
|
-
type: "redevplugin.bridge.stream.read",
|
|
143
|
-
id,
|
|
144
|
-
stream_handle: streamHandle,
|
|
145
|
-
}, { cancellationKind: "stream", signal });
|
|
146
|
-
const { delivery_id: deliveryID, ...result } = privateResult;
|
|
147
|
-
if (!deliveryID)
|
|
148
|
-
return result;
|
|
149
|
-
this.#pendingStreamDeliveries.set(streamHandle, { deliveryID, result });
|
|
150
|
-
await this.#acknowledgeStream(streamHandle, deliveryID, signal);
|
|
151
|
-
this.#pendingStreamDeliveries.delete(streamHandle);
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
async #acknowledgeStream(streamHandle, deliveryID, signal) {
|
|
155
|
-
const id = this.#requestID("stream_ack");
|
|
156
|
-
await this.#request(id, {
|
|
157
|
-
type: "redevplugin.bridge.stream.ack",
|
|
98
|
+
const id = this.#requestID("execution");
|
|
99
|
+
return this.#request(id, {
|
|
100
|
+
type: "redevplugin.bridge.execution.events",
|
|
158
101
|
id,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}, {
|
|
102
|
+
execution_id: executionID,
|
|
103
|
+
after_cursor: afterCursor,
|
|
104
|
+
}, { signal: options.signal });
|
|
162
105
|
}
|
|
163
|
-
|
|
106
|
+
cancelExecution(executionID, reason, options = {}) {
|
|
164
107
|
this.#assertActive();
|
|
165
|
-
if (!validOpaqueHandle(
|
|
166
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin
|
|
108
|
+
if (!validOpaqueHandle(executionID, "execution") || (reason !== undefined && (typeof reason !== "string" || reason.length > 256))) {
|
|
109
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin execution cancellation is invalid");
|
|
167
110
|
}
|
|
168
|
-
const id = this.#requestID("
|
|
111
|
+
const id = this.#requestID("execution");
|
|
169
112
|
return this.#request(id, removeUndefined({
|
|
170
|
-
type: "redevplugin.bridge.
|
|
113
|
+
type: "redevplugin.bridge.execution.cancel",
|
|
171
114
|
id,
|
|
172
|
-
|
|
115
|
+
execution_id: executionID,
|
|
173
116
|
reason,
|
|
174
117
|
}), { mutation: true, signal: options.signal });
|
|
175
118
|
}
|
|
176
|
-
|
|
119
|
+
executionSnapshot(executionID, options = {}) {
|
|
177
120
|
this.#assertActive();
|
|
178
|
-
if (!validOpaqueHandle(
|
|
179
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin
|
|
121
|
+
if (!validOpaqueHandle(executionID, "execution")) {
|
|
122
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin execution handle is invalid");
|
|
180
123
|
}
|
|
181
|
-
const id = this.#requestID("
|
|
124
|
+
const id = this.#requestID("execution");
|
|
182
125
|
return this.#request(id, {
|
|
183
|
-
type: "redevplugin.bridge.
|
|
126
|
+
type: "redevplugin.bridge.execution.query",
|
|
184
127
|
id,
|
|
185
|
-
|
|
128
|
+
execution_id: executionID,
|
|
186
129
|
}, { signal: options.signal }).then((snapshot) => {
|
|
187
|
-
if (!
|
|
188
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin
|
|
130
|
+
if (!isPluginExecutionSnapshot(snapshot) || snapshot.execution_id !== executionID) {
|
|
131
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin execution snapshot is invalid");
|
|
189
132
|
}
|
|
190
133
|
return snapshot;
|
|
191
134
|
});
|
|
@@ -313,7 +256,6 @@ export class PluginBridgeClient {
|
|
|
313
256
|
pending.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", `Plugin bridge request ${id} was disposed`));
|
|
314
257
|
}
|
|
315
258
|
this.#pending.clear();
|
|
316
|
-
this.#pendingStreamDeliveries.clear();
|
|
317
259
|
this.#actionHandlers.clear();
|
|
318
260
|
this.#canvasInputHandlers.clear();
|
|
319
261
|
this.#lifecycleHandlers.clear();
|
|
@@ -655,18 +597,19 @@ export function isPluginRiskPlan(plan) {
|
|
|
655
597
|
(plan.effect == null || isPluginRiskEffect(plan.effect)) &&
|
|
656
598
|
(plan.details == null || isRecord(plan.details));
|
|
657
599
|
}
|
|
658
|
-
export function
|
|
659
|
-
|
|
600
|
+
export function decodePluginEventText(event) {
|
|
601
|
+
const data = event.payload?.data;
|
|
602
|
+
if (typeof data !== "string" || data === "")
|
|
660
603
|
return "";
|
|
661
604
|
if (typeof TextDecoder === "function" && typeof atob === "function") {
|
|
662
|
-
const binary = atob(
|
|
605
|
+
const binary = atob(data);
|
|
663
606
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
664
607
|
return new TextDecoder().decode(bytes);
|
|
665
608
|
}
|
|
666
609
|
const bufferLike = globalThis.Buffer;
|
|
667
610
|
if (bufferLike)
|
|
668
|
-
return bufferLike.from(
|
|
669
|
-
throw new PluginBridgeError("PLUGIN_STREAM_FAILED", "No base64 decoder is available for plugin
|
|
611
|
+
return bufferLike.from(data, "base64").toString("utf8");
|
|
612
|
+
throw new PluginBridgeError("PLUGIN_STREAM_FAILED", "No base64 decoder is available for plugin execution event data");
|
|
670
613
|
}
|
|
671
614
|
export async function trustedParentBridgeHandshakeTranscriptSHA256(handshake, bridgeChannelID) {
|
|
672
615
|
const subtle = globalThis.crypto?.subtle;
|
|
@@ -771,8 +714,8 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
771
714
|
throw new Error("scriptNonce must contain 8-128 URL-safe characters");
|
|
772
715
|
}
|
|
773
716
|
const uiProtocolVersion = options.uiProtocolVersion ?? pluginUIProtocolVersion;
|
|
774
|
-
if (uiProtocolVersion !==
|
|
775
|
-
throw new Error(
|
|
717
|
+
if (uiProtocolVersion !== pluginUIProtocolVersion) {
|
|
718
|
+
throw new Error(`uiProtocolVersion must be ${pluginUIProtocolVersion}`);
|
|
776
719
|
}
|
|
777
720
|
const csp = [
|
|
778
721
|
"default-src 'none'",
|
|
@@ -863,7 +806,6 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
863
806
|
const validIdentifier = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
864
807
|
const validResourceIdentifier = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
|
|
865
808
|
const validOpaqueHandle = (value, prefix) => typeof value === "string" && value.startsWith(prefix + "_") && /^[A-Za-z0-9_-]{8,160}$/.test(value);
|
|
866
|
-
const validDeliveryID = (value) => typeof value === "string" && /^delivery_[A-Za-z0-9_-]{8,128}$/.test(value);
|
|
867
809
|
const validDigest = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
868
810
|
const validPath = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.startsWith("/") && !value.includes("\\\\") && !value.split("/").some((part) => !part || part === "." || part === "..");
|
|
869
811
|
const validAttribute = (tag, name, value) => {
|
|
@@ -928,14 +870,14 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
928
870
|
let workerHeartbeatTimeout;
|
|
929
871
|
let pendingQuiesceID;
|
|
930
872
|
const pendingWorkerRequests = new Set();
|
|
931
|
-
const requestSequence = { rpc: 0,
|
|
932
|
-
let
|
|
933
|
-
let
|
|
934
|
-
const
|
|
935
|
-
let
|
|
936
|
-
const
|
|
937
|
-
const
|
|
938
|
-
const
|
|
873
|
+
const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0 };
|
|
874
|
+
let executionQuerySurfaceTokens = 8;
|
|
875
|
+
let executionQuerySurfaceRefillAt = performance.now();
|
|
876
|
+
const executionQueryStateIdleMS = 120000;
|
|
877
|
+
let executionQueryNextPruneAt = performance.now() + 30000;
|
|
878
|
+
const maxExecutionQueryStates = 1024;
|
|
879
|
+
const executionQueryStates = new Map();
|
|
880
|
+
const executionQueryRequests = new Map();
|
|
939
881
|
let renderWindowStartedAt = 0;
|
|
940
882
|
let renderCount = 0;
|
|
941
883
|
let uiRevision = 0;
|
|
@@ -1045,8 +987,8 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1045
987
|
for (const url of blobURLs) URL.revokeObjectURL(url);
|
|
1046
988
|
blobURLs.clear();
|
|
1047
989
|
pendingWorkerRequests.clear();
|
|
1048
|
-
|
|
1049
|
-
|
|
990
|
+
executionQueryStates.clear();
|
|
991
|
+
executionQueryRequests.clear();
|
|
1050
992
|
pendingAssets.clear();
|
|
1051
993
|
queuedAssets.length = 0;
|
|
1052
994
|
activeAssetReads = 0;
|
|
@@ -2041,7 +1983,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2041
1983
|
const validCall = (value) => exactKeys(value, ["type", "request"]) && value.type === "redevplugin.bridge.call" && isRecord(value.request) && Object.keys(value.request).every((key) => ["id", "method", "params"].includes(key)) && typeof value.request.id === "string" && value.request.id.length <= 128 && typeof value.request.method === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value.request.method) && (value.request.params === undefined || isRecord(value.request.params));
|
|
2042
1984
|
const requestID = (value, expectedKind) => {
|
|
2043
1985
|
if (typeof value !== "string") return undefined;
|
|
2044
|
-
const match = /^(rpc|
|
|
1986
|
+
const match = /^(rpc|execution|render|canvas|asset)_([1-9][0-9]{0,15})$/.exec(value);
|
|
2045
1987
|
if (!match || match[1] !== expectedKind) return undefined;
|
|
2046
1988
|
const sequence = Number(match[2]);
|
|
2047
1989
|
return Number.isSafeInteger(sequence) ? { kind: match[1], sequence } : undefined;
|
|
@@ -2054,10 +1996,10 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2054
1996
|
return true;
|
|
2055
1997
|
};
|
|
2056
1998
|
const completeWorkerRequest = (id) => pendingWorkerRequests.delete(id);
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
const state =
|
|
1999
|
+
const completeExecutionQueryRequest = (id) => {
|
|
2000
|
+
const executionID = executionQueryRequests.get(id);
|
|
2001
|
+
executionQueryRequests.delete(id);
|
|
2002
|
+
const state = executionQueryStates.get(executionID);
|
|
2061
2003
|
if (state) { state.inFlight = false; state.lastSeen = performance.now(); }
|
|
2062
2004
|
completeWorkerRequest(id);
|
|
2063
2005
|
};
|
|
@@ -2065,24 +2007,24 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2065
2007
|
tokens: Math.min(burst, tokens + Math.max(0, now - refillAt) * rate / 1000),
|
|
2066
2008
|
refillAt: Math.max(refillAt, now),
|
|
2067
2009
|
});
|
|
2068
|
-
const
|
|
2010
|
+
const executionQueryRateLimit = (executionID, requestID) => {
|
|
2069
2011
|
const now = performance.now();
|
|
2070
|
-
const surface = refillSnapshotTokens(
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
if (
|
|
2074
|
-
|
|
2075
|
-
let existing =
|
|
2012
|
+
const surface = refillSnapshotTokens(executionQuerySurfaceTokens, executionQuerySurfaceRefillAt, 8, 8, now);
|
|
2013
|
+
executionQuerySurfaceTokens = surface.tokens;
|
|
2014
|
+
executionQuerySurfaceRefillAt = surface.refillAt;
|
|
2015
|
+
if (executionQuerySurfaceTokens < 1) return Math.max(500, Math.min(10000, Math.ceil((1 - executionQuerySurfaceTokens) / 8 * 1000)));
|
|
2016
|
+
executionQuerySurfaceTokens -= 1;
|
|
2017
|
+
let existing = executionQueryStates.get(executionID);
|
|
2076
2018
|
if (!existing) {
|
|
2077
|
-
if (now >=
|
|
2078
|
-
for (const [
|
|
2079
|
-
if (!state.inFlight && now - state.lastSeen >=
|
|
2019
|
+
if (now >= executionQueryNextPruneAt) {
|
|
2020
|
+
for (const [retainedExecutionID, state] of executionQueryStates) {
|
|
2021
|
+
if (!state.inFlight && now - state.lastSeen >= executionQueryStateIdleMS) executionQueryStates.delete(retainedExecutionID);
|
|
2080
2022
|
}
|
|
2081
|
-
|
|
2023
|
+
executionQueryNextPruneAt = now + 30000;
|
|
2082
2024
|
}
|
|
2083
|
-
if (
|
|
2025
|
+
if (executionQueryStates.size >= maxExecutionQueryStates) return 10000;
|
|
2084
2026
|
existing = { tokens: 2, refillAt: now, lastSeen: now, inFlight: false };
|
|
2085
|
-
|
|
2027
|
+
executionQueryStates.set(executionID, existing);
|
|
2086
2028
|
}
|
|
2087
2029
|
const operation = refillSnapshotTokens(existing.tokens, existing.refillAt, 2, 2, now);
|
|
2088
2030
|
existing.tokens = operation.tokens;
|
|
@@ -2093,7 +2035,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2093
2035
|
if (retryAfterMS > 0) return Math.max(500, Math.min(10000, retryAfterMS));
|
|
2094
2036
|
existing.tokens -= 1;
|
|
2095
2037
|
existing.inFlight = true;
|
|
2096
|
-
|
|
2038
|
+
executionQueryRequests.set(requestID, executionID);
|
|
2097
2039
|
return 0;
|
|
2098
2040
|
};
|
|
2099
2041
|
const renderRateAllowed = () => {
|
|
@@ -2309,36 +2251,30 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2309
2251
|
sendParent(message);
|
|
2310
2252
|
return;
|
|
2311
2253
|
}
|
|
2312
|
-
if (
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
if (exactKeys(message, ["type", "id", "stream_handle", "delivery_id"]) && message.type === "redevplugin.bridge.stream.ack" &&
|
|
2318
|
-
requestID(message.id, "stream_ack") && validOpaqueHandle(message.stream_handle, "stream") && validDeliveryID(message.delivery_id)) {
|
|
2319
|
-
if (!acceptWorkerRequest(message.id, "stream_ack")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2320
|
-
sendParent(message);
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
if (isRecord(message) && Object.keys(message).every((key) => ["type", "id", "operation_id", "reason"].includes(key)) &&
|
|
2324
|
-
message.type === "redevplugin.bridge.operation.cancel" && typeof message.id === "string" &&
|
|
2325
|
-
validOpaqueHandle(message.operation_id, "operation") && (message.reason === undefined || (typeof message.reason === "string" && message.reason.length <= 256))) {
|
|
2326
|
-
if (!acceptWorkerRequest(message.id, "operation")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2254
|
+
if (isRecord(message) && Object.keys(message).every((key) => ["type", "id", "execution_id", "reason"].includes(key)) &&
|
|
2255
|
+
message.type === "redevplugin.bridge.execution.cancel" && typeof message.id === "string" &&
|
|
2256
|
+
validOpaqueHandle(message.execution_id, "execution") && (message.reason === undefined || (typeof message.reason === "string" && message.reason.length <= 256))) {
|
|
2257
|
+
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2327
2258
|
sendParent(message);
|
|
2328
2259
|
return;
|
|
2329
2260
|
}
|
|
2330
|
-
if (exactKeys(message, ["type", "id", "
|
|
2331
|
-
typeof message.id === "string" && validOpaqueHandle(message.
|
|
2332
|
-
if (
|
|
2333
|
-
|
|
2334
|
-
const retryAfterMS = operationSnapshotRateLimit(message.operation_id, message.id);
|
|
2261
|
+
if (exactKeys(message, ["type", "id", "execution_id"]) && message.type === "redevplugin.bridge.execution.query" &&
|
|
2262
|
+
typeof message.id === "string" && validOpaqueHandle(message.execution_id, "execution")) {
|
|
2263
|
+
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2264
|
+
const retryAfterMS = executionQueryRateLimit(message.execution_id, message.id);
|
|
2335
2265
|
if (retryAfterMS > 0) {
|
|
2336
2266
|
completeWorkerRequest(message.id);
|
|
2337
|
-
return sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: false, error_code: "
|
|
2267
|
+
return sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: false, error_code: "PLUGIN_EXECUTION_BLOCKED", error: "plugin execution query rate limited" });
|
|
2338
2268
|
}
|
|
2339
2269
|
sendParent(message);
|
|
2340
2270
|
return;
|
|
2341
2271
|
}
|
|
2272
|
+
if (exactKeys(message, ["type", "id", "execution_id", "after_cursor"]) && message.type === "redevplugin.bridge.execution.events" &&
|
|
2273
|
+
typeof message.id === "string" && validOpaqueHandle(message.execution_id, "execution") && Number.isSafeInteger(message.after_cursor) && message.after_cursor >= 0) {
|
|
2274
|
+
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2275
|
+
sendParent(message);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2342
2278
|
if (exactKeys(message, ["type", "id", "canvas_id"]) && message.type === "redevplugin.ui.canvas.open" && typeof message.id === "string" && validResourceIdentifier(message.canvas_id)) {
|
|
2343
2279
|
if (!acceptWorkerRequest(message.id, "canvas")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2344
2280
|
openCanvas(message.id, message.canvas_id);
|
|
@@ -2392,7 +2328,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2392
2328
|
completeWorkerRequest(message.id);
|
|
2393
2329
|
return;
|
|
2394
2330
|
}
|
|
2395
|
-
if (
|
|
2331
|
+
if (executionQueryRequests.has(message.id)) completeExecutionQueryRequest(message.id);
|
|
2396
2332
|
else completeWorkerRequest(message.id);
|
|
2397
2333
|
sendParent(message);
|
|
2398
2334
|
return;
|
|
@@ -2401,13 +2337,12 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2401
2337
|
const onParentMessage = async (event) => {
|
|
2402
2338
|
const message = event.data;
|
|
2403
2339
|
if (!initialized) {
|
|
2404
|
-
const initKeys = ["type", "frame_generation_id", "surface_handle", "document"];
|
|
2405
|
-
if (
|
|
2406
|
-
if (!isRecord(message) || !Object.keys(message).every((key) => initKeys.includes(key)) || !initKeys.slice(0, 4).every((key) => Object.prototype.hasOwnProperty.call(message, key)) || message.type !== "redevplugin.surface.initialize" || message.frame_generation_id !== frameGenerationID || !validOpaqueHandle(message.surface_handle, "surface") || !validDocument(message.document) || (protocolVersion === "plugin-ui-v7" && message.context !== undefined && !validSurfaceContext(message.context))) return fail("invalid private initialize message");
|
|
2340
|
+
const initKeys = ["type", "frame_generation_id", "surface_handle", "document", "context"];
|
|
2341
|
+
if (!isRecord(message) || !Object.keys(message).every((key) => initKeys.includes(key)) || !initKeys.slice(0, 4).every((key) => Object.prototype.hasOwnProperty.call(message, key)) || message.type !== "redevplugin.surface.initialize" || message.frame_generation_id !== frameGenerationID || !validOpaqueHandle(message.surface_handle, "surface") || !validDocument(message.document) || (message.context !== undefined && !validSurfaceContext(message.context))) return fail("invalid private initialize message");
|
|
2407
2342
|
initialized = true;
|
|
2408
2343
|
surfaceHandle = message.surface_handle;
|
|
2409
2344
|
currentDocument = message.document;
|
|
2410
|
-
currentContext =
|
|
2345
|
+
currentContext = message.context;
|
|
2411
2346
|
try { applyStaticDocument(currentDocument); if (currentContext) applySurfaceContext(currentContext); startWorker(currentDocument); }
|
|
2412
2347
|
catch (error) { return fail(error); }
|
|
2413
2348
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
@@ -2416,7 +2351,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2416
2351
|
}));
|
|
2417
2352
|
return;
|
|
2418
2353
|
}
|
|
2419
|
-
if (
|
|
2354
|
+
if (isRecord(message) && exactKeys(message, ["type", "frame_generation_id", "surface_handle", "context"]) && message.type === "redevplugin.surface.context" && message.frame_generation_id === frameGenerationID && message.surface_handle === surfaceHandle && validSurfaceContext(message.context)) {
|
|
2420
2355
|
if (currentContext && message.context.revision <= currentContext.revision) return;
|
|
2421
2356
|
currentContext = message.context;
|
|
2422
2357
|
applySurfaceContext(currentContext);
|
|
@@ -2424,7 +2359,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2424
2359
|
return;
|
|
2425
2360
|
}
|
|
2426
2361
|
if (message && message.type === "redevplugin.bridge.response" && typeof message.id === "string" && pendingWorkerRequests.has(message.id) && withinLimit(message)) {
|
|
2427
|
-
if (
|
|
2362
|
+
if (executionQueryRequests.has(message.id)) completeExecutionQueryRequest(message.id);
|
|
2428
2363
|
else completeWorkerRequest(message.id);
|
|
2429
2364
|
sendWorker(message);
|
|
2430
2365
|
return;
|
|
@@ -2502,7 +2437,6 @@ class PluginSurfaceHostImplementation {
|
|
|
2502
2437
|
#document;
|
|
2503
2438
|
#surfaceContext;
|
|
2504
2439
|
#assets = new Map();
|
|
2505
|
-
#streamCredentials = new Map();
|
|
2506
2440
|
#pendingRequestControllers = new Map();
|
|
2507
2441
|
#activeTransportRequests = 0;
|
|
2508
2442
|
#activeAssetReads = 0;
|
|
@@ -2639,7 +2573,7 @@ class PluginSurfaceHostImplementation {
|
|
|
2639
2573
|
frame_generation_id: this.frameGenerationId,
|
|
2640
2574
|
surface_handle: this.surfaceHandle,
|
|
2641
2575
|
document: preparation.document,
|
|
2642
|
-
context: this
|
|
2576
|
+
context: this.#surfaceContext,
|
|
2643
2577
|
}));
|
|
2644
2578
|
this.#rendererInitialized = true;
|
|
2645
2579
|
await Promise.all([signals.firstPaint.promise, signals.workerReady.promise]);
|
|
@@ -2658,9 +2592,6 @@ class PluginSurfaceHostImplementation {
|
|
|
2658
2592
|
}
|
|
2659
2593
|
updateContext(context) {
|
|
2660
2594
|
this.#assertActive();
|
|
2661
|
-
if (this.bootstrap.uiProtocolVersion !== "plugin-ui-v7") {
|
|
2662
|
-
throw new PluginBridgeError("PLUGIN_UI_PROTOCOL_UNSUPPORTED", "Plugin surface context requires plugin-ui-v7");
|
|
2663
|
-
}
|
|
2664
2595
|
const normalized = normalizePluginSurfaceContext(context);
|
|
2665
2596
|
if (this.#surfaceContext && normalized.revision <= this.#surfaceContext.revision) {
|
|
2666
2597
|
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface context revision must increase monotonically");
|
|
@@ -2757,7 +2688,6 @@ class PluginSurfaceHostImplementation {
|
|
|
2757
2688
|
this.#assetSessionID = undefined;
|
|
2758
2689
|
this.#document = undefined;
|
|
2759
2690
|
this.#assets.clear();
|
|
2760
|
-
this.#streamCredentials.clear();
|
|
2761
2691
|
if (this.#port) {
|
|
2762
2692
|
try {
|
|
2763
2693
|
this.#port.postMessage({ type: "redevplugin.bridge.lifecycle", event: { type: "dispose" } });
|
|
@@ -2824,20 +2754,16 @@ class PluginSurfaceHostImplementation {
|
|
|
2824
2754
|
await this.#handleCall(data.request);
|
|
2825
2755
|
return;
|
|
2826
2756
|
}
|
|
2827
|
-
if (
|
|
2828
|
-
await this.#
|
|
2829
|
-
return;
|
|
2830
|
-
}
|
|
2831
|
-
if (isStreamAcknowledgeMessage(data)) {
|
|
2832
|
-
await this.#handleStreamAcknowledge(data.id, data.stream_handle, data.delivery_id);
|
|
2757
|
+
if (isExecutionCancelMessage(data)) {
|
|
2758
|
+
await this.#handleExecutionCancel(data);
|
|
2833
2759
|
return;
|
|
2834
2760
|
}
|
|
2835
|
-
if (
|
|
2836
|
-
await this.#
|
|
2761
|
+
if (isExecutionQueryMessage(data)) {
|
|
2762
|
+
await this.#handleExecutionQuery(data);
|
|
2837
2763
|
return;
|
|
2838
2764
|
}
|
|
2839
|
-
if (
|
|
2840
|
-
await this.#
|
|
2765
|
+
if (isExecutionEventsMessage(data)) {
|
|
2766
|
+
await this.#handleExecutionEvents(data);
|
|
2841
2767
|
return;
|
|
2842
2768
|
}
|
|
2843
2769
|
if (isAssetReadMessage(data)) {
|
|
@@ -2939,152 +2865,57 @@ class PluginSurfaceHostImplementation {
|
|
|
2939
2865
|
this.#postError(request.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
2940
2866
|
}
|
|
2941
2867
|
}
|
|
2942
|
-
async #
|
|
2943
|
-
const
|
|
2944
|
-
if (!credential || credential.completed || credential.reading || credential.acknowledging) {
|
|
2945
|
-
this.#postError(id, "PLUGIN_STREAM_TICKET_INVALID", "Plugin stream handle is invalid, completed, or busy");
|
|
2946
|
-
return;
|
|
2947
|
-
}
|
|
2948
|
-
if (credential.expiresAtMs <= Date.now()) {
|
|
2949
|
-
this.#postError(id, "PLUGIN_STREAM_TICKET_INVALID", "Plugin stream handle is expired");
|
|
2950
|
-
return;
|
|
2951
|
-
}
|
|
2952
|
-
if (credential.pending) {
|
|
2953
|
-
this.#postResponse(id, credential.pending.response);
|
|
2954
|
-
return;
|
|
2955
|
-
}
|
|
2956
|
-
credential.reading = true;
|
|
2957
|
-
const controller = this.#registerPendingRequest(id);
|
|
2958
|
-
try {
|
|
2959
|
-
const readPath = `/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/streams/read`;
|
|
2960
|
-
const readBody = () => ({ stream_id: credential.streamID, stream_ticket: credential.streamTicket, read_id: credential.readID });
|
|
2961
|
-
let result;
|
|
2962
|
-
try {
|
|
2963
|
-
result = await this.#postJSON(readPath, readBody, controller.signal);
|
|
2964
|
-
}
|
|
2965
|
-
catch (error) {
|
|
2966
|
-
if (!retryableStreamReadTransportFailure(error) || controller.signal.aborted || this.#disposed)
|
|
2967
|
-
throw error;
|
|
2968
|
-
result = await this.#postJSON(readPath, readBody, controller.signal);
|
|
2969
|
-
}
|
|
2970
|
-
if (!isStreamReadResult(result, credential.streamID, credential.readID, credential.lastSequence)) {
|
|
2971
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin stream endpoint returned an invalid response");
|
|
2972
|
-
}
|
|
2973
|
-
const lastSequence = result.events.length > 0 ? result.events[result.events.length - 1].sequence : credential.lastSequence;
|
|
2974
|
-
const events = result.events.map(publicPluginStreamEvent);
|
|
2975
|
-
const response = result.done
|
|
2976
|
-
? { delivery_id: result.delivery_id, events, done: true, terminal_status: result.terminal_status, retry_after_ms: 0 }
|
|
2977
|
-
: { delivery_id: result.delivery_id, events, done: false, retry_after_ms: events.length === 0 ? 25 : 0 };
|
|
2978
|
-
if (result.delivery_id) {
|
|
2979
|
-
credential.pending = {
|
|
2980
|
-
deliveryID: result.delivery_id,
|
|
2981
|
-
lastSequence,
|
|
2982
|
-
done: result.done,
|
|
2983
|
-
response,
|
|
2984
|
-
};
|
|
2985
|
-
}
|
|
2986
|
-
credential.reading = false;
|
|
2987
|
-
if (!controller.signal.aborted && !this.#disposed)
|
|
2988
|
-
this.#postResponse(id, response);
|
|
2989
|
-
}
|
|
2990
|
-
catch (error) {
|
|
2991
|
-
const bridgeError = toBridgeError(error, "PLUGIN_RUNTIME_UNAVAILABLE");
|
|
2992
|
-
if (streamReadFailureInvalidatesCredential(bridgeError)) {
|
|
2993
|
-
this.#streamCredentials.delete(streamHandle);
|
|
2994
|
-
}
|
|
2995
|
-
else {
|
|
2996
|
-
credential.reading = false;
|
|
2997
|
-
}
|
|
2998
|
-
if (controller.signal.aborted || this.#disposed)
|
|
2999
|
-
return;
|
|
3000
|
-
this.#postError(id, bridgeError.errorCode, bridgeError.message);
|
|
3001
|
-
}
|
|
3002
|
-
finally {
|
|
3003
|
-
this.#pendingRequestControllers.delete(id);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
async #handleStreamAcknowledge(id, streamHandle, deliveryID) {
|
|
3007
|
-
const credential = this.#streamCredentials.get(streamHandle);
|
|
3008
|
-
if (!credential || credential.reading || credential.acknowledging) {
|
|
3009
|
-
this.#postError(id, "PLUGIN_STREAM_DELIVERY_INVALID", "Plugin stream delivery is invalid or busy", undefined, "not_committed");
|
|
3010
|
-
return;
|
|
3011
|
-
}
|
|
3012
|
-
if (credential.lastAcknowledgedDeliveryID === deliveryID) {
|
|
3013
|
-
this.#postResponse(id, undefined);
|
|
3014
|
-
return;
|
|
3015
|
-
}
|
|
3016
|
-
if (!credential.pending || credential.pending.deliveryID !== deliveryID) {
|
|
3017
|
-
this.#postError(id, "PLUGIN_STREAM_DELIVERY_INVALID", "Plugin stream delivery does not match the pending batch", undefined, "not_committed");
|
|
3018
|
-
return;
|
|
3019
|
-
}
|
|
3020
|
-
credential.acknowledging = true;
|
|
3021
|
-
const controller = this.#registerPendingRequest(id);
|
|
2868
|
+
async #handleExecutionCancel(message) {
|
|
2869
|
+
const controller = this.#registerPendingRequest(message.id);
|
|
3022
2870
|
try {
|
|
3023
|
-
|
|
3024
|
-
stream_id: credential.streamID,
|
|
3025
|
-
stream_ticket: credential.streamTicket,
|
|
3026
|
-
delivery_id: deliveryID,
|
|
3027
|
-
}), controller.signal);
|
|
3028
|
-
if (!hasExactKeys(result, ["acknowledged"]) || result.acknowledged !== true) {
|
|
3029
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin stream acknowledgement endpoint returned an invalid response", undefined, undefined, "unknown");
|
|
3030
|
-
}
|
|
3031
|
-
const pending = credential.pending;
|
|
3032
|
-
credential.lastAcknowledgedDeliveryID = deliveryID;
|
|
3033
|
-
credential.lastSequence = pending.lastSequence;
|
|
3034
|
-
credential.pending = undefined;
|
|
3035
|
-
credential.readID = randomOpaqueHandle("read");
|
|
3036
|
-
credential.completed = pending.done;
|
|
3037
|
-
credential.acknowledging = false;
|
|
2871
|
+
await this.#postMutationJSON(`/_redevplugin/api/plugins/executions/${encodeURIComponent(message.execution_id)}/cancel`, { reason: message.reason }, controller.signal);
|
|
3038
2872
|
if (!controller.signal.aborted && !this.#disposed)
|
|
3039
|
-
this.#postResponse(id, undefined);
|
|
2873
|
+
this.#postResponse(message.id, undefined);
|
|
3040
2874
|
}
|
|
3041
2875
|
catch (error) {
|
|
3042
|
-
credential.acknowledging = false;
|
|
3043
2876
|
if (controller.signal.aborted || this.#disposed)
|
|
3044
2877
|
return;
|
|
3045
|
-
const bridgeError = toBridgeError(error, "
|
|
3046
|
-
this.#postError(id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
2878
|
+
const bridgeError = toBridgeError(error, "PLUGIN_EXECUTION_BLOCKED");
|
|
2879
|
+
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3047
2880
|
}
|
|
3048
2881
|
finally {
|
|
3049
|
-
this.#pendingRequestControllers.delete(id);
|
|
2882
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3050
2883
|
}
|
|
3051
2884
|
}
|
|
3052
|
-
async #
|
|
2885
|
+
async #handleExecutionQuery(message) {
|
|
3053
2886
|
const controller = this.#registerPendingRequest(message.id);
|
|
3054
2887
|
try {
|
|
3055
|
-
await this.#
|
|
3056
|
-
|
|
2888
|
+
const snapshot = await this.#postJSON(`/_redevplugin/api/plugins/executions/${encodeURIComponent(message.execution_id)}/query`, {}, controller.signal);
|
|
2889
|
+
if (!isPluginExecutionSnapshot(snapshot) || snapshot.execution_id !== message.execution_id) {
|
|
2890
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin execution snapshot endpoint returned an invalid response");
|
|
2891
|
+
}
|
|
3057
2892
|
if (!controller.signal.aborted && !this.#disposed)
|
|
3058
|
-
this.#postResponse(message.id,
|
|
2893
|
+
this.#postResponse(message.id, snapshot);
|
|
3059
2894
|
}
|
|
3060
2895
|
catch (error) {
|
|
3061
2896
|
if (controller.signal.aborted || this.#disposed)
|
|
3062
2897
|
return;
|
|
3063
|
-
const bridgeError = toBridgeError(error, "
|
|
2898
|
+
const bridgeError = toBridgeError(error, "PLUGIN_EXECUTION_NOT_FOUND");
|
|
3064
2899
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3065
2900
|
}
|
|
3066
2901
|
finally {
|
|
3067
2902
|
this.#pendingRequestControllers.delete(message.id);
|
|
3068
2903
|
}
|
|
3069
2904
|
}
|
|
3070
|
-
async #
|
|
3071
|
-
if (this.bootstrap.uiProtocolVersion !== "plugin-ui-v6" && this.bootstrap.uiProtocolVersion !== "plugin-ui-v7") {
|
|
3072
|
-
this.#postError(message.id, "PLUGIN_UI_PROTOCOL_UNSUPPORTED", "Plugin operation observation requires plugin-ui-v6 or plugin-ui-v7");
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
2905
|
+
async #handleExecutionEvents(message) {
|
|
3075
2906
|
const controller = this.#registerPendingRequest(message.id);
|
|
3076
2907
|
try {
|
|
3077
|
-
const
|
|
3078
|
-
if (!
|
|
3079
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin
|
|
2908
|
+
const result = await this.#postJSON(`/_redevplugin/api/plugins/executions/${encodeURIComponent(message.execution_id)}/events/query`, { after_cursor: message.after_cursor }, controller.signal);
|
|
2909
|
+
if (!isPluginExecutionEventList(result, message.execution_id, message.after_cursor)) {
|
|
2910
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin execution event endpoint returned an invalid response");
|
|
3080
2911
|
}
|
|
3081
2912
|
if (!controller.signal.aborted && !this.#disposed)
|
|
3082
|
-
this.#postResponse(message.id,
|
|
2913
|
+
this.#postResponse(message.id, result);
|
|
3083
2914
|
}
|
|
3084
2915
|
catch (error) {
|
|
3085
2916
|
if (controller.signal.aborted || this.#disposed)
|
|
3086
2917
|
return;
|
|
3087
|
-
const bridgeError = toBridgeError(error, "
|
|
2918
|
+
const bridgeError = toBridgeError(error, "PLUGIN_EXECUTION_NOT_FOUND");
|
|
3088
2919
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3089
2920
|
}
|
|
3090
2921
|
finally {
|
|
@@ -3133,52 +2964,13 @@ class PluginSurfaceHostImplementation {
|
|
|
3133
2964
|
return controller;
|
|
3134
2965
|
}
|
|
3135
2966
|
#publicMethodResult(result) {
|
|
3136
|
-
|
|
2967
|
+
return removeUndefined({
|
|
3137
2968
|
data: result.data,
|
|
3138
|
-
|
|
2969
|
+
execution_id: result.execution_id,
|
|
3139
2970
|
confirmation_required: result.confirmation_required,
|
|
3140
2971
|
confirmation_token_id: result.confirmation_token_id,
|
|
3141
2972
|
request_hash: result.request_hash,
|
|
3142
|
-
};
|
|
3143
|
-
if (result.stream_id || result.stream_ticket || result.stream_ticket_id || result.stream_expires_at) {
|
|
3144
|
-
if (!result.operation_id || !result.stream_id || !result.stream_ticket || !result.stream_ticket_id || !result.stream_expires_at) {
|
|
3145
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin RPC returned incomplete stream credentials");
|
|
3146
|
-
}
|
|
3147
|
-
const expiresAtMs = Date.parse(result.stream_expires_at);
|
|
3148
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
3149
|
-
throw new PluginBridgeError("PLUGIN_STREAM_TICKET_INVALID", "Plugin RPC returned an expired stream ticket");
|
|
3150
|
-
}
|
|
3151
|
-
this.#pruneExpiredStreamCredentials();
|
|
3152
|
-
if (this.#streamCredentials.size >= maxRetainedPluginStreamHandles) {
|
|
3153
|
-
throw new PluginBridgeError("PLUGIN_JSON_LIMIT_EXCEEDED", "Plugin surface retained too many unread stream handles");
|
|
3154
|
-
}
|
|
3155
|
-
const handle = randomOpaqueHandle("stream");
|
|
3156
|
-
this.#streamCredentials.set(handle, {
|
|
3157
|
-
streamID: result.stream_id,
|
|
3158
|
-
operationID: result.operation_id,
|
|
3159
|
-
streamTicket: result.stream_ticket,
|
|
3160
|
-
expiresAtMs,
|
|
3161
|
-
lastSequence: 0,
|
|
3162
|
-
readID: randomOpaqueHandle("read"),
|
|
3163
|
-
reading: false,
|
|
3164
|
-
acknowledging: false,
|
|
3165
|
-
completed: false,
|
|
3166
|
-
});
|
|
3167
|
-
publicResult.stream_handle = handle;
|
|
3168
|
-
}
|
|
3169
|
-
return removeUndefined(publicResult);
|
|
3170
|
-
}
|
|
3171
|
-
#pruneExpiredStreamCredentials(now = Date.now()) {
|
|
3172
|
-
for (const [handle, credential] of this.#streamCredentials) {
|
|
3173
|
-
if (credential.expiresAtMs <= now)
|
|
3174
|
-
this.#streamCredentials.delete(handle);
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
#releaseOperationStreams(operationID) {
|
|
3178
|
-
for (const [handle, credential] of this.#streamCredentials) {
|
|
3179
|
-
if (credential.operationID === operationID)
|
|
3180
|
-
this.#streamCredentials.delete(handle);
|
|
3181
|
-
}
|
|
2973
|
+
});
|
|
3182
2974
|
}
|
|
3183
2975
|
#callRPC(request, confirmationID, signal) {
|
|
3184
2976
|
return this.#postMutationJSON("/_redevplugin/api/plugins/rpc", () => this.#rpcBody(request, confirmationID), signal);
|
|
@@ -4066,7 +3858,7 @@ function validateHostBootstrap(bootstrap) {
|
|
|
4066
3858
|
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface bootstrap is incomplete");
|
|
4067
3859
|
}
|
|
4068
3860
|
}
|
|
4069
|
-
if (bootstrap.uiProtocolVersion !==
|
|
3861
|
+
if (bootstrap.uiProtocolVersion !== pluginUIProtocolVersion) {
|
|
4070
3862
|
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface UI protocol is unsupported");
|
|
4071
3863
|
}
|
|
4072
3864
|
if (!Number.isSafeInteger(bootstrap.managementRevision) || bootstrap.managementRevision < 1 ||
|
|
@@ -4214,42 +4006,32 @@ function isBridgeCallMessage(value) {
|
|
|
4214
4006
|
validMethod(value.request.method) &&
|
|
4215
4007
|
validRPCParams(value.request.params);
|
|
4216
4008
|
}
|
|
4217
|
-
function
|
|
4218
|
-
return
|
|
4219
|
-
value.type === "redevplugin.bridge.
|
|
4220
|
-
validBridgeRequestID(value.id, "
|
|
4221
|
-
validOpaqueHandle(value.
|
|
4222
|
-
}
|
|
4223
|
-
function isStreamAcknowledgeMessage(value) {
|
|
4224
|
-
return hasExactKeys(value, ["type", "id", "stream_handle", "delivery_id"]) &&
|
|
4225
|
-
value.type === "redevplugin.bridge.stream.ack" &&
|
|
4226
|
-
validBridgeRequestID(value.id, "stream_ack") &&
|
|
4227
|
-
validOpaqueHandle(value.stream_handle, "stream") &&
|
|
4228
|
-
validDeliveryID(value.delivery_id);
|
|
4229
|
-
}
|
|
4230
|
-
function isOperationCancelMessage(value) {
|
|
4231
|
-
return hasAllowedKeys(value, ["type", "id", "operation_id", "reason"]) &&
|
|
4232
|
-
value.type === "redevplugin.bridge.operation.cancel" &&
|
|
4233
|
-
validBridgeRequestID(value.id, "operation") &&
|
|
4234
|
-
validOpaqueHandle(value.operation_id, "operation") &&
|
|
4009
|
+
function isExecutionCancelMessage(value) {
|
|
4010
|
+
return hasAllowedKeys(value, ["type", "id", "execution_id", "reason"]) &&
|
|
4011
|
+
value.type === "redevplugin.bridge.execution.cancel" &&
|
|
4012
|
+
validBridgeRequestID(value.id, "execution") &&
|
|
4013
|
+
validOpaqueHandle(value.execution_id, "execution") &&
|
|
4235
4014
|
(value.reason === undefined || (typeof value.reason === "string" && value.reason.length <= 256));
|
|
4236
4015
|
}
|
|
4237
|
-
function
|
|
4238
|
-
return hasExactKeys(value, ["type", "id", "
|
|
4239
|
-
value.
|
|
4240
|
-
|
|
4241
|
-
|
|
4016
|
+
function isExecutionQueryMessage(value) {
|
|
4017
|
+
return hasExactKeys(value, ["type", "id", "execution_id"]) && value.type === "redevplugin.bridge.execution.query" &&
|
|
4018
|
+
validBridgeRequestID(value.id, "execution") && validOpaqueHandle(value.execution_id, "execution");
|
|
4019
|
+
}
|
|
4020
|
+
function isExecutionEventsMessage(value) {
|
|
4021
|
+
return hasExactKeys(value, ["type", "id", "execution_id", "after_cursor"]) && value.type === "redevplugin.bridge.execution.events" &&
|
|
4022
|
+
validBridgeRequestID(value.id, "execution") && validOpaqueHandle(value.execution_id, "execution") &&
|
|
4023
|
+
Number.isSafeInteger(value.after_cursor) && Number(value.after_cursor) >= 0;
|
|
4242
4024
|
}
|
|
4243
|
-
function
|
|
4244
|
-
if (!isRecord(value) || !validOpaqueHandle(value.
|
|
4245
|
-
|
|
4246
|
-
!validDateTime(value.created_at) || !validDateTime(value.updated_at))
|
|
4025
|
+
function isPluginExecutionSnapshot(value) {
|
|
4026
|
+
if (!isRecord(value) || !validOpaqueHandle(value.execution_id, "execution") || typeof value.plugin_instance_id !== "string" ||
|
|
4027
|
+
(value.kind !== "operation" && value.kind !== "subscription") || typeof value.cancelable !== "boolean" ||
|
|
4028
|
+
!Number.isSafeInteger(value.cursor) || Number(value.cursor) < 0 || !validDateTime(value.created_at) || !validDateTime(value.updated_at))
|
|
4247
4029
|
return false;
|
|
4248
|
-
const common = ["
|
|
4030
|
+
const common = ["execution_id", "plugin_instance_id", "kind", "status", "cursor", "cancelable", "created_at", "updated_at"];
|
|
4249
4031
|
const withProgress = (keys) => hasAllowedKeys(value, [...keys, "progress"]) && (value.progress === undefined || validOperationProgress(value.progress));
|
|
4250
4032
|
if (value.status === "running" || value.status === "cancel_requested")
|
|
4251
4033
|
return withProgress(common);
|
|
4252
|
-
if (["completed", "canceled", "
|
|
4034
|
+
if (["completed", "canceled", "orphaned"].includes(String(value.status))) {
|
|
4253
4035
|
return withProgress([...common, "terminal_at"]) && validDateTime(value.terminal_at);
|
|
4254
4036
|
}
|
|
4255
4037
|
return value.status === "failed" && withProgress([...common, "terminal_at", "failure_code"]) &&
|
|
@@ -4268,6 +4050,22 @@ function validOperationProgress(value) {
|
|
|
4268
4050
|
function validDateTime(value) {
|
|
4269
4051
|
return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
|
|
4270
4052
|
}
|
|
4053
|
+
function isPluginExecutionEventList(value, executionID, afterCursor) {
|
|
4054
|
+
if (!hasExactKeys(value, ["execution_id", "events", "cursor"]) || value.execution_id !== executionID ||
|
|
4055
|
+
!Array.isArray(value.events) || !Number.isSafeInteger(value.cursor) || Number(value.cursor) < afterCursor)
|
|
4056
|
+
return false;
|
|
4057
|
+
let cursor = afterCursor;
|
|
4058
|
+
for (const event of value.events) {
|
|
4059
|
+
if (!isRecord(event) || !hasAllowedKeys(event, ["execution_id", "sequence", "kind", "payload", "error"]) ||
|
|
4060
|
+
event.execution_id !== executionID || !Number.isSafeInteger(event.sequence) || Number(event.sequence) <= cursor ||
|
|
4061
|
+
!["progress", "data", "diagnostic", "terminal"].includes(String(event.kind)) ||
|
|
4062
|
+
(event.payload !== undefined && !isRecord(event.payload)) ||
|
|
4063
|
+
(event.error !== undefined && (!hasExactKeys(event.error, ["code", "message"]) || typeof event.error.code !== "string" || typeof event.error.message !== "string")))
|
|
4064
|
+
return false;
|
|
4065
|
+
cursor = Number(event.sequence);
|
|
4066
|
+
}
|
|
4067
|
+
return Number(value.cursor) === cursor;
|
|
4068
|
+
}
|
|
4271
4069
|
function isBridgeCancelMessage(value) {
|
|
4272
4070
|
return hasExactKeys(value, ["type", "id"]) &&
|
|
4273
4071
|
value.type === "redevplugin.bridge.cancel" &&
|
|
@@ -4539,48 +4337,6 @@ function isGatewayTokenResult(value) {
|
|
|
4539
4337
|
typeof value.asset_session_id === "string" && value.asset_session_id.length > 0 &&
|
|
4540
4338
|
typeof value.issued_at === "string" && typeof value.expires_at === "string";
|
|
4541
4339
|
}
|
|
4542
|
-
function isStreamReadResult(value, expectedStreamID, expectedReadID, previousSequence) {
|
|
4543
|
-
if (!isRecord(value) || typeof value.done !== "boolean" || !Array.isArray(value.events))
|
|
4544
|
-
return false;
|
|
4545
|
-
const hasDelivery = value.events.length > 0 || value.done;
|
|
4546
|
-
const expectedKeys = value.done
|
|
4547
|
-
? ["delivery_id", "done", "events", "read_id", "terminal_status"]
|
|
4548
|
-
: hasDelivery
|
|
4549
|
-
? ["delivery_id", "done", "events", "read_id"]
|
|
4550
|
-
: ["done", "events", "read_id"];
|
|
4551
|
-
if (!hasExactKeys(value, expectedKeys))
|
|
4552
|
-
return false;
|
|
4553
|
-
if (value.read_id !== expectedReadID || (hasDelivery && !validDeliveryID(value.delivery_id)))
|
|
4554
|
-
return false;
|
|
4555
|
-
if (value.done) {
|
|
4556
|
-
if (!validPluginStreamTerminalStatus(value.terminal_status))
|
|
4557
|
-
return false;
|
|
4558
|
-
}
|
|
4559
|
-
let terminal = false;
|
|
4560
|
-
for (const event of value.events) {
|
|
4561
|
-
if (!isStreamEvent(event) || event.stream_id !== expectedStreamID || event.sequence <= previousSequence || terminal)
|
|
4562
|
-
return false;
|
|
4563
|
-
previousSequence = event.sequence;
|
|
4564
|
-
terminal = event.kind === "end" || event.kind === "error";
|
|
4565
|
-
}
|
|
4566
|
-
return true;
|
|
4567
|
-
}
|
|
4568
|
-
function isStreamEvent(value) {
|
|
4569
|
-
return hasAllowedKeys(value, ["stream_id", "sequence", "kind", "data", "error", "at"]) &&
|
|
4570
|
-
typeof value.stream_id === "string" &&
|
|
4571
|
-
Number.isSafeInteger(value.sequence) && Number(value.sequence) > 0 &&
|
|
4572
|
-
typeof value.kind === "string" && value.kind.length > 0 &&
|
|
4573
|
-
(value.data == null || typeof value.data === "string") &&
|
|
4574
|
-
(value.error == null || typeof value.error === "string") &&
|
|
4575
|
-
typeof value.at === "string";
|
|
4576
|
-
}
|
|
4577
|
-
function publicPluginStreamEvent(event) {
|
|
4578
|
-
return removeUndefined({ sequence: event.sequence, kind: event.kind, data: event.data, error: event.error, at: event.at });
|
|
4579
|
-
}
|
|
4580
|
-
function validPluginStreamTerminalStatus(value) {
|
|
4581
|
-
return value === "closed" || value === "canceled" || value === "failed" ||
|
|
4582
|
-
value === "orphaned_after_disable" || value === "orphaned_after_uninstall";
|
|
4583
|
-
}
|
|
4584
4340
|
function validRPCParams(value) {
|
|
4585
4341
|
if (value === undefined)
|
|
4586
4342
|
return true;
|
|
@@ -4596,7 +4352,7 @@ const pluginMethodPattern = new RegExp("^[-A-Za-z0-9._:]{1,256}$");
|
|
|
4596
4352
|
const pluginActionPattern = new RegExp("^[-A-Za-z0-9._:]{1,128}$");
|
|
4597
4353
|
const pluginUIIdentifierPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$");
|
|
4598
4354
|
const opaqueHandlePattern = new RegExp("^[-A-Za-z0-9_]{8,160}$");
|
|
4599
|
-
const bridgeRequestIDPattern = /^(rpc|
|
|
4355
|
+
const bridgeRequestIDPattern = /^(rpc|execution|render|canvas|asset)_([1-9][0-9]{0,15})$/;
|
|
4600
4356
|
function validBridgeRequestID(value, expectedKind) {
|
|
4601
4357
|
if (typeof value !== "string")
|
|
4602
4358
|
return false;
|
|
@@ -4617,9 +4373,6 @@ function validUIIdentifier(value) {
|
|
|
4617
4373
|
function validOpaqueHandle(value, prefix) {
|
|
4618
4374
|
return typeof value === "string" && value.startsWith(`${prefix}_`) && opaqueHandlePattern.test(value);
|
|
4619
4375
|
}
|
|
4620
|
-
function validDeliveryID(value) {
|
|
4621
|
-
return typeof value === "string" && /^delivery_[A-Za-z0-9_-]{8,128}$/.test(value);
|
|
4622
|
-
}
|
|
4623
4376
|
function validSHA256(value) {
|
|
4624
4377
|
return typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
4625
4378
|
}
|
|
@@ -4720,25 +4473,6 @@ function requestCancelledError(options, posted) {
|
|
|
4720
4473
|
return streamReadAbortedError();
|
|
4721
4474
|
return new PluginBridgeError("PLUGIN_BRIDGE_CANCELLED", "Plugin bridge request was cancelled", undefined, undefined, mutationOutcome(options, posted));
|
|
4722
4475
|
}
|
|
4723
|
-
function abortableStreamRead(read, signal) {
|
|
4724
|
-
if (!signal)
|
|
4725
|
-
return read;
|
|
4726
|
-
if (signal.aborted)
|
|
4727
|
-
return Promise.reject(streamReadAbortedError());
|
|
4728
|
-
return new Promise((resolve, reject) => {
|
|
4729
|
-
let settled = false;
|
|
4730
|
-
const finish = (callback, value) => {
|
|
4731
|
-
if (settled)
|
|
4732
|
-
return;
|
|
4733
|
-
settled = true;
|
|
4734
|
-
signal.removeEventListener("abort", onAbort);
|
|
4735
|
-
callback(value);
|
|
4736
|
-
};
|
|
4737
|
-
const onAbort = () => finish(reject, streamReadAbortedError());
|
|
4738
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
4739
|
-
read.then((value) => finish(resolve, value), (error) => finish(reject, error));
|
|
4740
|
-
});
|
|
4741
|
-
}
|
|
4742
4476
|
function abortableConfirmationDecision(decision, signal) {
|
|
4743
4477
|
if (signal.aborted) {
|
|
4744
4478
|
return Promise.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin confirmation was aborted"));
|
|
@@ -4877,12 +4611,3 @@ function toBridgeError(error, defaultCode) {
|
|
|
4877
4611
|
return new PluginBridgeError(defaultCode, error.message);
|
|
4878
4612
|
return new PluginBridgeError(defaultCode, String(error));
|
|
4879
4613
|
}
|
|
4880
|
-
function streamReadFailureInvalidatesCredential(error) {
|
|
4881
|
-
if (!(error instanceof PluginBridgeError))
|
|
4882
|
-
return true;
|
|
4883
|
-
return streamCredentialInvalidatingErrorCodes.has(error.errorCode);
|
|
4884
|
-
}
|
|
4885
|
-
function retryableStreamReadTransportFailure(error) {
|
|
4886
|
-
return error instanceof PluginTransportError ||
|
|
4887
|
-
(error instanceof PluginBridgeError && error.errorCode === "PLUGIN_BRIDGE_TIMEOUT");
|
|
4888
|
-
}
|