@fias/arche-sdk 2.19.1 → 2.20.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.
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
3
+ exports.FiasContentError = exports.PLUGIN_CONTENT_ENTITY_ID = exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
4
4
  exports.useFiasUser = useFiasUser;
5
5
  exports.useFiasTheme = useFiasTheme;
6
6
  exports.useFiasVendoredAssets = useFiasVendoredAssets;
@@ -20,6 +20,7 @@ exports.useFiasStore = useFiasStore;
20
20
  exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
21
21
  exports.useVaultDocuments = useVaultDocuments;
22
22
  exports.useArcheAssets = useArcheAssets;
23
+ exports.useFiasContent = useFiasContent;
23
24
  exports.useFiasPreviewState = useFiasPreviewState;
24
25
  exports.useDataSubscription = useDataSubscription;
25
26
  exports.useVaultUserDocuments = useVaultUserDocuments;
@@ -185,7 +186,13 @@ function useFiasStorage() {
185
186
  path,
186
187
  });
187
188
  }, [bridge]);
188
- return { readFile, writeFile, listFiles, deleteFile };
189
+ // Memoized, like useArcheAssets. An unmemoized object literal here is a
190
+ // footgun rather than a style question: the natural way to use this hook is
191
+ // `useEffect(() => { void storage.readFile(...) }, [storage])`, and a fresh
192
+ // identity every render turns that into an infinite loop — effect, setState,
193
+ // render, effect. The inner callbacks were already stable; only the
194
+ // container was not.
195
+ return (0, react_1.useMemo)(() => ({ readFile, writeFile, listFiles, deleteFile }), [readFile, writeFile, listFiles, deleteFile]);
189
196
  }
