@floegence/redevplugin-ui 0.4.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/platform.js CHANGED
@@ -1,6 +1,8 @@
1
- import { defaultFetch, readHostEnvelope, trimTrailingSlash } from "./http.js";
2
- import { defaultPluginSurfaceScope, disposePluginSurfaceScope, } from "./surface-scope.js";
3
- export function toPluginSurfaceHostBootstrap(value) {
1
+ import { assertMutationDispatchable, defaultFetch, dispatchMutationRequest, dispatchQueryRequest, readMutationPlatformResponse, readPlatformResponse, trimTrailingSlash, } from "./http.js";
2
+ import { PluginBridgeError, PluginMutationLifecycleError, PluginTransportError, pluginMutationOutcome, } from "./errors.js";
3
+ import { createReDevPluginSurfaceTransport, openPluginSurfaceInSlot, } from "./surface.js";
4
+ import { defaultPluginSurfaceScope, disposePluginSurfaceScope, invalidatePluginSurfaceScope, } from "./surface-scope.js";
5
+ function toPluginSurfaceHostBootstrap(value) {
4
6
  return {
5
7
  pluginId: value.plugin_id,
6
8
  pluginInstanceId: value.plugin_instance_id,
@@ -13,7 +15,7 @@ export function toPluginSurfaceHostBootstrap(value) {
13
15
  entrySHA256: value.entry_sha256,
14
16
  assetTicket: value.asset_ticket,
15
17
  assetSessionNonce: value.asset_session_nonce,
16
- pluginStateVersion: value.plugin_state_version,
18
+ managementRevision: value.management_revision,
17
19
  revokeEpoch: value.revoke_epoch,
18
20
  runtimeGenerationId: value.runtime_generation_id,
19
21
  };
@@ -22,121 +24,320 @@ export class PluginPlatformClient {
22
24
  #fetch;
23
25
  #apiBaseURL;
24
26
  #surfaceScope;
27
+ #surfaceTransport;
28
+ #onMutationOutcomeUnknown;
25
29
  constructor(options = {}) {
26
30
  this.#fetch = options.fetch ?? defaultFetch();
27
31
  this.#apiBaseURL = trimTrailingSlash(options.apiBaseURL ?? "");
28
32
  this.#surfaceScope = options.surfaceScope ?? defaultPluginSurfaceScope;
33
+ this.#surfaceTransport = options.surfaceTransport;
34
+ this.#onMutationOutcomeUnknown = options.onMutationOutcomeUnknown;
29
35
  }
30
- catalog() { return this.#getJSON("/_redevplugin/api/plugins/catalog"); }
31
- getCompatibility() { return this.#getJSON("/_redevplugin/api/plugins/platform/compatibility"); }
32
- installReleaseRef(request) { return this.#postJSON("/_redevplugin/api/plugins/install-release-ref", request); }
33
- updateReleaseRef(request) { return this.#mutatePlugin("/_redevplugin/api/plugins/update-release-ref", request); }
34
- downgradePlugin(request) { return this.#mutatePlugin("/_redevplugin/api/plugins/downgrade", request); }
35
- enablePlugin(request) { return this.#postJSON("/_redevplugin/api/plugins/enable", request); }
36
- disablePlugin(request) { return this.#mutatePlugin("/_redevplugin/api/plugins/disable", request); }
37
- uninstallPlugin(request) { return this.#mutatePlugin("/_redevplugin/api/plugins/uninstall", request); }
38
- openSurface(request) { return this.#postJSON("/_redevplugin/api/plugins/surfaces/open", request); }
39
- async revokeSurfaceScope() {
36
+ catalog(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/catalog/query", {}, options); }
37
+ features(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/features/query", {}, options); }
38
+ getCompatibility(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/platform/compatibility/query", {}, options); }
39
+ installReleaseRef(request, options = {}) {
40
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/install-release-ref", request, options);
41
+ }
42
+ updateReleaseRef(request, options = {}) {
43
+ return this.#mutatePlugin("/_redevplugin/api/plugins/update-release-ref", request, options);
44
+ }
45
+ downgradePlugin(request, options = {}) {
46
+ return this.#mutatePlugin("/_redevplugin/api/plugins/downgrade", request, options);
47
+ }
48
+ enablePlugin(request, options = {}) {
49
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/enable", request, options);
50
+ }
51
+ disablePlugin(request, options = {}) {
52
+ return this.#mutatePlugin("/_redevplugin/api/plugins/disable", request, options);
53
+ }
54
+ uninstallPlugin(request, options = {}) {
55
+ return this.#mutatePlugin("/_redevplugin/api/plugins/uninstall", request, options);
56
+ }
57
+ #openSurface(request) {
58
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/surfaces/open", request, {});
59
+ }
60
+ openSurfaceInSlot(slot, request, options = {}) {
61
+ const { signal, ...hostOptions } = options;
62
+ const pluginInstanceId = typeof request.plugin_instance_id === "string"
63
+ ? request.plugin_instance_id.trim()
64
+ : "";
65
+ if (!pluginInstanceId) {
66
+ return Promise.reject(new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface opening requires a canonical plugin instance identifier"));
67
+ }
68
+ if (signal?.aborted) {
69
+ return Promise.reject(new PluginTransportError("Plugin surface opening was aborted before dispatch", signal.reason, "not_committed"));
70
+ }
71
+ const surfaceTransport = this.#surfaceTransport ??= createReDevPluginSurfaceTransport({
72
+ fetch: this.#fetch,
73
+ apiBaseURL: surfaceTransportAPIBaseURL(this.#apiBaseURL),
74
+ });
75
+ const canonicalRequest = {
76
+ ...request,
77
+ plugin_instance_id: pluginInstanceId,
78
+ };
79
+ return openPluginSurfaceInSlot(slot, {
80
+ pluginInstanceId,
81
+ surfaceScope: this.#surfaceScope,
82
+ signal,
83
+ abortError: () => new PluginTransportError("Plugin surface opening was aborted after dispatch", signal?.reason, "unknown"),
84
+ open: () => this.#openSurface(canonicalRequest).then((result) => ({
85
+ ...hostOptions,
86
+ bootstrap: toPluginSurfaceHostBootstrap(result),
87
+ hostTransport: surfaceTransport,
88
+ surfaceScope: this.#surfaceScope,
89
+ })),
90
+ });
91
+ }
92
+ async revokeSessionScope(options = {}) {
93
+ let result;
94
+ try {
95
+ const raw = await this.#requestMutation("POST", "/_redevplugin/api/plugins/session/revoke-scope", {}, options);
96
+ if (!isPluginSessionScopeRevokeResult(raw)) {
97
+ throw new PluginTransportError("Plugin session scope revocation returned an invalid result", new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Invalid session scope revocation result"), "unknown");
98
+ }
99
+ result = raw;
100
+ }
101
+ catch (error) {
102
+ await this.#handleSessionRevokeFailure(error);
103
+ throw error;
104
+ }
105
+ await invalidatePluginSurfaceScope(this.#surfaceScope);
106
+ return result;
107
+ }
108
+ startRuntime(request, options = {}) {
109
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/runtime/start", request, options);
110
+ }
111
+ stopRuntime(options = {}) {
112
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/runtime/stop", {}, options);
113
+ }
114
+ refreshEnabledRuntimeState(options = {}) {
115
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/runtime/refresh-enabled", {}, options);
116
+ }
117
+ runtimeHealth(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/runtime/health/query", {}, options); }
118
+ getSettingsSchema(pluginInstanceId, scope, options = {}) {
119
+ return this.#requestQuery(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/schema/query`, { scope }, options);
120
+ }
121
+ getSettings(pluginInstanceId, scope, options = {}) {
122
+ return this.#requestQuery(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/query`, { scope }, options);
123
+ }
124
+ patchSettings(pluginInstanceId, request, options = {}) {
125
+ return this.#mutatePluginAt("PATCH", `/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`, pluginInstanceId, request, options);
126
+ }
127
+ listOperations(options = {}, requestOptions = {}) {
128
+ return this.#requestQuery("/_redevplugin/api/plugins/operations/query", options, requestOptions);
129
+ }
130
+ getOperation(operationId, options = {}) {
131
+ return this.#requestQuery(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/query`, {}, options);
132
+ }
133
+ cancelOperation(operationId, reason, options = {}) {
134
+ return this.#requestMutation("POST", `/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/cancel`, reason ? { reason } : {}, options);
135
+ }
136
+ listIntents(options = {}, requestOptions = {}) {
137
+ return this.#requestQuery("/_redevplugin/api/plugins/intents/query", options, requestOptions);
138
+ }
139
+ invokeIntent(request, options = {}) {
140
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/intents/invoke", request, options);
141
+ }
142
+ exportData(request, options = {}) {
143
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/data/export", request, options);
144
+ }
145
+ deleteDataExport(request, options = {}) {
146
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/data/export/delete", request, options);
147
+ }
148
+ importData(request, options = {}) {
149
+ return this.#mutatePluginAt("POST", "/_redevplugin/api/plugins/data/import", request.plugin_instance_id, request, options);
150
+ }
151
+ listRetainedData(options = {}, requestOptions = {}) {
152
+ return this.#requestQuery("/_redevplugin/api/plugins/retained-data/query", options, requestOptions);
153
+ }
154
+ deleteRetainedData(request, options = {}) {
155
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/retained-data/delete", request, options);
156
+ }
157
+ bindRetainedData(request, options = {}) {
158
+ return this.#mutatePluginAt("POST", "/_redevplugin/api/plugins/retained-data/bind", request.target_plugin_instance_id, request, options);
159
+ }
160
+ cleanupExpiredRetainedData(request = {}, options = {}) {
161
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/retained-data/cleanup-expired", request, options);
162
+ }
163
+ listPermissions(options = {}, requestOptions = {}) {
164
+ return this.#requestQuery("/_redevplugin/api/plugins/permissions/query", options, requestOptions);
165
+ }
166
+ grantPermission(request, options = {}) {
167
+ return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/grant", request, options);
168
+ }
169
+ revokePermission(request, options = {}) {
170
+ return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/revoke", request, options);
171
+ }
172
+ listSecurityPolicies(options = {}) {
173
+ return this.#requestQuery("/_redevplugin/api/plugins/security-policies/query", {}, options);
174
+ }
175
+ getSecurityPolicy(pluginInstanceId, options = {}) {
176
+ return this.#requestQuery(`/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}/query`, {}, options);
177
+ }
178
+ putSecurityPolicy(pluginInstanceId, request, options = {}) {
179
+ return this.#mutatePluginAt("PUT", `/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}`, pluginInstanceId, request, options);
180
+ }
181
+ deleteSecurityPolicy(pluginInstanceId, request, options = {}) {
182
+ return this.#mutatePluginAt("DELETE", `/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}`, pluginInstanceId, request, options);
183
+ }
184
+ bindSecret(request, options = {}) {
185
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/secrets/bind", request, options);
186
+ }
187
+ testSecret(request, options = {}) {
188
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/secrets/test", request, options);
189
+ }
190
+ deleteSecret(request, options = {}) {
191
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/secrets/delete", request, options);
192
+ }
193
+ listDiagnosticEvents(options = {}, requestOptions = {}) {
194
+ return this.#requestQuery("/_redevplugin/api/plugins/diagnostics/query", options, requestOptions);
195
+ }
196
+ #mutatePlugin(path, request, options) {
197
+ return this.#mutatePluginAt("POST", path, request.plugin_instance_id, request, options);
198
+ }
199
+ async #mutatePluginAt(method, path, pluginInstanceId, request, options) {
200
+ let result;
40
201
  try {
41
- return await this.#postJSON("/_redevplugin/api/plugins/surfaces/revoke-scope", {});
202
+ result = await this.#requestMutation(method, path, request, options);
42
203
  }
43
- finally {
44
- disposePluginSurfaceScope(this.#surfaceScope);
204
+ catch (error) {
205
+ await this.#handleMutationFailure(error, pluginInstanceId, "Plugin mutation and local surface teardown failed");
206
+ throw error;
45
207
  }
208
+ await disposePluginSurfaceScope(this.#surfaceScope, pluginInstanceId);
209
+ return result;
46
210
  }
47
- startRuntime(request = {}) { return this.#postJSON("/_redevplugin/api/plugins/runtime/start", request); }
48
- async stopRuntime() {
211
+ async #handleMutationFailure(error, pluginInstanceId, message) {
212
+ const outcome = pluginMutationOutcome(error);
213
+ if (outcome === "not_committed" || outcome === undefined)
214
+ return;
215
+ const lifecycleErrors = [];
49
216
  try {
50
- return await this.#postJSON("/_redevplugin/api/plugins/runtime/stop", {});
51
- }
52
- finally {
53
- disposePluginSurfaceScope(this.#surfaceScope);
54
- }
55
- }
56
- refreshEnabledRuntimeState() { return this.#postJSON("/_redevplugin/api/plugins/runtime/refresh-enabled", {}); }
57
- runtimeHealth() { return this.#getJSON("/_redevplugin/api/plugins/runtime/health"); }
58
- getSettingsSchema(pluginInstanceId) { return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/schema`); }
59
- getSettings(pluginInstanceId) { return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`); }
60
- patchSettings(pluginInstanceId, values) { return this.#patchJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`, { values }); }
61
- listOperations(pluginInstanceId) { return this.#getJSON(`/_redevplugin/api/plugins/operations${pluginInstanceId ? `?plugin_instance_id=${encodeURIComponent(pluginInstanceId)}` : ""}`); }
62
- getOperation(operationId) { return this.#getJSON(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}`); }
63
- cancelOperation(operationId, reason) { return this.#postJSON(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/cancel`, reason ? { reason } : {}); }
64
- listIntents(options = {}) {
65
- const params = new URLSearchParams();
66
- if (options.intent_id)
67
- params.set("intent_id", options.intent_id);
68
- if (options.plugin_instance_id)
69
- params.set("plugin_instance_id", options.plugin_instance_id);
70
- const query = params.toString();
71
- return this.#getJSON(`/_redevplugin/api/plugins/intents${query ? `?${query}` : ""}`);
72
- }
73
- invokeIntent(request) {
74
- return this.#postJSON("/_redevplugin/api/plugins/intents/invoke", request);
75
- }
76
- exportData(request) { return this.#postJSON("/_redevplugin/api/plugins/data/export", request); }
77
- importData(request) { return this.#postJSON("/_redevplugin/api/plugins/data/import", request); }
78
- listRetainedData(options = {}) {
79
- const params = new URLSearchParams();
80
- if (options.publisher_id)
81
- params.set("publisher_id", options.publisher_id);
82
- if (options.plugin_id)
83
- params.set("plugin_id", options.plugin_id);
84
- if (options.source_plugin_instance_id)
85
- params.set("source_plugin_instance_id", options.source_plugin_instance_id);
86
- if (options.state)
87
- params.set("state", options.state);
88
- const query = params.toString();
89
- return this.#getJSON(`/_redevplugin/api/plugins/retained-data${query ? `?${query}` : ""}`);
90
- }
91
- deleteRetainedData(retainedId) { return this.#postJSON("/_redevplugin/api/plugins/retained-data/delete", { retained_id: retainedId }); }
92
- bindRetainedData(request) { return this.#postJSON("/_redevplugin/api/plugins/retained-data/bind", request); }
93
- cleanupExpiredRetainedData(request = {}) { return this.#postJSON("/_redevplugin/api/plugins/retained-data/cleanup-expired", request); }
94
- listPermissions(pluginInstanceId, activeOnly) {
95
- const params = new URLSearchParams();
96
- if (pluginInstanceId)
97
- params.set("plugin_instance_id", pluginInstanceId);
98
- if (activeOnly != null)
99
- params.set("active_only", activeOnly ? "true" : "false");
100
- const query = params.toString();
101
- return this.#getJSON(`/_redevplugin/api/plugins/permissions${query ? `?${query}` : ""}`);
102
- }
103
- grantPermission(request) { return this.#postJSON("/_redevplugin/api/plugins/permissions/grant", request); }
104
- revokePermission(request) { return this.#postJSON("/_redevplugin/api/plugins/permissions/revoke", request); }
105
- bindSecret(request) { return this.#postJSON("/_redevplugin/api/plugins/secrets/bind", request); }
106
- testSecret(request) { return this.#postJSON("/_redevplugin/api/plugins/secrets/test", request); }
107
- deleteSecret(request) { return this.#postJSON("/_redevplugin/api/plugins/secrets/delete", request); }
108
- listAuditEvents(options = {}) { return this.#getJSON(`/_redevplugin/api/plugins/audit${queryString(options)}`); }
109
- listDiagnosticEvents(options = {}) { return this.#getJSON(`/_redevplugin/api/plugins/diagnostics${queryString(options)}`); }
110
- #getJSON(path) { return this.#requestJSON("GET", path); }
111
- #postJSON(path, body) { return this.#requestJSON("POST", path, body); }
112
- #patchJSON(path, body) { return this.#requestJSON("PATCH", path, body); }
113
- async #mutatePlugin(path, request) {
217
+ await disposePluginSurfaceScope(this.#surfaceScope, pluginInstanceId);
218
+ }
219
+ catch (caught) {
220
+ lifecycleErrors.push(caught);
221
+ }
222
+ if (outcome === "unknown") {
223
+ try {
224
+ this.#onMutationOutcomeUnknown?.(pluginInstanceId);
225
+ }
226
+ catch (caught) {
227
+ lifecycleErrors.push(caught);
228
+ }
229
+ }
230
+ if (lifecycleErrors.length > 0)
231
+ throw new PluginMutationLifecycleError(message, error, lifecycleErrors);
232
+ }
233
+ async #handleSessionRevokeFailure(error) {
234
+ const outcome = pluginMutationOutcome(error);
235
+ if (outcome === "not_committed")
236
+ return;
237
+ const lifecycleErrors = [];
238
+ if (outcome === "committed" || outcome === "unknown") {
239
+ try {
240
+ await invalidatePluginSurfaceScope(this.#surfaceScope);
241
+ }
242
+ catch (caught) {
243
+ lifecycleErrors.push(caught);
244
+ }
245
+ }
246
+ if (outcome === "unknown") {
247
+ try {
248
+ this.#onMutationOutcomeUnknown?.();
249
+ }
250
+ catch (caught) {
251
+ lifecycleErrors.push(caught);
252
+ }
253
+ }
254
+ if (lifecycleErrors.length > 0) {
255
+ throw new PluginMutationLifecycleError("Plugin session scope revocation and local invalidation failed", error, lifecycleErrors);
256
+ }
257
+ }
258
+ async #requestQuery(path, body, options) {
259
+ const operation = `POST ${path}`;
260
+ let encodedBody;
114
261
  try {
115
- return await this.#postJSON(path, request);
262
+ encodedBody = JSON.stringify(body);
116
263
  }
117
- finally {
118
- disposePluginSurfaceScope(this.#surfaceScope, request.plugin_instance_id);
264
+ catch (cause) {
265
+ throw new PluginTransportError(`Plugin platform query body serialization failed for ${operation}`, cause);
119
266
  }
267
+ const response = await dispatchQueryRequest(this.#fetch, this.#apiBaseURL + path, {
268
+ method: "POST",
269
+ headers: { "Accept": "application/json", "Content-Type": "application/json" },
270
+ body: encodedBody,
271
+ credentials: "same-origin",
272
+ signal: options.signal,
273
+ }, operation);
274
+ return readPlatformResponse(response);
120
275
  }
121
- async #requestJSON(method, path, body) {
122
- const headers = { "Accept": "application/json" };
123
- if (body !== undefined)
124
- headers["Content-Type"] = "application/json";
125
- const response = await this.#fetch(this.#apiBaseURL + path, {
276
+ async #requestMutation(method, path, body, options) {
277
+ const operation = `${method} ${path}`;
278
+ assertMutationDispatchable(options.signal, operation);
279
+ const headers = {
280
+ "Accept": "application/json",
281
+ "Content-Type": "application/json",
282
+ };
283
+ let encodedBody;
284
+ try {
285
+ encodedBody = JSON.stringify(body);
286
+ }
287
+ catch (cause) {
288
+ throw new PluginTransportError(`Plugin platform request body serialization failed for ${operation}`, cause, "not_committed");
289
+ }
290
+ const response = await dispatchMutationRequest(this.#fetch, this.#apiBaseURL + path, {
126
291
  method,
127
292
  headers,
128
- body: body === undefined ? undefined : JSON.stringify(body),
293
+ body: encodedBody,
129
294
  credentials: "same-origin",
130
- });
131
- return readHostEnvelope(response, "PLUGIN_PLATFORM_REQUEST_FAILED");
295
+ signal: options.signal,
296
+ }, operation);
297
+ return readMutationPlatformResponse(response);
132
298
  }
133
299
  }
134
- function queryString(values) {
135
- const params = new URLSearchParams();
136
- for (const [key, value] of Object.entries(values)) {
137
- if (value != null && value !== "")
138
- params.set(key, String(value));
139
- }
140
- const query = params.toString();
141
- return query ? `?${query}` : "";
300
+ const sessionScopeCountKeys = [
301
+ "surfaces",
302
+ "asset_tickets",
303
+ "asset_sessions",
304
+ "plugin_gateway_tokens",
305
+ "confirmation_tokens",
306
+ "stream_tickets",
307
+ "handle_grants",
308
+ "confirmations",
309
+ "operations",
310
+ "streams",
311
+ "runtime_executions",
312
+ "active_network_requests",
313
+ "sockets",
314
+ "network_streams",
315
+ "storage_hostcalls",
316
+ ];
317
+ function isPluginSessionScopeRevokeResult(value) {
318
+ if (!isExactRecord(value, ["state", "fenced", "complete", "counts"]) ||
319
+ value.state !== "complete" || value.fenced !== true || value.complete !== true)
320
+ return false;
321
+ const counts = value.counts;
322
+ if (!isExactRecord(counts, sessionScopeCountKeys))
323
+ return false;
324
+ return sessionScopeCountKeys.every((key) => Number.isSafeInteger(counts[key]) && Number(counts[key]) >= 0);
325
+ }
326
+ function isExactRecord(value, keys) {
327
+ if (typeof value !== "object" || value === null || Array.isArray(value))
328
+ return false;
329
+ const actual = Object.keys(value).sort();
330
+ const expected = [...keys].sort();
331
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
332
+ }
333
+ function surfaceTransportAPIBaseURL(value) {
334
+ if (!value || value.startsWith("/"))
335
+ return value;
336
+ const parsed = new URL(value);
337
+ const currentOrigin = globalThis.location?.origin;
338
+ if (!currentOrigin || currentOrigin === "null" || parsed.origin !== currentOrigin ||
339
+ parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") {
340
+ throw new TypeError("Plugin surface transport apiBaseURL must be same-origin");
341
+ }
342
+ return trimTrailingSlash(parsed.pathname);
142
343
  }
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { PluginBridgeClient } from "./surface.js";
2
2
  export { PluginBridgeError } from "./errors.js";
3
3
  export { callCapabilityOperation, callCapabilityStream, callCapabilitySync, isCapabilityBusinessError, } from "./capability-client.js";
4
- export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginStreamTerminalStatus, PluginUIActionEvent, PluginUIAttributeValue, PluginUIVNode, } from "./surface.js";
5
- export type { PluginCapabilitySchema, PluginCapabilityStreamEvent, PluginCapabilityStreamReadResult, PluginOperation, PluginStream, } from "./capability-client.js";
4
+ export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginStreamTerminalStatus, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
5
+ export type { PluginCapabilitySchema, PluginCapabilityEffect, PluginCapabilityOperationContract, PluginCapabilityStreamContract, PluginCapabilitySyncContract, PluginCapabilityStreamEvent, PluginCapabilityStreamReadResult, PluginOperation, PluginStream, } from "./capability-client.js";
@@ -4,5 +4,6 @@ export declare class PluginSurfaceScope {
4
4
  }
5
5
  export declare function createPluginSurfaceScope(): PluginSurfaceScope;
6
6
  export declare const defaultPluginSurfaceScope: PluginSurfaceScope;
7
- export declare function registerPluginSurface(scope: PluginSurfaceScope, pluginInstanceId: string, dispose: () => void): () => void;
8
- export declare function disposePluginSurfaceScope(scope: PluginSurfaceScope, pluginInstanceId?: string): void;
7
+ export declare function registerPluginSurface(scope: PluginSurfaceScope, pluginInstanceId: string, dispose: () => Promise<void> | void, invalidate: () => Promise<void> | void): () => void;
8
+ export declare function disposePluginSurfaceScope(scope: PluginSurfaceScope, pluginInstanceId?: string): Promise<void>;
9
+ export declare function invalidatePluginSurfaceScope(scope: PluginSurfaceScope): Promise<void>;
@@ -1,7 +1,13 @@
1
- const registrations = new WeakMap();
1
+ const scopes = new WeakMap();
2
+ function canonicalPluginInstanceId(pluginInstanceId) {
3
+ const canonical = typeof pluginInstanceId === "string" ? pluginInstanceId.trim() : "";
4
+ if (!canonical)
5
+ throw new TypeError("Plugin instance identifier must be a non-empty string");
6
+ return canonical;
7
+ }
2
8
  export class PluginSurfaceScope {
3
9
  constructor() {
4
- registrations.set(this, new Map());
10
+ scopes.set(this, { invalidated: false, registrations: new Map() });
5
11
  }
6
12
  static create() {
7
13
  return new PluginSurfaceScope();
@@ -11,21 +17,56 @@ export function createPluginSurfaceScope() {
11
17
  return PluginSurfaceScope.create();
12
18
  }
13
19
  export const defaultPluginSurfaceScope = createPluginSurfaceScope();
14
- export function registerPluginSurface(scope, pluginInstanceId, dispose) {
15
- const state = registrations.get(scope);
20
+ export function registerPluginSurface(scope, pluginInstanceId, dispose, invalidate) {
21
+ const state = scopes.get(scope);
16
22
  if (!state)
17
23
  throw new TypeError("Plugin surface scope is invalid");
18
- const registration = Symbol(pluginInstanceId);
19
- state.set(registration, { pluginInstanceId, dispose });
20
- return () => state.delete(registration);
24
+ const canonicalPluginId = canonicalPluginInstanceId(pluginInstanceId);
25
+ const registration = Symbol(canonicalPluginId);
26
+ if (state.invalidated) {
27
+ try {
28
+ void Promise.resolve(invalidate()).catch(() => undefined);
29
+ }
30
+ catch {
31
+ // A fenced session remains invalid even when a local observer fails.
32
+ }
33
+ return () => undefined;
34
+ }
35
+ state.registrations.set(registration, { pluginInstanceId: canonicalPluginId, dispose, invalidate });
36
+ return () => state.registrations.delete(registration);
21
37
  }
22
- export function disposePluginSurfaceScope(scope, pluginInstanceId) {
23
- const state = registrations.get(scope);
38
+ export async function disposePluginSurfaceScope(scope, pluginInstanceId) {
39
+ const state = scopes.get(scope);
24
40
  if (!state)
25
41
  throw new TypeError("Plugin surface scope is invalid");
26
- const selected = [...state.entries()].filter(([, registration]) => pluginInstanceId === undefined || registration.pluginInstanceId === pluginInstanceId);
42
+ const canonicalPluginId = pluginInstanceId === undefined
43
+ ? undefined
44
+ : canonicalPluginInstanceId(pluginInstanceId);
45
+ const selected = [...state.registrations.entries()].filter(([, registration]) => canonicalPluginId === undefined || registration.pluginInstanceId === canonicalPluginId);
27
46
  for (const [key] of selected)
28
- state.delete(key);
29
- for (const [, registration] of selected)
30
- registration.dispose();
47
+ state.registrations.delete(key);
48
+ const results = await Promise.allSettled(selected.map(([, registration]) => Promise.resolve().then(() => registration.dispose())));
49
+ const failures = results
50
+ .filter((result) => result.status === "rejected")
51
+ .map((result) => result.reason);
52
+ if (failures.length === 1)
53
+ throw failures[0];
54
+ if (failures.length > 1)
55
+ throw new AggregateError(failures, "Plugin surface scope teardown failed");
56
+ }
57
+ export async function invalidatePluginSurfaceScope(scope) {
58
+ const state = scopes.get(scope);
59
+ if (!state)
60
+ throw new TypeError("Plugin surface scope is invalid");
61
+ state.invalidated = true;
62
+ const selected = [...state.registrations.values()];
63
+ state.registrations.clear();
64
+ const results = await Promise.allSettled(selected.map((registration) => Promise.resolve().then(() => registration.invalidate())));
65
+ const failures = results
66
+ .filter((result) => result.status === "rejected")
67
+ .map((result) => result.reason);
68
+ if (failures.length === 1)
69
+ throw failures[0];
70
+ if (failures.length > 1)
71
+ throw new AggregateError(failures, "Plugin surface scope invalidation failed");
31
72
  }