@floegence/redevplugin-ui 3.0.37 → 3.2.0

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.
@@ -684,7 +684,7 @@ export interface paths {
684
684
  };
685
685
  get?: never;
686
686
  put?: never;
687
- /** @description Projects required permissions only from the active plugin version's Host-verified capability contracts. */
687
+ /** @description Projects active Worker permission ceilings and Host-verified capability requirements, bound to the active plugin version. */
688
688
  post: operations["getPluginPermissionRequirements"];
689
689
  delete?: never;
690
690
  options?: never;
@@ -1292,7 +1292,13 @@ export interface components {
1292
1292
  SessionScopeRevokeResult: components["schemas"]["SessionScopeV1PublicRevokeResult"];
1293
1293
  SessionScopeRevokeCompleteResult: components["schemas"]["SessionScopeV1CompleteRevokeResult"];
1294
1294
  SessionScopeRevokeIncompleteResult: components["schemas"]["SessionScopeV1IncompleteRevokeResult"];
1295
+ PluginSurfaceActionState: {
1296
+ surface_id: string;
1297
+ can_open: boolean;
1298
+ blocked_reason?: components["schemas"]["PluginActionState"]["blocked_reason"];
1299
+ };
1295
1300
  PluginActionState: {
1301
+ surfaces?: components["schemas"]["PluginSurfaceActionState"][];
1296
1302
  can_open: boolean;
1297
1303
  can_enable: boolean;
1298
1304
  can_disable: boolean;
@@ -1372,6 +1378,7 @@ export interface components {
1372
1378
  active_fingerprint: string;
1373
1379
  management_revision: number;
1374
1380
  contracts: components["schemas"]["PluginPermissionRequirementContract"][];
1381
+ methods?: components["schemas"]["PluginPermissionRequirementMethod"][];
1375
1382
  required_permissions: string[];
1376
1383
  };
1377
1384
  AuthorizationRevisions: {
@@ -1803,6 +1810,7 @@ export interface components {
1803
1810
  method_access: components["schemas"]["ExternalPackageProcessMethodAccessSummary"][];
1804
1811
  };
1805
1812
  ExternalPackageBackgroundSummary: {
1813
+ method?: string;
1806
1814
  /** @enum {string} */
1807
1815
  strategy: "runtime_start" | "on_demand";
1808
1816
  worker_id: string;
@@ -2602,6 +2610,8 @@ export interface components {
2602
2610
  width: number;
2603
2611
  height: number;
2604
2612
  };
2613
+ /** @description Methods required to open this surface. Omitted retains aggregate authorization. An empty array allows opening without method grants; calls remain individually authorized. Requires authorization.scoped.v1. */
2614
+ required_methods?: string[];
2605
2615
  }[];
2606
2616
  capability_bindings?: {
2607
2617
  binding_id: string;
@@ -2666,7 +2676,9 @@ export interface components {
2666
2676
  /** @constant */
2667
2677
  additionalProperties: false;
2668
2678
  };
2669
- } & (unknown & unknown & unknown & unknown))[];
2679
+ /** @description Explicit Worker permission ceiling. Omitted retains the package ceiling; an empty array grants no resource permissions. Requires authorization.scoped.v1. */
2680
+ permissions?: ("fs.workspace.read" | "fs.workspace.write" | "fs.home.read" | "fs.home.write" | "fs.environment.read" | "fs.environment.write" | "network.client" | "network.listen" | "process.local")[];
2681
+ } & (unknown & unknown & unknown & unknown & unknown))[];
2670
2682
  workers: {
2671
2683
  worker_id: string;
2672
2684
  artifact: string;
@@ -2710,6 +2722,8 @@ export interface components {
2710
2722
  /** @enum {unknown} */
2711
2723
  strategy: "runtime_start" | "on_demand";
2712
2724
  worker_id: string;
2725
+ method?: string;
2726
+ params?: Record<string, never>;
2713
2727
  };
2714
2728
  settings?: {
2715
2729
  schema_version: number;
@@ -2737,7 +2751,7 @@ export interface components {
2737
2751
  optional_features?: components["schemas"]["ManifestV9OptionalFeatures"];
2738
2752
  };
2739
2753
  permissions: ("fs.workspace.read" | "fs.workspace.write" | "fs.home.read" | "fs.home.write" | "fs.environment.read" | "fs.environment.write" | "network.client" | "network.listen" | "process.local")[];
2740
- };
2754
+ } & (unknown & unknown & unknown);
2741
2755
  ManifestV9Locale: string;
