@floegence/redevplugin-ui 3.4.0 → 3.5.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.
@@ -1,5 +1,6 @@
1
1
  import { type FetchLike } from "./http.js";
2
2
  import type { components } from "./openapi.gen.js";
3
+ import type { PluginFileAccessConfirmation } from "./files.js";
3
4
  import { type PluginConfirmationHandler, type PluginWebOriginPreparationHandler, type PluginSurfaceHost, type PluginSurfaceContext, type PluginSurfaceOpeningProgress, type PluginSurfaceInteractionEvent, type PluginSurfaceFileExportHandler, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
4
5
  import { type PluginSurfaceScope } from "./surface-scope.js";
5
6
  export type PluginPlatformClientOptions = {
@@ -41,6 +42,7 @@ export type PluginDisableRequest = PlatformSchemas["DisableRequest"];
41
42
  export type PluginUninstallRequest = PlatformSchemas["UninstallRequest"];
42
43
  export type PluginOpenSurfaceRequest = PlatformSchemas["OpenSurfaceRequest"];
43
44
  export type PluginOpenSurfaceInSlotOptions = PluginRequestOptions & {
45
+ confirmFileAccess?: PluginFileAccessConfirmation;
44
46
  bridgeChannelId?: string;
45
47
  loadTimeoutMs?: number;
46
48
  requestTimeoutMs?: number;
@@ -170,6 +172,15 @@ export declare class PluginPlatformClient {
170
172
  listRetainedData(options?: PluginRetainedDataListOptions, requestOptions?: PluginRequestOptions): Promise<PluginRetainedDataList>;
171
173
  deleteRetainedData(request: PluginRetainedDataDeleteRequest, options?: PluginRequestOptions): Promise<PluginDataBinding>;
172
174
  cleanupExpiredRetainedData(request?: PluginRetainedDataCleanupRequest, options?: PluginRequestOptions): Promise<PluginRetainedDataCleanupResult>;
175
+ selectFileResource(request: PlatformSchemas["FileSelectRequest"], options?: PluginRequestOptions): Promise<PlatformSchemas["SelectFileResourceSuccessResponse"]["data"]>;
176
+ approveFileResource(request: PlatformSchemas["FileReviewRequest"], options?: PluginRequestOptions): Promise<PlatformSchemas["FileResource"]>;
177
+ cancelFileResource(request: PlatformSchemas["FileReviewRequest"], options?: PluginRequestOptions): Promise<{
178
+ canceled: true;
179
+ }>;
180
+ listFileResources(request: PlatformSchemas["FileListRequest"], options?: PluginRequestOptions): Promise<PlatformSchemas["ListFileResourcesSuccessResponse"]["data"]>;
181
+ revokeFileResource(request: PlatformSchemas["FileRevokeRequest"], options?: PluginRequestOptions): Promise<{
182
+ revoked: true;
183
+ }>;
173
184
  listPermissions(options?: PluginPermissionListOptions, requestOptions?: PluginRequestOptions): Promise<PluginPermissionList>;
174
185
  getPermissionRequirements(request: PluginPermissionRequirementsRequest, options?: PluginRequestOptions): Promise<PluginPermissionRequirements>;
175
186
  grantPermission(request: PluginPermissionGrantRequest, options?: PluginRequestOptions): Promise<PluginPermissionMutationResult>;
package/dist/platform.js CHANGED
@@ -261,6 +261,21 @@ export class PluginPlatformClient {
261
261
  cleanupExpiredRetainedData(request = {}, options = {}) {
262
262
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/retained-data/cleanup-expired", request, options);
263
263
  }
264
+ selectFileResource(request, options = {}) {
265
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/file-resources/select", request, options);
266
+ }
267
+ approveFileResource(request, options = {}) {
268
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/file-resources/approve", request, options);
269
+ }
270
+ cancelFileResource(request, options = {}) {
271
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/file-resources/cancel", request, options);
272
+ }
273
+ listFileResources(request, options = {}) {
274
+ return this.#requestQuery("/_redevplugin/api/plugins/file-resources/query", request, options);
275
+ }
276
+ revokeFileResource(request, options = {}) {
277
+ return this.#requestMutation("POST", "/_redevplugin/api/plugins/file-resources/revoke", request, options);
278
+ }
264
279
  listPermissions(options = {}, requestOptions = {}) {
265
280
  return this.#requestQuery("/_redevplugin/api/plugins/permissions/query", options, requestOptions);
266
281
  }
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { PluginBridgeClient } from "./surface.js";
2
+ export { PluginFilesClient, PluginFileHandle, PluginFileWriteError } from "./files.js";
3
+ export type { PluginFileResource, PluginFileSelectionOptions, PluginFileOpenOptions, PluginFileStat, PluginDirectoryEntry } from "./files.js";
2
4
  export { PluginBridgeError } from "./errors.js";
3
5
  export { pluginAPIMajor } from "./contracts.gen.js";
4
6
  export { createPluginProcessClient } from "./process.js";
package/dist/plugin.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { PluginBridgeClient } from "./surface.js";
2
+ export { PluginFilesClient, PluginFileHandle, PluginFileWriteError } from "./files.js";
2
3
  export { PluginBridgeError } from "./errors.js";
3
4
  export { pluginAPIMajor } from "./contracts.gen.js";
4
5
  export { createPluginProcessClient } from "./process.js";
package/dist/surface.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { PluginFilesClient, type PluginFileAccessConfirmation } from "./files.js";
1
2
  import { PluginBridgeError, type PluginMutationOutcome } from "./errors.js";
2
3
  import { type FetchLike } from "./http.js";
3
4
  import { type PluginSurfaceScope } from "./surface-scope.js";
@@ -242,6 +243,7 @@ export type PluginBridgeRequestOptions = {
242
243
  };
243
244
  export declare class PluginBridgeClient {
244
245
  #private;
246
+ readonly files: PluginFilesClient;
245
247
  readonly surfaceHandle: string;
246
248
  readonly timeoutMs: number;
247
249
  constructor(options?: PluginBridgeClientOptions);
@@ -380,6 +382,7 @@ export type PluginWebOriginPreparation = Readonly<{
380
382
  /** Prepare only the allocated route; return no URL, token, or alternate bootstrap. */
381
383
  export type PluginWebOriginPreparationHandler = (request: PluginWebOriginPreparation) => void | Promise<void>;
382
384
  export type PluginSurfaceHostOptions = {
385
+ confirmFileAccess?: PluginFileAccessConfirmation;
383
386
  bootstrap: PluginSurfaceHostBootstrap;
384
387
  hostTransport: ReDevPluginSurfaceTransport;
385
388
  surfaceScope?: PluginSurfaceScope;
package/dist/surface.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { sha256 } from "@noble/hashes/sha2.js";
2
+ import { PluginFilesClient, isPluginFileResource, isPluginFileResourceReview, validFileIORequest } from "./files.js";
2
3
  import { bytesToHex } from "@noble/hashes/utils.js";
3
4
  import { PluginBridgeError, PluginPlatformRequestError, PluginTransportError, pluginBridgeErrorCodes, } from "./errors.js";
4
5
  import { pluginBridgeCapabilityEffect, } from "./bridge-capability.js";
@@ -29,6 +30,7 @@ const hostCapabilityIDPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._-]*$");
29
30
  const lowercaseSHA256Pattern = new RegExp("^[0-9a-f]{64}$");
30
31
  const businessErrorCodePattern = new RegExp("^[A-Z][A-Z0-9_]*$");
31
32
  export class PluginBridgeClient {
33
+ files = new PluginFilesClient((request, options) => this.#fileIO(request, options));
32
34
  surfaceHandle;
33
35
  timeoutMs;
34
36
  #nextID = 1;
@@ -74,6 +76,13 @@ export class PluginBridgeClient {
74
76
  const effect = options[pluginBridgeCapabilityEffect];
75
77
  return this.#call(method, params, options, effect === undefined || effect !== "read");
76
78
  }
79
+ #fileIO(request, options = {}) {
80
+ this.#assertActive();
81
+ if (!validFileIORequest(request))
82
+ throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "File operation is invalid");
83
+ const id = this.#requestID("file");
84
+ return this.#request(id, { type: "redevplugin.bridge.file_io", id, request }, { mutation: true, signal: options.signal });
85
+ }
77
86
  #call(method, params, options, mutation) {
78
87
  this.#assertActive();
79
88
  if (!validMethod(method)) {
@@ -573,6 +582,7 @@ class PluginSurfaceHostImplementation {
573
582
  #leaseRenewalLeadMs;
574
583
  #reloadLimiter;
575
584
  #confirm;
585
+ #confirmFileAccess;
576
586
  #prepareWebOrigin;
577
587
  #onOpeningProgress;
578
588
  #onInteraction;
@@ -662,6 +672,7 @@ class PluginSurfaceHostImplementation {
662
672
  this.#leaseRenewalLeadMs = normalizeLeaseRenewalLead(options.leaseRenewalLeadMs);
663
673
  this.#reloadLimiter = options.reloadLimiter ?? new PluginSurfaceReloadLimiter();
664
674
  this.#confirm = options.confirm;
675
+ this.#confirmFileAccess = options.confirmFileAccess;
665
676
  this.#prepareWebOrigin = options.prepareWebOrigin;
666
677
  this.#onOpeningProgress = options.onOpeningProgress;
667
678
  this.#onInteraction = options.onInteraction;
@@ -1073,6 +1084,12 @@ class PluginSurfaceHostImplementation {
1073
1084
  await this.#handleCall(data.request);
1074
1085
  return;
1075
1086
  }
1087
+ if (hasExactKeys(data, ["type", "id", "request", "frame_generation_id", "surface_handle"]) && data.type === "redevplugin.surface.file_io" && validBridgeRequestID(data.id, "file") && validFileIORequest(data.request)) {
1088
+ if (data.frame_generation_id !== this.frameGenerationId || data.surface_handle !== this.surfaceHandle)
1089
+ return;
1090
+ await this.#handleFileIO(data.id, data.request);
1091
+ return;
1092
+ }
1076
1093
  if (isExecutionCancelMessage(data)) {
1077
1094
  await this.#handleExecutionCancel(data);
1078
1095
  return;
@@ -1184,6 +1201,61 @@ class PluginSurfaceHostImplementation {
1184
1201
  this.#pendingRequestControllers.delete(request.id);
1185
1202
  }
1186
1203
  }
1204
+ async #handleFileIO(id, request) {
1205
+ if (!this.#ready || !this.#bridgeReady || !this.#gatewayToken) {
1206
+ this.#postError(id, "PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "File operation arrived before the surface became ready");
1207
+ return;
1208
+ }
1209
+ if (request.operation === "select" && !this.#confirmFileAccess) {
1210
+ this.#postError(id, "PLUGIN_FEATURE_NOT_CONFIGURED", "File access confirmation is not configured");
1211
+ return;
1212
+ }
1213
+ const controller = this.#registerPendingRequest(id);
1214
+ let reviewID;
1215
+ let approvalSent = false;
1216
+ try {
1217
+ let result = await this.#postMutationJSON("/_redevplugin/api/plugins/file-resources/surface-io", () => ({ plugin_instance_id: this.bootstrap.pluginInstanceId, surface_instance_id: this.bootstrap.surfaceInstanceId, bridge_channel_id: this.bridgeChannelId, plugin_gateway_token: this.#gatewayToken, ...request }), controller.signal);
1218
+ if (request.operation === "select" && result.canceled !== true) {
1219
+ if (!isPluginFileResourceReview(result.review) || result.review.plugin_instance_id !== this.bootstrap.pluginInstanceId)
1220
+ throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "File review is invalid");
1221
+ reviewID = result.review.review_id;
1222
+ const approved = await abortableConfirmationDecision(this.#confirmFileAccess({ ...result.review, signal: controller.signal }), controller.signal);
1223
+ if (controller.signal.aborted || this.#disposed)
1224
+ return;
1225
+ if (!confirmationDecisionAccepted(approved)) {
1226
+ await this.#postMutationJSON("/_redevplugin/api/plugins/file-resources/cancel", { plugin_instance_id: this.bootstrap.pluginInstanceId, review_id: reviewID }, controller.signal);
1227
+ reviewID = undefined;
1228
+ result = { canceled: true };
1229
+ }
1230
+ else {
1231
+ approvalSent = true;
1232
+ const resource = await this.#postMutationJSON("/_redevplugin/api/plugins/file-resources/approve", { plugin_instance_id: this.bootstrap.pluginInstanceId, review_id: reviewID }, controller.signal);
1233
+ if (!isPluginFileResource(resource))
1234
+ throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Approved file resource is invalid");
1235
+ reviewID = undefined;
1236
+ result = { resource };
1237
+ }
1238
+ }
1239
+ if (!controller.signal.aborted && !this.#disposed)
1240
+ this.#postResponse(id, result);
1241
+ }
1242
+ catch (error) {
1243
+ if (controller.signal.aborted || this.#disposed)
1244
+ return;
1245
+ const failure = toBridgeError(error, "PLUGIN_INTERNAL_FAILURE");
1246
+ this.#postError(id, failure.errorCode, failure.message, failure.details, failure.mutationOutcome);
1247
+ }
1248
+ finally {
1249
+ if (reviewID && !approvalSent) {
1250
+ try {
1251
+ await this.#fetchJSONRequest("/_redevplugin/api/plugins/file-resources/cancel", { plugin_instance_id: this.bootstrap.pluginInstanceId, review_id: reviewID }, { mutation: true, independentLifecycle: true });
1252
+ }
1253
+ catch { /* Expired reviews are reclaimed by Host maintenance. */ }
1254
+ }
1255
+ if (this.#pendingRequestControllers.get(id) === controller)
1256
+ this.#pendingRequestControllers.delete(id);
1257
+ }
1258
+ }
1187
1259
  async #handleConfirmationRequired(request, originalError, signal) {
1188
1260
  if (!this.#confirm) {
1189
1261
  this.#postError(request.id, originalError.errorCode, originalError.message, originalError.details, originalError.mutationOutcome);
@@ -2421,6 +2493,8 @@ function isBridgeResponse(value) {
2421
2493
  return isCapabilityBusinessErrorDetails(value.error_details);
2422
2494
  if (value.error_code === "PLUGIN_WORKER_ERROR")
2423
2495
  return isWorkerErrorDetails(value.error_details);
2496
+ if (value.error_code === "PLUGIN_FILE_OPERATION_FAILED")
2497
+ return hasExactKeys(value.error_details, ["file_error_code", "file_retryable"]) && typeof value.error_details.file_retryable === "boolean" && ["INVALID_ARGUMENT", "PERMISSION_DENIED", "NOT_FOUND", "NOT_EMPTY", "ALREADY_EXISTS", "RESOURCE_CLOSED", "RESOURCE_LIMIT", "CROSS_DEVICE", "MOUNT_UNAVAILABLE", "RUNTIME_UNAVAILABLE", "CANCELED", "TIMEOUT", "IO_ERROR"].includes(String(value.error_details.file_error_code));
2424
2498
  if (value.error_details === undefined)
2425
2499
  return true;
2426
2500
  try {
package/dist/testing.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare function createPluginTestHarness(options?: PluginTestHarnessOptio
18
18
  mode: "fixture";
19
19
  BridgeClient: {
20
20
  new (clientOptions?: PluginBridgeClientOptions): {
21
+ readonly files: import("./files.js").PluginFilesClient;
21
22
  readonly surfaceHandle: string;
22
23
  readonly timeoutMs: number;
23
24
  "__#private@#nextID": number;
@@ -45,6 +46,7 @@ export declare function createPluginTestHarness(options?: PluginTestHarnessOptio
45
46
  "__#private@#onMessage": (event: MessageEventLike) => void;
46
47
  ready(): Promise<void>;
47
48
  call<T = unknown>(method: string, params?: PluginJSONObject, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<T>;
49
+ "__#private@#fileIO"(request: import("./files.js").PluginFileIORequest, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./files.js").PluginFileIOResult>;
48
50
  "__#private@#call"<T>(method: string, params: PluginJSONObject | undefined, options: import("./surface.js").PluginBridgeRequestOptions, mutation: boolean): Promise<T>;
49
51
  executionEvents(executionID: string, afterCursor: number, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./surface.js").PluginExecutionEventList>;
50
52
  cancelExecution(executionID: string, reason?: string, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<void>;
@@ -5,3 +5,4 @@ export { PluginSurfaceScope, createPluginSurfaceScope } from "./surface-scope.js
5
5
  export { PluginSurfaceReloadLimiter, PluginSurfaceSlot, createReDevPluginSurfaceTransport, defaultPluginSurfaceReloadMax, defaultPluginSurfaceReloadWindowMs, isPluginRiskPlan, webSurfaceDocumentSchemaVersion, pluginSurfaceContextSchemaVersion, pluginRiskPlanSchemaVersion, pluginSurfaceFileExportMaxBytes, pluginSurfaceFileExportMediaTypes, trustedParentBridgeHandshakeTranscriptSHA256, } from "./surface.js";
6
6
  export type { BridgeLifecycleEvent, MessageChannelLike, MessageEventLike, MessagePortLike, WebSurfaceDocument, PluginConfirmationDecision, PluginConfirmationHandler, PluginConfirmationIntent, PluginConfirmationPlan, PluginMethodResult, PluginRiskEffect, PluginRiskFlag, PluginRiskPlan, PluginRiskSeverity, PluginExecutionEvent, PluginSurfaceHost, PluginWebOriginPreparation, PluginWebOriginPreparationHandler, PluginSurfaceCloseResult, PluginSurfaceOpeningProgress, PluginSurfaceOpeningStage, PluginSurfaceOpeningMilestone, PluginSurfaceInteractionEvent, PluginSurfaceInteractionKind, PluginSurfaceFileExport, PluginSurfaceFileExportHandler, PluginSurfaceFileExportMediaType, PluginSurfaceQuiesceResult, PluginSurfaceRevocationReconciliation, PluginSurfacePreparationResult, PluginSurfaceContext, PluginSurfaceReloadDecision, PluginSurfaceReloadLimiterOptions, PluginSurfaceReloadState, PluginSurfaceSlotOptions, PluginSurfaceSlotState, ReDevPluginSurfaceTransport, ReDevPluginSurfaceTransportOptions, TrustedParentBridgeHandshake, TrustedParentBridgeTokenRequest, WindowLike, } from "./surface.js";
7
7
  export type { FetchInitLike, FetchLike, FetchResponseLike, PlatformResponse } from "./http.js";
8
+ export type { PluginFileAccessConfirmation, PluginFileResourceReview, PluginFileResource } from "./files.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@floegence/redevplugin-contracts": "3.4.0",
39
+ "@floegence/redevplugin-contracts": "3.5.0",
40
40
  "@noble/hashes": "2.4.0"
41
41
  },
42
42
  "publishConfig": {