@floegence/redevplugin-ui 0.6.7 → 0.6.10
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/contracts.gen.d.ts +4 -4
- package/dist/contracts.gen.js +4 -4
- package/dist/openapi.gen.d.ts +604 -1
- package/dist/platform.d.ts +15 -1
- package/dist/platform.js +106 -0
- package/dist/surface.d.ts +17 -0
- package/dist/surface.js +164 -24
- package/dist/trusted-parent.d.ts +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 PluginSurfaceOpeningProgress, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginTrustedMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
|
|
3
|
+
import { type PluginConfirmationHandler, type PluginSurfaceHost, type PluginSurfaceOpeningProgress, type PluginSurfaceInteractionEvent, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginTrustedMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
|
|
4
4
|
import { type PluginSurfaceScope } from "./surface-scope.js";
|
|
5
5
|
export type PluginPlatformClientOptions = {
|
|
6
6
|
fetch?: FetchLike;
|
|
@@ -31,6 +31,12 @@ export type PluginPackageHashSet = PlatformSchemas["PackageHashSet"];
|
|
|
31
31
|
export type PluginReleaseRef = PlatformSchemas["PluginReleaseRef"];
|
|
32
32
|
export type PluginInstallReleaseRefRequest = PlatformSchemas["InstallReleaseRefRequest"];
|
|
33
33
|
export type PluginUpdateReleaseRefRequest = PlatformSchemas["UpdateReleaseRefRequest"];
|
|
34
|
+
export type PluginInspectExternalPackageRequest = PlatformSchemas["InspectExternalPackageRequest"];
|
|
35
|
+
export type PluginUploadedExternalPackageIntent = PlatformSchemas["ExternalPackageIntentRequest"];
|
|
36
|
+
export type PluginExternalPackageInspection = PlatformSchemas["ExternalPackageInspection"];
|
|
37
|
+
export type PluginCommitExternalPackageRequest = PlatformSchemas["CommitExternalPackageRequest"];
|
|
38
|
+
export type PluginQueryExternalPackageCommitRequest = PlatformSchemas["QueryExternalPackageCommitRequest"];
|
|
39
|
+
export type PluginExternalPackageCommitResult = PlatformSchemas["ExternalPackageCommitResult"];
|
|
34
40
|
export type PluginDowngradeRequest = PlatformSchemas["DowngradeRequest"];
|
|
35
41
|
export type PluginEnableRequest = PlatformSchemas["EnableRequest"];
|
|
36
42
|
export type PluginDisableRequest = PlatformSchemas["DisableRequest"];
|
|
@@ -44,6 +50,7 @@ export type PluginOpenSurfaceInSlotOptions = PluginRequestOptions & {
|
|
|
44
50
|
reloadLimiter?: PluginSurfaceReloadLimiter;
|
|
45
51
|
confirm?: PluginConfirmationHandler;
|
|
46
52
|
onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
|
|
53
|
+
onInteraction?: (event: PluginSurfaceInteractionEvent) => void;
|
|
47
54
|
onError?: (error: import("./errors.js").PluginBridgeError) => void;
|
|
48
55
|
};
|
|
49
56
|
export type PluginSessionScopeRevokeCounts = Readonly<PlatformSchemas["SessionScopeRevokeCounts"]>;
|
|
@@ -83,6 +90,8 @@ export type PluginPermissionGrant = PlatformSchemas["PluginPermissionGrant"];
|
|
|
83
90
|
export type PluginPermissionMutationResult = PlatformSchemas["PluginPermissionMutationResult"];
|
|
84
91
|
export type PluginPermissionList = PlatformSchemas["PluginPermissionList"];
|
|
85
92
|
export type PluginPermissionListOptions = PlatformSchemas["ListPermissionsQueryRequest"];
|
|
93
|
+
export type PluginPermissionRequirements = PlatformSchemas["PluginPermissionRequirements"];
|
|
94
|
+
export type PluginPermissionRequirementsRequest = PlatformSchemas["PermissionRequirementsQueryRequest"];
|
|
86
95
|
export type PluginPermissionGrantRequest = PlatformSchemas["GrantPermissionRequest"];
|
|
87
96
|
export type PluginPermissionRevokeRequest = PlatformSchemas["RevokePermissionRequest"];
|
|
88
97
|
export type PluginSecurityPolicy = PlatformSchemas["PluginSecurityPolicy"];
|
|
@@ -119,6 +128,10 @@ export declare class PluginPlatformClient {
|
|
|
119
128
|
features(options?: PluginRequestOptions): Promise<PluginFeatures>;
|
|
120
129
|
getCompatibility(options?: PluginRequestOptions): Promise<PluginCompatibilityManifest>;
|
|
121
130
|
installReleaseRef(request: PluginInstallReleaseRefRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
|
|
131
|
+
inspectExternalPackage(request: PluginInspectExternalPackageRequest, options?: PluginRequestOptions): Promise<PluginExternalPackageInspection>;
|
|
132
|
+
inspectUploadedExternalPackage(intent: PluginUploadedExternalPackageIntent, packageBlob: Blob, options?: PluginRequestOptions): Promise<PluginExternalPackageInspection>;
|
|
133
|
+
commitExternalPackage(request: PluginCommitExternalPackageRequest, options?: PluginRequestOptions): Promise<PluginExternalPackageCommitResult>;
|
|
134
|
+
queryExternalPackageCommit(request: PluginQueryExternalPackageCommitRequest, options?: PluginRequestOptions): Promise<PluginExternalPackageCommitResult>;
|
|
122
135
|
updateReleaseRef(request: PluginUpdateReleaseRefRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
|
|
123
136
|
downgradePlugin(request: PluginDowngradeRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
|
|
124
137
|
enablePlugin(request: PluginEnableRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
|
|
@@ -146,6 +159,7 @@ export declare class PluginPlatformClient {
|
|
|
146
159
|
bindRetainedData(request: PluginRetainedDataBindRequest, options?: PluginRequestOptions): Promise<PluginDataBinding>;
|
|
147
160
|
cleanupExpiredRetainedData(request?: PluginRetainedDataCleanupRequest, options?: PluginRequestOptions): Promise<PluginRetainedDataCleanupResult>;
|
|
148
161
|
listPermissions(options?: PluginPermissionListOptions, requestOptions?: PluginRequestOptions): Promise<PluginPermissionList>;
|
|
162
|
+
getPermissionRequirements(request: PluginPermissionRequirementsRequest, options?: PluginRequestOptions): Promise<PluginPermissionRequirements>;
|
|
149
163
|
grantPermission(request: PluginPermissionGrantRequest, options?: PluginRequestOptions): Promise<PluginPermissionMutationResult>;
|
|
150
164
|
revokePermission(request: PluginPermissionRevokeRequest, options?: PluginRequestOptions): Promise<PluginPermissionMutationResult>;
|
|
151
165
|
listSecurityPolicies(options?: PluginRequestOptions): Promise<PluginSecurityPolicyList>;
|
package/dist/platform.js
CHANGED
|
@@ -26,6 +26,7 @@ export class PluginPlatformClient {
|
|
|
26
26
|
#surfaceScope;
|
|
27
27
|
#surfaceTransport;
|
|
28
28
|
#onMutationOutcomeUnknown;
|
|
29
|
+
#externalInspectionTargets = new Map();
|
|
29
30
|
constructor(options = {}) {
|
|
30
31
|
this.#fetch = options.fetch ?? defaultFetch();
|
|
31
32
|
this.#apiBaseURL = trimTrailingSlash(options.apiBaseURL ?? "");
|
|
@@ -39,6 +40,56 @@ export class PluginPlatformClient {
|
|
|
39
40
|
installReleaseRef(request, options = {}) {
|
|
40
41
|
return this.#requestMutation("POST", "/_redevplugin/api/plugins/install-release-ref", request, options);
|
|
41
42
|
}
|
|
43
|
+
async inspectExternalPackage(request, options = {}) {
|
|
44
|
+
const inspection = await this.#requestMutation("POST", "/_redevplugin/api/plugins/external-packages/inspect", request, options);
|
|
45
|
+
this.#externalInspectionTargets.set(inspection.inspection_id, inspection.intent.plugin_instance_id ?? "");
|
|
46
|
+
return inspection;
|
|
47
|
+
}
|
|
48
|
+
async inspectUploadedExternalPackage(intent, packageBlob, options = {}) {
|
|
49
|
+
const canonicalIntent = canonicalUploadedExternalPackageIntent(intent);
|
|
50
|
+
if (!(packageBlob instanceof Blob)) {
|
|
51
|
+
throw new TypeError("external package upload must be a Blob");
|
|
52
|
+
}
|
|
53
|
+
const update = canonicalIntent.action === "update";
|
|
54
|
+
const path = update
|
|
55
|
+
? `/_redevplugin/api/plugins/${encodeURIComponent(canonicalIntent.plugin_instance_id)}/external-packages/upload/inspect`
|
|
56
|
+
: "/_redevplugin/api/plugins/external-packages/upload/inspect";
|
|
57
|
+
const inspection = await this.#requestPackageUploadInspection(update ? "PUT" : "POST", path, packageBlob, options, update
|
|
58
|
+
? { "X-ReDevPlugin-Expected-Management-Revision": String(canonicalIntent.expected_management_revision) }
|
|
59
|
+
: {});
|
|
60
|
+
this.#externalInspectionTargets.set(inspection.inspection_id, update ? canonicalIntent.plugin_instance_id : "");
|
|
61
|
+
return inspection;
|
|
62
|
+
}
|
|
63
|
+
async commitExternalPackage(request, options = {}) {
|
|
64
|
+
const target = this.#externalInspectionTargets.get(request.inspection_id);
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = await this.#requestMutation("POST", "/_redevplugin/api/plugins/external-packages/commit", request, options);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await this.#handleMutationFailure(error, target || undefined, "External package commit and local surface teardown failed");
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
if (result.status === "committed") {
|
|
74
|
+
await disposePluginSurfaceScope(this.#surfaceScope, result.plugin.plugin_instance_id);
|
|
75
|
+
this.#externalInspectionTargets.delete(request.inspection_id);
|
|
76
|
+
}
|
|
77
|
+
else if (result.status === "failed") {
|
|
78
|
+
this.#externalInspectionTargets.delete(request.inspection_id);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
async queryExternalPackageCommit(request, options = {}) {
|
|
83
|
+
const result = await this.#requestQuery("/_redevplugin/api/plugins/external-packages/commit/query", request, options);
|
|
84
|
+
if (result.status === "committed") {
|
|
85
|
+
await disposePluginSurfaceScope(this.#surfaceScope, result.plugin.plugin_instance_id);
|
|
86
|
+
this.#externalInspectionTargets.delete(request.inspection_id);
|
|
87
|
+
}
|
|
88
|
+
else if (result.status === "failed") {
|
|
89
|
+
this.#externalInspectionTargets.delete(request.inspection_id);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
42
93
|
updateReleaseRef(request, options = {}) {
|
|
43
94
|
return this.#mutatePlugin("/_redevplugin/api/plugins/update-release-ref", request, options);
|
|
44
95
|
}
|
|
@@ -163,6 +214,9 @@ export class PluginPlatformClient {
|
|
|
163
214
|
listPermissions(options = {}, requestOptions = {}) {
|
|
164
215
|
return this.#requestQuery("/_redevplugin/api/plugins/permissions/query", options, requestOptions);
|
|
165
216
|
}
|
|
217
|
+
getPermissionRequirements(request, options = {}) {
|
|
218
|
+
return this.#requestQuery("/_redevplugin/api/plugins/permissions/requirements/query", request, options);
|
|
219
|
+
}
|
|
166
220
|
grantPermission(request, options = {}) {
|
|
167
221
|
return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/grant", request, options);
|
|
168
222
|
}
|
|
@@ -296,6 +350,58 @@ export class PluginPlatformClient {
|
|
|
296
350
|
}, operation);
|
|
297
351
|
return readMutationPlatformResponse(response);
|
|
298
352
|
}
|
|
353
|
+
async #requestPackageUploadInspection(method, path, body, options, metadataHeaders) {
|
|
354
|
+
const operation = `${method} ${path}`;
|
|
355
|
+
assertMutationDispatchable(options.signal, operation);
|
|
356
|
+
const response = await dispatchMutationRequest(this.#fetch, this.#apiBaseURL + path, {
|
|
357
|
+
method,
|
|
358
|
+
headers: {
|
|
359
|
+
"Accept": "application/json",
|
|
360
|
+
"Content-Type": "application/vnd.redevplugin.package+zip",
|
|
361
|
+
...metadataHeaders,
|
|
362
|
+
},
|
|
363
|
+
body,
|
|
364
|
+
credentials: "same-origin",
|
|
365
|
+
signal: options.signal,
|
|
366
|
+
}, operation);
|
|
367
|
+
return readMutationPlatformResponse(response);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function canonicalUploadedExternalPackageIntent(intent) {
|
|
371
|
+
if (typeof intent !== "object" || intent === null || Array.isArray(intent)) {
|
|
372
|
+
throw new TypeError("uploaded external package intent must be a closed install or update object");
|
|
373
|
+
}
|
|
374
|
+
const value = intent;
|
|
375
|
+
const keys = Object.keys(value).sort();
|
|
376
|
+
if (value.action === "install") {
|
|
377
|
+
if (keys.length !== 1 || keys[0] !== "action") {
|
|
378
|
+
throw new TypeError("uploaded external package install intent accepts only action");
|
|
379
|
+
}
|
|
380
|
+
return { action: "install" };
|
|
381
|
+
}
|
|
382
|
+
if (value.action !== "update") {
|
|
383
|
+
throw new TypeError("uploaded external package intent action must be install or update");
|
|
384
|
+
}
|
|
385
|
+
const updateKeys = ["action", "expected_management_revision", "plugin_instance_id"];
|
|
386
|
+
if (keys.length !== updateKeys.length || keys.some((key, index) => key !== updateKeys[index])) {
|
|
387
|
+
throw new TypeError("uploaded external package update intent requires only action, plugin_instance_id, and expected_management_revision");
|
|
388
|
+
}
|
|
389
|
+
if (typeof value.plugin_instance_id !== "string" ||
|
|
390
|
+
value.plugin_instance_id.length === 0 ||
|
|
391
|
+
value.plugin_instance_id.trim() !== value.plugin_instance_id ||
|
|
392
|
+
value.plugin_instance_id.startsWith(".") ||
|
|
393
|
+
value.plugin_instance_id.includes("/")) {
|
|
394
|
+
throw new TypeError("uploaded external package update plugin_instance_id must be a canonical non-empty string");
|
|
395
|
+
}
|
|
396
|
+
if (!Number.isSafeInteger(value.expected_management_revision) ||
|
|
397
|
+
value.expected_management_revision <= 0) {
|
|
398
|
+
throw new TypeError("uploaded external package update expected_management_revision must be a positive safe integer");
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
action: "update",
|
|
402
|
+
plugin_instance_id: value.plugin_instance_id,
|
|
403
|
+
expected_management_revision: value.expected_management_revision,
|
|
404
|
+
};
|
|
299
405
|
}
|
|
300
406
|
const sessionScopeCountKeys = [
|
|
301
407
|
"surfaces",
|
package/dist/surface.d.ts
CHANGED
|
@@ -349,6 +349,15 @@ export type PluginSurfaceOpeningProgress = {
|
|
|
349
349
|
phase: "opening";
|
|
350
350
|
elapsedMs: number;
|
|
351
351
|
};
|
|
352
|
+
export type PluginSurfaceInteractionKind = "activation" | "focus" | "wheel" | "text-selection" | "action";
|
|
353
|
+
export type PluginSurfaceInteractionEvent = {
|
|
354
|
+
kind: PluginSurfaceInteractionKind;
|
|
355
|
+
sequence: number;
|
|
356
|
+
targetKey?: string;
|
|
357
|
+
action?: string;
|
|
358
|
+
localScroll: boolean;
|
|
359
|
+
selectionActive: boolean;
|
|
360
|
+
};
|
|
352
361
|
export type PluginSurfaceHost = {
|
|
353
362
|
readonly element: HTMLIFrameElement;
|
|
354
363
|
readonly surfaceInstanceId: string;
|
|
@@ -376,6 +385,7 @@ export type PluginSurfaceHostOptions = {
|
|
|
376
385
|
reloadLimiter?: PluginSurfaceReloadLimiter;
|
|
377
386
|
confirm?: PluginConfirmationHandler;
|
|
378
387
|
onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
|
|
388
|
+
onInteraction?: (event: PluginSurfaceInteractionEvent) => void;
|
|
379
389
|
onError?: (error: PluginBridgeError) => void;
|
|
380
390
|
};
|
|
381
391
|
type PluginSurfaceOpeningRequest = {
|
|
@@ -403,9 +413,16 @@ export type PluginSurfaceQuiesceResult = {
|
|
|
403
413
|
};
|
|
404
414
|
export type PluginSurfaceCloseResult = {
|
|
405
415
|
quiesce: PluginSurfaceQuiesceResult;
|
|
416
|
+
readonly revocation?: PluginSurfaceRevocationReconciliation;
|
|
406
417
|
revokeDurationMs: number;
|
|
407
418
|
totalDurationMs: number;
|
|
408
419
|
};
|
|
420
|
+
export type PluginSurfaceRevocationReconciliation = {
|
|
421
|
+
disposed: true;
|
|
422
|
+
state: "closed" | "absent";
|
|
423
|
+
previous_state: "active" | "closed" | "absent";
|
|
424
|
+
revoked: boolean;
|
|
425
|
+
};
|
|
409
426
|
export declare function createOpaquePluginBootstrapHTML(options?: OpaquePluginBootstrapHTMLOptions): string;
|
|
410
427
|
export declare function createPreparedPluginSurfaceHost(options: PluginSurfaceHostOptions): PreparedPluginSurfaceHost;
|
|
411
428
|
export type PluginSurfaceSlotState = "empty" | "opening" | "ready" | "error" | "disposed";
|
package/dist/surface.js
CHANGED
|
@@ -34,6 +34,7 @@ const maxOpaqueSurfaceLazyAssets = 128;
|
|
|
34
34
|
const maxOpaqueSurfaceLazyBytes = 32 * 1024 * 1024;
|
|
35
35
|
const maxConcurrentAssetReads = 4;
|
|
36
36
|
const maxSurfaceQuiesceMs = 1500;
|
|
37
|
+
const maxSurfaceInteractionEventsPerSecond = 120;
|
|
37
38
|
const pluginBridgeErrorCodeSet = new Set(pluginBridgeErrorCodes);
|
|
38
39
|
const hostCapabilityIDPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._-]*$");
|
|
39
40
|
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-]+)*)?$");
|
|
@@ -688,7 +689,11 @@ async function revokeSurfaceBootstrap(transport, bootstrap, timeoutMs, keepalive
|
|
|
688
689
|
}),
|
|
689
690
|
timeout,
|
|
690
691
|
]);
|
|
691
|
-
await readMutationPlatformResponse(response);
|
|
692
|
+
const result = await readMutationPlatformResponse(response);
|
|
693
|
+
if (!isSurfaceRevocationReconciliation(result)) {
|
|
694
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface revocation reconciliation returned an invalid result", undefined, undefined, "unknown");
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
692
697
|
}
|
|
693
698
|
catch (error) {
|
|
694
699
|
if (timedOut) {
|
|
@@ -880,6 +885,48 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
880
885
|
const sendParent = (message) => {
|
|
881
886
|
if (parentPort && withinLimit(message)) parentPort.postMessage(message);
|
|
882
887
|
};
|
|
888
|
+
let interactionSequence = 0;
|
|
889
|
+
const interactionTargetKey = (target) => {
|
|
890
|
+
const element = target instanceof Element ? target.closest("[data-redevplugin-key]") : null;
|
|
891
|
+
const key = element?.getAttribute("data-redevplugin-key");
|
|
892
|
+
return validResourceIdentifier(key) ? key : null;
|
|
893
|
+
};
|
|
894
|
+
const ownsLocalScroll = (event) => {
|
|
895
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [event.target];
|
|
896
|
+
for (const candidate of path) {
|
|
897
|
+
if (!(candidate instanceof Element)) continue;
|
|
898
|
+
if (candidate === document.scrollingElement &&
|
|
899
|
+
(candidate.scrollHeight > candidate.clientHeight || candidate.scrollWidth > candidate.clientWidth)) return true;
|
|
900
|
+
const style = getComputedStyle(candidate);
|
|
901
|
+
const vertical = /(auto|scroll|overlay)/.test(style.overflowY) && candidate.scrollHeight > candidate.clientHeight;
|
|
902
|
+
const horizontal = /(auto|scroll|overlay)/.test(style.overflowX) && candidate.scrollWidth > candidate.clientWidth;
|
|
903
|
+
if (vertical || horizontal) return true;
|
|
904
|
+
}
|
|
905
|
+
return false;
|
|
906
|
+
};
|
|
907
|
+
const sendInteraction = (kind, target, options = {}) => {
|
|
908
|
+
if (!initialized || pendingQuiesceID) return;
|
|
909
|
+
interactionSequence += 1;
|
|
910
|
+
sendParent({
|
|
911
|
+
type: "redevplugin.surface.interaction",
|
|
912
|
+
frame_generation_id: frameGenerationID,
|
|
913
|
+
surface_handle: surfaceHandle,
|
|
914
|
+
sequence: interactionSequence,
|
|
915
|
+
kind,
|
|
916
|
+
target_key: interactionTargetKey(target),
|
|
917
|
+
action: options.action || null,
|
|
918
|
+
local_scroll: options.localScroll === true,
|
|
919
|
+
selection_active: options.selectionActive === true,
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
document.addEventListener("pointerdown", (event) => sendInteraction("activation", event.target), true);
|
|
923
|
+
document.addEventListener("focusin", (event) => sendInteraction("focus", event.target), true);
|
|
924
|
+
document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsLocalScroll(event) }), { capture: true, passive: true });
|
|
925
|
+
document.addEventListener("selectionchange", () => {
|
|
926
|
+
const selection = document.getSelection();
|
|
927
|
+
const target = selection?.anchorNode instanceof Element ? selection.anchorNode : selection?.anchorNode?.parentElement;
|
|
928
|
+
sendInteraction("text-selection", target, { selectionActive: Boolean(selection && !selection.isCollapsed) });
|
|
929
|
+
});
|
|
883
930
|
const sendWorker = (message) => {
|
|
884
931
|
if (workerPort && withinLimit(message)) workerPort.postMessage(message);
|
|
885
932
|
};
|
|
@@ -1513,6 +1560,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1513
1560
|
if (event.type === "submit" || (event.type === "click" && element.tagName === "BUTTON")) event.preventDefault();
|
|
1514
1561
|
const action = element.getAttribute("data-redevplugin-action");
|
|
1515
1562
|
if (!validIdentifier(action)) return;
|
|
1563
|
+
sendInteraction("action", event.target, { action });
|
|
1516
1564
|
sendWorker(actionPayload(event, element));
|
|
1517
1565
|
};
|
|
1518
1566
|
for (const eventType of ["click", "input", "change", "submit"]) document.addEventListener(eventType, handleAction, true);
|
|
@@ -1551,6 +1599,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
|
1551
1599
|
const targetKey = owner.getAttribute("data-redevplugin-key");
|
|
1552
1600
|
if (!validResourceIdentifier(targetKey)) return;
|
|
1553
1601
|
const edit = controlEdits.get(targetKey) || { revision: 0, isComposing: false };
|
|
1602
|
+
sendInteraction("action", origin, { action });
|
|
1554
1603
|
sendWorker({
|
|
1555
1604
|
type: "redevplugin.ui.action",
|
|
1556
1605
|
action,
|
|
@@ -2265,7 +2314,11 @@ class PluginSurfaceHostImplementation {
|
|
|
2265
2314
|
#reloadLimiter;
|
|
2266
2315
|
#confirm;
|
|
2267
2316
|
#onOpeningProgress;
|
|
2317
|
+
#onInteraction;
|
|
2268
2318
|
#onError;
|
|
2319
|
+
#lastInteractionSequence = 0;
|
|
2320
|
+
#interactionWindowStartedAt = 0;
|
|
2321
|
+
#interactionWindowCount = 0;
|
|
2269
2322
|
#abortController = new AbortController();
|
|
2270
2323
|
#gatewayToken;
|
|
2271
2324
|
#leaseExpiresAtMs = 0;
|
|
@@ -2324,6 +2377,7 @@ class PluginSurfaceHostImplementation {
|
|
|
2324
2377
|
this.#reloadLimiter = options.reloadLimiter ?? new PluginSurfaceReloadLimiter();
|
|
2325
2378
|
this.#confirm = options.confirm;
|
|
2326
2379
|
this.#onOpeningProgress = options.onOpeningProgress;
|
|
2380
|
+
this.#onInteraction = options.onInteraction;
|
|
2327
2381
|
this.#onError = options.onError;
|
|
2328
2382
|
hardenPluginSurfaceFrame(this.#iframe);
|
|
2329
2383
|
this.#unregisterSurfaceScope = registerPluginSurface(options.surfaceScope ?? defaultPluginSurfaceScope, this.bootstrap.pluginInstanceId, () => this.#disposeLocal(), () => this.#invalidateLocal());
|
|
@@ -2430,23 +2484,29 @@ class PluginSurfaceHostImplementation {
|
|
|
2430
2484
|
if (this.#disposed) {
|
|
2431
2485
|
const result = {
|
|
2432
2486
|
quiesce: { outcome: "not_ready", durationMs: 0 },
|
|
2487
|
+
revocation: { disposed: true, state: "closed", previous_state: "closed", revoked: false },
|
|
2433
2488
|
revokeDurationMs: 0,
|
|
2434
2489
|
totalDurationMs: 0,
|
|
2435
2490
|
};
|
|
2436
|
-
return this.#
|
|
2491
|
+
return this.#revokeSurface(false).then((revocation) => ({ ...result, revocation }));
|
|
2437
2492
|
}
|
|
2438
2493
|
if (this.#closePromise)
|
|
2439
2494
|
return this.#closePromise;
|
|
2440
|
-
|
|
2441
|
-
|
|
2495
|
+
const closing = this.#closeSurface();
|
|
2496
|
+
this.#closePromise = closing;
|
|
2497
|
+
void closing.catch(() => {
|
|
2498
|
+
if (this.#closePromise === closing)
|
|
2499
|
+
this.#closePromise = undefined;
|
|
2500
|
+
});
|
|
2501
|
+
return closing;
|
|
2442
2502
|
}
|
|
2443
2503
|
dispose() {
|
|
2444
2504
|
if (this.#disposed)
|
|
2445
|
-
return this.#
|
|
2505
|
+
return this.#revokeSurface(true).then(() => undefined);
|
|
2446
2506
|
const revoke = this.#revokeSurface(true);
|
|
2447
2507
|
void revoke.catch(() => undefined);
|
|
2448
2508
|
this.#disposeLocal();
|
|
2449
|
-
return revoke;
|
|
2509
|
+
return revoke.then(() => undefined);
|
|
2450
2510
|
}
|
|
2451
2511
|
observeLocalInvalidation(observer) {
|
|
2452
2512
|
this.#onLocalInvalidation = observer;
|
|
@@ -2457,22 +2517,24 @@ class PluginSurfaceHostImplementation {
|
|
|
2457
2517
|
const startedAt = performance.now();
|
|
2458
2518
|
const quiesce = await this.#quiesceSurface();
|
|
2459
2519
|
if (this.#disposed) {
|
|
2460
|
-
|
|
2520
|
+
const revocation = this.#revokePromise ? await this.#revokePromise : { disposed: true, state: "closed", previous_state: "closed", revoked: false };
|
|
2521
|
+
return { quiesce, revocation, revokeDurationMs: 0, totalDurationMs: performance.now() - startedAt };
|
|
2461
2522
|
}
|
|
2462
2523
|
const revokeStartedAt = performance.now();
|
|
2463
2524
|
const revoke = this.#revokeSurface(false);
|
|
2464
2525
|
this.#disposeLocal();
|
|
2465
2526
|
try {
|
|
2466
|
-
await revoke;
|
|
2527
|
+
const revocation = await revoke;
|
|
2528
|
+
return {
|
|
2529
|
+
quiesce,
|
|
2530
|
+
revocation,
|
|
2531
|
+
revokeDurationMs: performance.now() - revokeStartedAt,
|
|
2532
|
+
totalDurationMs: performance.now() - startedAt,
|
|
2533
|
+
};
|
|
2467
2534
|
}
|
|
2468
2535
|
catch (error) {
|
|
2469
2536
|
throw toBridgeError(error, "PLUGIN_BRIDGE_DISPOSED");
|
|
2470
2537
|
}
|
|
2471
|
-
return {
|
|
2472
|
-
quiesce,
|
|
2473
|
-
revokeDurationMs: performance.now() - revokeStartedAt,
|
|
2474
|
-
totalDurationMs: performance.now() - startedAt,
|
|
2475
|
-
};
|
|
2476
2538
|
}
|
|
2477
2539
|
#disposeLocal() {
|
|
2478
2540
|
if (this.#disposed)
|
|
@@ -2550,6 +2612,10 @@ class PluginSurfaceHostImplementation {
|
|
|
2550
2612
|
this.#openSignals?.firstCommit.resolve();
|
|
2551
2613
|
return;
|
|
2552
2614
|
}
|
|
2615
|
+
if (isSurfaceInteractionMessage(data)) {
|
|
2616
|
+
this.#handleInteraction(data);
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2553
2619
|
if (hasExactKeys(data, ["type", "quiesce_id"]) && data.type === "redevplugin.surface.quiesce_ack" && validOpaqueHandle(data.quiesce_id, "quiesce") && data.quiesce_id === this.#quiesce?.id) {
|
|
2554
2620
|
this.#quiesce.acknowledged.resolve();
|
|
2555
2621
|
return;
|
|
@@ -2587,6 +2653,35 @@ class PluginSurfaceHostImplementation {
|
|
|
2587
2653
|
await this.#failSurface(toBridgeError(error, "PLUGIN_CONTRACT_MISMATCH"));
|
|
2588
2654
|
}
|
|
2589
2655
|
}
|
|
2656
|
+
#handleInteraction(message) {
|
|
2657
|
+
if (!this.#ready || message.frame_generation_id !== this.frameGenerationId || message.surface_handle !== this.surfaceHandle)
|
|
2658
|
+
return;
|
|
2659
|
+
if (message.sequence <= this.#lastInteractionSequence)
|
|
2660
|
+
return;
|
|
2661
|
+
this.#lastInteractionSequence = message.sequence;
|
|
2662
|
+
const now = performance.now();
|
|
2663
|
+
if (now - this.#interactionWindowStartedAt >= 1000) {
|
|
2664
|
+
this.#interactionWindowStartedAt = now;
|
|
2665
|
+
this.#interactionWindowCount = 0;
|
|
2666
|
+
}
|
|
2667
|
+
this.#interactionWindowCount += 1;
|
|
2668
|
+
if (this.#interactionWindowCount > maxSurfaceInteractionEventsPerSecond)
|
|
2669
|
+
return;
|
|
2670
|
+
const interaction = {
|
|
2671
|
+
kind: message.kind,
|
|
2672
|
+
sequence: message.sequence,
|
|
2673
|
+
localScroll: message.local_scroll,
|
|
2674
|
+
selectionActive: message.selection_active,
|
|
2675
|
+
...(message.target_key === null ? {} : { targetKey: message.target_key }),
|
|
2676
|
+
...(message.action === null ? {} : { action: message.action }),
|
|
2677
|
+
};
|
|
2678
|
+
try {
|
|
2679
|
+
this.#onInteraction?.(interaction);
|
|
2680
|
+
}
|
|
2681
|
+
catch {
|
|
2682
|
+
// Interaction observers cannot alter the surface lifecycle.
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2590
2685
|
async #handleCall(request) {
|
|
2591
2686
|
if ((!this.#bridgeReady && !this.#quiesce) || !this.#gatewayToken) {
|
|
2592
2687
|
this.#postError(request.id, "PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Plugin bridge call arrived before the surface became ready");
|
|
@@ -3072,8 +3167,13 @@ class PluginSurfaceHostImplementation {
|
|
|
3072
3167
|
#revokeSurface(keepalive) {
|
|
3073
3168
|
if (this.#revokePromise)
|
|
3074
3169
|
return this.#revokePromise;
|
|
3075
|
-
|
|
3076
|
-
|
|
3170
|
+
const revocation = revokeSurfaceBootstrap(this.#transport, this.bootstrap, this.#requestTimeoutMs, keepalive);
|
|
3171
|
+
this.#revokePromise = revocation;
|
|
3172
|
+
void revocation.catch(() => {
|
|
3173
|
+
if (this.#revokePromise === revocation)
|
|
3174
|
+
this.#revokePromise = undefined;
|
|
3175
|
+
});
|
|
3176
|
+
return revocation;
|
|
3077
3177
|
}
|
|
3078
3178
|
async #quiesceSurface() {
|
|
3079
3179
|
const startedAt = performance.now();
|
|
@@ -3309,6 +3409,7 @@ export class PluginSurfaceSlot {
|
|
|
3309
3409
|
#tail = Promise.resolve();
|
|
3310
3410
|
#disposed = false;
|
|
3311
3411
|
#retired = new WeakMap();
|
|
3412
|
+
#pendingRetirements = new Set();
|
|
3312
3413
|
#disposePromise;
|
|
3313
3414
|
static create(options) {
|
|
3314
3415
|
if (!options.stage || typeof options.stage.append !== "function" || !options.stage.dataset) {
|
|
@@ -3449,8 +3550,8 @@ export class PluginSurfaceSlot {
|
|
|
3449
3550
|
this.#transitionController?.abort();
|
|
3450
3551
|
this.#transitionController = undefined;
|
|
3451
3552
|
this.#setState("empty");
|
|
3452
|
-
const closing = this.#tail.then(() => this.#closeCurrentSurface());
|
|
3453
|
-
this.#tail = closing.then(() => undefined, (
|
|
3553
|
+
const closing = this.#tail.then(() => this.#closeCurrentSurface(), () => this.#closeCurrentSurface());
|
|
3554
|
+
this.#tail = closing.then(() => undefined, () => undefined);
|
|
3454
3555
|
void this.#tail.catch(() => undefined);
|
|
3455
3556
|
return this.#unwrapCleanupError(closing);
|
|
3456
3557
|
}
|
|
@@ -3461,17 +3562,22 @@ export class PluginSurfaceSlot {
|
|
|
3461
3562
|
this.#transitionController?.abort();
|
|
3462
3563
|
this.#transitionController = undefined;
|
|
3463
3564
|
this.#setState("disposed");
|
|
3464
|
-
const disposal = this.#tail.then(
|
|
3465
|
-
|
|
3565
|
+
const disposal = this.#tail.then(() => this.#closeCurrentSurface().then(() => undefined), () => this.#closeCurrentSurface().then(() => undefined));
|
|
3566
|
+
const disposePromise = this.#unwrapCleanupError(disposal);
|
|
3567
|
+
this.#disposePromise = disposePromise;
|
|
3568
|
+
void disposePromise.catch(() => {
|
|
3569
|
+
if (this.#disposePromise === disposePromise)
|
|
3570
|
+
this.#disposePromise = undefined;
|
|
3466
3571
|
});
|
|
3467
|
-
this.#
|
|
3468
|
-
void this.#disposePromise.catch(() => undefined);
|
|
3469
|
-
this.#tail = disposal.then(() => undefined, (error) => error instanceof PluginSurfaceCleanupError ? Promise.reject(error) : undefined);
|
|
3572
|
+
this.#tail = disposal.then(() => undefined, () => undefined);
|
|
3470
3573
|
void this.#tail.catch(() => undefined);
|
|
3471
|
-
return
|
|
3574
|
+
return disposePromise;
|
|
3472
3575
|
}
|
|
3473
3576
|
async #closeCurrentSurface() {
|
|
3474
|
-
const hosts = new Set([
|
|
3577
|
+
const hosts = new Set([
|
|
3578
|
+
...this.#pendingRetirements,
|
|
3579
|
+
...[this.#active, this.#opening].filter((host) => host !== undefined),
|
|
3580
|
+
]);
|
|
3475
3581
|
this.#active = undefined;
|
|
3476
3582
|
this.#opening = undefined;
|
|
3477
3583
|
let result;
|
|
@@ -3495,6 +3601,7 @@ export class PluginSurfaceSlot {
|
|
|
3495
3601
|
const existing = this.#retired.get(host);
|
|
3496
3602
|
if (existing)
|
|
3497
3603
|
return existing;
|
|
3604
|
+
this.#pendingRetirements.add(host);
|
|
3498
3605
|
setSurfaceInteractive(host.element, false);
|
|
3499
3606
|
const closing = Promise.resolve().then(async () => {
|
|
3500
3607
|
try {
|
|
@@ -3512,6 +3619,14 @@ export class PluginSurfaceSlot {
|
|
|
3512
3619
|
}
|
|
3513
3620
|
});
|
|
3514
3621
|
this.#retired.set(host, closing);
|
|
3622
|
+
void closing.then(() => {
|
|
3623
|
+
this.#pendingRetirements.delete(host);
|
|
3624
|
+
if (this.#retired.get(host) === closing)
|
|
3625
|
+
this.#retired.delete(host);
|
|
3626
|
+
}, () => {
|
|
3627
|
+
if (this.#retired.get(host) === closing)
|
|
3628
|
+
this.#retired.delete(host);
|
|
3629
|
+
});
|
|
3515
3630
|
return closing;
|
|
3516
3631
|
}
|
|
3517
3632
|
async #awaitRetirement(host) {
|
|
@@ -3596,6 +3711,31 @@ function setSurfaceInteractive(element, interactive) {
|
|
|
3596
3711
|
element.inert = !interactive;
|
|
3597
3712
|
element.setAttribute("aria-hidden", interactive ? "false" : "true");
|
|
3598
3713
|
}
|
|
3714
|
+
function isSurfaceInteractionMessage(value) {
|
|
3715
|
+
if (!hasExactKeys(value, [
|
|
3716
|
+
"type", "frame_generation_id", "surface_handle", "sequence", "kind", "target_key", "action", "local_scroll", "selection_active",
|
|
3717
|
+
]))
|
|
3718
|
+
return false;
|
|
3719
|
+
return value.type === "redevplugin.surface.interaction" &&
|
|
3720
|
+
validOpaqueHandle(value.frame_generation_id, "frame") &&
|
|
3721
|
+
validOpaqueHandle(value.surface_handle, "surface") &&
|
|
3722
|
+
typeof value.sequence === "number" && Number.isSafeInteger(value.sequence) && value.sequence > 0 &&
|
|
3723
|
+
["activation", "focus", "wheel", "text-selection", "action"].includes(String(value.kind)) &&
|
|
3724
|
+
(value.target_key === null || validUIIdentifier(value.target_key)) &&
|
|
3725
|
+
(value.action === null || validActionID(value.action)) &&
|
|
3726
|
+
typeof value.local_scroll === "boolean" && typeof value.selection_active === "boolean" &&
|
|
3727
|
+
(value.kind === "action" ? value.action !== null : value.action === null) &&
|
|
3728
|
+
(value.kind === "wheel" || value.local_scroll === false) &&
|
|
3729
|
+
(value.kind === "text-selection" || value.selection_active === false);
|
|
3730
|
+
}
|
|
3731
|
+
function isSurfaceRevocationReconciliation(value) {
|
|
3732
|
+
if (!hasExactKeys(value, ["disposed", "state", "previous_state", "revoked"]))
|
|
3733
|
+
return false;
|
|
3734
|
+
return value.disposed === true && ["closed", "absent"].includes(String(value.state)) &&
|
|
3735
|
+
["active", "closed", "absent"].includes(String(value.previous_state)) &&
|
|
3736
|
+
typeof value.revoked === "boolean" &&
|
|
3737
|
+
(value.revoked ? value.state === "closed" && value.previous_state === "active" : true);
|
|
3738
|
+
}
|
|
3599
3739
|
function claimOpaquePluginBridge() {
|
|
3600
3740
|
const value = globalThis[opaquePluginBridgeGlobalKey];
|
|
3601
3741
|
if (!isRecord(value) || typeof value.claim !== "function") {
|
package/dist/trusted-parent.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export * from "./errors.js";
|
|
|
3
3
|
export * from "./platform.js";
|
|
4
4
|
export { PluginSurfaceScope, createPluginSurfaceScope } from "./surface-scope.js";
|
|
5
5
|
export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, opaqueSurfaceDocumentSchemaVersion, 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, PluginStreamEvent, PluginSurfaceHost, PluginSurfaceCloseResult, PluginSurfaceOpeningProgress, PluginSurfaceQuiesceResult, PluginSurfacePreparationResult, PluginSurfaceReloadDecision, PluginSurfaceReloadLimiterOptions, PluginSurfaceReloadState, PluginSurfaceSlotOptions, PluginSurfaceSlotState, PluginTrustedMethodResult, ReDevPluginSurfaceTransport, ReDevPluginSurfaceTransportOptions, TrustedParentBridgeHandshake, TrustedParentBridgeTokenRequest, WindowLike, } from "./surface.js";
|
|
6
|
+
export type { BridgeLifecycleEvent, MessageChannelLike, MessageEventLike, MessagePortLike, OpaqueSurfaceAsset, OpaqueSurfaceDocument, OpaqueSurfaceStyle, OpaqueSurfaceWorker, PluginConfirmationDecision, PluginConfirmationHandler, PluginConfirmationIntent, PluginConfirmationPlan, PluginMethodResult, PluginRiskEffect, PluginRiskFlag, PluginRiskPlan, PluginRiskSeverity, PluginStreamEvent, PluginSurfaceHost, PluginSurfaceCloseResult, PluginSurfaceOpeningProgress, PluginSurfaceInteractionEvent, PluginSurfaceInteractionKind, PluginSurfaceQuiesceResult, PluginSurfaceRevocationReconciliation, PluginSurfacePreparationResult, PluginSurfaceReloadDecision, PluginSurfaceReloadLimiterOptions, PluginSurfaceReloadState, PluginSurfaceSlotOptions, PluginSurfaceSlotState, PluginTrustedMethodResult, ReDevPluginSurfaceTransport, ReDevPluginSurfaceTransportOptions, TrustedParentBridgeHandshake, TrustedParentBridgeTokenRequest, WindowLike, } from "./surface.js";
|
|
7
7
|
export type { FetchInitLike, FetchLike, FetchResponseLike, PlatformResponse } from "./http.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/redevplugin-ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@floegence/redevplugin-contracts": "0.6.
|
|
35
|
+
"@floegence/redevplugin-contracts": "0.6.10"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|