190
197
  /**
191
198
  * Invoke platform entities (AI models, etc.) through the bridge.
@@ -573,16 +580,21 @@ function useFiasNavigation() {
573
580
  bridge.request('navigate', { path });
574
581
  }, [bridge]);
575
582
  const openArche = (0, react_1.useCallback)((archeId, opts) => {
576
- // `path` is spread in only when supplied: an older host ignores unknown
577
- // keys, but sending `path: undefined` would add a key to every call for
578
- // no reason and churn the wire shape shipped plugins already emit.
583
+ // `path` / `payload` are spread in only when supplied: an older host
584
+ // ignores unknown keys, but sending `path: undefined` would add a key to
585
+ // every call for no reason and churn the wire shape shipped plugins
586
+ // already emit.
579
587
  bridge.request('open_arche', {
580
588
  archeId,
581
589
  newTab: opts?.newTab === true,
582
590
  ...(opts?.path ? { path: opts.path } : {}),
591
+ ...(opts?.payload ? { payload: opts.payload } : {}),
583
592
  });
584
593
  }, [bridge]);
585
- return { navigateTo, openArche, currentPath };
594
+ // Memoized for the same reason as useFiasStorage. `currentPath` is state, so
595
+ // this identity still changes when the path does — which is correct, and is
596
+ // exactly what a consumer depending on it wants.
597
+ return (0, react_1.useMemo)(() => ({ navigateTo, openArche, currentPath }), [navigateTo, openArche, currentPath]);
586
598
  }
587
599
  /**
588
600
  * Like useState, but auto-persists to bridge storage.
@@ -596,12 +608,22 @@ function usePersistentState(key, initialValue) {
596
608
  const [value, setValueInternal] = (0, react_1.useState)(initialValue);
597
609
  const initializedRef = (0, react_1.useRef)(false);
598
610
  const valueRef = (0, react_1.useRef)(initialValue);
611
+ // Set once the plugin calls the setter. A value set BEFORE the mount read
612
+ // resolves is newer than anything in storage, so the read must not
613
+ // overwrite it — it used to, silently reverting e.g. a click that landed
614
+ // while the read was in flight (the debounced write then persisted the
615
+ // user's value while the UI showed the stale one).
616
+ const setByPluginRef = (0, react_1.useRef)(false);
599
617
  const writer = (0, react_1.useMemo)(() => createDebouncedWriter(bridge), [bridge]);
600
618
  // Load from storage on mount; flush any pending write on unmount/key change
601
619
  // so rapid updates in a game loop never lose their last value.
602
620
  (0, react_1.useEffect)(() => {
603
621
  (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${key}` })
604
622
  .then((result) => {
623
+ if (setByPluginRef.current) {
624
+ initializedRef.current = true;
625
+ return;
626
+ }
605
627
  if (result.exists && result.content !== null) {
606
628
  try {
607
629
  const parsed = JSON.parse(result.content);
@@ -627,6 +649,7 @@ function usePersistentState(key, initialValue) {
627
649
  }, [bridge, key, writer]);
628
650
  const setValue = (0, react_1.useCallback)((next) => {
629
651
  const resolved = typeof next === 'function' ? next(valueRef.current) : next;
652
+ setByPluginRef.current = true;
630
653
  valueRef.current = resolved;
631
654
  setValueInternal(resolved);
632
655
  writer.schedule(`__state/${key}`, JSON.stringify(resolved));
@@ -1079,9 +1102,7 @@ function useVaultDocuments() {
1079
1102
  });
1080
1103
  }, [bridge]);
1081
1104
  const read = (0, react_1.useCallback)(async (documentId) => {
1082
- return bridge.request('vault_documents_read', {
1083
- documentId,
1084
- });
1105
+ return bridge.request('vault_documents_read', { documentId });
1085
1106
  }, [bridge]);
1086
1107
  const getDownloadUrl = (0, react_1.useCallback)((documentId) => fetchVaultDocumentDownloadUrl(documentId),
1087
1108
  // The hook closes over the React context's bridge, but the resolver
@@ -1098,16 +1119,47 @@ function useVaultDocuments() {
1098
1119
  };
1099
1120
  }, [bridge]);
1100
1121
  const saveContent = (0, react_1.useCallback)(async (params) => {
1101
- const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_ARCHE_DOCUMENTS_ENTITY_ID, 'save_own_document_content', {
1102
- documentId: params.documentId,
1103
- content: params.content,
1104
- contentType: params.contentType,
1105
- });
1122
+ const routed = await routeSaveContent(params.content);
1123
+ let res;
1124
+ if (routed.lane === 'body') {
1125
+ // Small text rides the entity_invoke body.
1126
+ res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_ARCHE_DOCUMENTS_ENTITY_ID, 'save_own_document_content', {
1127
+ documentId: params.documentId,
1128
+ content: routed.text,
1129
+ contentType: params.contentType,
1130
+ expectedRevision: params.expectedRevision,
1131
+ });
1132
+ }
1133
+ else {
1134
+ // Bytes — or text too large for the bridge body — go through the
1135
+ // host, which does the presigned PUT the iframe's CSP forbids (same
1136
+ // reason `upload` does; see there). `contentKind: 'text'` keeps the
1137
+ // text lane's must-be-a-text-type rule in force at any size.
1138
+ //
1139
+ // Deliberately NO `name` in this payload: a host that predates
1140
+ // in-place saves requires one for an upload, so it refuses this
1141
+ // message instead of silently creating a NEW document.
1142
+ res = await bridge.request('vault_documents_upload', {
1143
+ saveToDocumentId: params.documentId,
1144
+ bytes: bytesToBase64(routed.bytes),
1145
+ mimeType: params.contentType,
1146
+ contentKind: routed.contentKind,
1147
+ expectedRevision: params.expectedRevision,
1148
+ });
1149
+ }
1106
1150
  // The bytes behind any cached download URL just changed.
1107
1151
  downloadUrlCache.delete(params.documentId);
1108
1152
  return res;
1109
1153
  }, [bridge]);
1110
1154
  const write = (0, react_1.useCallback)(async (params) => {
1155
+ // `write` has no second lane to fall back to, so say so here rather
1156
+ // than let the request die as an opaque 413 at the bridge.
1157
+ if (params.content.length > SAVE_CONTENT_BODY_LANE_MAX_BYTES ||
1158
+ !fitsBridgeBodyAsJsonText(params.content)) {
1159
+ throw new bridge_1.FiasBridgeError('write() takes small text only: at most 200,000 characters, and small enough to ' +
1160
+ 'travel in one bridge request once JSON-escaped (quotes, backslashes and newlines ' +
1161
+ 'double). Use upload() for larger documents.', 'INVALID_PARAMS');
1162
+ }
1111
1163
  return bridge.request('vault_documents_write', {
1112
1164
  name: params.name,
1113
1165
  content: params.content,
@@ -1152,9 +1204,9 @@ function useVaultDocuments() {
1152
1204
  await bridge.request('vault_documents_detach', { documentId, referenceId });
1153
1205
  }, [bridge]);
1154
1206
  const uploadInit = (0, react_1.useCallback)(async (params) => {
1155
- // No `userVisible` here by construction: a user-visible save needs the
1156
- // host's confirmation, which it can only run when it orchestrates the
1157
- // whole upload. The host REJECTS a `visibility` on this op.
1207
+ // No destination here by construction: a save into the user's
1208
+ // Documents needs the host's Save dialog, which it can only show when it
1209
+ // orchestrates the whole upload. The host REJECTS a `visibility` here.
1158
1210
  return bridge.request('vault_documents_upload_init', {
1159
1211
  name: params.name,
1160
1212
  mimeType: params.mimeType,
@@ -1189,13 +1241,22 @@ function useVaultDocuments() {
1189
1241
  sensitivity: params.sensitivity,
1190
1242
  folderPath: params.folderPath,
1191
1243
  replacesDocumentId: params.replacesDocumentId,
1192
- // Opt-in user-visible save. The host turns this into a confirmation
1193
- // the user sees and the iframe cannot script; the server independently
1194
- // requires `vault:user-documents:write` plus a registered folder for
1195
- // this arche. Omitted (the common case) keeps the arche-private default.
1244
+ // A save into the user's Documents. The host turns this into its Save
1245
+ // dialog, which the iframe cannot see or script, and performs the
1246
+ // write itself from the user's session. Omitted (the common case)
1247
+ // keeps the arche-private default.
1248
+ destination: params.destination,
1196
1249
  userVisible: params.userVisible,
1197
1250
  });
1198
1251
  }, [bridge]);
1252
+ const saveCopy = (0, react_1.useCallback)(async (documentId, options) => {
1253
+ // Rides the update op, intercepted by the host: it reads the bytes and
1254
+ // shows the Save dialog. Never forwarded — the original is unchanged.
1255
+ return bridge.request('vault_documents_update', {
1256
+ documentId,
1257
+ saveCopy: { ...(options?.suggestedName ? { suggestedName: options.suggestedName } : {}) },
1258
+ });
1259
+ }, [bridge]);
1199
1260
  return (0, react_1.useMemo)(() => ({
1200
1261
  list,
1201
1262
  read,
@@ -1209,6 +1270,7 @@ function useVaultDocuments() {
1209
1270
  attach,
1210
1271
  detach,
1211
1272
  upload,
1273
+ saveCopy,
1212
1274
  uploadInit,
1213
1275
  uploadFinalize,
1214
1276
  }), [
@@ -1224,10 +1286,79 @@ function useVaultDocuments() {
1224
1286
  attach,
1225
1287
  detach,
1226
1288
  upload,
1289
+ saveCopy,
1227
1290
  uploadInit,
1228
1291
  uploadFinalize,
1229
1292
  ]);
1230
1293
  }
1294
+ /** Encoded-size ceiling of the text lane (`save_own_document_content`). Must
1295
+ * match the server's MAX_OWN_DOCUMENT_SAVE_BYTES; content past it is routed
1296
+ * through the host-uploaded lane rather than refused. */
1297
+ const SAVE_CONTENT_BODY_LANE_MAX_BYTES = 200000;
1298
+ /**
1299
+ * What the text may occupy ON THE WIRE. The bridge request body is capped at
1300
+ * 256 KB (the server's `PLUGIN_BRIDGE_BODY_LIMIT_BYTES`) and the text travels
1301
+ * inside it as a JSON string, where every `"`, `\` and newline doubles and
1302
+ * every other control character becomes six bytes. So 200 KB of text can be
1303
+ * 400 KB — or 1.2 MB — of request, and the answer is an opaque 413 rather
1304
+ * than a save. Routing therefore looks at the SERIALIZED size too, with ~16 KB
1305
+ * left for the envelope (ids, revision, content type, JSON keys).
1306
+ */
1307
+ const BRIDGE_BODY_TEXT_MAX_SERIALIZED_BYTES = 256 * 1024 - 16 * 1024;
1308
+ /** True when `text` fits a bridge request body as a JSON string. Encodes at
1309
+ * most once, and not at all for short strings (a UTF-16 unit is at most six
1310
+ * bytes of JSON). */
1311
+ function fitsBridgeBodyAsJsonText(text, utf8Bytes) {
1312
+ if (text.length * 6 <= BRIDGE_BODY_TEXT_MAX_SERIALIZED_BYTES)
1313
+ return true;
1314
+ // JSON escapes are ASCII, so they cost the same in UTF-16 units as in
1315
+ // bytes: serialized bytes = UTF-8 bytes + the units escaping added.
1316
+ const escapeOverhead = JSON.stringify(text).length - text.length;
1317
+ const bytes = utf8Bytes ?? new TextEncoder().encode(text).length;
1318
+ return bytes + escapeOverhead <= BRIDGE_BODY_TEXT_MAX_SERIALIZED_BYTES;
1319
+ }
1320
+ /** Mirrors the server's `MAX_OWN_DOCUMENT_PRESIGNED_SAVE_BYTES` — the most an
1321
+ * in-place save accepts, pinned to what `readBytes` / `getBytes` can serve
1322
+ * back, so anything saved can be reopened. */
1323
+ const SAVE_CONTENT_MAX_BYTES = 15 * 1024 * 1024;
1324
+ /**
1325
+ * Pick the lane for `saveContent` from the content itself, encoding text AT
1326
+ * MOST ONCE (a UTF-16 code unit is at most 3 UTF-8 bytes, so short strings
1327
+ * are routed without encoding at all) and refusing anything past the cap
1328
+ * BEFORE the base64 step — which would otherwise build a 20 MB string on the
1329
+ * plugin's main thread for a save the server is certain to refuse.
1330
+ */
1331
+ async function routeSaveContent(content) {
1332
+ let routed;
1333
+ if (typeof content === 'string') {
1334
+ // Short enough that neither limit can be reached, whatever it contains:
1335
+ // six bytes of JSON per unit is the worst case, three of UTF-8.
1336
+ if (content.length * 6 <= SAVE_CONTENT_BODY_LANE_MAX_BYTES) {
1337
+ return { lane: 'body', text: content };
1338
+ }
1339
+ const bytes = new TextEncoder().encode(content);
1340
+ routed =
1341
+ bytes.length <= SAVE_CONTENT_BODY_LANE_MAX_BYTES &&
1342
+ fitsBridgeBodyAsJsonText(content, bytes.length)
1343
+ ? { lane: 'body', text: content }
1344
+ : { lane: 'host', bytes, contentKind: 'text' };
1345
+ }
1346
+ else {
1347
+ routed = { lane: 'host', bytes: await toUint8Array(content), contentKind: 'binary' };
1348
+ }
1349
+ if (routed.lane === 'host' && routed.bytes.length > SAVE_CONTENT_MAX_BYTES) {
1350
+ throw new bridge_1.FiasBridgeError(`Content is ${routed.bytes.length} bytes; saveContent accepts at most ` +
1351
+ `${SAVE_CONTENT_MAX_BYTES} bytes (15 MB).`, 'OWN_DOCUMENT_SAVE_TOO_LARGE');
1352
+ }
1353
+ return routed;
1354
+ }
1355
+ async function toUint8Array(content) {
1356
+ if (content instanceof Uint8Array)
1357
+ return content;
1358
+ if (content instanceof ArrayBuffer)
1359
+ return new Uint8Array(content);
1360
+ return new Uint8Array(await content.arrayBuffer());
1361
+ }
1231
1362
  /** Decode base64 transport back to bytes (`get_consented_document_bytes`). */
