@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/entity-ops.d.ts +7 -4
- package/dist/entity-ops.d.ts.map +1 -1
- package/dist/entity-ops.js +7 -4
- package/dist/entity-ops.js.map +1 -1
- package/dist/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +16 -12
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +136 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +324 -24
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +414 -2
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +274 -35
- package/dist/types.d.ts +280 -24
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +191 -18
- package/templates/default/CLAUDE.md +191 -18
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
|
-
|
|
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`
|
|
577
|
-
// keys, but sending `path: undefined` would add a key to
|
|
578
|
-
// no reason and churn the wire shape shipped plugins
|
|
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
|
-
|
|
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
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
|
1156
|
-
// host's
|
|
1157
|
-
// whole upload. The host REJECTS a `visibility`
|
|
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
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
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 →
|
|
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 →
|
|
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
|
*
|