@floegence/redevplugin-ui 3.0.24 → 3.0.26
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/platform.d.ts +2 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/surface.d.ts +15 -0
- package/dist/surface.js +175 -12
- package/dist/trusted-parent.d.ts +2 -2
- package/dist/trusted-parent.js +1 -1
- package/package.json +2 -2
package/dist/platform.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FetchLike } from "./http.js";
|
|
2
2
|
import type { components } from "./openapi.gen.js";
|
|
3
|
-
import { type PluginConfirmationHandler, type PluginSurfaceHost, type PluginSurfaceContext, type PluginSurfaceOpeningProgress, type PluginSurfaceInteractionEvent, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
|
|
3
|
+
import { type PluginConfirmationHandler, type PluginSurfaceHost, type PluginSurfaceContext, type PluginSurfaceOpeningProgress, type PluginSurfaceInteractionEvent, type PluginSurfaceFileExportHandler, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
|
|
4
4
|
import { type PluginSurfaceScope } from "./surface-scope.js";
|
|
5
5
|
export type PluginPlatformClientOptions = {
|
|
6
6
|
fetch?: FetchLike;
|
|
@@ -46,6 +46,7 @@ export type PluginOpenSurfaceInSlotOptions = PluginRequestOptions & {
|
|
|
46
46
|
confirm?: PluginConfirmationHandler;
|
|
47
47
|
onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
|
|
48
48
|
onInteraction?: (event: PluginSurfaceInteractionEvent) => void;
|
|
49
|
+
onFileExport?: PluginSurfaceFileExportHandler;
|
|
49
50
|
onError?: (error: import("./errors.js").PluginBridgeError) => void;
|
|
50
51
|
surfaceContext?: PluginSurfaceContext;
|
|
51
52
|
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { PluginBridgeClient } from "./surface.js";
|
|
|
2
2
|
export { PluginBridgeError } from "./errors.js";
|
|
3
3
|
export { pluginAPIMajor } from "./contracts.gen.js";
|
|
4
4
|
export { callCapabilityOperation, callCapabilityStream, callCapabilitySync, isCapabilityBusinessError, } from "./capability-client.js";
|
|
5
|
-
export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasWheelEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginSurfaceKeyboardBinding, PluginSurfaceKeyboardEvent, PluginSurfaceKeyboardTargetKind, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
|
|
5
|
+
export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasWheelEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginFileExportRequest, PluginSurfaceKeyboardBinding, PluginSurfaceKeyboardEvent, PluginSurfaceKeyboardTargetKind, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginSurfaceFileExportMediaType, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
|
|
6
6
|
export type { PluginCapabilitySchema, PluginCapabilityEffect, PluginCapabilityOperationContract, PluginCapabilityStreamContract, PluginCapabilitySyncContract, PluginCapabilityStreamEvent, PluginCapabilityStreamReadResult, PluginExecution, PluginExecutionTerminalStatus, PluginExecutionWaitOptions, PluginStream, } from "./capability-client.js";
|
package/dist/surface.d.ts
CHANGED
|
@@ -101,6 +101,19 @@ export type PluginCanvasSurface = {
|
|
|
101
101
|
cssHeight: number;
|
|
102
102
|
devicePixelRatio: number;
|
|
103
103
|
};
|
|
104
|
+
export declare const pluginSurfaceFileExportMaxBytes: number;
|
|
105
|
+
export declare const pluginSurfaceFileExportActionWindowMs = 30000;
|
|
106
|
+
export declare const pluginSurfaceFileExportMediaTypes: readonly ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "text/plain"];
|
|
107
|
+
export type PluginSurfaceFileExportMediaType = (typeof pluginSurfaceFileExportMediaTypes)[number];
|
|
108
|
+
export type PluginFileExportRequest = {
|
|
109
|
+
fileName: string;
|
|
110
|
+
mediaType: PluginSurfaceFileExportMediaType;
|
|
111
|
+
bytes: Uint8Array;
|
|
112
|
+
};
|
|
113
|
+
export type PluginSurfaceFileExport = Readonly<PluginFileExportRequest & {
|
|
114
|
+
signal: AbortSignal;
|
|
115
|
+
}>;
|
|
116
|
+
export type PluginSurfaceFileExportHandler = (request: PluginSurfaceFileExport) => Promise<void> | void;
|
|
104
117
|
export type PluginCanvasAccessibilityState = {
|
|
105
118
|
label: string;
|
|
106
119
|
description: string;
|
|
@@ -336,6 +349,7 @@ export declare class PluginBridgeClient {
|
|
|
336
349
|
setKeyboardBindings(bindings: readonly PluginSurfaceKeyboardBinding[]): Promise<void>;
|
|
337
350
|
onKeyboardInput(handler: (event: PluginSurfaceKeyboardEvent) => void): () => void;
|
|
338
351
|
loadImageAsset(assetId: string): Promise<ImageBitmap>;
|
|
352
|
+
exportFile(request: PluginFileExportRequest, options?: PluginBridgeRequestOptions): Promise<void>;
|
|
339
353
|
onAction(action: string, handler: (event: PluginUIActionEvent) => void): () => void;
|
|
340
354
|
onCanvasInput(canvasId: string, handler: (event: PluginCanvasInputEvent) => void): () => void;
|
|
341
355
|
onLifecycle(handler: (event: BridgeLifecycleEvent) => Promise<void> | void): () => void;
|
|
@@ -483,6 +497,7 @@ export type PluginSurfaceHostOptions = {
|
|
|
483
497
|
confirm?: PluginConfirmationHandler;
|
|
484
498
|
onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
|
|
485
499
|
onInteraction?: (event: PluginSurfaceInteractionEvent) => void;
|
|
500
|
+
onFileExport?: PluginSurfaceFileExportHandler;
|
|
486
501
|
onError?: (error: PluginBridgeError) => void;
|
|
487
502
|
surfaceContext?: PluginSurfaceContext;
|
|
488
503
|
};
|
package/dist/surface.js
CHANGED
|
@@ -7,6 +7,15 @@ import { PluginUIReconcileError, reconcileValidatedPluginUITrees, validatePlugin
|
|
|
7
7
|
export const opaqueSurfaceDocumentSchemaVersion = "redevplugin.opaque_surface_document.v3";
|
|
8
8
|
export const pluginRiskPlanSchemaVersion = "redevplugin.capability.risk_plan.v1";
|
|
9
9
|
export const pluginSurfaceContextSchemaVersion = "redevplugin.surface_context.v1";
|
|
10
|
+
export const pluginSurfaceFileExportMaxBytes = 16 * 1024 * 1024;
|
|
11
|
+
export const pluginSurfaceFileExportActionWindowMs = 30_000;
|
|
12
|
+
export const pluginSurfaceFileExportMediaTypes = [
|
|
13
|
+
"image/png",
|
|
14
|
+
"image/jpeg",
|
|
15
|
+
"image/webp",
|
|
16
|
+
"image/svg+xml",
|
|
17
|
+
"text/plain",
|
|
18
|
+
];
|
|
10
19
|
const opaquePluginBridgeGlobalKey = "__redevpluginWorkerBridge";
|
|
11
20
|
const maxPendingPluginBridgeRequests = 256;
|
|
12
21
|
const maxPluginBridgeMessageBytes = opaqueSurfaceRenderLimits.max_message_bytes;
|
|
@@ -238,6 +247,23 @@ export class PluginBridgeClient {
|
|
|
238
247
|
asset_id: assetId,
|
|
239
248
|
}, { kind: "asset", identifier: assetId });
|
|
240
249
|
}
|
|
250
|
+
exportFile(request, options = {}) {
|
|
251
|
+
this.#assertActive();
|
|
252
|
+
if (!validFileExportName(request?.fileName) || !validFileExportMediaType(request?.mediaType) ||
|
|
253
|
+
!(request?.bytes instanceof Uint8Array) || request.bytes.byteLength === 0 ||
|
|
254
|
+
request.bytes.byteLength > pluginSurfaceFileExportMaxBytes) {
|
|
255
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin file export request is invalid");
|
|
256
|
+
}
|
|
257
|
+
const id = this.#requestID("file");
|
|
258
|
+
const content = Uint8Array.from(request.bytes).buffer;
|
|
259
|
+
return this.#dispatchRequest(id, {
|
|
260
|
+
type: "redevplugin.ui.file.export",
|
|
261
|
+
id,
|
|
262
|
+
file_name: request.fileName,
|
|
263
|
+
media_type: request.mediaType,
|
|
264
|
+
content,
|
|
265
|
+
}, { mutation: true, signal: options.signal }, [content]);
|
|
266
|
+
}
|
|
241
267
|
onAction(action, handler) {
|
|
242
268
|
this.#assertActive();
|
|
243
269
|
if (!validActionID(action) || typeof handler !== "function") {
|
|
@@ -377,6 +403,9 @@ export class PluginBridgeClient {
|
|
|
377
403
|
if (new TextEncoder().encode(JSON.stringify(normalizedMessage)).byteLength > maxPluginBridgeMessageBytes) {
|
|
378
404
|
throw new PluginBridgeError("PLUGIN_JSON_LIMIT_EXCEEDED", "Plugin bridge request exceeds the message size limit");
|
|
379
405
|
}
|
|
406
|
+
return this.#dispatchRequest(id, normalizedMessage, options);
|
|
407
|
+
}
|
|
408
|
+
#dispatchRequest(id, message, options, transfer) {
|
|
380
409
|
if (this.#pending.size >= maxPendingPluginBridgeRequests) {
|
|
381
410
|
throw new PluginBridgeError("PLUGIN_JSON_LIMIT_EXCEEDED", "Plugin bridge has too many pending requests");
|
|
382
411
|
}
|
|
@@ -425,7 +454,7 @@ export class PluginBridgeClient {
|
|
|
425
454
|
if (!this.#pending.has(id))
|
|
426
455
|
return result;
|
|
427
456
|
try {
|
|
428
|
-
this.#port.postMessage(
|
|
457
|
+
this.#port.postMessage(message, transfer);
|
|
429
458
|
posted = true;
|
|
430
459
|
}
|
|
431
460
|
catch {
|
|
@@ -789,6 +818,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
789
818
|
const maxImageCount = ${opaqueSurfaceRenderLimits.max_image_count};
|
|
790
819
|
const maxImageDimension = ${opaqueSurfaceRenderLimits.max_image_dimension};
|
|
791
820
|
const maxImageTotalPixels = ${opaqueSurfaceRenderLimits.max_image_total_pixels};
|
|
821
|
+
const maxFileExportBytes = ${pluginSurfaceFileExportMaxBytes};
|
|
822
|
+
const fileExportActionWindowMs = ${pluginSurfaceFileExportActionWindowMs};
|
|
823
|
+
const fileExportMediaTypes = new Set(${JSON.stringify(pluginSurfaceFileExportMediaTypes)});
|
|
792
824
|
const workerHeartbeatIntervalMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_interval_ms};
|
|
793
825
|
const workerHeartbeatTimeoutMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_timeout_ms};
|
|
794
826
|
const rendererCommitFallbackMs = 100;
|
|
@@ -842,6 +874,18 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
842
874
|
const validOpaqueHandle = (value, prefix) => typeof value === "string" && value.startsWith(prefix + "_") && /^[A-Za-z0-9_-]{8,160}$/.test(value);
|
|
843
875
|
const validDigest = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
844
876
|
const validPath = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.startsWith("/") && !value.includes("\\\\") && !value.split("/").some((part) => !part || part === "." || part === "..");
|
|
877
|
+
const validFileExportName = (value) => {
|
|
878
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128 || value.trim() !== value || value === "." || value === ".." || new TextEncoder().encode(value).byteLength > 255) return false;
|
|
879
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
880
|
+
const code = value.charCodeAt(index);
|
|
881
|
+
if (code < 32 || code === 47 || code === 92 || code === 127) return false;
|
|
882
|
+
}
|
|
883
|
+
return true;
|
|
884
|
+
};
|
|
885
|
+
const validWorkerFileExport = (value) => exactKeys(value, ["type", "id", "file_name", "media_type", "content"]) &&
|
|
886
|
+
value.type === "redevplugin.ui.file.export" && typeof value.id === "string" && validFileExportName(value.file_name) &&
|
|
887
|
+
fileExportMediaTypes.has(value.media_type) && value.content instanceof ArrayBuffer &&
|
|
888
|
+
value.content.byteLength > 0 && value.content.byteLength <= maxFileExportBytes;
|
|
845
889
|
const validAttribute = (tag, name, value) => {
|
|
846
890
|
const lower = name.toLowerCase();
|
|
847
891
|
if (lower.startsWith("on") || lower === "style" || lower === "src" || lower === "srcset" || lower === "href" || lower === "srcdoc" || lower === "action" || lower === "formaction") return false;
|
|
@@ -904,10 +948,11 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
904
948
|
let workerHeartbeatTimer;
|
|
905
949
|
let workerHeartbeatTimeout;
|
|
906
950
|
let pendingQuiesceID;
|
|
951
|
+
let pendingFileExportAction;
|
|
907
952
|
let runtimeDisposed = false;
|
|
908
953
|
const pendingWorkerRequests = new Set();
|
|
909
954
|
const pendingRendererCommits = new Set();
|
|
910
|
-
const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0, keyboard: 0 };
|
|
955
|
+
const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0, keyboard: 0, file: 0 };
|
|
911
956
|
let executionQuerySurfaceTokens = 8;
|
|
912
957
|
let executionQuerySurfaceRefillAt = performance.now();
|
|
913
958
|
const executionQueryStateIdleMS = 120000;
|
|
@@ -991,6 +1036,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
991
1036
|
const sendParent = (message) => {
|
|
992
1037
|
if (parentPort && withinLimit(message)) parentPort.postMessage(message);
|
|
993
1038
|
};
|
|
1039
|
+
const sendParentTransfer = (message, transfer) => {
|
|
1040
|
+
if (parentPort) parentPort.postMessage(message, transfer);
|
|
1041
|
+
};
|
|
994
1042
|
let interactionSequence = 0;
|
|
995
1043
|
const interactionTargetKey = (target) => {
|
|
996
1044
|
const element = target instanceof Element ? target.closest("[data-redevplugin-key]") : null;
|
|
@@ -1011,7 +1059,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1011
1059
|
return false;
|
|
1012
1060
|
};
|
|
1013
1061
|
const sendInteraction = (kind, target, options = {}) => {
|
|
1014
|
-
if (!initialized || pendingQuiesceID) return;
|
|
1062
|
+
if (!initialized || pendingQuiesceID) return 0;
|
|
1015
1063
|
interactionSequence += 1;
|
|
1016
1064
|
sendParent({
|
|
1017
1065
|
type: "redevplugin.surface.interaction",
|
|
@@ -1024,6 +1072,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1024
1072
|
local_scroll: options.localScroll === true,
|
|
1025
1073
|
selection_active: options.selectionActive === true,
|
|
1026
1074
|
});
|
|
1075
|
+
return interactionSequence;
|
|
1027
1076
|
};
|
|
1028
1077
|
document.addEventListener("pointerdown", (event) => sendInteraction("activation", event.target), true);
|
|
1029
1078
|
document.addEventListener("focusin", (event) => sendInteraction("focus", event.target), true);
|
|
@@ -1659,7 +1708,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1659
1708
|
edit_revision: edit.revision,
|
|
1660
1709
|
is_composing: edit.isComposing,
|
|
1661
1710
|
};
|
|
1662
|
-
const eventTarget = event.target;
|
|
1711
|
+
const eventTarget = eventType === "click" && element.tagName === "BUTTON" ? element : event.target;
|
|
1663
1712
|
if (eventTarget && typeof eventTarget.value === "string") payload.value = eventTarget.value.slice(0, maxTextLength);
|
|
1664
1713
|
if (eventTarget && typeof eventTarget.checked === "boolean") payload.checked = eventTarget.checked;
|
|
1665
1714
|
if (eventType === "submit" && element.tagName === "FORM") {
|
|
@@ -1701,6 +1750,8 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1701
1750
|
const buttonType = submitButton && element.contains(submitButton) ? (submitButton.getAttribute("type") || "submit").toLowerCase() : "";
|
|
1702
1751
|
if (event.type === "click" && buttonType === "submit") {
|
|
1703
1752
|
event.preventDefault();
|
|
1753
|
+
const sequence = sendInteraction("action", event.target, { action: element.getAttribute("data-redevplugin-action") });
|
|
1754
|
+
if (sequence > 0) pendingFileExportAction = { sequence, expires_at: performance.now() + fileExportActionWindowMs };
|
|
1704
1755
|
sendWorker(actionPayload(event, element, "submit"));
|
|
1705
1756
|
}
|
|
1706
1757
|
return;
|
|
@@ -1708,7 +1759,10 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1708
1759
|
if (event.type === "submit" || (event.type === "click" && element.tagName === "BUTTON")) event.preventDefault();
|
|
1709
1760
|
const action = element.getAttribute("data-redevplugin-action");
|
|
1710
1761
|
if (!validIdentifier(action)) return;
|
|
1711
|
-
sendInteraction("action", event.target, { action });
|
|
1762
|
+
const sequence = sendInteraction("action", event.target, { action });
|
|
1763
|
+
if (sequence > 0 && (event.type === "click" || event.type === "submit")) {
|
|
1764
|
+
pendingFileExportAction = { sequence, expires_at: performance.now() + fileExportActionWindowMs };
|
|
1765
|
+
}
|
|
1712
1766
|
sendWorker(actionPayload(event, element));
|
|
1713
1767
|
};
|
|
1714
1768
|
for (const eventType of ["click", "input", "change", "submit"]) document.addEventListener(eventType, handleAction, true);
|
|
@@ -1994,7 +2048,8 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1994
2048
|
' const widthDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "width");',
|
|
1995
2049
|
' const heightDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "height");',
|
|
1996
2050
|
' const contextDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "getContext");',
|
|
1997
|
-
'
|
|
2051
|
+
' const convertDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "convertToBlob");',
|
|
2052
|
+
' if (!widthDescriptor || !heightDescriptor || typeof widthDescriptor.get !== "function" || typeof widthDescriptor.set !== "function" || typeof heightDescriptor.get !== "function" || typeof heightDescriptor.set !== "function" || !contextDescriptor || typeof contextDescriptor.value !== "function" || !convertDescriptor || typeof convertDescriptor.value !== "function") throw new TypeError("OffscreenCanvas descriptors are unavailable");',
|
|
1998
2053
|
' const readWidth = (canvas) => __rpApply(widthDescriptor.get, canvas, []);',
|
|
1999
2054
|
' const readHeight = (canvas) => __rpApply(heightDescriptor.get, canvas, []);',
|
|
2000
2055
|
' const validateResize = (canvas, axis, value) => {',
|
|
@@ -2013,7 +2068,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2013
2068
|
' __rpDefineProperty(__rpCanvasPrototype, "height", { configurable: false, enumerable: Boolean(heightDescriptor.enumerable), get() { return readHeight(this); }, set(value) { validateResize(this, "height", value); __rpApply(heightDescriptor.set, this, [value]); } });',
|
|
2014
2069
|
' __rpDefineProperty(__rpCanvasPrototype, "getContext", { configurable: false, enumerable: Boolean(contextDescriptor.enumerable), writable: false, value(type, ...options) { if (type !== "2d") throw new TypeError("only 2d canvas contexts are available"); __rpTrackCanvas(this); return __rpApply(contextDescriptor.value, this, [type, ...options]); } });',
|
|
2015
2070
|
' __rpSealDescriptor(__rpCanvasPrototype, "constructor", undefined);',
|
|
2016
|
-
' __rpSealDescriptor(__rpCanvasPrototype, "convertToBlob",
|
|
2071
|
+
' __rpSealDescriptor(__rpCanvasPrototype, "convertToBlob", convertDescriptor.value);',
|
|
2017
2072
|
' __rpSealDescriptor(__rpCanvasPrototype, "transferToImageBitmap", __rpBlocked);',
|
|
2018
2073
|
'};',
|
|
2019
2074
|
'try {',
|
|
@@ -2080,7 +2135,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2080
2135
|
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));
|
|
2081
2136
|
const requestID = (value, expectedKind) => {
|
|
2082
2137
|
if (typeof value !== "string") return undefined;
|
|
2083
|
-
const match = /^(rpc|execution|render|canvas|asset|keyboard)_([1-9][0-9]{0,15})$/.exec(value);
|
|
2138
|
+
const match = /^(rpc|execution|render|canvas|asset|keyboard|file)_([1-9][0-9]{0,15})$/.exec(value);
|
|
2084
2139
|
if (!match || match[1] !== expectedKind) return undefined;
|
|
2085
2140
|
const sequence = Number(match[2]);
|
|
2086
2141
|
return Number.isSafeInteger(sequence) ? { kind: match[1], sequence } : undefined;
|
|
@@ -2162,7 +2217,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2162
2217
|
renderCount += 1;
|
|
2163
2218
|
return true;
|
|
2164
2219
|
};
|
|
2165
|
-
const rejectWorkerRequest = (id, error) => sendWorker({ type: "redevplugin.bridge.response", id, ok: false, error_code:
|
|
2220
|
+
const rejectWorkerRequest = (id, error, errorCode = "PLUGIN_INVALID_REQUEST") => sendWorker({ type: "redevplugin.bridge.response", id, ok: false, error_code: errorCode, error });
|
|
2166
2221
|
const findCanvas = (canvasID) => {
|
|
2167
2222
|
for (const element of document.querySelectorAll("canvas[data-redevplugin-canvas]")) {
|
|
2168
2223
|
if (element.getAttribute("data-redevplugin-canvas") === canvasID) return element;
|
|
@@ -2372,8 +2427,30 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
2372
2427
|
}
|
|
2373
2428
|
};
|
|
2374
2429
|
const onWorkerMessage = (event) => {
|
|
2375
|
-
if (!withinLimit(event.data)) return;
|
|
2376
2430
|
const message = event.data;
|
|
2431
|
+
if (isRecord(message) && message.type === "redevplugin.ui.file.export" && typeof message.id === "string") {
|
|
2432
|
+
if (!validWorkerFileExport(message)) return rejectWorkerRequest(message.id, "plugin file export request is invalid");
|
|
2433
|
+
if (!acceptWorkerRequest(message.id, "file")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2434
|
+
const action = pendingFileExportAction;
|
|
2435
|
+
if (!action || performance.now() > action.expires_at) {
|
|
2436
|
+
completeWorkerRequest(message.id);
|
|
2437
|
+
pendingFileExportAction = undefined;
|
|
2438
|
+
return rejectWorkerRequest(message.id, "plugin file export requires a recent user action", "PLUGIN_ACTION_DENIED");
|
|
2439
|
+
}
|
|
2440
|
+
pendingFileExportAction = undefined;
|
|
2441
|
+
sendParentTransfer({
|
|
2442
|
+
type: "redevplugin.surface.file.export",
|
|
2443
|
+
frame_generation_id: frameGenerationID,
|
|
2444
|
+
surface_handle: surfaceHandle,
|
|
2445
|
+
action_sequence: action.sequence,
|
|
2446
|
+
id: message.id,
|
|
2447
|
+
file_name: message.file_name,
|
|
2448
|
+
media_type: message.media_type,
|
|
2449
|
+
content: message.content,
|
|
2450
|
+
}, [message.content]);
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
if (!withinLimit(message)) return;
|
|
2377
2454
|
if (exactKeys(message, ["type", "quiesce_id"]) && message.type === "redevplugin.bridge.lifecycle_ack" && validOpaqueHandle(message.quiesce_id, "quiesce") && message.quiesce_id === pendingQuiesceID) {
|
|
2378
2455
|
pendingQuiesceID = undefined;
|
|
2379
2456
|
sendParent({ type: "redevplugin.surface.quiesce_ack", quiesce_id: message.quiesce_id });
|
|
@@ -2567,8 +2644,10 @@ class PluginSurfaceHostImplementation {
|
|
|
2567
2644
|
#confirm;
|
|
2568
2645
|
#onOpeningProgress;
|
|
2569
2646
|
#onInteraction;
|
|
2647
|
+
#onFileExport;
|
|
2570
2648
|
#onError;
|
|
2571
2649
|
#lastInteractionSequence = 0;
|
|
2650
|
+
#pendingFileExportAction;
|
|
2572
2651
|
#interactionWindowStartedAt = 0;
|
|
2573
2652
|
#interactionWindowCount = 0;
|
|
2574
2653
|
#abortController = new AbortController();
|
|
@@ -2631,6 +2710,7 @@ class PluginSurfaceHostImplementation {
|
|
|
2631
2710
|
this.#confirm = options.confirm;
|
|
2632
2711
|
this.#onOpeningProgress = options.onOpeningProgress;
|
|
2633
2712
|
this.#onInteraction = options.onInteraction;
|
|
2713
|
+
this.#onFileExport = options.onFileExport;
|
|
2634
2714
|
this.#onError = options.onError;
|
|
2635
2715
|
this.#surfaceContext = options.surfaceContext === undefined ? undefined : normalizePluginSurfaceContext(options.surfaceContext);
|
|
2636
2716
|
hardenPluginSurfaceFrame(this.#iframe);
|
|
@@ -2835,6 +2915,7 @@ class PluginSurfaceHostImplementation {
|
|
|
2835
2915
|
this.#assetSessionID = undefined;
|
|
2836
2916
|
this.#document = undefined;
|
|
2837
2917
|
this.#assets.clear();
|
|
2918
|
+
this.#pendingFileExportAction = undefined;
|
|
2838
2919
|
if (this.#port) {
|
|
2839
2920
|
try {
|
|
2840
2921
|
this.#port.postMessage({ type: "redevplugin.bridge.lifecycle", event: { type: "dispose" } });
|
|
@@ -2859,10 +2940,16 @@ class PluginSurfaceHostImplementation {
|
|
|
2859
2940
|
}
|
|
2860
2941
|
}
|
|
2861
2942
|
async #handlePortMessage(event) {
|
|
2862
|
-
if (this.#disposed
|
|
2943
|
+
if (this.#disposed)
|
|
2863
2944
|
return;
|
|
2864
2945
|
const data = event.data;
|
|
2865
2946
|
try {
|
|
2947
|
+
if (isSurfaceFileExportCandidate(data)) {
|
|
2948
|
+
await this.#handleFileExport(data);
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
if (!messageWithinLimit(data))
|
|
2952
|
+
return;
|
|
2866
2953
|
if (hasExactKeys(data, ["type"]) && data.type === "redevplugin.surface.first_paint") {
|
|
2867
2954
|
this.#openSignals?.firstPaint.resolve();
|
|
2868
2955
|
return;
|
|
@@ -2928,6 +3015,12 @@ class PluginSurfaceHostImplementation {
|
|
|
2928
3015
|
return;
|
|
2929
3016
|
this.#lastInteractionSequence = message.sequence;
|
|
2930
3017
|
const now = performance.now();
|
|
3018
|
+
if (message.kind === "action") {
|
|
3019
|
+
this.#pendingFileExportAction = {
|
|
3020
|
+
sequence: message.sequence,
|
|
3021
|
+
expiresAt: now + pluginSurfaceFileExportActionWindowMs,
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
2931
3024
|
if (now - this.#interactionWindowStartedAt >= 1000) {
|
|
2932
3025
|
this.#interactionWindowStartedAt = now;
|
|
2933
3026
|
this.#interactionWindowCount = 0;
|
|
@@ -2950,6 +3043,50 @@ class PluginSurfaceHostImplementation {
|
|
|
2950
3043
|
// Interaction observers cannot alter the surface lifecycle.
|
|
2951
3044
|
}
|
|
2952
3045
|
}
|
|
3046
|
+
async #handleFileExport(message) {
|
|
3047
|
+
if (!validBridgeRequestID(message.id, "file"))
|
|
3048
|
+
return;
|
|
3049
|
+
if (!isSurfaceFileExportMessage(message)) {
|
|
3050
|
+
this.#postError(message.id, "PLUGIN_INVALID_REQUEST", "Plugin file export request is invalid");
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
if (!this.#bridgeReady || !this.#ready) {
|
|
3054
|
+
this.#postError(message.id, "PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Plugin file export arrived before the surface became ready");
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (message.frame_generation_id !== this.frameGenerationId || message.surface_handle !== this.surfaceHandle)
|
|
3058
|
+
return;
|
|
3059
|
+
const action = this.#pendingFileExportAction;
|
|
3060
|
+
if (!action || action.sequence !== message.action_sequence || performance.now() > action.expiresAt) {
|
|
3061
|
+
this.#postError(message.id, "PLUGIN_ACTION_DENIED", "Plugin file export requires a recent user action");
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
this.#pendingFileExportAction = undefined;
|
|
3065
|
+
if (!this.#onFileExport) {
|
|
3066
|
+
this.#postError(message.id, "PLUGIN_FEATURE_NOT_CONFIGURED", "Plugin file export is not configured by the host");
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
const controller = this.#registerPendingRequest(message.id);
|
|
3070
|
+
try {
|
|
3071
|
+
await this.#onFileExport({
|
|
3072
|
+
fileName: message.file_name,
|
|
3073
|
+
mediaType: message.media_type,
|
|
3074
|
+
bytes: new Uint8Array(message.content),
|
|
3075
|
+
signal: controller.signal,
|
|
3076
|
+
});
|
|
3077
|
+
if (!controller.signal.aborted && !this.#disposed)
|
|
3078
|
+
this.#postResponse(message.id);
|
|
3079
|
+
}
|
|
3080
|
+
catch (error) {
|
|
3081
|
+
if (controller.signal.aborted || this.#disposed)
|
|
3082
|
+
return;
|
|
3083
|
+
const bridgeError = toBridgeError(error, "PLUGIN_ADAPTER_FAILURE");
|
|
3084
|
+
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3085
|
+
}
|
|
3086
|
+
finally {
|
|
3087
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2953
3090
|
async #handleCall(request) {
|
|
2954
3091
|
if ((!this.#bridgeReady && !this.#quiesce) || !this.#gatewayToken) {
|
|
2955
3092
|
this.#postError(request.id, "PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Plugin bridge call arrived before the surface became ready");
|
|
@@ -3878,6 +4015,18 @@ function setSurfacePresentation(element, state) {
|
|
|
3878
4015
|
element.style.opacity = state === "opening" ? "0" : "";
|
|
3879
4016
|
element.style.pointerEvents = ready ? "" : "none";
|
|
3880
4017
|
}
|
|
4018
|
+
function isSurfaceFileExportCandidate(value) {
|
|
4019
|
+
return isRecord(value) && value.type === "redevplugin.surface.file.export" && typeof value.id === "string";
|
|
4020
|
+
}
|
|
4021
|
+
function isSurfaceFileExportMessage(value) {
|
|
4022
|
+
return hasExactKeys(value, [
|
|
4023
|
+
"type", "frame_generation_id", "surface_handle", "action_sequence", "id", "file_name", "media_type", "content",
|
|
4024
|
+
]) && validOpaqueHandle(value.frame_generation_id, "frame") && validOpaqueHandle(value.surface_handle, "surface") &&
|
|
4025
|
+
Number.isSafeInteger(value.action_sequence) && Number(value.action_sequence) > 0 &&
|
|
4026
|
+
validBridgeRequestID(value.id, "file") && validFileExportName(value.file_name) &&
|
|
4027
|
+
validFileExportMediaType(value.media_type) && value.content instanceof ArrayBuffer &&
|
|
4028
|
+
value.content.byteLength > 0 && value.content.byteLength <= pluginSurfaceFileExportMaxBytes;
|
|
4029
|
+
}
|
|
3881
4030
|
function isSurfaceInteractionMessage(value) {
|
|
3882
4031
|
if (!hasExactKeys(value, [
|
|
3883
4032
|
"type", "frame_generation_id", "surface_handle", "sequence", "kind", "target_key", "action", "local_scroll", "selection_active",
|
|
@@ -4553,7 +4702,7 @@ const pluginMethodPattern = new RegExp("^[-A-Za-z0-9._:]{1,256}$");
|
|
|
4553
4702
|
const pluginActionPattern = new RegExp("^[-A-Za-z0-9._:]{1,128}$");
|
|
4554
4703
|
const pluginUIIdentifierPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$");
|
|
4555
4704
|
const opaqueHandlePattern = new RegExp("^[-A-Za-z0-9_]{8,160}$");
|
|
4556
|
-
const bridgeRequestIDPattern = /^(rpc|execution|render|canvas|asset|keyboard)_([1-9][0-9]{0,15})$/;
|
|
4705
|
+
const bridgeRequestIDPattern = /^(rpc|execution|render|canvas|asset|keyboard|file)_([1-9][0-9]{0,15})$/;
|
|
4557
4706
|
function validBridgeRequestID(value, expectedKind) {
|
|
4558
4707
|
if (typeof value !== "string")
|
|
4559
4708
|
return false;
|
|
@@ -4599,6 +4748,20 @@ function validActionID(value) {
|
|
|
4599
4748
|
function validUIIdentifier(value) {
|
|
4600
4749
|
return typeof value === "string" && pluginUIIdentifierPattern.test(value);
|
|
4601
4750
|
}
|
|
4751
|
+
function validFileExportName(value) {
|
|
4752
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128 || value.trim() !== value ||
|
|
4753
|
+
value === "." || value === ".." || new TextEncoder().encode(value).byteLength > 255)
|
|
4754
|
+
return false;
|
|
4755
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
4756
|
+
const code = value.charCodeAt(index);
|
|
4757
|
+
if (code < 32 || code === 47 || code === 92 || code === 127)
|
|
4758
|
+
return false;
|
|
4759
|
+
}
|
|
4760
|
+
return true;
|
|
4761
|
+
}
|
|
4762
|
+
function validFileExportMediaType(value) {
|
|
4763
|
+
return pluginSurfaceFileExportMediaTypes.includes(value);
|
|
4764
|
+
}
|
|
4602
4765
|
function validOpaqueHandle(value, prefix) {
|
|
4603
4766
|
return typeof value === "string" && value.startsWith(`${prefix}_`) && opaqueHandlePattern.test(value);
|
|
4604
4767
|
}
|
package/dist/trusted-parent.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export * from "./contracts.gen.js";
|
|
|
2
2
|
export * from "./errors.js";
|
|
3
3
|
export * from "./platform.js";
|
|
4
4
|
export { PluginSurfaceScope, createPluginSurfaceScope } from "./surface-scope.js";
|
|
5
|
-
export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, opaqueSurfaceDocumentSchemaVersion, pluginSurfaceContextSchemaVersion, pluginRiskPlanSchemaVersion, trustedParentBridgeHandshakeTranscriptSHA256, } from "./surface.js";
|
|
6
|
-
export type { BridgeLifecycleEvent, MessageChannelLike, MessageEventLike, MessagePortLike, OpaqueSurfaceAsset, OpaqueSurfaceDocument, OpaqueSurfaceStyle, OpaqueSurfaceWorker, PluginConfirmationDecision, PluginConfirmationHandler, PluginConfirmationIntent, PluginConfirmationPlan, PluginMethodResult, PluginRiskEffect, PluginRiskFlag, PluginRiskPlan, PluginRiskSeverity, PluginExecutionEvent, PluginSurfaceHost, PluginSurfaceCloseResult, PluginSurfaceOpeningProgress, PluginSurfaceInteractionEvent, PluginSurfaceInteractionKind, PluginSurfaceQuiesceResult, PluginSurfaceRevocationReconciliation, PluginSurfacePreparationResult, PluginSurfaceContext, PluginSurfaceReloadDecision, PluginSurfaceReloadLimiterOptions, PluginSurfaceReloadState, PluginSurfaceSlotOptions, PluginSurfaceSlotState, ReDevPluginSurfaceTransport, ReDevPluginSurfaceTransportOptions, TrustedParentBridgeHandshake, TrustedParentBridgeTokenRequest, WindowLike, } from "./surface.js";
|
|
5
|
+
export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, opaqueSurfaceDocumentSchemaVersion, pluginSurfaceContextSchemaVersion, pluginRiskPlanSchemaVersion, pluginSurfaceFileExportActionWindowMs, pluginSurfaceFileExportMaxBytes, pluginSurfaceFileExportMediaTypes, trustedParentBridgeHandshakeTranscriptSHA256, } from "./surface.js";
|
|
6
|
+
export type { BridgeLifecycleEvent, MessageChannelLike, MessageEventLike, MessagePortLike, OpaqueSurfaceAsset, OpaqueSurfaceDocument, OpaqueSurfaceStyle, OpaqueSurfaceWorker, PluginConfirmationDecision, PluginConfirmationHandler, PluginConfirmationIntent, PluginConfirmationPlan, PluginMethodResult, PluginRiskEffect, PluginRiskFlag, PluginRiskPlan, PluginRiskSeverity, PluginExecutionEvent, PluginSurfaceHost, PluginSurfaceCloseResult, PluginSurfaceOpeningProgress, PluginSurfaceInteractionEvent, PluginSurfaceInteractionKind, PluginSurfaceFileExport, PluginSurfaceFileExportHandler, PluginSurfaceFileExportMediaType, PluginSurfaceQuiesceResult, PluginSurfaceRevocationReconciliation, PluginSurfacePreparationResult, PluginSurfaceContext, PluginSurfaceReloadDecision, PluginSurfaceReloadLimiterOptions, PluginSurfaceReloadState, PluginSurfaceSlotOptions, PluginSurfaceSlotState, ReDevPluginSurfaceTransport, ReDevPluginSurfaceTransportOptions, TrustedParentBridgeHandshake, TrustedParentBridgeTokenRequest, WindowLike, } from "./surface.js";
|
|
7
7
|
export type { FetchInitLike, FetchLike, FetchResponseLike, PlatformResponse } from "./http.js";
|
package/dist/trusted-parent.js
CHANGED
|
@@ -2,4 +2,4 @@ export * from "./contracts.gen.js";
|
|
|
2
2
|
export * from "./errors.js";
|
|
3
3
|
export * from "./platform.js";
|
|
4
4
|
export { PluginSurfaceScope, createPluginSurfaceScope } from "./surface-scope.js";
|
|
5
|
-
export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, opaqueSurfaceDocumentSchemaVersion, pluginSurfaceContextSchemaVersion, pluginRiskPlanSchemaVersion, trustedParentBridgeHandshakeTranscriptSHA256, } from "./surface.js";
|
|
5
|
+
export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, opaqueSurfaceDocumentSchemaVersion, pluginSurfaceContextSchemaVersion, pluginRiskPlanSchemaVersion, pluginSurfaceFileExportActionWindowMs, pluginSurfaceFileExportMaxBytes, pluginSurfaceFileExportMediaTypes, trustedParentBridgeHandshakeTranscriptSHA256, } from "./surface.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/redevplugin-ui",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dist"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@floegence/redevplugin-contracts": "3.0.
|
|
43
|
+
"@floegence/redevplugin-contracts": "3.0.26"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|