@floegence/redevplugin-ui 0.6.8 → 0.6.13

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.
@@ -42,8 +42,8 @@ export declare const redevPluginContractVersions: {
42
42
  readonly host_capability_signature_schema_version: "host-capability-signature-v1";
43
43
  readonly host_capability_notices_schema_version: "host-capability-notices-v1";
44
44
  readonly error_codes_schema_version: "error-codes-v6";
45
- readonly performance_contract_version: "performance-contract-v3";
46
- readonly performance_evidence_schema_version: "performance-evidence-v3";
45
+ readonly performance_contract_version: "performance-contract-v4";
46
+ readonly performance_evidence_schema_version: "performance-evidence-v4";
47
47
  readonly contract_registry_version: "contract-registry-v2";
48
48
  readonly platform_package_set_schema_version: "platform-package-set-v1";
49
49
  readonly platform_package_publication_schema_version: "platform-package-publication-v1";
@@ -66,7 +66,7 @@ export declare const redevPluginContractArtifacts: readonly [{
66
66
  readonly id: "compatibility-manifest-schema";
67
67
  readonly path: "spec/plugin/compatibility-manifest-v8.schema.json";
68
68
  readonly version: "compatibility-manifest-v8";
69
- readonly sha256: "a1831e0ae9a1b47d92e7ad9879b1ca4e3510ebc212b87360595b672e8b87bc79";
69
+ readonly sha256: "fe966f66e23a6d7558156608f269d08fbe57d8bca16514f1d095445cea697189";
70
70
  }, {
71
71
  readonly id: "contract-registry-schema";
72
72
  readonly path: "spec/plugin/contract-registry-v2.schema.json";
@@ -131,7 +131,7 @@ export declare const redevPluginContractArtifacts: readonly [{
131
131
  readonly id: "opaque-surface-transport-schema";
132
132
  readonly path: "spec/plugin/opaque-surface-transport-v4.schema.json";
133
133
  readonly version: "opaque-surface-transport-v4";
134
- readonly sha256: "70a32dc8bf281cea2c92492c466fd477070c62156a3f593d45eb460cde62dca2";
134
+ readonly sha256: "004da73aba882cf8c2130af0c69d3eed4c710539f21287841bc2e7e870e60f8f";
135
135
  }, {
136
136
  readonly id: "owner-scope-inventory-registry";
137
137
  readonly path: "spec/plugin/owner-scope-inventories-v1.json";
@@ -154,14 +154,14 @@ export declare const redevPluginContractArtifacts: readonly [{
154
154
  readonly sha256: "13951c0f6831ba28647774368c76a817868aeb7984628e2cf3dc4ad1b54f8284";
155
155
  }, {
156
156
  readonly id: "performance-contract";
157
- readonly path: "spec/plugin/performance-contract-v3.json";
158
- readonly version: "performance-contract-v3";
159
- readonly sha256: "a4e1a099dc16f2b0a4d6bdc5fdf8f6ce622948ffec40f70bfc2743286a2b816b";
157
+ readonly path: "spec/plugin/performance-contract-v4.json";
158
+ readonly version: "performance-contract-v4";
159
+ readonly sha256: "aedc50ed8899346140965dc46e8adf2048c47bcf6c146c3b7a51646eda7725e1";
160
160
  }, {
161
161
  readonly id: "performance-evidence-schema";
162
- readonly path: "spec/plugin/performance-evidence-v3.schema.json";
163
- readonly version: "performance-evidence-v3";
164
- readonly sha256: "a2f5e94071bb09a9327fc55ad3f6c39052b01c3e54cceb8b974dc244667a0326";
162
+ readonly path: "spec/plugin/performance-evidence-v4.schema.json";
163
+ readonly version: "performance-evidence-v4";
164
+ readonly sha256: "f51ba9d425f0e40bc4f2db7ed26454e7575b14ffb6937a06dc01b8e7a66b3f61";
165
165
  }, {
166
166
  readonly id: "platform-package-publication-schema";
167
167
  readonly path: "spec/plugin/platform-package-publication-v1.schema.json";
@@ -176,7 +176,7 @@ export declare const redevPluginContractArtifacts: readonly [{
176
176
  readonly id: "plugin-platform-openapi";
177
177
  readonly path: "spec/openapi/plugin-platform-v8.yaml";
178
178
  readonly version: "plugin-platform-v8";
179
- readonly sha256: "39bcfcfb846c868945ab0d21404747b2f5f083574c7ead5aca64f65b98212b7d";
179
+ readonly sha256: "94e0cc88cb5ad8e7773677617530028578859e0bf229a6b75997f24885eb36d2";
180
180
  }, {
181
181
  readonly id: "process-containment-schema";
182
182
  readonly path: "spec/plugin/process-containment-v1.schema.json";
@@ -43,8 +43,8 @@ export const redevPluginContractVersions = {
43
43
  "host_capability_signature_schema_version": "host-capability-signature-v1",
44
44
  "host_capability_notices_schema_version": "host-capability-notices-v1",
45
45
  "error_codes_schema_version": "error-codes-v6",
46
- "performance_contract_version": "performance-contract-v3",
47
- "performance_evidence_schema_version": "performance-evidence-v3",
46
+ "performance_contract_version": "performance-contract-v4",
47
+ "performance_evidence_schema_version": "performance-evidence-v4",
48
48
  "contract_registry_version": "contract-registry-v2",
49
49
  "platform_package_set_schema_version": "platform-package-set-v1",
50
50
  "platform_package_publication_schema_version": "platform-package-publication-v1",
@@ -62,7 +62,7 @@ export const redevPluginContractArtifacts = [
62
62
  id: "compatibility-manifest-schema",
63
63
  path: "spec/plugin/compatibility-manifest-v8.schema.json",
64
64
  version: "compatibility-manifest-v8",
65
- sha256: "a1831e0ae9a1b47d92e7ad9879b1ca4e3510ebc212b87360595b672e8b87bc79",
65
+ sha256: "fe966f66e23a6d7558156608f269d08fbe57d8bca16514f1d095445cea697189",
66
66
  },
67
67
  {
68
68
  id: "contract-registry-schema",
@@ -140,7 +140,7 @@ export const redevPluginContractArtifacts = [
140
140
  id: "opaque-surface-transport-schema",
141
141
  path: "spec/plugin/opaque-surface-transport-v4.schema.json",
142
142
  version: "opaque-surface-transport-v4",
143
- sha256: "70a32dc8bf281cea2c92492c466fd477070c62156a3f593d45eb460cde62dca2",
143
+ sha256: "004da73aba882cf8c2130af0c69d3eed4c710539f21287841bc2e7e870e60f8f",
144
144
  },
145
145
  {
146
146
  id: "owner-scope-inventory-registry",
@@ -168,15 +168,15 @@ export const redevPluginContractArtifacts = [
168
168
  },
169
169
  {
170
170
  id: "performance-contract",
171
- path: "spec/plugin/performance-contract-v3.json",
172
- version: "performance-contract-v3",
173
- sha256: "a4e1a099dc16f2b0a4d6bdc5fdf8f6ce622948ffec40f70bfc2743286a2b816b",
171
+ path: "spec/plugin/performance-contract-v4.json",
172
+ version: "performance-contract-v4",
173
+ sha256: "aedc50ed8899346140965dc46e8adf2048c47bcf6c146c3b7a51646eda7725e1",
174
174
  },
175
175
  {
176
176
  id: "performance-evidence-schema",
177
- path: "spec/plugin/performance-evidence-v3.schema.json",
178
- version: "performance-evidence-v3",
179
- sha256: "a2f5e94071bb09a9327fc55ad3f6c39052b01c3e54cceb8b974dc244667a0326",
177
+ path: "spec/plugin/performance-evidence-v4.schema.json",
178
+ version: "performance-evidence-v4",
179
+ sha256: "f51ba9d425f0e40bc4f2db7ed26454e7575b14ffb6937a06dc01b8e7a66b3f61",
180
180
  },
181
181
  {
182
182
  id: "platform-package-publication-schema",
@@ -194,7 +194,7 @@ export const redevPluginContractArtifacts = [
194
194
  id: "plugin-platform-openapi",
195
195
  path: "spec/openapi/plugin-platform-v8.yaml",
196
196
  version: "plugin-platform-v8",
197
- sha256: "39bcfcfb846c868945ab0d21404747b2f5f083574c7ead5aca64f65b98212b7d",
197
+ sha256: "94e0cc88cb5ad8e7773677617530028578859e0bf229a6b75997f24885eb36d2",
198
198
  },
199
199
  {
200
200
  id: "process-containment-schema",
@@ -405,6 +405,7 @@ export interface paths {
405
405
  };
406
406
  get?: never;
407
407
  put?: never;
408
+ /** @description Atomically observes and revokes one exact authenticated surface. An active surface becomes closed, a retained closure confirms closed, and an absent result is authoritative inactivity; transport failures remain an unknown mutation outcome. */
408
409
  post: operations["disposePluginSurface"];
409
410
  delete?: never;
410
411
  options?: never;
@@ -719,6 +720,23 @@ export interface paths {
719
720
  patch?: never;
720
721
  trace?: never;
721
722
  };
723
+ "/_redevplugin/api/plugins/permissions/requirements/query": {
724
+ parameters: {
725
+ query?: never;
726
+ header?: never;
727
+ path?: never;
728
+ cookie?: never;
729
+ };
730
+ get?: never;
731
+ put?: never;
732
+ /** @description Projects required permissions only from the active plugin version's Host-verified capability contracts. */
733
+ post: operations["getPluginPermissionRequirements"];
734
+ delete?: never;
735
+ options?: never;
736
+ head?: never;
737
+ patch?: never;
738
+ trace?: never;
739
+ };
722
740
  "/_redevplugin/api/plugins/permissions/grant": {
723
741
  parameters: {
724
742
  query?: never;
@@ -1070,6 +1088,11 @@ export interface components {
1070
1088
  ok: true;
1071
1089
  data: components["schemas"]["PluginPermissionList"];
1072
1090
  };
1091
+ PermissionRequirementsSuccessResponse: {
1092
+ /** @constant */
1093
+ ok: true;
1094
+ data: components["schemas"]["PluginPermissionRequirements"];
1095
+ };
1073
1096
  PermissionMutationSuccessResponse: {
1074
1097
  /** @constant */
1075
1098
  ok: true;
@@ -1309,6 +1332,11 @@ export interface components {
1309
1332
  SurfaceDisposeResult: {
1310
1333
  /** @constant */
1311
1334
  disposed: true;
1335
+ /** @enum {string} */
1336
+ state: "closed" | "absent";
1337
+ /** @enum {string} */
1338
+ previous_state: "active" | "closed" | "absent";
1339
+ revoked: boolean;
1312
1340
  };
1313
1341
  SessionScopeRevokeCounts: components["schemas"]["SessionScopeV1RevokeCounts"];
1314
1342
  SessionScopeRevokeResult: components["schemas"]["SessionScopeV1PublicRevokeResult"];
@@ -1359,6 +1387,26 @@ export interface components {
1359
1387
  PluginPermissionList: {
1360
1388
  permissions: components["schemas"]["PluginPermissionGrant"][];
1361
1389
  };
1390
+ PluginPermissionRequirementMethod: {
1391
+ method: string;
1392
+ required_permissions: string[];
1393
+ };
1394
+ PluginPermissionRequirementContract: {
1395
+ contract_id: string;
1396
+ contract_version: string;
1397
+ contract_sha256: string;
1398
+ capability_id: string;
1399
+ capability_version: string;
1400
+ methods: components["schemas"]["PluginPermissionRequirementMethod"][];
1401
+ };
1402
+ PluginPermissionRequirements: {
1403
+ plugin_instance_id: string;
1404
+ plugin_version: string;
1405
+ active_fingerprint: string;
1406
+ management_revision: number;
1407
+ contracts: components["schemas"]["PluginPermissionRequirementContract"][];
1408
+ required_permissions: string[];
1409
+ };
1362
1410
  AuthorizationRevisions: {
1363
1411
  policy_revision: number;
1364
1412
  management_revision: number;
@@ -2105,6 +2153,9 @@ export interface components {
2105
2153
  plugin_instance_id?: string;
2106
2154
  active_only?: boolean;
2107
2155
  };
2156
+ PermissionRequirementsQueryRequest: {
2157
+ plugin_instance_id: string;
2158
+ };
2108
2159
  ListDiagnosticsQueryRequest: {
2109
2160
  plugin_id?: string;
2110
2161
  plugin_instance_id?: string;
@@ -2721,9 +2772,9 @@ export interface components {
2721
2772
  /** @constant */
2722
2773
  error_codes_schema_version: "error-codes-v6";
2723
2774
  /** @constant */
2724
- performance_contract_version: "performance-contract-v3";
2775
+ performance_contract_version: "performance-contract-v4";
2725
2776
  /** @constant */
2726
- performance_evidence_schema_version: "performance-evidence-v3";
2777
+ performance_evidence_schema_version: "performance-evidence-v4";
2727
2778
  /** @constant */
2728
2779
  contract_registry_version: "contract-registry-v2";
2729
2780
  /** @constant */
@@ -3376,6 +3427,15 @@ export interface components {
3376
3427
  "application/json": components["schemas"]["PermissionListSuccessResponse"];
3377
3428
  };
3378
3429
  };
3430
+ /** @description Active-version permission requirements derived from Host-verified capability contracts. */
3431
+ PermissionRequirementsResponse: {
3432
+ headers: {
3433
+ [name: string]: unknown;
3434
+ };
3435
+ content: {
3436
+ "application/json": components["schemas"]["PermissionRequirementsSuccessResponse"];
3437
+ };
3438
+ };
3379
3439
  /** @description Permission grant mutation result. */
3380
3440
  PermissionMutationResponse: {
3381
3441
  headers: {
@@ -3558,6 +3618,11 @@ export interface components {
3558
3618
  "application/json": components["schemas"]["ListPermissionsQueryRequest"];
3559
3619
  };
3560
3620
  };
3621
+ PermissionRequirementsQueryRequest: {
3622
+ content: {
3623
+ "application/json": components["schemas"]["PermissionRequirementsQueryRequest"];
3624
+ };
3625
+ };
3561
3626
  ListDiagnosticsQueryRequest: {
3562
3627
  content: {
3563
3628
  "application/json": components["schemas"]["ListDiagnosticsQueryRequest"];
@@ -4332,6 +4397,19 @@ export interface operations {
4332
4397
  default: components["responses"]["PlatformErrorResponse"];
4333
4398
  };
4334
4399
  };
4400
+ getPluginPermissionRequirements: {
4401
+ parameters: {
4402
+ query?: never;
4403
+ header?: never;
4404
+ path?: never;
4405
+ cookie?: never;
4406
+ };
4407
+ requestBody: components["requestBodies"]["PermissionRequirementsQueryRequest"];
4408
+ responses: {
4409
+ 200: components["responses"]["PermissionRequirementsResponse"];
4410
+ default: components["responses"]["PlatformErrorResponse"];
4411
+ };
4412
+ };
4335
4413
  grantPluginPermission: {
4336
4414
  parameters: {
4337
4415
  query?: never;
@@ -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;
@@ -50,6 +50,7 @@ export type PluginOpenSurfaceInSlotOptions = PluginRequestOptions & {
50
50
  reloadLimiter?: PluginSurfaceReloadLimiter;
51
51
  confirm?: PluginConfirmationHandler;
52
52
  onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
53
+ onInteraction?: (event: PluginSurfaceInteractionEvent) => void;
53
54
  onError?: (error: import("./errors.js").PluginBridgeError) => void;
54
55
  };
55
56
  export type PluginSessionScopeRevokeCounts = Readonly<PlatformSchemas["SessionScopeRevokeCounts"]>;
@@ -89,6 +90,8 @@ export type PluginPermissionGrant = PlatformSchemas["PluginPermissionGrant"];
89
90
  export type PluginPermissionMutationResult = PlatformSchemas["PluginPermissionMutationResult"];
90
91
  export type PluginPermissionList = PlatformSchemas["PluginPermissionList"];
91
92
  export type PluginPermissionListOptions = PlatformSchemas["ListPermissionsQueryRequest"];
93
+ export type PluginPermissionRequirements = PlatformSchemas["PluginPermissionRequirements"];
94
+ export type PluginPermissionRequirementsRequest = PlatformSchemas["PermissionRequirementsQueryRequest"];
92
95
  export type PluginPermissionGrantRequest = PlatformSchemas["GrantPermissionRequest"];
93
96
  export type PluginPermissionRevokeRequest = PlatformSchemas["RevokePermissionRequest"];
94
97
  export type PluginSecurityPolicy = PlatformSchemas["PluginSecurityPolicy"];
@@ -156,6 +159,7 @@ export declare class PluginPlatformClient {
156
159
  bindRetainedData(request: PluginRetainedDataBindRequest, options?: PluginRequestOptions): Promise<PluginDataBinding>;
157
160
  cleanupExpiredRetainedData(request?: PluginRetainedDataCleanupRequest, options?: PluginRequestOptions): Promise<PluginRetainedDataCleanupResult>;
158
161
  listPermissions(options?: PluginPermissionListOptions, requestOptions?: PluginRequestOptions): Promise<PluginPermissionList>;
162
+ getPermissionRequirements(request: PluginPermissionRequirementsRequest, options?: PluginRequestOptions): Promise<PluginPermissionRequirements>;
159
163
  grantPermission(request: PluginPermissionGrantRequest, options?: PluginRequestOptions): Promise<PluginPermissionMutationResult>;
160
164
  revokePermission(request: PluginPermissionRevokeRequest, options?: PluginRequestOptions): Promise<PluginPermissionMutationResult>;
161
165
  listSecurityPolicies(options?: PluginRequestOptions): Promise<PluginSecurityPolicyList>;
package/dist/platform.js CHANGED
@@ -214,6 +214,9 @@ export class PluginPlatformClient {
214
214
  listPermissions(options = {}, requestOptions = {}) {
215
215
  return this.#requestQuery("/_redevplugin/api/plugins/permissions/query", options, requestOptions);
216
216
  }
217
+ getPermissionRequirements(request, options = {}) {
218
+ return this.#requestQuery("/_redevplugin/api/plugins/permissions/requirements/query", request, options);
219
+ }
217
220
  grantPermission(request, options = {}) {
218
221
  return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/grant", request, options);
219
222
  }
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.#revokePromise ? this.#revokePromise.then(() => result) : Promise.resolve(result);
2491
+ return this.#revokeSurface(false).then((revocation) => ({ ...result, revocation }));
2437
2492
  }
2438
2493
  if (this.#closePromise)
2439
2494
  return this.#closePromise;
2440
- this.#closePromise = this.#closeSurface();
2441
- return this.#closePromise;
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.#revokePromise ?? Promise.resolve();
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
- return { quiesce, revokeDurationMs: 0, totalDurationMs: performance.now() - startedAt };
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
- this.#revokePromise = revokeSurfaceBootstrap(this.#transport, this.bootstrap, this.#requestTimeoutMs, keepalive);
3076
- return this.#revokePromise;
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, (error) => error instanceof PluginSurfaceCleanupError ? Promise.reject(error) : 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(async () => {
3465
- await this.#closeCurrentSurface();
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.#disposePromise = this.#unwrapCleanupError(disposal);
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 this.#disposePromise;
3574
+ return disposePromise;
3472
3575
  }
3473
3576
  async #closeCurrentSurface() {
3474
- const hosts = new Set([this.#active, this.#opening].filter((host) => host !== undefined));
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") {
@@ -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.8",
3
+ "version": "0.6.13",
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.8"
35
+ "@floegence/redevplugin-contracts": "0.6.13"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"