2742
2756
  ManifestV9Summary: string;
2743
2757
  ManifestV9Description: string[];
@@ -2806,7 +2820,7 @@ export interface components {
2806
2820
  minProperties?: number;
2807
2821
  maxProperties?: number;
2808
2822
  } & (unknown & unknown)) | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown);
2809
- ManifestV9RequiredFeatures: ("io.stream.v1" | "fs.workspace.v1" | "fs.home.v1" | "fs.environment.v1" | "fs.watch.v1" | "net.http.v1" | "net.websocket.v1" | "net.tcp.v1" | "net.udp.v1" | "process.local.v1")[];
2823
+ ManifestV9RequiredFeatures: ("io.stream.v1" | "fs.workspace.v1" | "fs.home.v1" | "fs.environment.v1" | "fs.watch.v1" | "net.http.v1" | "net.websocket.v1" | "net.tcp.v1" | "net.udp.v1" | "process.local.v1" | "authorization.scoped.v1" | "background.method.v1")[];
2810
2824
  ManifestV9OptionalFeatures: string[];
2811
2825
  HostCapabilityPinV1: {
2812
2826
  publisher_id: components["schemas"]["HostCapabilityPinV1Id"];
@@ -15,6 +15,9 @@ export type PluginRequestOptions = {
15
15
  type PlatformSchemas = components["schemas"];
16
16
  export type PluginCatalogResult = PlatformSchemas["PluginCatalogResult"];
17
17
  export type PluginCatalogRecord = PluginCatalogResult["plugins"][number];
18
+ export type PluginSurfaceActionState = PlatformSchemas["PluginSurfaceActionState"];
19
+ /** Read the Host projection; never infer authorization from manifest permissions. */
20
+ export declare function getPluginSurfaceActionState(record: Pick<PluginCatalogRecord, "action_state">, surfaceId: string): PluginSurfaceActionState;
18
21
  export type PluginFeatures = PlatformSchemas["PluginFeaturesSuccessResponse"]["data"];
19
22
  export type PluginTrustState = PlatformSchemas["TrustState"];
20
23
  export type PluginTrustHashSet = PlatformSchemas["TrustHashSet"];
package/dist/platform.js CHANGED
@@ -2,6 +2,15 @@ import { assertMutationDispatchable, defaultFetch, dispatchMutationRequest, disp
2
2
  import { PluginBridgeError, PluginMutationLifecycleError, PluginTransportError, pluginMutationOutcome, } from "./errors.js";
3
3
  import { createReDevPluginSurfaceTransport, openPluginSurfaceInSlot, } from "./surface.js";
4
4
  import { defaultPluginSurfaceScope, disposePluginSurfaceScope, invalidatePluginSurfaceScope, } from "./surface-scope.js";
5
+ /** Read the Host projection; never infer authorization from manifest permissions. */
6
+ export function getPluginSurfaceActionState(record, surfaceId) {
7
+ const state = record.action_state;
8
+ if (state.surfaces !== undefined) {
9
+ return state.surfaces.find((surface) => surface.surface_id === surfaceId)
10
+ ?? { surface_id: surfaceId, can_open: false, blocked_reason: "incompatible" };
11
+ }
12
+ return { surface_id: surfaceId, can_open: state.can_open, ...(state.blocked_reason ? { blocked_reason: state.blocked_reason } : {}) };
13
+ }
5
14
  function toPluginSurfaceHostBootstrap(value) {
6
15
  return {
7
16
  pluginId: value.plugin_id,
@@ -0,0 +1,111 @@
1
+ import { type MessagePortLike, type MessageEventLike, type PluginJSONObject, type PluginUIElementVNode, type PluginSurfaceContext, type PluginUIActionEvent, type PluginBridgeClientOptions } from "./surface.js";
2
+ /** Business/bridge fixture only. It does not certify Host permissions or WASM execution. */
3
+ export type PluginTestCall = {
4
+ method: string;
5
+ input?: PluginJSONObject;
6
+ tree?: PluginUIElementVNode;
7
+ cancelled: boolean;
8
+ resolve(value: unknown): void;
9
+ reject(error: Error): void;
10
+ };
11
+ export type PluginTestHarnessOptions = {
12
+ context?: PluginSurfaceContext;
13
+ call?: (method: string, input?: PluginJSONObject) => unknown | Promise<unknown>;
14
+ };
15
+ export declare function createPluginTestContext(languageTag?: string, revision?: number): PluginSurfaceContext;
16
+ /** A deterministic transport adapter around the actual production bridge client. */
17
+ export declare function createPluginTestHarness(options?: PluginTestHarnessOptions): {
18
+ mode: "fixture";
19
+ BridgeClient: {
20
+ new (clientOptions?: PluginBridgeClientOptions): {
21
+ render(next: PluginUIElementVNode): Promise<void>;
22
+ readonly surfaceHandle: string;
23
+ readonly timeoutMs: number;
24
+ "__#private@#nextID": number;
25
+ "__#private@#port": MessagePortLike;
26
+ "__#private@#pending": Map<string, {
27
+ resolve: (value: unknown) => void;
28
+ reject: (reason: unknown) => void;
29
+ timer: ReturnType<typeof setTimeout> | undefined;
30
+ abortCleanup?: () => void;
31
+ kind: "json" | "canvas" | "asset";
32
+ identifier?: string;
33
+ }>;
34
+ "__#private@#actionHandlers": Map<string, Set<(event: PluginUIActionEvent) => void>>;
35
+ "__#private@#canvasInputHandlers": Map<string, Set<(event: import("./surface.js").PluginCanvasInputEvent) => void>>;
36
+ "__#private@#keyboardInputHandlers": Set<(event: import("./surface.js").PluginSurfaceKeyboardEvent) => void>;
37
+ "__#private@#lifecycleHandlers": Set<(event: import("./surface.js").BridgeLifecycleEvent) => Promise<void> | void>;
38
+ "__#private@#contextHandlers": Set<(context: PluginSurfaceContext) => void>;
39
+ "__#private@#context"?: PluginSurfaceContext;
40
+ "__#private@#ready": boolean;
41
+ "__#private@#readyPromise": Promise<void>;
42
+ "__#private@#resolveReady": () => void;
43
+ "__#private@#rejectReady": (reason: unknown) => void;
44
+ "__#private@#lifecycleState": "active" | "quiescing" | "quiesced" | "disposed";
45
+ "__#private@#handlingLifecycle": boolean;
46
+ "__#private@#renderRevision": number;
47
+ "__#private@#committedTree"?: PluginUIElementVNode;
48
+ "__#private@#pendingRender"?: {
49
+ tree: PluginUIElementVNode;
50
+ waiters: Array<{
51
+ resolve: () => void;
52
+ reject: (reason: unknown) => void;
53
+ }>;
54
+ };
55
+ "__#private@#renderLoop"?: Promise<void>;
56
+ "__#private@#controlEditRevisions": Map<string, number>;
57
+ "__#private@#onMessage": (event: MessageEventLike) => void;
58
+ ready(): Promise<void>;
59
+ call<T = unknown>(method: string, params?: PluginJSONObject, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<T>;
60
+ "__#private@#call"<T>(method: string, params: PluginJSONObject | undefined, options: import("./surface.js").PluginBridgeRequestOptions, mutation: boolean): Promise<T>;
61
+ executionEvents(executionID: string, afterCursor: number, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./surface.js").PluginExecutionEventList>;
62
+ cancelExecution(executionID: string, reason?: string, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<void>;
63
+ executionSnapshot(executionID: string, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./surface.js").PluginExecutionSnapshot>;
64
+ context(): PluginSurfaceContext | undefined;
65
+ onContext(handler: (context: PluginSurfaceContext) => void): () => void;
66
+ openCanvas(canvasId: string): Promise<import("./surface.js").PluginCanvasSurface>;
67
+ updateCanvasAccessibility(canvasId: string, state: import("./surface.js").PluginCanvasAccessibilityState): Promise<void>;
68
+ setKeyboardBindings(bindings: readonly import("./surface.js").PluginSurfaceKeyboardBinding[]): Promise<void>;
69
+ onKeyboardInput(handler: (event: import("./surface.js").PluginSurfaceKeyboardEvent) => void): () => void;
70
+ loadImageAsset(assetId: string): Promise<ImageBitmap>;
71
+ exportFile(request: import("./surface.js").PluginFileExportRequest, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<void>;
72
+ onAction(action: string, handler: (event: PluginUIActionEvent) => void): () => void;
73
+ onCanvasInput(canvasId: string, handler: (event: import("./surface.js").PluginCanvasInputEvent) => void): () => void;
74
+ onLifecycle(handler: (event: import("./surface.js").BridgeLifecycleEvent) => Promise<void> | void): () => void;
75
+ dispose(): void;
76
+ "__#private@#drainRenders"(): Promise<void>;
77
+ "__#private@#startRenderLoop"(): void;
78
+ "__#private@#request"<T>(id: string, message: unknown, options?: {
79
+ kind?: "canvas" | "json" | "asset";
80
+ identifier?: string;
81
+ cancellationKind?: "bridge" | "stream";
82
+ mutation?: boolean;
83
+ signal?: AbortSignal;
84
+ }): Promise<T>;
85
+ "__#private@#dispatchRequest"<T>(id: string, message: unknown, options: {
86
+ kind?: "canvas" | "json" | "asset";
87
+ identifier?: string;
88
+ cancellationKind?: "bridge" | "stream";
89
+ mutation?: boolean;
90
+ signal?: AbortSignal;
91
+ }, transfer?: readonly unknown[]): Promise<T>;
92
+ "__#private@#takePending"(id: string): {
93
+ resolve: (value: unknown) => void;
94
+ reject: (reason: unknown) => void;
95
+ timer: ReturnType<typeof setTimeout> | undefined;
96
+ abortCleanup?: () => void;
97
+ kind: "json" | "canvas" | "asset";
98
+ identifier?: string;
99
+ } | undefined;
100
+ "__#private@#handleMessage"(event: MessageEventLike): Promise<void>;
101
+ "__#private@#requestID"(prefix: string): string;
102
+ "__#private@#assertActive"(): void;
103
+ "__#private@#acceptsPluginWork"(): boolean;
104
+ };
105
+ };
106
+ calls: PluginTestCall[];
107
+ readonly tree: PluginUIElementVNode | undefined;
108
+ context(next: PluginSurfaceContext): void;
109
+ action(action: string, event?: Partial<PluginUIActionEvent>): void;
110
+ dispose(): void;
111
+ };
@@ -0,0 +1,83 @@
1
+ import { PluginBridgeClient } from "./surface.js";
2
+ import { PluginBridgeError } from "./errors.js";
3
+ class FixturePort {
4
+ dispatch;
5
+ listeners = new Set();
6
+ closed = false;
7
+ constructor(dispatch) {
8
+ this.dispatch = dispatch;
9
+ }
10
+ postMessage(message) { if (this.closed)
11
+ throw new Error("Fixture port is closed"); queueMicrotask(() => { if (!this.closed)
12
+ this.dispatch(message); }); }
13
+ addEventListener(_type, listener) { this.listeners.add(listener); }
14
+ removeEventListener(_type, listener) { this.listeners.delete(listener); }
15
+ start() { }
16
+ close() { this.closed = true; this.listeners.clear(); }
17
+ emit(data) { if (!this.closed)
18
+ for (const listener of this.listeners)
19
+ listener({ origin: "null", data }); }
20
+ }
21
+ export function createPluginTestContext(languageTag = "en-US", revision = 1) {
22
+ return { schema_version: "redevplugin.surface_context.v1", revision, appearance: { color_scheme: "dark", colors: {
23
+ canvas: "#101010", surface: "#181818", surface_elevated: "#202020", text: "#ffffff", text_muted: "#aaaaaa", border: "#444444", accent: "#66aaff", accent_text: "#000000", success: "#44cc88", warning: "#ffcc44", danger: "#ff6677", focus: "#88bbff",
24
+ } }, locale: { language_tag: languageTag, direction: "ltr" } };
25
+ }
26
+ /** A deterministic transport adapter around the actual production bridge client. */
27
+ export function createPluginTestHarness(options = {}) {
28
+ const calls = [], pending = new Map();
29
+ const clients = new Set();
30
+ let tree, disposed = false;
31
+ let context = options.context ?? createPluginTestContext();
32
+ const port = new FixturePort(message => {
33
+ const value = message;
34
+ if (value.type === "redevplugin.bridge.cancel") {
35
+ const call = pending.get(value.id);
36
+ if (call)
37
+ call.cancelled = true;
38
+ pending.delete(value.id);
39
+ return;
40
+ }
41
+ if (value.type === "redevplugin.ui.mount" || value.type === "redevplugin.ui.patch") {
42
+ port.emit({ type: "redevplugin.bridge.response", id: value.id, ok: true });
43
+ return;
44
+ }
45
+ if (value.type !== "redevplugin.bridge.call" || !value.request)
46
+ return;
47
+ const request = value.request;
48
+ if (calls.length >= 1024) {
49
+ port.emit({ type: "redevplugin.bridge.response", id: request.id, ok: false, error_code: "PLUGIN_INVALID_REQUEST", error: "Fixture call history exceeds 1024 entries" });
50
+ return;
51
+ }
52
+ const finish = (response) => { if (!pending.delete(request.id))
53
+ return; port.emit({ type: "redevplugin.bridge.response", id: request.id, ...response }); };
54
+ const call = { method: request.method, input: request.params, tree, cancelled: false,
55
+ resolve: value => finish({ ok: true, data: value }),
56
+ reject: failure => finish({ ok: false, error_code: failure instanceof PluginBridgeError ? failure.errorCode : "PLUGIN_CAPABILITY_ERROR", error: failure.message }),
57
+ };
58
+ calls.push(call);
59
+ pending.set(request.id, call);
60
+ if (options.call)
61
+ void Promise.resolve().then(() => options.call(call.method, call.input)).then(call.resolve, failure => call.reject(failure instanceof Error ? failure : new Error(String(failure))));
62
+ });
63
+ class BridgeClient extends PluginBridgeClient {
64
+ constructor(clientOptions = {}) {
65
+ if (clients.size || disposed)
66
+ throw new Error("A fixture owns exactly one bridge client");
67
+ super({ ...clientOptions, port, surfaceHandle: "surface_author_fixture" });
68
+ clients.add(this);
69
+ queueMicrotask(() => { port.emit({ type: "redevplugin.bridge.context", context }); port.emit({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } }); });
70
+ }
71
+ async render(next) { await super.render(next); tree = structuredClone(next); }
72
+ }
73
+ return {
74
+ mode: "fixture", BridgeClient, calls,
75
+ get tree() { return tree; },
76
+ context(next) { context = next; port.emit({ type: "redevplugin.bridge.context", context }); },
77
+ action(action, event = {}) {
78
+ port.emit({ type: "redevplugin.ui.action", action, event: event.event ?? "click", target_key: event.targetKey ?? "fixture-target", edit_revision: event.editRevision ?? 0, is_composing: event.isComposing ?? false, ...(event.value !== undefined ? { value: event.value } : {}), ...(event.checked !== undefined ? { checked: event.checked } : {}), ...(event.form_data ? { form_data: event.form_data } : {}) });
79
+ },
80
+ dispose() { disposed = true; for (const client of clients)
81
+ client.dispose(); clients.clear(); port.close(); pending.clear(); },
82
+ };
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "3.0.37",
3
+ "version": "3.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -34,13 +34,17 @@
34
34
  "./local-import": {
35
35
  "types": "./dist/local-import.d.ts",
36
36
  "default": "./dist/local-import.js"
37
+ },
38
+ "./testing": {
39
+ "types": "./dist/testing.d.ts",
40
+ "default": "./dist/testing.js"
37
41
  }
38
42
  },
39
43
  "files": [
40
44
  "dist"
41
45
  ],
42
46
  "dependencies": {
43
- "@floegence/redevplugin-contracts": "3.0.37",
47
+ "@floegence/redevplugin-contracts": "3.2.0",
44
48
  "@noble/hashes": "2.4.0"
45
49
  },
46
50
  "publishConfig": {