@floegence/redevplugin-ui 0.2.2 → 0.3.1
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 +40 -0
- package/dist/capability-client.js +501 -0
- package/dist/contracts.gen.d.ts +47 -11
- package/dist/contracts.gen.js +53 -11
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +1 -0
- package/dist/platform.d.ts +81 -8
- package/dist/plugin.d.ts +4 -1
- package/dist/plugin.js +2 -0
- package/dist/surface.d.ts +15 -3
- package/dist/surface.js +234 -29
- package/package.json +1 -1
package/dist/surface.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginBridgeError } from "./errors.js";
|
|
1
|
+
import { PluginBridgeError, pluginBridgeErrorCodes } from "./errors.js";
|
|
2
2
|
import { pluginUIProtocolVersion } from "./contracts.gen.js";
|
|
3
3
|
import { opaqueSurfaceAllowedTags, opaqueSurfaceGlobalAttributes, opaqueSurfaceRenderLimits, opaqueSurfaceSafeInputTypes, opaqueSurfaceTagAttributes, } from "./opaque-surface-policy.gen.js";
|
|
4
4
|
import { defaultFetch, hasAllowedKeys, hasExactKeys, isRecord, readHostEnvelope, } from "./http.js";
|
|
@@ -9,9 +9,32 @@ const opaquePluginBridgeGlobalKey = "__redevpluginWorkerBridgeV2";
|
|
|
9
9
|
const maxPendingPluginBridgeRequests = 256;
|
|
10
10
|
const maxPluginBridgeMessageBytes = 256 * 1024;
|
|
11
11
|
const maxRetainedPluginStreamHandles = 128;
|
|
12
|
+
const streamCredentialInvalidatingErrorCodes = new Set([
|
|
13
|
+
"PLUGIN_BRIDGE_DISPOSED",
|
|
14
|
+
"PLUGIN_BRIDGE_HANDSHAKE_FAILED",
|
|
15
|
+
"PLUGIN_BRIDGE_HANDSHAKE_REQUIRED",
|
|
16
|
+
"PLUGIN_BRIDGE_TIMEOUT",
|
|
17
|
+
"PLUGIN_CONTRACT_MISMATCH",
|
|
18
|
+
"PLUGIN_GATEWAY_TOKEN_CHANNEL_MISMATCH",
|
|
19
|
+
"PLUGIN_GATEWAY_TOKEN_INVALID",
|
|
20
|
+
"PLUGIN_GATEWAY_TOKEN_REPLAYED",
|
|
21
|
+
"PLUGIN_GRANT_INVALID",
|
|
22
|
+
"PLUGIN_LEASE_INVALID",
|
|
23
|
+
"PLUGIN_LEASE_REPLAYED",
|
|
24
|
+
"PLUGIN_STATE_VERSION_MISMATCH",
|
|
25
|
+
"PLUGIN_STREAM_CANCELLED",
|
|
26
|
+
"PLUGIN_STREAM_TICKET_INVALID",
|
|
27
|
+
"PLUGIN_TOKEN_EXPIRED",
|
|
28
|
+
"PLUGIN_TOKEN_REPLAY",
|
|
29
|
+
]);
|
|
12
30
|
const maxOpaqueSurfaceLazyAssets = 128;
|
|
13
31
|
const maxOpaqueSurfaceLazyBytes = 32 * 1024 * 1024;
|
|
14
32
|
const maxConcurrentAssetReads = 4;
|
|
33
|
+
const pluginBridgeErrorCodeSet = new Set(pluginBridgeErrorCodes);
|
|
34
|
+
const hostCapabilityIDPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._-]*$");
|
|
35
|
+
const canonicalSemverPattern = new RegExp("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$");
|
|
36
|
+
const lowercaseSHA256Pattern = new RegExp("^[0-9a-f]{64}$");
|
|
37
|
+
const businessErrorCodePattern = new RegExp("^[A-Z][A-Z0-9_]*$");
|
|
15
38
|
export class PluginBridgeClient {
|
|
16
39
|
surfaceHandle;
|
|
17
40
|
timeoutMs;
|
|
@@ -80,6 +103,19 @@ export class PluginBridgeClient {
|
|
|
80
103
|
stream_handle: streamHandle,
|
|
81
104
|
});
|
|
82
105
|
}
|
|
106
|
+
cancelOperation(operationID, reason) {
|
|
107
|
+
this.#assertActive();
|
|
108
|
+
if (!validOpaqueHandle(operationID, "operation") || (reason !== undefined && (typeof reason !== "string" || reason.length > 256))) {
|
|
109
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin operation cancellation is invalid");
|
|
110
|
+
}
|
|
111
|
+
const id = this.#requestID("operation");
|
|
112
|
+
return this.#request(id, removeUndefined({
|
|
113
|
+
type: "redevplugin.bridge.operation.cancel",
|
|
114
|
+
id,
|
|
115
|
+
operation_id: operationID,
|
|
116
|
+
reason,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
83
119
|
render(tree) {
|
|
84
120
|
this.#assertActive();
|
|
85
121
|
const id = this.#requestID("render");
|
|
@@ -176,16 +212,20 @@ export class PluginBridgeClient {
|
|
|
176
212
|
if (this.#disposed || !messageWithinLimit(event.data))
|
|
177
213
|
return;
|
|
178
214
|
const data = event.data;
|
|
179
|
-
if (
|
|
215
|
+
if (isBridgeResponseCandidate(data)) {
|
|
180
216
|
const pending = this.#pending.get(data.id);
|
|
181
217
|
if (!pending)
|
|
182
218
|
return;
|
|
183
219
|
this.#pending.delete(data.id);
|
|
184
220
|
clearTimeout(pending.timer);
|
|
221
|
+
if (!isBridgeResponse(data)) {
|
|
222
|
+
pending.reject(new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", `Plugin bridge response ${data.id} is invalid`));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
185
225
|
if (data.ok)
|
|
186
226
|
pending.resolve(data.data);
|
|
187
227
|
else
|
|
188
|
-
pending.reject(new PluginBridgeError(data.error_code, data.error));
|
|
228
|
+
pending.reject(new PluginBridgeError(data.error_code, data.error, undefined, data.error_details));
|
|
189
229
|
return;
|
|
190
230
|
}
|
|
191
231
|
if (isLifecycleMessage(data)) {
|
|
@@ -465,7 +505,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
465
505
|
let currentDocument;
|
|
466
506
|
let workerReady = false;
|
|
467
507
|
const pendingWorkerRequests = new Set();
|
|
468
|
-
const requestSequence = { rpc: 0, stream: 0, render: 0 };
|
|
508
|
+
const requestSequence = { rpc: 0, stream: 0, render: 0, operation: 0 };
|
|
469
509
|
let renderWindowStartedAt = 0;
|
|
470
510
|
let renderCount = 0;
|
|
471
511
|
const pendingAssets = new Map();
|
|
@@ -735,7 +775,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
735
775
|
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));
|
|
736
776
|
const requestID = (value, expectedKind) => {
|
|
737
777
|
if (typeof value !== "string") return undefined;
|
|
738
|
-
const match = /^(rpc|stream|render)_([1-9][0-9]{0,15})$/.exec(value);
|
|
778
|
+
const match = /^(rpc|stream|render|operation)_([1-9][0-9]{0,15})$/.exec(value);
|
|
739
779
|
if (!match || match[1] !== expectedKind) return undefined;
|
|
740
780
|
const sequence = Number(match[2]);
|
|
741
781
|
return Number.isSafeInteger(sequence) ? { kind: match[1], sequence } : undefined;
|
|
@@ -783,6 +823,13 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
783
823
|
sendParent(message);
|
|
784
824
|
return;
|
|
785
825
|
}
|
|
826
|
+
if (isRecord(message) && Object.keys(message).every((key) => ["type", "id", "operation_id", "reason"].includes(key)) &&
|
|
827
|
+
message.type === "redevplugin.bridge.operation.cancel" && typeof message.id === "string" &&
|
|
828
|
+
validOpaqueHandle(message.operation_id, "operation") && (message.reason === undefined || (typeof message.reason === "string" && message.reason.length <= 256))) {
|
|
829
|
+
if (!acceptWorkerRequest(message.id, "operation")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
830
|
+
sendParent(message);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
786
833
|
if (exactKeys(message, ["type", "id", "tree"]) && message.type === "redevplugin.ui.render" && typeof message.id === "string") {
|
|
787
834
|
if (!acceptWorkerRequest(message.id, "render")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
788
835
|
if (!renderRateAllowed()) {
|
|
@@ -1131,6 +1178,10 @@ export class PluginSurfaceHost {
|
|
|
1131
1178
|
await this.#handleStreamRead(data.id, data.stream_handle);
|
|
1132
1179
|
return;
|
|
1133
1180
|
}
|
|
1181
|
+
if (isOperationCancelMessage(data)) {
|
|
1182
|
+
await this.#handleOperationCancel(data);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1134
1185
|
if (isAssetReadMessage(data)) {
|
|
1135
1186
|
await this.#handleAssetRead(data);
|
|
1136
1187
|
}
|
|
@@ -1158,7 +1209,7 @@ export class PluginSurfaceHost {
|
|
|
1158
1209
|
await this.#handleConfirmationRequired(request, bridgeError, controller.signal);
|
|
1159
1210
|
return;
|
|
1160
1211
|
}
|
|
1161
|
-
this.#postError(request.id, bridgeError.errorCode, bridgeError.message);
|
|
1212
|
+
this.#postError(request.id, bridgeError.errorCode, bridgeError.message, bridgeError.details);
|
|
1162
1213
|
}
|
|
1163
1214
|
finally {
|
|
1164
1215
|
this.#pendingRequestControllers.delete(request.id);
|
|
@@ -1166,7 +1217,7 @@ export class PluginSurfaceHost {
|
|
|
1166
1217
|
}
|
|
1167
1218
|
async #handleConfirmationRequired(request, originalError, signal) {
|
|
1168
1219
|
if (!this.#confirm) {
|
|
1169
|
-
this.#postError(request.id, originalError.errorCode, originalError.message);
|
|
1220
|
+
this.#postError(request.id, originalError.errorCode, originalError.message, originalError.details);
|
|
1170
1221
|
return;
|
|
1171
1222
|
}
|
|
1172
1223
|
try {
|
|
@@ -1184,6 +1235,9 @@ export class PluginSurfaceHost {
|
|
|
1184
1235
|
if (signal.aborted || this.#disposed)
|
|
1185
1236
|
return;
|
|
1186
1237
|
if (!confirmationDecisionAccepted(decision)) {
|
|
1238
|
+
await this.#rejectConfirmation(confirmation.confirmation_id, signal);
|
|
1239
|
+
if (signal.aborted || this.#disposed)
|
|
1240
|
+
return;
|
|
1187
1241
|
this.#postError(request.id, "PLUGIN_CONFIRMATION_REJECTED", "Plugin method confirmation was rejected");
|
|
1188
1242
|
return;
|
|
1189
1243
|
}
|
|
@@ -1195,13 +1249,12 @@ export class PluginSurfaceHost {
|
|
|
1195
1249
|
if (signal.aborted || this.#disposed)
|
|
1196
1250
|
return;
|
|
1197
1251
|
const bridgeError = toBridgeError(error, "PLUGIN_PERMISSION_DENIED");
|
|
1198
|
-
this.#postError(request.id, bridgeError.errorCode, bridgeError.message);
|
|
1252
|
+
this.#postError(request.id, bridgeError.errorCode, bridgeError.message, bridgeError.details);
|
|
1199
1253
|
}
|
|
1200
1254
|
}
|
|
1201
1255
|
async #handleStreamRead(id, streamHandle) {
|
|
1202
1256
|
const credential = this.#streamCredentials.get(streamHandle);
|
|
1203
|
-
|
|
1204
|
-
if (!credential) {
|
|
1257
|
+
if (!credential || credential.reading) {
|
|
1205
1258
|
this.#postError(id, "PLUGIN_STREAM_TICKET_INVALID", "Plugin stream handle is invalid or already consumed");
|
|
1206
1259
|
return;
|
|
1207
1260
|
}
|
|
@@ -1209,16 +1262,39 @@ export class PluginSurfaceHost {
|
|
|
1209
1262
|
this.#postError(id, "PLUGIN_STREAM_TICKET_INVALID", "Plugin stream handle is expired");
|
|
1210
1263
|
return;
|
|
1211
1264
|
}
|
|
1265
|
+
credential.reading = true;
|
|
1212
1266
|
const controller = this.#registerPendingRequest(id);
|
|
1213
1267
|
try {
|
|
1214
|
-
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/streams/read`, { stream_id: credential.streamID, stream_ticket: credential.streamTicket }, controller.signal);
|
|
1215
|
-
if (!isStreamReadResult(result, credential.streamID)) {
|
|
1268
|
+
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/streams/read`, () => ({ stream_id: credential.streamID, stream_ticket: credential.streamTicket }), controller.signal);
|
|
1269
|
+
if (!isStreamReadResult(result, credential.streamID, credential.lastSequence)) {
|
|
1216
1270
|
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin stream endpoint returned an invalid response");
|
|
1217
1271
|
}
|
|
1272
|
+
const lastSequence = result.events.length > 0 ? result.events[result.events.length - 1].sequence : credential.lastSequence;
|
|
1273
|
+
if (result.done) {
|
|
1274
|
+
this.#streamCredentials.delete(streamHandle);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
const expiresAtMs = Date.parse(result.next_stream_expires_at);
|
|
1278
|
+
credential.streamTicket = result.next_stream_ticket;
|
|
1279
|
+
credential.expiresAtMs = expiresAtMs;
|
|
1280
|
+
credential.lastSequence = lastSequence;
|
|
1281
|
+
credential.reading = false;
|
|
1282
|
+
}
|
|
1218
1283
|
if (!controller.signal.aborted && !this.#disposed)
|
|
1219
|
-
this.#postResponse(id,
|
|
1284
|
+
this.#postResponse(id, {
|
|
1285
|
+
events: result.events.map(publicPluginStreamEvent),
|
|
1286
|
+
done: result.done,
|
|
1287
|
+
...(result.done ? { terminal_status: result.terminal_status } : {}),
|
|
1288
|
+
retry_after_ms: result.events.length === 0 && !result.done ? 25 : 0,
|
|
1289
|
+
});
|
|
1220
1290
|
}
|
|
1221
1291
|
catch (error) {
|
|
1292
|
+
if (streamReadFailureInvalidatesCredential(error)) {
|
|
1293
|
+
this.#streamCredentials.delete(streamHandle);
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
credential.reading = false;
|
|
1297
|
+
}
|
|
1222
1298
|
if (controller.signal.aborted || this.#disposed)
|
|
1223
1299
|
return;
|
|
1224
1300
|
const bridgeError = toBridgeError(error, "PLUGIN_RUNTIME_UNAVAILABLE");
|
|
@@ -1228,6 +1304,24 @@ export class PluginSurfaceHost {
|
|
|
1228
1304
|
this.#pendingRequestControllers.delete(id);
|
|
1229
1305
|
}
|
|
1230
1306
|
}
|
|
1307
|
+
async #handleOperationCancel(message) {
|
|
1308
|
+
const controller = this.#registerPendingRequest(message.id);
|
|
1309
|
+
try {
|
|
1310
|
+
await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/operations/cancel`, { operation_id: message.operation_id, bridge_channel_id: this.bridgeChannelId, reason: message.reason }, controller.signal);
|
|
1311
|
+
this.#releaseOperationStreams(message.operation_id);
|
|
1312
|
+
if (!controller.signal.aborted && !this.#disposed)
|
|
1313
|
+
this.#postResponse(message.id, undefined);
|
|
1314
|
+
}
|
|
1315
|
+
catch (error) {
|
|
1316
|
+
if (controller.signal.aborted || this.#disposed)
|
|
1317
|
+
return;
|
|
1318
|
+
const bridgeError = toBridgeError(error, "PLUGIN_OPERATION_BLOCKED");
|
|
1319
|
+
this.#postError(message.id, bridgeError.errorCode, bridgeError.message);
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1231
1325
|
async #handleAssetRead(message) {
|
|
1232
1326
|
if (this.#activeAssetReads >= maxConcurrentAssetReads) {
|
|
1233
1327
|
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin asset reads exceed the concurrency limit");
|
|
@@ -1238,11 +1332,11 @@ export class PluginSurfaceHost {
|
|
|
1238
1332
|
}
|
|
1239
1333
|
this.#activeAssetReads += 1;
|
|
1240
1334
|
try {
|
|
1241
|
-
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/assets/read`, {
|
|
1335
|
+
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/assets/read`, () => ({
|
|
1242
1336
|
asset_session: this.#assetSession,
|
|
1243
1337
|
asset_session_id: this.#assetSessionID,
|
|
1244
1338
|
binding_id: asset.binding_id,
|
|
1245
|
-
});
|
|
1339
|
+
}));
|
|
1246
1340
|
if (!isAssetReadResult(result) || result.path !== asset.path || result.sha256 !== asset.sha256 || result.content_type !== asset.content_type) {
|
|
1247
1341
|
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin asset endpoint returned mismatched content");
|
|
1248
1342
|
}
|
|
@@ -1278,7 +1372,7 @@ export class PluginSurfaceHost {
|
|
|
1278
1372
|
request_hash: result.request_hash,
|
|
1279
1373
|
};
|
|
1280
1374
|
if (result.stream_id || result.stream_ticket || result.stream_ticket_id || result.stream_expires_at) {
|
|
1281
|
-
if (!result.stream_id || !result.stream_ticket || !result.stream_ticket_id || !result.stream_expires_at) {
|
|
1375
|
+
if (!result.operation_id || !result.stream_id || !result.stream_ticket || !result.stream_ticket_id || !result.stream_expires_at) {
|
|
1282
1376
|
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin RPC returned incomplete stream credentials");
|
|
1283
1377
|
}
|
|
1284
1378
|
const expiresAtMs = Date.parse(result.stream_expires_at);
|
|
@@ -1290,7 +1384,14 @@ export class PluginSurfaceHost {
|
|
|
1290
1384
|
throw new PluginBridgeError("PLUGIN_JSON_LIMIT_EXCEEDED", "Plugin surface retained too many unread stream handles");
|
|
1291
1385
|
}
|
|
1292
1386
|
const handle = randomOpaqueHandle("stream");
|
|
1293
|
-
this.#streamCredentials.set(handle, {
|
|
1387
|
+
this.#streamCredentials.set(handle, {
|
|
1388
|
+
streamID: result.stream_id,
|
|
1389
|
+
operationID: result.operation_id,
|
|
1390
|
+
streamTicket: result.stream_ticket,
|
|
1391
|
+
expiresAtMs,
|
|
1392
|
+
lastSequence: 0,
|
|
1393
|
+
reading: false,
|
|
1394
|
+
});
|
|
1294
1395
|
publicResult.stream_handle = handle;
|
|
1295
1396
|
}
|
|
1296
1397
|
return removeUndefined(publicResult);
|
|
@@ -1301,11 +1402,28 @@ export class PluginSurfaceHost {
|
|
|
1301
1402
|
this.#streamCredentials.delete(handle);
|
|
1302
1403
|
}
|
|
1303
1404
|
}
|
|
1405
|
+
#releaseOperationStreams(operationID) {
|
|
1406
|
+
for (const [handle, credential] of this.#streamCredentials) {
|
|
1407
|
+
if (credential.operationID === operationID)
|
|
1408
|
+
this.#streamCredentials.delete(handle);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1304
1411
|
#callRPC(request, confirmationID, signal) {
|
|
1305
|
-
return this.#postJSON("/_redevplugin/api/plugins/rpc", this.#rpcBody(request, confirmationID), signal);
|
|
1412
|
+
return this.#postJSON("/_redevplugin/api/plugins/rpc", () => this.#rpcBody(request, confirmationID), signal);
|
|
1306
1413
|
}
|
|
1307
1414
|
#prepareConfirmation(request, signal) {
|
|
1308
|
-
return this.#postJSON("/_redevplugin/api/plugins/confirm", this.#rpcBody(request), signal);
|
|
1415
|
+
return this.#postJSON("/_redevplugin/api/plugins/confirm", () => this.#rpcBody(request), signal);
|
|
1416
|
+
}
|
|
1417
|
+
async #rejectConfirmation(confirmationID, signal) {
|
|
1418
|
+
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/confirmations/reject`, () => ({
|
|
1419
|
+
plugin_instance_id: this.bootstrap.pluginInstanceId,
|
|
1420
|
+
bridge_channel_id: this.bridgeChannelId,
|
|
1421
|
+
plugin_gateway_token: this.#gatewayToken,
|
|
1422
|
+
confirmation_id: confirmationID,
|
|
1423
|
+
}), signal);
|
|
1424
|
+
if (!hasExactKeys(result, ["rejected"]) || result.rejected !== true) {
|
|
1425
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin confirmation rejection endpoint returned an invalid response");
|
|
1426
|
+
}
|
|
1309
1427
|
}
|
|
1310
1428
|
#rpcBody(request, confirmationID) {
|
|
1311
1429
|
const body = {
|
|
@@ -1356,9 +1474,10 @@ export class PluginSurfaceHost {
|
|
|
1356
1474
|
else if (this.#leaseRenewalPromise)
|
|
1357
1475
|
await this.#leaseRenewalPromise;
|
|
1358
1476
|
}
|
|
1477
|
+
const requestBody = typeof body === "function" ? body() : body;
|
|
1359
1478
|
this.#activeTransportRequests += 1;
|
|
1360
1479
|
try {
|
|
1361
|
-
return await this.#fetchJSON(path,
|
|
1480
|
+
return await this.#fetchJSON(path, requestBody, signal);
|
|
1362
1481
|
}
|
|
1363
1482
|
finally {
|
|
1364
1483
|
this.#activeTransportRequests -= 1;
|
|
@@ -1507,8 +1626,16 @@ export class PluginSurfaceHost {
|
|
|
1507
1626
|
#postResponse(id, data) {
|
|
1508
1627
|
this.#postToRenderer({ type: "redevplugin.bridge.response", id, ok: true, data });
|
|
1509
1628
|
}
|
|
1510
|
-
#postError(id, errorCode, error) {
|
|
1511
|
-
|
|
1629
|
+
#postError(id, errorCode, error, details) {
|
|
1630
|
+
const errorDetails = details === undefined ? undefined : normalizePluginJSONObject(details);
|
|
1631
|
+
this.#postToRenderer(removeUndefined({
|
|
1632
|
+
type: "redevplugin.bridge.response",
|
|
1633
|
+
id,
|
|
1634
|
+
ok: false,
|
|
1635
|
+
error_code: errorCode,
|
|
1636
|
+
error,
|
|
1637
|
+
error_details: errorDetails,
|
|
1638
|
+
}));
|
|
1512
1639
|
}
|
|
1513
1640
|
#postToRenderer(message) {
|
|
1514
1641
|
if (!this.#port) {
|
|
@@ -1743,6 +1870,13 @@ function isStreamReadMessage(value) {
|
|
|
1743
1870
|
validBridgeRequestID(value.id, "stream") &&
|
|
1744
1871
|
validOpaqueHandle(value.stream_handle, "stream");
|
|
1745
1872
|
}
|
|
1873
|
+
function isOperationCancelMessage(value) {
|
|
1874
|
+
return hasAllowedKeys(value, ["type", "id", "operation_id", "reason"]) &&
|
|
1875
|
+
value.type === "redevplugin.bridge.operation.cancel" &&
|
|
1876
|
+
validBridgeRequestID(value.id, "operation") &&
|
|
1877
|
+
validOpaqueHandle(value.operation_id, "operation") &&
|
|
1878
|
+
(value.reason === undefined || (typeof value.reason === "string" && value.reason.length <= 256));
|
|
1879
|
+
}
|
|
1746
1880
|
function isBridgeCancelMessage(value) {
|
|
1747
1881
|
return hasExactKeys(value, ["type", "id"]) &&
|
|
1748
1882
|
value.type === "redevplugin.bridge.cancel" &&
|
|
@@ -1756,13 +1890,54 @@ function isAssetReadMessage(value) {
|
|
|
1756
1890
|
validPackagePath(value.path) &&
|
|
1757
1891
|
validSHA256(value.sha256);
|
|
1758
1892
|
}
|
|
1893
|
+
function isBridgeResponseCandidate(value) {
|
|
1894
|
+
return isRecord(value) && value.type === "redevplugin.bridge.response" && typeof value.id === "string";
|
|
1895
|
+
}
|
|
1759
1896
|
function isBridgeResponse(value) {
|
|
1760
|
-
if (!
|
|
1897
|
+
if (!isBridgeResponseCandidate(value) || !validBridgeRequestID(value.id))
|
|
1761
1898
|
return false;
|
|
1762
1899
|
if (value.ok === true)
|
|
1763
1900
|
return Object.keys(value).every((key) => ["type", "id", "ok", "data"].includes(key));
|
|
1764
|
-
|
|
1765
|
-
|
|
1901
|
+
if (value.ok !== false || typeof value.error_code !== "string" || !pluginBridgeErrorCodeSet.has(value.error_code) ||
|
|
1902
|
+
typeof value.error !== "string" || value.error.length > 4096 ||
|
|
1903
|
+
!Object.keys(value).every((key) => ["type", "id", "ok", "error_code", "error", "error_details"].includes(key))) {
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
if (value.error_code === "PLUGIN_CAPABILITY_ERROR")
|
|
1907
|
+
return isCapabilityBusinessErrorDetails(value.error_details);
|
|
1908
|
+
if (value.error_details === undefined)
|
|
1909
|
+
return true;
|
|
1910
|
+
try {
|
|
1911
|
+
return Object.keys(normalizePluginJSONObject(value.error_details)).length <= 8;
|
|
1912
|
+
}
|
|
1913
|
+
catch {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
function isCapabilityBusinessErrorDetails(value) {
|
|
1918
|
+
if (!hasAllowedKeys(value, [
|
|
1919
|
+
"capability_id",
|
|
1920
|
+
"capability_version",
|
|
1921
|
+
"detail_schema_sha256",
|
|
1922
|
+
"business_error_code",
|
|
1923
|
+
"business_error_details",
|
|
1924
|
+
]))
|
|
1925
|
+
return false;
|
|
1926
|
+
if (typeof value.capability_id !== "string" || !hostCapabilityIDPattern.test(value.capability_id) ||
|
|
1927
|
+
typeof value.capability_version !== "string" || !canonicalSemverPattern.test(value.capability_version) ||
|
|
1928
|
+
typeof value.detail_schema_sha256 !== "string" || !lowercaseSHA256Pattern.test(value.detail_schema_sha256) ||
|
|
1929
|
+
typeof value.business_error_code !== "string" || !businessErrorCodePattern.test(value.business_error_code)) {
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
if (value.business_error_details === undefined)
|
|
1933
|
+
return true;
|
|
1934
|
+
try {
|
|
1935
|
+
normalizePluginJSONObject(value.business_error_details);
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
catch {
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1766
1941
|
}
|
|
1767
1942
|
function isLifecycleMessage(value) {
|
|
1768
1943
|
return hasExactKeys(value, ["type", "event"]) &&
|
|
@@ -1801,10 +1976,25 @@ function isGatewayTokenResult(value) {
|
|
|
1801
1976
|
typeof value.asset_session_id === "string" && value.asset_session_id.length > 0 &&
|
|
1802
1977
|
typeof value.issued_at === "string" && typeof value.expires_at === "string";
|
|
1803
1978
|
}
|
|
1804
|
-
function isStreamReadResult(value, expectedStreamID) {
|
|
1805
|
-
if (!
|
|
1979
|
+
function isStreamReadResult(value, expectedStreamID, previousSequence) {
|
|
1980
|
+
if (!isRecord(value) || typeof value.done !== "boolean" || !Array.isArray(value.events))
|
|
1981
|
+
return false;
|
|
1982
|
+
const expectedKeys = value.done
|
|
1983
|
+
? ["done", "events", "terminal_status"]
|
|
1984
|
+
: ["done", "events", "next_stream_expires_at", "next_stream_ticket", "next_stream_ticket_id"];
|
|
1985
|
+
if (!hasExactKeys(value, expectedKeys))
|
|
1806
1986
|
return false;
|
|
1807
|
-
|
|
1987
|
+
if (value.done) {
|
|
1988
|
+
if (!validPluginStreamTerminalStatus(value.terminal_status))
|
|
1989
|
+
return false;
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
const expiresAt = Date.parse(String(value.next_stream_expires_at));
|
|
1993
|
+
if (typeof value.next_stream_ticket !== "string" || value.next_stream_ticket.length === 0 ||
|
|
1994
|
+
typeof value.next_stream_ticket_id !== "string" || value.next_stream_ticket_id.length === 0 ||
|
|
1995
|
+
!Number.isFinite(expiresAt) || expiresAt <= Date.now())
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1808
1998
|
let terminal = false;
|
|
1809
1999
|
for (const event of value.events) {
|
|
1810
2000
|
if (!isStreamEvent(event) || event.stream_id !== expectedStreamID || event.sequence <= previousSequence || terminal)
|
|
@@ -1823,6 +2013,13 @@ function isStreamEvent(value) {
|
|
|
1823
2013
|
(value.error == null || typeof value.error === "string") &&
|
|
1824
2014
|
typeof value.at === "string";
|
|
1825
2015
|
}
|
|
2016
|
+
function publicPluginStreamEvent(event) {
|
|
2017
|
+
return removeUndefined({ sequence: event.sequence, kind: event.kind, data: event.data, error: event.error, at: event.at });
|
|
2018
|
+
}
|
|
2019
|
+
function validPluginStreamTerminalStatus(value) {
|
|
2020
|
+
return value === "closed" || value === "canceled" || value === "failed" ||
|
|
2021
|
+
value === "orphaned_after_disable" || value === "orphaned_after_uninstall";
|
|
2022
|
+
}
|
|
1826
2023
|
function validRPCParams(value) {
|
|
1827
2024
|
if (value === undefined)
|
|
1828
2025
|
return true;
|
|
@@ -1837,7 +2034,7 @@ function validRPCParams(value) {
|
|
|
1837
2034
|
const pluginMethodPattern = new RegExp("^[-A-Za-z0-9._:]{1,256}$");
|
|
1838
2035
|
const pluginActionPattern = new RegExp("^[-A-Za-z0-9._:]{1,128}$");
|
|
1839
2036
|
const opaqueHandlePattern = new RegExp("^[-A-Za-z0-9_]{8,160}$");
|
|
1840
|
-
const bridgeRequestIDPattern = /^(rpc|stream|render)_([1-9][0-9]{0,15})$/;
|
|
2037
|
+
const bridgeRequestIDPattern = /^(rpc|stream|render|operation)_([1-9][0-9]{0,15})$/;
|
|
1841
2038
|
function validBridgeRequestID(value, expectedKind) {
|
|
1842
2039
|
if (typeof value !== "string")
|
|
1843
2040
|
return false;
|
|
@@ -2003,6 +2200,8 @@ function normalizePluginJSONValue(value, depth = 0, state = { nodes: 0, seen: ne
|
|
|
2003
2200
|
for (const key of Reflect.ownKeys(value)) {
|
|
2004
2201
|
if (typeof key !== "string")
|
|
2005
2202
|
throw new TypeError("JSON object keys must be strings");
|
|
2203
|
+
if (prototypeSensitivePropertyNames.has(key))
|
|
2204
|
+
throw new TypeError("JSON object keys must not alter object prototypes");
|
|
2006
2205
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2007
2206
|
if (!descriptor?.enumerable || !("value" in descriptor))
|
|
2008
2207
|
throw new TypeError("JSON object fields must be enumerable data properties");
|
|
@@ -2014,6 +2213,7 @@ function normalizePluginJSONValue(value, depth = 0, state = { nodes: 0, seen: ne
|
|
|
2014
2213
|
state.seen.delete(value);
|
|
2015
2214
|
}
|
|
2016
2215
|
}
|
|
2216
|
+
const prototypeSensitivePropertyNames = new Set(["__proto__", "constructor", "prototype"]);
|
|
2017
2217
|
function normalizeTimeout(timeoutMs) {
|
|
2018
2218
|
if (timeoutMs == null)
|
|
2019
2219
|
return 30_000;
|
|
@@ -2071,3 +2271,8 @@ function toBridgeError(error, fallbackCode) {
|
|
|
2071
2271
|
return new PluginBridgeError(fallbackCode, error.message);
|
|
2072
2272
|
return new PluginBridgeError(fallbackCode, String(error));
|
|
2073
2273
|
}
|
|
2274
|
+
function streamReadFailureInvalidatesCredential(error) {
|
|
2275
|
+
if (!(error instanceof PluginBridgeError))
|
|
2276
|
+
return true;
|
|
2277
|
+
return streamCredentialInvalidatingErrorCodes.has(error.errorCode);
|
|
2278
|
+
}
|