1232
1363
  function base64ToBytes(base64) {
1233
1364
  const binary = atob(base64);
@@ -1292,6 +1423,134 @@ function useArcheAssets() {
1292
1423
  }, [bridge]);
1293
1424
  return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
1294
1425
  }
1426
+ /**
1427
+ * Entity ID of the plugin content pack client-side entity (ADR-021) — the
1428
+ * host-side reader behind {@link useFiasContent}. Literal here so the SDK
1429
+ * stays bundle-clean (no `@fias/entities` runtime dep).
1430
+ *
1431
+ * SYNC NOTE: must match `PLUGIN_CONTENT_ENTITY_ID` in
1432
+ * `packages/entities/src/integrations/plugin-content/config.ts`. The
1433
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this.
1434
+ */
1435
+ exports.PLUGIN_CONTENT_ENTITY_ID = 'ent_intg_plugin_content_00000000';
1436
+ /** A failed content read: `code` says why, `path` names the file when there is one. */
1437
+ class FiasContentError extends Error {
1438
+ constructor(message, code, path) {
1439
+ super(message);
1440
+ this.code = code;
1441
+ this.path = path;
1442
+ this.name = 'FiasContentError';
1443
+ }
1444
+ }
1445
+ exports.FiasContentError = FiasContentError;
1446
+ function toContentError(err) {
1447
+ if (err instanceof FiasContentError)
1448
+ return err;
1449
+ const message = err instanceof Error ? err.message : String(err);
1450
+ const code = err?.code;
1451
+ if (/permission denied/i.test(message))
1452
+ return new FiasContentError(message, 'PERMISSION_DENIED');
1453
+ if (code === 'RATE_LIMITED' || /rate limit/i.test(message)) {
1454
+ return new FiasContentError(message, 'RATE_LIMIT');
1455
+ }
1456
+ return new FiasContentError(message, 'CONTENT_UNAVAILABLE');
1457
+ }
1458
+ function fileResult(results, path) {
1459
+ const result = results?.[path];
1460
+ if (!result) {
1461
+ throw new FiasContentError(`No result for content/${path}`, 'CONTENT_UNAVAILABLE', path);
1462
+ }
1463
+ if ('error' in result)
1464
+ throw new FiasContentError(result.error.message, result.error.code, path);
1465
+ return result;
1466
+ }
1467
+ /**
1468
+ * `blob:` URLs built in this iframe, per path. A published pack is immutable,
1469
+ * so one URL per path serves the plugin for its whole life.
1470
+ */
1471
+ const contentObjectUrls = new Map();
1472
+ /**
1473
+ * Read this plugin's own content pack — the reviewed `content/` directory it
1474
+ * was published with (lessons, data, images, audio, fonts).
1475
+ *
1476
+ * The host page reads the pack from the Fias content CDN, verifies every file
1477
+ * against its hash, and caches it; nothing here touches the network, and a
1478
+ * plugin can only read its own pack. Reads are free for your users; the
1479
+ * contributor pays the pack's storage and bandwidth, and cached repeat reads
1480
+ * cost nothing.
1481
+ *
1482
+ * Paths are relative to `content/`. In the Arche Builder and developer
1483
+ * previews there is no published pack: calls fail with
1484
+ * `CONTENT_UNAVAILABLE_IN_PREVIEW` (the dev harness serves your local
1485
+ * `content/` instead).
1486
+ *
1487
+ * Permission: `entities:client_invoke`.
1488
+ *
1489
+ * @example
1490
+ * const content = useFiasContent();
1491
+ * const lessons = await content.list('lessons/');
1492
+ * const markdown = await content.getText(lessons[0].path);
1493
+ * const cover = await content.getObjectUrl('img/cover.webp'); // <img src={cover}>
1494
+ */
1495
+ function useFiasContent() {
1496
+ const bridge = useBridge();
1497
+ const call = (0, react_1.useCallback)(async (input) => {
1498
+ let response;
1499
+ try {
1500
+ response = await bridge.request('client_entity_invoke', { entityId: exports.PLUGIN_CONTENT_ENTITY_ID, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
1501
+ }
1502
+ catch (err) {
1503
+ throw toContentError(err);
1504
+ }
1505
+ if (response?.error) {
1506
+ throw new FiasContentError(response.error.message, response.error.code);
1507
+ }
1508
+ return response ?? {};
1509
+ }, [bridge]);
1510
+ const list = (0, react_1.useCallback)(async (prefix) => (await call({ verb: 'list', ...(prefix !== undefined ? { prefix } : {}) })).files ?? [], [call]);
1511
+ const getText = (0, react_1.useCallback)(async (path) => {
1512
+ const { results } = await call({ verb: 'read', paths: [path], as: 'text' });
1513
+ return fileResult(results, path).text;
1514
+ }, [call]);
1515
+ const getJson = (0, react_1.useCallback)(async (path) => JSON.parse(await getText(path)), [getText]);
1516
+ const readBytes = (0, react_1.useCallback)(async (path) => {
1517
+ const { results } = await call({ verb: 'read', paths: [path], as: 'bytes' });
1518
+ return fileResult(results, path);
1519
+ }, [call]);
1520
+ const getBytes = (0, react_1.useCallback)(async (path) => (await readBytes(path)).bytes, [readBytes]);
1521
+ const getMany = (0, react_1.useCallback)(async (paths, as) => {
1522
+ const { results } = await call({ verb: 'read', paths, as });
1523
+ const out = {};
1524
+ for (const path of paths) {
1525
+ try {
1526
+ const result = fileResult(results, path);
1527
+ out[path] =
1528
+ as === 'text'
1529
+ ? result.text
1530
+ : result.bytes;
1531
+ }
1532
+ catch (err) {
1533
+ out[path] = toContentError(err);
1534
+ }
1535
+ }
1536
+ return out;
1537
+ }, [call]);
1538
+ const getObjectUrl = (0, react_1.useCallback)(async (path) => {
1539
+ const cached = contentObjectUrls.get(path);
1540
+ if (cached)
1541
+ return cached;
1542
+ const pending = readBytes(path).then(({ bytes, mimeType }) => URL.createObjectURL(new Blob([bytes], { type: mimeType })));
1543
+ contentObjectUrls.set(path, pending);
1544
+ pending.catch(() => contentObjectUrls.delete(path));
1545
+ return pending;
1546
+ }, [readBytes]);
1547
+ const getUrl = (0, react_1.useCallback)(async (path) => {
1548
+ const { results } = await call({ verb: 'url', paths: [path] });
1549
+ const { url, expiresAt } = fileResult(results, path);
1550
+ return { url, expiresAt };
1551
+ }, [call]);
1552
+ return (0, react_1.useMemo)(() => ({ list, getText, getJson, getBytes, getMany, getObjectUrl, getUrl }), [list, getText, getJson, getBytes, getMany, getObjectUrl, getUrl]);
1553
+ }
1295
1554
  /**
1296
1555
  * Preserve transient UI state across a builder-preview rebuild.
1297
1556
  *
@@ -1448,7 +1707,15 @@ function useDataSubscription(collection, onInvalidate, options) {
1448
1707
  * surface that reaches data your plugin did not create. Requires the
1449
1708
  * `vault:user-documents:read` permission. Every access is user-consented
1450
1709
  * (per-document grants created in the host picker, revocable any time in
1451
- * Vault → App Access) and audited by the platform. Proprietary-sensitivity
1710
+ * Vault → Arche Access) and audited by the platform.
1711
+ *
1712
+ * The picker offers the files of the workspace the user is acting in (the
1713
+ * host's workspace switcher — Personal, or an org workspace they belong to)
1714
+ * and `list()` returns the grants made in it; a document you hold a grant on
1715
+ * stays readable by id whichever workspace the user is in, because a grant
1716
+ * belongs to the workspace the document lives in. A plugin never names a
1717
+ * workspace; the host does. A member's role caps what they can share and
1718
+ * read. Proprietary-sensitivity
1452
1719
  * documents are never grantable. For documents your plugin CREATES, use
1453
1720
  * `useVaultDocuments` instead.
1454
1721
  */
@@ -1457,6 +1724,39 @@ function useVaultUserDocuments() {
1457
1724
  const pick = (0, react_1.useCallback)(async (options) => {
1458
1725
  return bridge.request('vault_documents_pick', {
1459
1726
  maxDocuments: options?.maxDocuments,
1727
+ access: options?.access,
1728
+ ...(options?.accept ? { accept: [...options.accept] } : {}),
1729
+ });
1730
+ }, [bridge]);
1731
+ const requestEditAccess = (0, react_1.useCallback)(async (documentId) => {
1732
+ // The same host-mediated op as `pick`, naming one document. The HOST
1733
+ // decides whether it may skip the picker (only for a document this arche
1734
+ // already holds a grant on) — nothing here can.
1735
+ return bridge.request('vault_documents_pick', {
1736
+ maxDocuments: 1,
1737
+ access: 'write',
1738
+ documentId,
1739
+ });
1740
+ }, [bridge]);
1741
+ const saveContent = (0, react_1.useCallback)(async (params) => {
1742
+ const routed = await routeSaveContent(params.content);
1743
+ if (routed.lane === 'body') {
1744
+ return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'save_consented_document_content', {
1745
+ documentId: params.documentId,
1746
+ content: routed.text,
1747
+ expectedRevision: params.expectedRevision,
1748
+ });
1749
+ }
1750
+ // Bytes, or text past the bridge body: the host does the presigned PUT
1751
+ // (the iframe's CSP forbids it). `saveTarget: 'user'` selects the
1752
+ // consented lane; no `name`, so an older host refuses rather than
1753
+ // creating a new document.
1754
+ return bridge.request('vault_documents_upload', {
1755
+ saveToDocumentId: params.documentId,
1756
+ saveTarget: 'user',
1757
+ bytes: bytesToBase64(routed.bytes),
1758
+ contentKind: routed.contentKind,
1759
+ expectedRevision: params.expectedRevision,
1460
1760
  });
1461
1761
  }, [bridge]);
1462
1762
  const list = (0, react_1.useCallback)(async (options) => {
@@ -1482,7 +1782,7 @@ function useVaultUserDocuments() {
1482
1782
  contentType: res.contentType,
1483
1783
  };
1484
1784
  }, [bridge]);
1485
- return (0, react_1.useMemo)(() => ({ pick, list, get, getDownloadUrl, getBytes }), [pick, list, get, getDownloadUrl, getBytes]);
1785
+ return (0, react_1.useMemo)(() => ({ pick, requestEditAccess, saveContent, list, get, getDownloadUrl, getBytes }), [pick, requestEditAccess, saveContent, list, get, getDownloadUrl, getBytes]);
1486
1786
  }
1487
1787
  /**
1488
1788
  * Refresh a community-asset URL this many ms before it expires. Larger than the
@@ -1689,7 +1989,7 @@ function useCommunityAssets() {
1689
1989
  * You get a document REFERENCE, not bytes. Read it with
1690
1990
  * `useVaultUserDocuments().getBytes(documentId)`: the host grants you that one
1691
1991
  * document as it relays the handoff, so no `pick()` is needed first. The grant
1692
- * is per-document and revocable in Vault → App Access exactly like a picked
1992
+ * is per-document and revocable in Vault → Arche Access exactly like a picked
1693
1993
  * one, and it needs `vault:user-documents:read` — declare it, or the grant is
1694
1994
  * refused and the file never arrives.
1695
1995
  *