@floegence/redevplugin-ui 0.0.0-dev
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/index.d.ts +665 -0
- package/dist/index.js +876 -0
- package/package.json +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
export const pluginPlatformErrorCodes = [
|
|
2
|
+
"PLUGIN_INVALID_REQUEST",
|
|
3
|
+
"PLUGIN_MANIFEST_INVALID",
|
|
4
|
+
"PLUGIN_PACKAGE_INVALID",
|
|
5
|
+
"PLUGIN_PACKAGE_TOO_LARGE",
|
|
6
|
+
"PLUGIN_PACKAGE_PATH_FORBIDDEN",
|
|
7
|
+
"PLUGIN_SIGNATURE_INVALID",
|
|
8
|
+
"PLUGIN_TRUST_STATE_DENIED",
|
|
9
|
+
"PLUGIN_TRUST_VERIFICATION_REQUIRED",
|
|
10
|
+
"PLUGIN_TRUST_VERIFICATION_INVALID",
|
|
11
|
+
"PLUGIN_DISABLED",
|
|
12
|
+
"PLUGIN_DISABLED_BY_POLICY",
|
|
13
|
+
"PLUGIN_PERMISSION_DENIED",
|
|
14
|
+
"PLUGIN_CONFIRMATION_REQUIRED",
|
|
15
|
+
"PLUGIN_CONFIRMATION_INVALID",
|
|
16
|
+
"PLUGIN_TOKEN_EXPIRED",
|
|
17
|
+
"PLUGIN_TOKEN_REPLAY",
|
|
18
|
+
"PLUGIN_GATEWAY_TOKEN_INVALID",
|
|
19
|
+
"PLUGIN_GATEWAY_TOKEN_REPLAYED",
|
|
20
|
+
"PLUGIN_GATEWAY_TOKEN_CHANNEL_MISMATCH",
|
|
21
|
+
"PLUGIN_ASSET_TICKET_INVALID",
|
|
22
|
+
"PLUGIN_ASSET_SESSION_INVALID",
|
|
23
|
+
"PLUGIN_STREAM_TICKET_INVALID",
|
|
24
|
+
"PLUGIN_STREAM_CANCELLED",
|
|
25
|
+
"PLUGIN_LEASE_INVALID",
|
|
26
|
+
"PLUGIN_LEASE_REPLAYED",
|
|
27
|
+
"PLUGIN_GRANT_INVALID",
|
|
28
|
+
"PLUGIN_STORAGE_QUOTA_EXCEEDED",
|
|
29
|
+
"PLUGIN_OPERATION_BLOCKED",
|
|
30
|
+
"PLUGIN_OPERATION_NOT_FOUND",
|
|
31
|
+
"PLUGIN_OPERATION_NOT_CANCELABLE",
|
|
32
|
+
"PLUGIN_NETWORK_TARGET_DENIED",
|
|
33
|
+
"PLUGIN_NETWORK_RATE_LIMITED",
|
|
34
|
+
"PLUGIN_RUNTIME_UNAVAILABLE",
|
|
35
|
+
"PLUGIN_RUNTIME_VERSION_MISMATCH",
|
|
36
|
+
"PLUGIN_JSON_LIMIT_EXCEEDED",
|
|
37
|
+
"PLUGIN_CONTRACT_MISMATCH",
|
|
38
|
+
"PLUGIN_CSRF_REQUIRED",
|
|
39
|
+
"PLUGIN_RETAINED_DATA_CLEANUP_FAILED",
|
|
40
|
+
"PLUGIN_RETAINED_DATA_BIND_FAILED",
|
|
41
|
+
];
|
|
42
|
+
export const pluginBridgeErrorCodes = [
|
|
43
|
+
...pluginPlatformErrorCodes,
|
|
44
|
+
"PLUGIN_CONFIRMATION_REJECTED",
|
|
45
|
+
"PLUGIN_BRIDGE_TIMEOUT",
|
|
46
|
+
"PLUGIN_BRIDGE_DISPOSED",
|
|
47
|
+
"PLUGIN_BRIDGE_HANDSHAKE_FAILED",
|
|
48
|
+
"PLUGIN_BRIDGE_HANDSHAKE_REQUIRED",
|
|
49
|
+
];
|
|
50
|
+
export const pluginClientErrorCodes = [
|
|
51
|
+
...pluginBridgeErrorCodes,
|
|
52
|
+
"PLUGIN_PLATFORM_REQUEST_FAILED",
|
|
53
|
+
"PLUGIN_STREAM_FAILED",
|
|
54
|
+
];
|
|
55
|
+
export const defaultPluginSurfaceReloadMax = 2;
|
|
56
|
+
export const defaultPluginSurfaceReloadWindowMs = 30_000;
|
|
57
|
+
export class PluginBridgeError extends Error {
|
|
58
|
+
errorCode;
|
|
59
|
+
data;
|
|
60
|
+
details;
|
|
61
|
+
constructor(errorCode, message, data, details) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "PluginBridgeError";
|
|
64
|
+
this.errorCode = errorCode;
|
|
65
|
+
this.data = data;
|
|
66
|
+
this.details = details ?? data;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function pluginBridgeHandshakeTranscriptSHA256(handshake, bridgeChannelID) {
|
|
70
|
+
const subtle = globalThis.crypto?.subtle;
|
|
71
|
+
if (!subtle) {
|
|
72
|
+
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", "Web Crypto SHA-256 is unavailable for plugin bridge handshake");
|
|
73
|
+
}
|
|
74
|
+
const encoder = new TextEncoder();
|
|
75
|
+
const fields = [
|
|
76
|
+
"redevplugin.bridge.handshake.v1",
|
|
77
|
+
handshake.plugin_id,
|
|
78
|
+
handshake.surface_id,
|
|
79
|
+
handshake.surface_instance_id,
|
|
80
|
+
handshake.active_fingerprint,
|
|
81
|
+
handshake.bridge_nonce,
|
|
82
|
+
handshake.ui_protocol_version,
|
|
83
|
+
bridgeChannelID,
|
|
84
|
+
];
|
|
85
|
+
const chunks = [];
|
|
86
|
+
let totalBytes = 0;
|
|
87
|
+
for (const field of fields) {
|
|
88
|
+
const data = encoder.encode(field);
|
|
89
|
+
const prefix = encoder.encode(`${data.byteLength}:`);
|
|
90
|
+
const terminator = new Uint8Array([0]);
|
|
91
|
+
chunks.push(prefix, data, terminator);
|
|
92
|
+
totalBytes += prefix.byteLength + data.byteLength + terminator.byteLength;
|
|
93
|
+
}
|
|
94
|
+
const transcript = new Uint8Array(totalBytes);
|
|
95
|
+
let offset = 0;
|
|
96
|
+
for (const chunk of chunks) {
|
|
97
|
+
transcript.set(chunk, offset);
|
|
98
|
+
offset += chunk.byteLength;
|
|
99
|
+
}
|
|
100
|
+
const digest = await subtle.digest("SHA-256", transcript);
|
|
101
|
+
const bytes = new Uint8Array(digest);
|
|
102
|
+
return `sha256:${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
103
|
+
}
|
|
104
|
+
export class PluginSurfaceReloadLimiter {
|
|
105
|
+
maxReloads;
|
|
106
|
+
windowMs;
|
|
107
|
+
#now;
|
|
108
|
+
#windowStartedAtMs;
|
|
109
|
+
#reloads = 0;
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.maxReloads = normalizeReloadMax(options.maxReloads ?? defaultPluginSurfaceReloadMax);
|
|
112
|
+
this.windowMs = normalizeReloadWindow(options.windowMs ?? defaultPluginSurfaceReloadWindowMs);
|
|
113
|
+
this.#now = options.now ?? (() => Date.now());
|
|
114
|
+
}
|
|
115
|
+
recordCrash(nowMs = this.#now()) {
|
|
116
|
+
nowMs = normalizeNowMs(nowMs);
|
|
117
|
+
this.#ensureWindow(nowMs);
|
|
118
|
+
const windowStartedAtMs = this.#windowStartedAtMs ?? nowMs;
|
|
119
|
+
if (this.#reloads >= this.maxReloads) {
|
|
120
|
+
return {
|
|
121
|
+
allowed: false,
|
|
122
|
+
attempt: this.#reloads + 1,
|
|
123
|
+
remaining: 0,
|
|
124
|
+
windowStartedAtMs,
|
|
125
|
+
nextRetryAtMs: windowStartedAtMs + this.windowMs,
|
|
126
|
+
reason: "reload_limit_exceeded",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
this.#reloads += 1;
|
|
130
|
+
return {
|
|
131
|
+
allowed: true,
|
|
132
|
+
attempt: this.#reloads,
|
|
133
|
+
remaining: this.maxReloads - this.#reloads,
|
|
134
|
+
windowStartedAtMs,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
recordHealthyLoad() {
|
|
138
|
+
this.reset();
|
|
139
|
+
}
|
|
140
|
+
reset() {
|
|
141
|
+
this.#windowStartedAtMs = undefined;
|
|
142
|
+
this.#reloads = 0;
|
|
143
|
+
}
|
|
144
|
+
get state() {
|
|
145
|
+
const remaining = Math.max(0, this.maxReloads - this.#reloads);
|
|
146
|
+
return {
|
|
147
|
+
reloads: this.#reloads,
|
|
148
|
+
remaining,
|
|
149
|
+
windowStartedAtMs: this.#windowStartedAtMs,
|
|
150
|
+
nextRetryAtMs: remaining === 0 && this.#windowStartedAtMs !== undefined
|
|
151
|
+
? this.#windowStartedAtMs + this.windowMs
|
|
152
|
+
: undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
#ensureWindow(nowMs) {
|
|
156
|
+
if (this.#windowStartedAtMs === undefined ||
|
|
157
|
+
nowMs < this.#windowStartedAtMs ||
|
|
158
|
+
nowMs >= this.#windowStartedAtMs + this.windowMs) {
|
|
159
|
+
this.#windowStartedAtMs = nowMs;
|
|
160
|
+
this.#reloads = 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
class PluginConfirmationRequiredError extends PluginBridgeError {
|
|
165
|
+
}
|
|
166
|
+
export class PluginBridgeClient {
|
|
167
|
+
bootstrap;
|
|
168
|
+
timeoutMs;
|
|
169
|
+
#nextID = 1;
|
|
170
|
+
#target;
|
|
171
|
+
#receiver;
|
|
172
|
+
#pending = new Map();
|
|
173
|
+
#lifecycleHandlers = new Set();
|
|
174
|
+
#disposed = false;
|
|
175
|
+
#onMessage = (event) => {
|
|
176
|
+
this.#handleMessage(event);
|
|
177
|
+
};
|
|
178
|
+
constructor(bootstrap, options = {}) {
|
|
179
|
+
if (!bootstrap.parentOrigin || bootstrap.parentOrigin === "*") {
|
|
180
|
+
throw new Error("parentOrigin must be an exact origin");
|
|
181
|
+
}
|
|
182
|
+
this.bootstrap = bootstrap;
|
|
183
|
+
this.timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
184
|
+
this.#target = options.target ?? window.parent;
|
|
185
|
+
this.#receiver = options.receiver ?? window;
|
|
186
|
+
this.#receiver.addEventListener?.("message", this.#onMessage);
|
|
187
|
+
}
|
|
188
|
+
handshake() {
|
|
189
|
+
this.#assertActive();
|
|
190
|
+
const message = {
|
|
191
|
+
type: "redevplugin.bridge.handshake",
|
|
192
|
+
plugin_id: this.bootstrap.pluginId,
|
|
193
|
+
surface_id: this.bootstrap.surfaceId,
|
|
194
|
+
surface_instance_id: this.bootstrap.surfaceInstanceId,
|
|
195
|
+
active_fingerprint: this.bootstrap.activeFingerprint,
|
|
196
|
+
bridge_nonce: this.bootstrap.bridgeNonce,
|
|
197
|
+
ui_protocol_version: "plugin-ui-v1",
|
|
198
|
+
};
|
|
199
|
+
this.#target.postMessage(message, this.bootstrap.parentOrigin);
|
|
200
|
+
}
|
|
201
|
+
call(method, params) {
|
|
202
|
+
this.#assertActive();
|
|
203
|
+
const request = {
|
|
204
|
+
id: String(this.#nextID++),
|
|
205
|
+
method,
|
|
206
|
+
params,
|
|
207
|
+
};
|
|
208
|
+
const message = { type: "redevplugin.bridge.call", request };
|
|
209
|
+
const result = new Promise((resolve, reject) => {
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
this.#pending.delete(request.id);
|
|
212
|
+
reject(new PluginBridgeError("PLUGIN_BRIDGE_TIMEOUT", `Plugin bridge call ${request.id} timed out`));
|
|
213
|
+
}, this.timeoutMs);
|
|
214
|
+
this.#pending.set(request.id, {
|
|
215
|
+
resolve: (value) => resolve(value),
|
|
216
|
+
reject,
|
|
217
|
+
timer,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
this.#target.postMessage(message, this.bootstrap.parentOrigin);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
onLifecycle(handler) {
|
|
224
|
+
this.#assertActive();
|
|
225
|
+
this.#lifecycleHandlers.add(handler);
|
|
226
|
+
return () => {
|
|
227
|
+
this.#lifecycleHandlers.delete(handler);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
dispose() {
|
|
231
|
+
if (this.#disposed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.#disposed = true;
|
|
235
|
+
this.#receiver.removeEventListener?.("message", this.#onMessage);
|
|
236
|
+
for (const [id, pending] of this.#pending) {
|
|
237
|
+
clearTimeout(pending.timer);
|
|
238
|
+
pending.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", `Plugin bridge call ${id} was disposed`));
|
|
239
|
+
}
|
|
240
|
+
this.#pending.clear();
|
|
241
|
+
this.#lifecycleHandlers.clear();
|
|
242
|
+
}
|
|
243
|
+
#handleMessage(event) {
|
|
244
|
+
if (this.#disposed || event.origin !== this.bootstrap.parentOrigin) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const data = event.data;
|
|
248
|
+
if (isBridgeResponse(data)) {
|
|
249
|
+
const pending = this.#pending.get(data.id);
|
|
250
|
+
if (!pending) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.#pending.delete(data.id);
|
|
254
|
+
clearTimeout(pending.timer);
|
|
255
|
+
if (data.ok) {
|
|
256
|
+
pending.resolve(data.data);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
pending.reject(new PluginBridgeError(data.error_code, data.error));
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (isLifecycleMessage(data)) {
|
|
264
|
+
for (const handler of this.#lifecycleHandlers) {
|
|
265
|
+
handler(data.event);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#assertActive() {
|
|
270
|
+
if (this.#disposed) {
|
|
271
|
+
throw new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin bridge client is disposed");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export const pluginRiskPlanSchemaVersion = "redevplugin.capability.risk_plan.v1";
|
|
276
|
+
export function isPluginRiskPlan(plan) {
|
|
277
|
+
return isRecord(plan) &&
|
|
278
|
+
plan.schema_version === pluginRiskPlanSchemaVersion &&
|
|
279
|
+
typeof plan.summary === "string" &&
|
|
280
|
+
Array.isArray(plan.risk_flags) &&
|
|
281
|
+
plan.risk_flags.every(isPluginRiskFlag) &&
|
|
282
|
+
(plan.effect == null || isPluginRiskEffect(plan.effect)) &&
|
|
283
|
+
(plan.details == null || isRecord(plan.details));
|
|
284
|
+
}
|
|
285
|
+
export class PluginPlatformClient {
|
|
286
|
+
#fetch;
|
|
287
|
+
#apiBaseURL;
|
|
288
|
+
#ownerSessionHashHeader;
|
|
289
|
+
constructor(options = {}) {
|
|
290
|
+
this.#fetch = options.fetch ?? defaultFetch();
|
|
291
|
+
this.#apiBaseURL = trimTrailingSlash(options.apiBaseURL ?? "");
|
|
292
|
+
this.#ownerSessionHashHeader = options.ownerSessionHashHeader;
|
|
293
|
+
}
|
|
294
|
+
catalog() {
|
|
295
|
+
return this.#getJSON("/_redevplugin/api/plugins/catalog");
|
|
296
|
+
}
|
|
297
|
+
getCompatibility() {
|
|
298
|
+
return this.#getJSON("/_redevplugin/api/plugins/platform/compatibility");
|
|
299
|
+
}
|
|
300
|
+
installPlugin(request) {
|
|
301
|
+
return this.#postJSON("/_redevplugin/api/plugins/install", request);
|
|
302
|
+
}
|
|
303
|
+
updatePlugin(request) {
|
|
304
|
+
return this.#postJSON("/_redevplugin/api/plugins/update", request);
|
|
305
|
+
}
|
|
306
|
+
downgradePlugin(request) {
|
|
307
|
+
return this.#postJSON("/_redevplugin/api/plugins/downgrade", request);
|
|
308
|
+
}
|
|
309
|
+
enablePlugin(pluginInstanceIdOrRequest) {
|
|
310
|
+
return this.#postJSON("/_redevplugin/api/plugins/enable", pluginInstanceRequest(pluginInstanceIdOrRequest));
|
|
311
|
+
}
|
|
312
|
+
disablePlugin(request) {
|
|
313
|
+
return this.#postJSON("/_redevplugin/api/plugins/disable", request);
|
|
314
|
+
}
|
|
315
|
+
uninstallPlugin(request) {
|
|
316
|
+
return this.#postJSON("/_redevplugin/api/plugins/uninstall", request);
|
|
317
|
+
}
|
|
318
|
+
openSurface(request) {
|
|
319
|
+
return this.#postJSON("/_redevplugin/api/plugins/surfaces/open", request);
|
|
320
|
+
}
|
|
321
|
+
startRuntime(request = {}) {
|
|
322
|
+
return this.#postJSON("/_redevplugin/api/plugins/runtime/start", request);
|
|
323
|
+
}
|
|
324
|
+
stopRuntime() {
|
|
325
|
+
return this.#postJSON("/_redevplugin/api/plugins/runtime/stop", {});
|
|
326
|
+
}
|
|
327
|
+
refreshEnabledRuntimeState() {
|
|
328
|
+
return this.#postJSON("/_redevplugin/api/plugins/runtime/refresh-enabled", {});
|
|
329
|
+
}
|
|
330
|
+
runtimeHealth() {
|
|
331
|
+
return this.#getJSON("/_redevplugin/api/plugins/runtime/health");
|
|
332
|
+
}
|
|
333
|
+
getSettingsSchema(pluginInstanceId) {
|
|
334
|
+
return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/schema`);
|
|
335
|
+
}
|
|
336
|
+
getSettings(pluginInstanceId) {
|
|
337
|
+
return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`);
|
|
338
|
+
}
|
|
339
|
+
patchSettings(pluginInstanceId, values) {
|
|
340
|
+
return this.#patchJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`, { values });
|
|
341
|
+
}
|
|
342
|
+
listOperations(pluginInstanceId) {
|
|
343
|
+
const query = pluginInstanceId ? `?plugin_instance_id=${encodeURIComponent(pluginInstanceId)}` : "";
|
|
344
|
+
return this.#getJSON(`/_redevplugin/api/plugins/operations${query}`);
|
|
345
|
+
}
|
|
346
|
+
getOperation(operationId) {
|
|
347
|
+
return this.#getJSON(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}`);
|
|
348
|
+
}
|
|
349
|
+
cancelOperation(operationId, reason) {
|
|
350
|
+
const body = reason ? { reason } : {};
|
|
351
|
+
return this.#postJSON(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/cancel`, body);
|
|
352
|
+
}
|
|
353
|
+
listIntents(options = {}) {
|
|
354
|
+
const params = new URLSearchParams();
|
|
355
|
+
if (options.intent_id) {
|
|
356
|
+
params.set("intent_id", options.intent_id);
|
|
357
|
+
}
|
|
358
|
+
if (options.plugin_instance_id) {
|
|
359
|
+
params.set("plugin_instance_id", options.plugin_instance_id);
|
|
360
|
+
}
|
|
361
|
+
const query = params.toString();
|
|
362
|
+
return this.#getJSON(`/_redevplugin/api/plugins/intents${query ? `?${query}` : ""}`);
|
|
363
|
+
}
|
|
364
|
+
invokeIntent(request) {
|
|
365
|
+
return this.#postJSON("/_redevplugin/api/plugins/intents/invoke", request);
|
|
366
|
+
}
|
|
367
|
+
exportData(request) {
|
|
368
|
+
return this.#postJSON("/_redevplugin/api/plugins/data/export", request);
|
|
369
|
+
}
|
|
370
|
+
importData(request) {
|
|
371
|
+
return this.#postJSON("/_redevplugin/api/plugins/data/import", request);
|
|
372
|
+
}
|
|
373
|
+
listRetainedData(options = {}) {
|
|
374
|
+
const params = new URLSearchParams();
|
|
375
|
+
if (options.publisher_id) {
|
|
376
|
+
params.set("publisher_id", options.publisher_id);
|
|
377
|
+
}
|
|
378
|
+
if (options.plugin_id) {
|
|
379
|
+
params.set("plugin_id", options.plugin_id);
|
|
380
|
+
}
|
|
381
|
+
if (options.source_plugin_instance_id) {
|
|
382
|
+
params.set("source_plugin_instance_id", options.source_plugin_instance_id);
|
|
383
|
+
}
|
|
384
|
+
if (options.state) {
|
|
385
|
+
params.set("state", options.state);
|
|
386
|
+
}
|
|
387
|
+
const query = params.toString();
|
|
388
|
+
return this.#getJSON(`/_redevplugin/api/plugins/retained-data${query ? `?${query}` : ""}`);
|
|
389
|
+
}
|
|
390
|
+
deleteRetainedData(retainedId) {
|
|
391
|
+
return this.#postJSON("/_redevplugin/api/plugins/retained-data/delete", { retained_id: retainedId });
|
|
392
|
+
}
|
|
393
|
+
bindRetainedData(request) {
|
|
394
|
+
return this.#postJSON("/_redevplugin/api/plugins/retained-data/bind", request);
|
|
395
|
+
}
|
|
396
|
+
cleanupExpiredRetainedData(request = {}) {
|
|
397
|
+
return this.#postJSON("/_redevplugin/api/plugins/retained-data/cleanup-expired", request);
|
|
398
|
+
}
|
|
399
|
+
listPermissions(pluginInstanceId, activeOnly) {
|
|
400
|
+
const params = new URLSearchParams();
|
|
401
|
+
if (pluginInstanceId) {
|
|
402
|
+
params.set("plugin_instance_id", pluginInstanceId);
|
|
403
|
+
}
|
|
404
|
+
if (activeOnly != null) {
|
|
405
|
+
params.set("active_only", activeOnly ? "true" : "false");
|
|
406
|
+
}
|
|
407
|
+
const query = params.toString();
|
|
408
|
+
return this.#getJSON(`/_redevplugin/api/plugins/permissions${query ? `?${query}` : ""}`);
|
|
409
|
+
}
|
|
410
|
+
grantPermission(request) {
|
|
411
|
+
return this.#postJSON("/_redevplugin/api/plugins/permissions/grant", request);
|
|
412
|
+
}
|
|
413
|
+
revokePermission(request) {
|
|
414
|
+
return this.#postJSON("/_redevplugin/api/plugins/permissions/revoke", request);
|
|
415
|
+
}
|
|
416
|
+
bindSecret(request) {
|
|
417
|
+
return this.#postJSON("/_redevplugin/api/plugins/secrets/bind", request);
|
|
418
|
+
}
|
|
419
|
+
testSecret(request) {
|
|
420
|
+
return this.#postJSON("/_redevplugin/api/plugins/secrets/test", request);
|
|
421
|
+
}
|
|
422
|
+
deleteSecret(request) {
|
|
423
|
+
return this.#postJSON("/_redevplugin/api/plugins/secrets/delete", request);
|
|
424
|
+
}
|
|
425
|
+
listAuditEvents(options = {}) {
|
|
426
|
+
return this.#getJSON(`/_redevplugin/api/plugins/audit${queryString(options)}`);
|
|
427
|
+
}
|
|
428
|
+
listDiagnosticEvents(options = {}) {
|
|
429
|
+
return this.#getJSON(`/_redevplugin/api/plugins/diagnostics${queryString(options)}`);
|
|
430
|
+
}
|
|
431
|
+
#getJSON(path) {
|
|
432
|
+
return this.#requestJSON("GET", path);
|
|
433
|
+
}
|
|
434
|
+
#postJSON(path, body) {
|
|
435
|
+
return this.#requestJSON("POST", path, body);
|
|
436
|
+
}
|
|
437
|
+
#patchJSON(path, body) {
|
|
438
|
+
return this.#requestJSON("PATCH", path, body);
|
|
439
|
+
}
|
|
440
|
+
async #requestJSON(method, path, body) {
|
|
441
|
+
const response = await this.#fetch(this.#apiBaseURL + path, {
|
|
442
|
+
method,
|
|
443
|
+
headers: this.#headers(body !== undefined),
|
|
444
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
445
|
+
credentials: "same-origin",
|
|
446
|
+
});
|
|
447
|
+
return readHostEnvelope(response, "PLUGIN_PLATFORM_REQUEST_FAILED");
|
|
448
|
+
}
|
|
449
|
+
#headers(hasBody) {
|
|
450
|
+
const headers = { "Accept": "application/json" };
|
|
451
|
+
if (hasBody) {
|
|
452
|
+
headers["Content-Type"] = "application/json";
|
|
453
|
+
}
|
|
454
|
+
if (this.#ownerSessionHashHeader) {
|
|
455
|
+
headers["X-ReDevPlugin-Owner-Session-Hash"] = this.#ownerSessionHashHeader;
|
|
456
|
+
}
|
|
457
|
+
return headers;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
export class PluginSurfaceHost {
|
|
461
|
+
bootstrap;
|
|
462
|
+
iframeOrigin;
|
|
463
|
+
bridgeChannelId;
|
|
464
|
+
#iframeWindow;
|
|
465
|
+
#parentWindow;
|
|
466
|
+
#fetch;
|
|
467
|
+
#apiBaseURL;
|
|
468
|
+
#ownerSessionHashHeader;
|
|
469
|
+
#confirm;
|
|
470
|
+
#onError;
|
|
471
|
+
#gatewayToken;
|
|
472
|
+
#disposed = false;
|
|
473
|
+
#onMessage = (event) => {
|
|
474
|
+
void this.#handleMessage(event);
|
|
475
|
+
};
|
|
476
|
+
constructor(options) {
|
|
477
|
+
if (!options.iframeOrigin || options.iframeOrigin === "*") {
|
|
478
|
+
throw new Error("iframeOrigin must be an exact origin");
|
|
479
|
+
}
|
|
480
|
+
this.bootstrap = options.bootstrap;
|
|
481
|
+
this.iframeOrigin = options.iframeOrigin;
|
|
482
|
+
this.bridgeChannelId = options.bridgeChannelId ?? randomBridgeChannelID();
|
|
483
|
+
this.#iframeWindow = options.iframeWindow;
|
|
484
|
+
this.#parentWindow = options.parentWindow ?? window;
|
|
485
|
+
this.#fetch = options.fetch ?? defaultFetch();
|
|
486
|
+
this.#apiBaseURL = trimTrailingSlash(options.apiBaseURL ?? "");
|
|
487
|
+
this.#ownerSessionHashHeader = options.ownerSessionHashHeader ?? options.bootstrap.ownerSessionHash;
|
|
488
|
+
this.#confirm = options.confirm;
|
|
489
|
+
this.#onError = options.onError;
|
|
490
|
+
this.#parentWindow.addEventListener?.("message", this.#onMessage);
|
|
491
|
+
}
|
|
492
|
+
sendLifecycle(event) {
|
|
493
|
+
this.#assertActive();
|
|
494
|
+
this.#postToIframe({ type: "redevplugin.bridge.lifecycle", event });
|
|
495
|
+
}
|
|
496
|
+
dispose() {
|
|
497
|
+
if (this.#disposed) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.#disposed = true;
|
|
501
|
+
this.#gatewayToken = undefined;
|
|
502
|
+
this.#parentWindow.removeEventListener?.("message", this.#onMessage);
|
|
503
|
+
this.#postToIframe({ type: "redevplugin.bridge.lifecycle", event: { type: "dispose" } });
|
|
504
|
+
}
|
|
505
|
+
async #handleMessage(event) {
|
|
506
|
+
if (this.#disposed || event.origin !== this.iframeOrigin) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (event.source != null && event.source !== this.#iframeWindow) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const data = event.data;
|
|
513
|
+
if (isBridgeHandshake(data)) {
|
|
514
|
+
await this.#handleHandshake(data);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (isBridgeCallMessage(data)) {
|
|
518
|
+
await this.#handleCall(data.request);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async #handleHandshake(handshake) {
|
|
522
|
+
if (!handshakeMatchesBootstrap(handshake, this.bootstrap)) {
|
|
523
|
+
this.#reportError(new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", "Plugin bridge handshake did not match the surface bootstrap"));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const request = {
|
|
528
|
+
bridge_channel_id: this.bridgeChannelId,
|
|
529
|
+
handshake,
|
|
530
|
+
handshake_transcript_sha256: await pluginBridgeHandshakeTranscriptSHA256(handshake, this.bridgeChannelId),
|
|
531
|
+
};
|
|
532
|
+
const token = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/bridge-token`, request);
|
|
533
|
+
this.#gatewayToken = token.plugin_gateway_token;
|
|
534
|
+
this.#postToIframe({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } });
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
this.#reportError(toBridgeError(error, "PLUGIN_BRIDGE_HANDSHAKE_FAILED"));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async #handleCall(request) {
|
|
541
|
+
if (!this.#gatewayToken) {
|
|
542
|
+
this.#postError(request.id, "PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Plugin bridge call arrived before a successful handshake");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!validRPCParams(request.params)) {
|
|
546
|
+
this.#postError(request.id, "PLUGIN_INVALID_REQUEST", "Plugin bridge call params must be a JSON object when present");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const result = await this.#callRPC(request);
|
|
551
|
+
this.#postToIframe({ type: "redevplugin.bridge.response", id: request.id, ok: true, data: result });
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const bridgeError = toBridgeError(error, "PLUGIN_PERMISSION_DENIED");
|
|
555
|
+
if (bridgeError instanceof PluginConfirmationRequiredError) {
|
|
556
|
+
await this.#handleConfirmationRequired(request, bridgeError);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.#postError(request.id, bridgeError.errorCode, bridgeError.message);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async #handleConfirmationRequired(request, originalError) {
|
|
563
|
+
if (!this.#confirm) {
|
|
564
|
+
this.#postError(request.id, originalError.errorCode, originalError.message);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const confirmation = await this.#prepareConfirmation(request);
|
|
569
|
+
const decision = await this.#confirm({
|
|
570
|
+
requestId: request.id,
|
|
571
|
+
method: request.method,
|
|
572
|
+
params: validRPCParams(request.params) ? request.params : undefined,
|
|
573
|
+
requestHash: confirmation.request_hash,
|
|
574
|
+
planHash: confirmation.plan_hash,
|
|
575
|
+
plan: confirmation.plan,
|
|
576
|
+
confirmationTokenId: confirmation.confirmation_token_id,
|
|
577
|
+
});
|
|
578
|
+
if (!confirmationDecisionAccepted(decision)) {
|
|
579
|
+
this.#postError(request.id, "PLUGIN_CONFIRMATION_REJECTED", "Plugin method confirmation was rejected");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const result = await this.#callRPC(request, confirmation.confirmation_id);
|
|
583
|
+
this.#postToIframe({ type: "redevplugin.bridge.response", id: request.id, ok: true, data: result });
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
const bridgeError = toBridgeError(error, "PLUGIN_PERMISSION_DENIED");
|
|
587
|
+
this.#postError(request.id, bridgeError.errorCode, bridgeError.message);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
#callRPC(request, confirmationId) {
|
|
591
|
+
return this.#postJSON(`/_redevplugin/api/plugins/rpc`, this.#rpcBody(request, confirmationId));
|
|
592
|
+
}
|
|
593
|
+
#prepareConfirmation(request) {
|
|
594
|
+
return this.#postJSON(`/_redevplugin/api/plugins/confirm`, this.#rpcBody(request));
|
|
595
|
+
}
|
|
596
|
+
#rpcBody(request, confirmationId) {
|
|
597
|
+
const body = {
|
|
598
|
+
plugin_instance_id: this.bootstrap.pluginInstanceId,
|
|
599
|
+
surface_instance_id: this.bootstrap.surfaceInstanceId,
|
|
600
|
+
session_channel_id_hash: this.bootstrap.sessionChannelIdHash,
|
|
601
|
+
owner_session_hash: this.bootstrap.ownerSessionHash,
|
|
602
|
+
owner_user_hash: this.bootstrap.ownerUserHash,
|
|
603
|
+
bridge_channel_id: this.bridgeChannelId,
|
|
604
|
+
plugin_gateway_token: this.#gatewayToken,
|
|
605
|
+
method: request.method,
|
|
606
|
+
params: request.params,
|
|
607
|
+
};
|
|
608
|
+
if (confirmationId) {
|
|
609
|
+
body.confirmation_id = confirmationId;
|
|
610
|
+
}
|
|
611
|
+
return body;
|
|
612
|
+
}
|
|
613
|
+
async #postJSON(path, body) {
|
|
614
|
+
const headers = {
|
|
615
|
+
"Accept": "application/json",
|
|
616
|
+
"Content-Type": "application/json",
|
|
617
|
+
};
|
|
618
|
+
if (this.#ownerSessionHashHeader) {
|
|
619
|
+
headers["X-ReDevPlugin-Owner-Session-Hash"] = this.#ownerSessionHashHeader;
|
|
620
|
+
}
|
|
621
|
+
const response = await this.#fetch(this.#apiBaseURL + path, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers,
|
|
624
|
+
body: JSON.stringify(body),
|
|
625
|
+
credentials: "same-origin",
|
|
626
|
+
});
|
|
627
|
+
return readHostEnvelope(response, "PLUGIN_PERMISSION_DENIED");
|
|
628
|
+
}
|
|
629
|
+
#postError(id, errorCode, error) {
|
|
630
|
+
this.#postToIframe({ type: "redevplugin.bridge.response", id, ok: false, error_code: errorCode, error });
|
|
631
|
+
}
|
|
632
|
+
#postToIframe(message) {
|
|
633
|
+
this.#iframeWindow.postMessage(message, this.iframeOrigin);
|
|
634
|
+
}
|
|
635
|
+
#reportError(error) {
|
|
636
|
+
this.#onError?.(error);
|
|
637
|
+
}
|
|
638
|
+
#assertActive() {
|
|
639
|
+
if (this.#disposed) {
|
|
640
|
+
throw new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin surface host is disposed");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
export async function readPluginStream(options) {
|
|
645
|
+
const streamId = options.streamId ?? options.result?.stream_id;
|
|
646
|
+
const streamTicket = options.streamTicket ?? options.result?.stream_ticket;
|
|
647
|
+
if (!streamId || !streamTicket) {
|
|
648
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "stream_id and stream_ticket are required to read a plugin stream");
|
|
649
|
+
}
|
|
650
|
+
const fetchLike = options.fetch ?? defaultStreamFetch();
|
|
651
|
+
const url = `${trimTrailingSlash(options.apiBaseURL ?? "")}/_redevplugin/stream/${encodeURIComponent(streamId)}?ticket=${encodeURIComponent(streamTicket)}`;
|
|
652
|
+
const response = await fetchLike(url, {
|
|
653
|
+
method: "GET",
|
|
654
|
+
headers: { "Accept": "application/x-ndjson, application/json" },
|
|
655
|
+
credentials: "same-origin",
|
|
656
|
+
});
|
|
657
|
+
const raw = await response.text();
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
throw streamErrorFromBody(raw, response.status);
|
|
660
|
+
}
|
|
661
|
+
return parseNDJSONEvents(raw);
|
|
662
|
+
}
|
|
663
|
+
export function decodePluginStreamText(event) {
|
|
664
|
+
if (!event.data) {
|
|
665
|
+
return "";
|
|
666
|
+
}
|
|
667
|
+
if (typeof TextDecoder === "function" && typeof atob === "function") {
|
|
668
|
+
const binary = atob(event.data);
|
|
669
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
670
|
+
return new TextDecoder().decode(bytes);
|
|
671
|
+
}
|
|
672
|
+
const bufferLike = globalThis.Buffer;
|
|
673
|
+
if (bufferLike) {
|
|
674
|
+
return bufferLike.from(event.data, "base64").toString("utf8");
|
|
675
|
+
}
|
|
676
|
+
throw new PluginBridgeError("PLUGIN_STREAM_FAILED", "No base64 decoder is available for plugin stream data");
|
|
677
|
+
}
|
|
678
|
+
function normalizeTimeout(timeoutMs) {
|
|
679
|
+
if (timeoutMs == null) {
|
|
680
|
+
return 30_000;
|
|
681
|
+
}
|
|
682
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
683
|
+
throw new Error("timeoutMs must be a positive finite number");
|
|
684
|
+
}
|
|
685
|
+
return timeoutMs;
|
|
686
|
+
}
|
|
687
|
+
function normalizeReloadMax(maxReloads) {
|
|
688
|
+
if (!Number.isInteger(maxReloads) || maxReloads < 0) {
|
|
689
|
+
throw new Error("maxReloads must be a non-negative integer");
|
|
690
|
+
}
|
|
691
|
+
return maxReloads;
|
|
692
|
+
}
|
|
693
|
+
function normalizeReloadWindow(windowMs) {
|
|
694
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
695
|
+
throw new Error("windowMs must be a positive finite number");
|
|
696
|
+
}
|
|
697
|
+
return windowMs;
|
|
698
|
+
}
|
|
699
|
+
function normalizeNowMs(nowMs) {
|
|
700
|
+
if (!Number.isFinite(nowMs)) {
|
|
701
|
+
throw new Error("nowMs must be a finite number");
|
|
702
|
+
}
|
|
703
|
+
return nowMs;
|
|
704
|
+
}
|
|
705
|
+
function defaultFetch() {
|
|
706
|
+
const fetchLike = globalThis.fetch;
|
|
707
|
+
if (!fetchLike) {
|
|
708
|
+
throw new Error("fetch is required when globalThis.fetch is unavailable");
|
|
709
|
+
}
|
|
710
|
+
return fetchLike.bind(globalThis);
|
|
711
|
+
}
|
|
712
|
+
function defaultStreamFetch() {
|
|
713
|
+
const fetchLike = globalThis.fetch;
|
|
714
|
+
if (!fetchLike) {
|
|
715
|
+
throw new Error("fetch is required when globalThis.fetch is unavailable");
|
|
716
|
+
}
|
|
717
|
+
return fetchLike.bind(globalThis);
|
|
718
|
+
}
|
|
719
|
+
function randomBridgeChannelID() {
|
|
720
|
+
const cryptoLike = globalThis;
|
|
721
|
+
if (cryptoLike.crypto?.randomUUID) {
|
|
722
|
+
return `bridge_${cryptoLike.crypto.randomUUID()}`;
|
|
723
|
+
}
|
|
724
|
+
return `bridge_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
725
|
+
}
|
|
726
|
+
function trimTrailingSlash(value) {
|
|
727
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
728
|
+
}
|
|
729
|
+
function queryString(values) {
|
|
730
|
+
const params = new URLSearchParams();
|
|
731
|
+
for (const [key, value] of Object.entries(values)) {
|
|
732
|
+
if (value != null && value !== "") {
|
|
733
|
+
params.set(key, String(value));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const query = params.toString();
|
|
737
|
+
return query ? `?${query}` : "";
|
|
738
|
+
}
|
|
739
|
+
function pluginInstanceRequest(value) {
|
|
740
|
+
return typeof value === "string" ? { plugin_instance_id: value } : value;
|
|
741
|
+
}
|
|
742
|
+
function isBridgeHandshake(value) {
|
|
743
|
+
return isRecord(value) &&
|
|
744
|
+
value.type === "redevplugin.bridge.handshake" &&
|
|
745
|
+
typeof value.plugin_id === "string" &&
|
|
746
|
+
typeof value.surface_id === "string" &&
|
|
747
|
+
typeof value.surface_instance_id === "string" &&
|
|
748
|
+
typeof value.active_fingerprint === "string" &&
|
|
749
|
+
typeof value.bridge_nonce === "string" &&
|
|
750
|
+
value.ui_protocol_version === "plugin-ui-v1";
|
|
751
|
+
}
|
|
752
|
+
function isBridgeCallMessage(value) {
|
|
753
|
+
return isRecord(value) &&
|
|
754
|
+
value.type === "redevplugin.bridge.call" &&
|
|
755
|
+
isRecord(value.request) &&
|
|
756
|
+
typeof value.request.id === "string" &&
|
|
757
|
+
typeof value.request.method === "string";
|
|
758
|
+
}
|
|
759
|
+
function isBridgeResponse(value) {
|
|
760
|
+
if (!isRecord(value) || value.type !== "redevplugin.bridge.response" || typeof value.id !== "string") {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
if (value.ok === true) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
return value.ok === false && typeof value.error_code === "string" && typeof value.error === "string";
|
|
767
|
+
}
|
|
768
|
+
function isHostEnvelope(value) {
|
|
769
|
+
if (!isRecord(value) || typeof value.ok !== "boolean") {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (value.ok) {
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
return (value.error == null || typeof value.error === "string") &&
|
|
776
|
+
(value.error_details == null || isRecord(value.error_details));
|
|
777
|
+
}
|
|
778
|
+
async function readHostEnvelope(response, fallbackCode) {
|
|
779
|
+
const raw = await response.json();
|
|
780
|
+
if (!isHostEnvelope(raw)) {
|
|
781
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", `Plugin platform endpoint returned an invalid envelope with HTTP ${response.status}`);
|
|
782
|
+
}
|
|
783
|
+
if (!raw.ok) {
|
|
784
|
+
const errorCode = raw.error_code ?? fallbackCode;
|
|
785
|
+
const message = raw.error ?? `Plugin platform endpoint failed with HTTP ${response.status}`;
|
|
786
|
+
if (errorCode === "PLUGIN_CONFIRMATION_REQUIRED") {
|
|
787
|
+
throw new PluginConfirmationRequiredError(errorCode, message, raw.data, raw.error_details ?? raw.data);
|
|
788
|
+
}
|
|
789
|
+
throw new PluginBridgeError(errorCode, message, raw.data, raw.error_details ?? raw.data);
|
|
790
|
+
}
|
|
791
|
+
return raw.data;
|
|
792
|
+
}
|
|
793
|
+
function parseNDJSONEvents(raw) {
|
|
794
|
+
const events = [];
|
|
795
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
796
|
+
if (line.trim() === "") {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const event = JSON.parse(line);
|
|
800
|
+
if (!isStreamEvent(event)) {
|
|
801
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin stream endpoint returned an invalid event");
|
|
802
|
+
}
|
|
803
|
+
events.push(event);
|
|
804
|
+
}
|
|
805
|
+
return events;
|
|
806
|
+
}
|
|
807
|
+
function streamErrorFromBody(raw, status) {
|
|
808
|
+
try {
|
|
809
|
+
const body = JSON.parse(raw);
|
|
810
|
+
if (isHostEnvelope(body) && !body.ok) {
|
|
811
|
+
return new PluginBridgeError(body.error_code ?? "PLUGIN_STREAM_FAILED", body.error ?? `Plugin stream endpoint failed with HTTP ${status}`, body.data, body.error_details ?? body.data);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
// Fall back to a generic error when the stream endpoint did not return JSON.
|
|
816
|
+
}
|
|
817
|
+
return new PluginBridgeError("PLUGIN_STREAM_FAILED", `Plugin stream endpoint failed with HTTP ${status}`);
|
|
818
|
+
}
|
|
819
|
+
function isStreamEvent(value) {
|
|
820
|
+
return isRecord(value) &&
|
|
821
|
+
typeof value.stream_id === "string" &&
|
|
822
|
+
typeof value.sequence === "number" &&
|
|
823
|
+
typeof value.kind === "string" &&
|
|
824
|
+
(value.data == null || typeof value.data === "string") &&
|
|
825
|
+
(value.error == null || typeof value.error === "string") &&
|
|
826
|
+
typeof value.at === "string";
|
|
827
|
+
}
|
|
828
|
+
function isLifecycleMessage(value) {
|
|
829
|
+
if (!isRecord(value) || value.type !== "redevplugin.bridge.lifecycle" || !isRecord(value.event)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
return value.event.type === "ready" || value.event.type === "visible" || value.event.type === "hidden" || value.event.type === "dispose";
|
|
833
|
+
}
|
|
834
|
+
function isRecord(value) {
|
|
835
|
+
return typeof value === "object" && value !== null;
|
|
836
|
+
}
|
|
837
|
+
function isPluginRiskFlag(value) {
|
|
838
|
+
return isRecord(value) &&
|
|
839
|
+
typeof value.id === "string" &&
|
|
840
|
+
isPluginRiskSeverity(value.severity) &&
|
|
841
|
+
typeof value.summary === "string" &&
|
|
842
|
+
(value.description == null || typeof value.description === "string") &&
|
|
843
|
+
(value.requires_confirmation == null || typeof value.requires_confirmation === "boolean") &&
|
|
844
|
+
(value.requires_admin == null || typeof value.requires_admin === "boolean") &&
|
|
845
|
+
(value.data_loss_risk == null || typeof value.data_loss_risk === "boolean") &&
|
|
846
|
+
(value.destructive == null || typeof value.destructive === "boolean");
|
|
847
|
+
}
|
|
848
|
+
function isPluginRiskSeverity(value) {
|
|
849
|
+
return value === "info" || value === "low" || value === "medium" || value === "high" || value === "critical";
|
|
850
|
+
}
|
|
851
|
+
function isPluginRiskEffect(value) {
|
|
852
|
+
return value === "read" || value === "write" || value === "execute" || value === "delete" || value === "admin";
|
|
853
|
+
}
|
|
854
|
+
function validRPCParams(value) {
|
|
855
|
+
return value == null || (isRecord(value) && !Array.isArray(value));
|
|
856
|
+
}
|
|
857
|
+
function confirmationDecisionAccepted(decision) {
|
|
858
|
+
return typeof decision === "boolean" ? decision : decision.confirmed;
|
|
859
|
+
}
|
|
860
|
+
function handshakeMatchesBootstrap(handshake, bootstrap) {
|
|
861
|
+
return handshake.plugin_id === bootstrap.pluginId &&
|
|
862
|
+
handshake.surface_id === bootstrap.surfaceId &&
|
|
863
|
+
handshake.surface_instance_id === bootstrap.surfaceInstanceId &&
|
|
864
|
+
handshake.active_fingerprint === bootstrap.activeFingerprint &&
|
|
865
|
+
handshake.bridge_nonce === bootstrap.bridgeNonce &&
|
|
866
|
+
handshake.ui_protocol_version === "plugin-ui-v1";
|
|
867
|
+
}
|
|
868
|
+
function toBridgeError(error, fallbackCode) {
|
|
869
|
+
if (error instanceof PluginBridgeError) {
|
|
870
|
+
return error;
|
|
871
|
+
}
|
|
872
|
+
if (error instanceof Error) {
|
|
873
|
+
return new PluginBridgeError(fallbackCode, error.message);
|
|
874
|
+
}
|
|
875
|
+
return new PluginBridgeError(fallbackCode, String(error));
|
|
876
|
+
}
|