@fias/arche-sdk 2.3.0 → 2.11.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.
Files changed (54) hide show
  1. package/dist/bridge.d.ts +76 -1
  2. package/dist/bridge.d.ts.map +1 -1
  3. package/dist/bridge.js +219 -6
  4. package/dist/bridge.js.map +1 -1
  5. package/dist/bridge.test.js +155 -6
  6. package/dist/bridge.test.js.map +1 -1
  7. package/dist/coverage-fills.test.js +26 -32
  8. package/dist/coverage-fills.test.js.map +1 -1
  9. package/dist/entity-ops.d.ts +55 -0
  10. package/dist/entity-ops.d.ts.map +1 -0
  11. package/dist/entity-ops.js +48 -0
  12. package/dist/entity-ops.js.map +1 -0
  13. package/dist/fias.d.ts +11 -1
  14. package/dist/fias.d.ts.map +1 -1
  15. package/dist/fias.js +39 -12
  16. package/dist/fias.js.map +1 -1
  17. package/dist/generated/permissions.d.ts +7 -1
  18. package/dist/generated/permissions.d.ts.map +1 -1
  19. package/dist/generated/permissions.js +35 -2
  20. package/dist/generated/permissions.js.map +1 -1
  21. package/dist/generated/surfaces.d.ts +424 -0
  22. package/dist/generated/surfaces.d.ts.map +1 -1
  23. package/dist/generated/surfaces.js +97 -1
  24. package/dist/generated/surfaces.js.map +1 -1
  25. package/dist/hooks.d.ts +228 -13
  26. package/dist/hooks.d.ts.map +1 -1
  27. package/dist/hooks.js +527 -104
  28. package/dist/hooks.js.map +1 -1
  29. package/dist/hooks.test.js +314 -247
  30. package/dist/hooks.test.js.map +1 -1
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +12 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +982 -115
  36. package/dist/protocol.d.ts +7 -0
  37. package/dist/protocol.d.ts.map +1 -1
  38. package/dist/protocol.js +7 -0
  39. package/dist/protocol.js.map +1 -1
  40. package/dist/provider.d.ts +50 -1
  41. package/dist/provider.d.ts.map +1 -1
  42. package/dist/provider.js +187 -2
  43. package/dist/provider.js.map +1 -1
  44. package/dist/provider.render.test.d.ts +17 -0
  45. package/dist/provider.render.test.d.ts.map +1 -0
  46. package/dist/provider.render.test.js +191 -0
  47. package/dist/provider.render.test.js.map +1 -0
  48. package/dist/provider.test.js +64 -8
  49. package/dist/provider.test.js.map +1 -1
  50. package/dist/types.d.ts +373 -27
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +2 -1
  53. package/templates/default/AGENTS.md +321 -34
  54. package/templates/default/CLAUDE.md +321 -34
package/dist/hooks.js CHANGED
@@ -1,26 +1,33 @@
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
4
  exports.useFiasUser = useFiasUser;
4
5
  exports.useFiasTheme = useFiasTheme;
6
+ exports.useFiasVendoredAssets = useFiasVendoredAssets;
5
7
  exports.useFiasStorage = useFiasStorage;
6
8
  exports.useEntityInvocation = useEntityInvocation;
7
9
  exports.useImageGeneration = useImageGeneration;
8
10
  exports.useImageEntities = useImageEntities;
9
- exports.useBackgroundRemoval = useBackgroundRemoval;
11
+ exports.useClientEntity = useClientEntity;
10
12
  exports.useAudioGeneration = useAudioGeneration;
11
13
  exports.useSurface = useSurface;
12
14
  exports.useFiasNavigation = useFiasNavigation;
13
15
  exports.usePersistentState = usePersistentState;
14
16
  exports.useStepNavigation = useStepNavigation;
15
17
  exports.useFiasDataStore = useFiasDataStore;
18
+ exports.useFiasWorkspaces = useFiasWorkspaces;
16
19
  exports.useFiasStore = useFiasStore;
17
20
  exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
18
21
  exports.useVaultDocuments = useVaultDocuments;
19
22
  exports.useArcheAssets = useArcheAssets;
23
+ exports.useFiasPreviewState = useFiasPreviewState;
24
+ exports.useDataSubscription = useDataSubscription;
25
+ exports.useVaultUserDocuments = useVaultUserDocuments;
20
26
  const react_1 = require("react");
21
27
  const react_dom_1 = require("react-dom");
22
28
  const context_1 = require("./context");
23
29
  const bridge_1 = require("./bridge");
30
+ const entity_ops_1 = require("./entity-ops");
24
31
  /**
25
32
  * Internal helper to get bridge from context, throws if not wrapped in FiasProvider.
26
33
  */
@@ -58,7 +65,7 @@ function createDebouncedWriter(bridge) {
58
65
  if (timerId !== null)
59
66
  clearTimeout(timerId);
60
67
  pending = null;
61
- bridge.request('storage_write', { path, content }).catch(() => { });
68
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', { path, content }).catch(() => { });
62
69
  }
63
70
  function schedule(path, content) {
64
71
  const now = Date.now();
@@ -84,21 +91,34 @@ function createDebouncedWriter(bridge) {
84
91
  * Access the current user's profile information.
85
92
  * Requires the `user:profile:read` permission in fias-plugin.json.
86
93
  *
94
+ * If the `get_user` bridge request fails — most commonly because the plugin
95
+ * called this hook without declaring `user:profile:read` — the error is
96
+ * thrown during render so it surfaces to the nearest error boundary
97
+ * (`FiasProvider` installs one). Previously a failed/denied request left the
98
+ * hook permanently returning `null`, indistinguishable from "still loading",
99
+ * so a UI that gated on `if (!user)` hung on an infinite spinner forever.
100
+ *
87
101
  * @returns The current user's profile or null while loading.
88
102
  */
89
103
  function useFiasUser() {
90
104
  const bridge = useBridge();
91
105
  const [user, setUser] = (0, react_1.useState)(null);
106
+ const [error, setError] = (0, react_1.useState)(null);
92
107
  (0, react_1.useEffect)(() => {
93
108
  let mounted = true;
94
109
  bridge.request('get_user', {}).then((data) => {
95
110
  if (mounted)
96
111
  setUser(data);
112
+ }, (err) => {
113
+ if (mounted)
114
+ setError(err instanceof Error ? err : new Error(String(err)));
97
115
  });
98
116
  return () => {
99
117
  mounted = false;
100
118
  };
101
119
  }, [bridge]);
120
+ if (error)
121
+ throw error;
102
122
  return user;
103
123
  }
104
124
  /**
@@ -116,6 +136,25 @@ function useFiasTheme() {
116
136
  }, [bridge]);
117
137
  return theme;
118
138
  }
139
+ /**
140
+ * Self-hosted asset URLs for a platform-vendored library, for plugins that
141
+ * declare the `sandbox:vendored-libraries` permission. Returns a file-key → URL
142
+ * map (for `pdfjs-dist`: `{ mainModule, worker, cMapUrl, standardFontDataUrl }`),
143
+ * or `null` when the host's CDN is unconfigured or the library isn't vendored
144
+ * (the plugin should then degrade gracefully).
145
+ *
146
+ * Delivered in the init payload (no gated request); values are static for the
147
+ * session. The import specifier is already pinned to its main module via the
148
+ * host importmap, so a plugin typically only needs `worker` (pass to the
149
+ * pdf-core loader's `workerModuleUrl`) and the CMap/font directories.
150
+ *
151
+ * @param importSpecifier The vendored library's bare module name (e.g. `'pdfjs-dist'`).
152
+ * @returns The library's asset URLs, or null if unavailable.
153
+ */
154
+ function useFiasVendoredAssets(importSpecifier) {
155
+ const bridge = useBridge();
156
+ return bridge.getVendoredAssets(importSpecifier);
157
+ }
119
158
  /**
120
159
  * Access the plugin's sandboxed storage.
121
160
  * Requires the `storage:sandbox` permission in fias-plugin.json.
@@ -125,18 +164,23 @@ function useFiasTheme() {
125
164
  function useFiasStorage() {
126
165
  const bridge = useBridge();
127
166
  const readFile = (0, react_1.useCallback)(async (path) => {
128
- const res = await bridge.request('storage_read', { path });
167
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path });
129
168
  return res?.content ?? null;
130
169
  }, [bridge]);
131
170
  const writeFile = (0, react_1.useCallback)(async (path, content) => {
132
- await bridge.request('storage_write', { path, content });
171
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', {
172
+ path,
173
+ content,
174
+ });
133
175
  }, [bridge]);
134
176
  const listFiles = (0, react_1.useCallback)(async (prefix) => {
135
- const res = await bridge.request('storage_list', { prefix });
177
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'list', { prefix });
136
178
  return res?.files ?? [];
137
179
  }, [bridge]);
138
180
  const deleteFile = (0, react_1.useCallback)(async (path) => {
139
- await bridge.request('storage_delete', { path });
181
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'delete', {
182
+ path,
183
+ });
140
184
  }, [bridge]);
141
185
  return { readFile, writeFile, listFiles, deleteFile };
142
186
  }
@@ -276,48 +320,63 @@ function useImageEntities(filter) {
276
320
  }, [fetchOnce]);
277
321
  return { entities, isLoading, error, refetch: fetchOnce };
278
322
  }
279
- /** Mirrors the host-side cap in the imgly executor. Pre-checked here so a
280
- * 100 MB blob isn't structured-cloned across postMessage just to be
281
- * rejected. */
282
- const BG_REMOVAL_MAX_BYTES = 25 * 1024 * 1024;
283
323
  /**
284
- * Entity ID of the imgly background-removal client-side entity. Literal
285
- * here so the SDK stays bundle-clean (no `@fias/entities` runtime dep).
324
+ * `useBackgroundRemoval()` was REMOVED. It targeted the on-device imgly
325
+ * background-removal entity, which was retired platform-wide for
326
+ * licensing reasons (AGPL-3.0), along with its
327
+ * `entities:image_remove_background` permission.
286
328
  *
287
- * SYNC NOTE: must match `IMGLY_BACKGROUND_REMOVAL_ENTITY_ID` in
288
- * `packages/entities/src/integrations/imgly-background-removal/config.ts`.
289
- * The architecture test `sdk-entity-id-constants-match.test.ts` pins
290
- * this — drift fails CI.
329
+ * There is no client-side replacement. Server-side background removal is
330
+ * available to plugins through the paid `image.smart-remove-background`
331
+ * invocation surface (Stability / Recraft) under the
332
+ * `entities:image_edit` permission.
291
333
  */
292
- const IMGLY_BACKGROUND_REMOVAL_ENTITY_ID = 'ent_intg_imgly_bgrm_000000000000';
293
334
  /**
294
- * Remove the background from an image via the bridge's generic
295
- * `client_entity_invoke` op, targeting the imgly background-removal
296
- * entity. The host page runs the WASM model locally — bytes never leave
297
- * the user's device. Returns a transparent PNG.
335
+ * Default timeout for a client-side entity invocation. Client-side entities
336
+ * (WASM models, dictionary engines) can take many seconds to warm up and
337
+ * run, so we use the same 2-minute ceiling as the image/audio surfaces.
338
+ */
339
+ const CLIENT_ENTITY_INVOKE_TIMEOUT_MS = 120000;
340
+ /**
341
+ * Invoke a client-side ("on-device") entity through the bridge's generic
342
+ * `client_entity_invoke` op. Client-side entities execute entirely in the
343
+ * browser (e.g. the nspell + retext grammar checker, the NLLB-200
344
+ * translator) — input never leaves the device and there is no per-call
345
+ * credit cost.
346
+ *
347
+ * This is the client-side counterpart to {@link useEntityInvocation}, which
348
+ * invokes server-side, credit-billed AI entities. Target an entity by its ID
349
+ * and supply the input shape that entity documents; the typed result flows
350
+ * back through the returned promise:
298
351
  *
299
- * Requires the `entities:image_remove_background` permission in
300
- * `fias-plugin.json`. Inputs must be a PNG/JPEG `Blob` no larger than
301
- * 25 MiB. The bridge rate-limits to 10 calls/minute per arche per user
302
- * (sourced from the entity's `pluginInvocable.rateLimitPerMinute`).
352
+ * ```tsx
353
+ * const { invoke, isLoading, error } = useClientEntity();
354
+ * const { issues } = await invoke<GrammarCheckInput, GrammarCheckResult>(
355
+ * GRAMMAR_CHECK_ENTITY_ID,
356
+ * { text },
357
+ * );
358
+ * ```
359
+ *
360
+ * Requires the `entities:client_invoke` permission in `fias-plugin.json`.
361
+ * Per-entity rate limits (each entity's `pluginInvocable.rateLimitPerMinute`)
362
+ * are enforced by the host.
363
+ *
364
+ * Input limits are enforced **host-side by each entity's executor**, not by
365
+ * this hook — being generic, it cannot know a given entity's caps (e.g. the
366
+ * grammar entity's 100,000-character limit). Oversized/invalid input is
367
+ * rejected after it crosses the bridge, surfacing as a rejected promise /
368
+ * `error`. If you want to fail fast before the round-trip, pre-validate in
369
+ * your own code against the limits the entity documents.
303
370
  */
304
- function useBackgroundRemoval() {
371
+ function useClientEntity() {
305
372
  const bridge = useBridge();
306
373
  const [isLoading, setIsLoading] = (0, react_1.useState)(false);
307
- const [result, setResult] = (0, react_1.useState)(null);
308
374
  const [error, setError] = (0, react_1.useState)(null);
309
- const removeBackground = (0, react_1.useCallback)(async (image) => {
310
- if (image.size > BG_REMOVAL_MAX_BYTES) {
311
- const e = new Error('background-removal: image exceeds 25 MiB limit');
312
- setError(e);
313
- throw e;
314
- }
375
+ const invoke = (0, react_1.useCallback)(async (entityId, input) => {
315
376
  setIsLoading(true);
316
377
  setError(null);
317
378
  try {
318
- const res = await bridge.request('client_entity_invoke', { entityId: IMGLY_BACKGROUND_REMOVAL_ENTITY_ID, input: { image } }, 120000);
319
- setResult(res.image);
320
- return res.image;
379
+ return await bridge.request('client_entity_invoke', { entityId, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
321
380
  }
322
381
  catch (err) {
323
382
  const e = err instanceof Error ? err : new Error(String(err));
@@ -328,8 +387,76 @@ function useBackgroundRemoval() {
328
387
  setIsLoading(false);
329
388
  }
330
389
  }, [bridge]);
331
- return { removeBackground, isLoading, result, error };
390
+ return { invoke, isLoading, error };
332
391
  }
392
+ /**
393
+ * Entity ID of the NLLB-200 on-device translation entity. Exported so plugins
394
+ * can target it via the generic {@link useClientEntity} hook, with
395
+ * `TranslateParams` / `TranslationResult` as the I/O types.
396
+ *
397
+ * SYNC NOTE: must match `NLLB_TRANSLATION_ENTITY_ID` in
398
+ * `packages/entities/src/integrations/nllb-translation/config.ts`. The
399
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this —
400
+ * drift fails CI.
401
+ */
402
+ exports.NLLB_TRANSLATION_ENTITY_ID = 'ent_intg_nllb_xlate_000000000000';
403
+ /**
404
+ * Curated list of target languages for the NLLB translator, as
405
+ * label/FLORES-200-code pairs. Not exhaustive (NLLB supports 200 languages) —
406
+ * a convenient default set for building a picker. Sorted by label.
407
+ */
408
+ exports.NLLB_LANGUAGES = [
409
+ { label: 'Arabic', code: 'arb_Arab' },
410
+ { label: 'Bengali', code: 'ben_Beng' },
411
+ { label: 'Bulgarian', code: 'bul_Cyrl' },
412
+ { label: 'Catalan', code: 'cat_Latn' },
413
+ { label: 'Chinese (Simplified)', code: 'zho_Hans' },
414
+ { label: 'Chinese (Traditional)', code: 'zho_Hant' },
415
+ { label: 'Croatian', code: 'hrv_Latn' },
416
+ { label: 'Czech', code: 'ces_Latn' },
417
+ { label: 'Danish', code: 'dan_Latn' },
418
+ { label: 'Dutch', code: 'nld_Latn' },
419
+ { label: 'English', code: 'eng_Latn' },
420
+ { label: 'Finnish', code: 'fin_Latn' },
421
+ { label: 'French', code: 'fra_Latn' },
422
+ { label: 'German', code: 'deu_Latn' },
423
+ { label: 'Greek', code: 'ell_Grek' },
424
+ { label: 'Hebrew', code: 'heb_Hebr' },
425
+ { label: 'Hindi', code: 'hin_Deva' },
426
+ { label: 'Hungarian', code: 'hun_Latn' },
427
+ { label: 'Indonesian', code: 'ind_Latn' },
428
+ { label: 'Italian', code: 'ita_Latn' },
429
+ { label: 'Japanese', code: 'jpn_Jpan' },
430
+ { label: 'Korean', code: 'kor_Hang' },
431
+ { label: 'Norwegian', code: 'nob_Latn' },
432
+ { label: 'Persian', code: 'pes_Arab' },
433
+ { label: 'Polish', code: 'pol_Latn' },
434
+ { label: 'Portuguese', code: 'por_Latn' },
435
+ { label: 'Romanian', code: 'ron_Latn' },
436
+ { label: 'Russian', code: 'rus_Cyrl' },
437
+ { label: 'Serbian', code: 'srp_Cyrl' },
438
+ { label: 'Slovak', code: 'slk_Latn' },
439
+ { label: 'Spanish', code: 'spa_Latn' },
440
+ { label: 'Swedish', code: 'swe_Latn' },
441
+ { label: 'Tamil', code: 'tam_Taml' },
442
+ { label: 'Telugu', code: 'tel_Telu' },
443
+ { label: 'Thai', code: 'tha_Thai' },
444
+ { label: 'Turkish', code: 'tur_Latn' },
445
+ { label: 'Ukrainian', code: 'ukr_Cyrl' },
446
+ { label: 'Urdu', code: 'urd_Arab' },
447
+ { label: 'Vietnamese', code: 'vie_Latn' },
448
+ ];
449
+ /**
450
+ * Entity ID of the grammar_check client-side entity. Exported so plugins can
451
+ * target it via the generic {@link useClientEntity} hook. Literal here so the
452
+ * SDK stays bundle-clean (no `@fias/entities` runtime dep).
453
+ *
454
+ * SYNC NOTE: must match `GRAMMAR_CHECK_ENTITY_ID` in
455
+ * `packages/entities/src/integrations/grammar-check/config.ts`. The
456
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this —
457
+ * drift fails CI.
458
+ */
459
+ exports.GRAMMAR_CHECK_ENTITY_ID = 'ent_intg_grammar_check_000000000';
333
460
  /**
334
461
  * Generate audio (music today; future speech/SFX sub-types via the same
335
462
  * hook) via the bridge's `audio_generate` op. Returns an array of clips
@@ -414,9 +541,10 @@ function useSurface(surfaceKey, timeoutMs = 180000) {
414
541
  return { invoke, isLoading, result, error };
415
542
  }
416
543
  /**
417
- * Navigate within the plugin's route space.
544
+ * Navigate within the plugin's route space (`navigateTo`), or open another
545
+ * arche's page (`openArche`, requires the `navigation:open_arche` permission).
418
546
  *
419
- * @returns Navigation API (navigateTo, currentPath).
547
+ * @returns Navigation API (navigateTo, openArche, currentPath).
420
548
  */
421
549
  function useFiasNavigation() {
422
550
  const bridge = useBridge();
@@ -441,7 +569,10 @@ function useFiasNavigation() {
441
569
  const navigateTo = (0, react_1.useCallback)((path) => {
442
570
  bridge.request('navigate', { path });
443
571
  }, [bridge]);
444
- return { navigateTo, currentPath };
572
+ const openArche = (0, react_1.useCallback)((archeId, opts) => {
573
+ bridge.request('open_arche', { archeId, newTab: opts?.newTab === true });
574
+ }, [bridge]);
575
+ return { navigateTo, openArche, currentPath };
445
576
  }
446
577
  /**
447
578
  * Like useState, but auto-persists to bridge storage.
@@ -459,8 +590,7 @@ function usePersistentState(key, initialValue) {
459
590
  // Load from storage on mount; flush any pending write on unmount/key change
460
591
  // so rapid updates in a game loop never lose their last value.
461
592
  (0, react_1.useEffect)(() => {
462
- bridge
463
- .request('storage_read', { path: `__state/${key}` })
593
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${key}` })
464
594
  .then((result) => {
465
595
  if (result.exists && result.content !== null) {
466
596
  try {
@@ -473,6 +603,13 @@ function usePersistentState(key, initialValue) {
473
603
  }
474
604
  }
475
605
  initializedRef.current = true;
606
+ })
607
+ .catch(() => {
608
+ // A failed/denied storage_read (e.g. the plugin didn't declare
609
+ // `storage:sandbox`) must not become a silent unhandled rejection.
610
+ // Persistence is best-effort — fall back to the initial value and
611
+ // let subsequent writes fail on their own rather than hanging here.
612
+ initializedRef.current = true;
476
613
  });
477
614
  return () => {
478
615
  writer.flush();
@@ -514,10 +651,7 @@ function useStepNavigation(initialStep, options) {
514
651
  return;
515
652
  }
516
653
  let cancelled = false;
517
- bridge
518
- .request('storage_read', {
519
- path: `__state/${persistKey}`,
520
- })
654
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${persistKey}` })
521
655
  .then((result) => {
522
656
  // Don't clobber a user-initiated setCurrentStep that landed before this resolved.
523
657
  if (cancelled || initializedRef.current)
@@ -590,34 +724,81 @@ function useStepNavigation(initialStep, options) {
590
724
  function useFiasDataStore() {
591
725
  const bridge = useBridge();
592
726
  const createCollection = (0, react_1.useCallback)(async (name, options) => {
593
- const res = await bridge.request('data_create_collection', { name, userScope: options?.userScope ?? 'user' });
727
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'create_collection', {
728
+ name,
729
+ userScope: options?.userScope ?? 'user',
730
+ searchable: options?.searchable,
731
+ writeMinRole: options?.writeMinRole,
732
+ readMinRole: options?.readMinRole,
733
+ writePolicy: options?.writePolicy,
734
+ });
594
735
  return res.collection;
595
736
  }, [bridge]);
596
737
  const listCollections = (0, react_1.useCallback)(async () => {
597
- const res = await bridge.request('data_list_collections', {});
738
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'list_collections');
598
739
  return res.collections;
599
740
  }, [bridge]);
600
741
  const deleteCollection = (0, react_1.useCallback)(async (name) => {
601
- await bridge.request('data_delete_collection', { name });
742
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete_collection', { name });
602
743
  }, [bridge]);
603
- const put = (0, react_1.useCallback)(async (collection, key, data) => {
604
- await bridge.request('data_put', { collection, key, data });
744
+ const put = (0, react_1.useCallback)(async (collection, key, data, options) => {
745
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'put', {
746
+ collection,
747
+ key,
748
+ data,
749
+ workspaceId: options?.workspaceId,
750
+ });
605
751
  }, [bridge]);
606
- const get = (0, react_1.useCallback)(async (collection, key) => {
607
- const res = await bridge.request('data_get', {
752
+ const get = (0, react_1.useCallback)(async (collection, key, options) => {
753
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'get', {
608
754
  collection,
609
755
  key,
756
+ workspaceId: options?.workspaceId,
610
757
  });
611
758
  return res.data;
612
759
  }, [bridge]);
613
- const query = (0, react_1.useCallback)(async (collection, options) => {
614
- return bridge.request('data_query', {
760
+ const getDocument = (0, react_1.useCallback)(async (collection, key, options) => {
761
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'get', {
762
+ collection,
763
+ key,
764
+ workspaceId: options?.workspaceId,
765
+ });
766
+ if (res.data === null || res.data === undefined)
767
+ return null;
768
+ return {
769
+ key: res.key ?? key,
770
+ data: res.data,
771
+ updatedAt: res.updatedAt ?? '',
772
+ ...(res.authoredByCollaborator !== undefined
773
+ ? { authoredByCollaborator: res.authoredByCollaborator }
774
+ : {}),
775
+ };
776
+ }, [bridge]);
777
+ const query = (0, react_1.useCallback)(async (collection, options, scope) => {
778
+ return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'query', {
615
779
  collection,
616
780
  ...options,
781
+ workspaceId: scope?.workspaceId,
617
782
  });
618
783
  }, [bridge]);
619
- const deleteDoc = (0, react_1.useCallback)(async (collection, key) => {
620
- await bridge.request('data_delete', { collection, key });
784
+ const search = (0, react_1.useCallback)(async (collection, queryText, options, scope) => {
785
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'search', {
786
+ collection,
787
+ query: queryText,
788
+ ...options,
789
+ workspaceId: scope?.workspaceId,
790
+ });
791
+ return res.matches;
792
+ }, [bridge]);
793
+ const deleteDoc = (0, react_1.useCallback)(async (collection, key, options) => {
794
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete', {
795
+ collection,
796
+ key,
797
+ workspaceId: options?.workspaceId,
798
+ });
799
+ }, [bridge]);
800
+ const batch = (0, react_1.useCallback)(async (operations) => {
801
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'batch', { operations });
621
802
  }, [bridge]);
622
803
  return (0, react_1.useMemo)(() => ({
623
804
  createCollection,
@@ -625,9 +806,84 @@ function useFiasDataStore() {
625
806
  deleteCollection,
626
807
  put,
627
808
  get,
809
+ getDocument,
628
810
  query,
811
+ search,
629
812
  delete: deleteDoc,
630
- }), [createCollection, listCollections, deleteCollection, put, get, query, deleteDoc]);
813
+ batch,
814
+ }), [
815
+ createCollection,
816
+ listCollections,
817
+ deleteCollection,
818
+ put,
819
+ get,
820
+ getDocument,
821
+ query,
822
+ search,
823
+ deleteDoc,
824
+ batch,
825
+ ]);
826
+ }
827
+ /**
828
+ * Access workspace tenancy — team/org workspaces with role-gated membership
829
+ * (owner/admin/member/viewer) over `workspace`-scoped Data Store collections
830
+ * (ADR-010). The creating user becomes the founding owner; a workspace always
831
+ * keeps at least one owner.
832
+ *
833
+ * Requires the `data:workspace` permission in fias-plugin.json. To read/write a
834
+ * workspace's documents, use `useFiasDataStore()` with `{ workspaceId }` on a
835
+ * collection created with `userScope: 'workspace'`.
836
+ *
837
+ * @example
838
+ * const workspaces = useFiasWorkspaces();
839
+ * const ws = await workspaces.create('Acme Corp');
840
+ * await workspaces.addMember(ws.workspaceId, otherUserId, 'member');
841
+ * // then, with useFiasDataStore():
842
+ * await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
843
+ *
844
+ * @returns Workspace API with lifecycle + membership methods.
845
+ */
846
+ function useFiasWorkspaces() {
847
+ const bridge = useBridge();
848
+ const create = (0, react_1.useCallback)(async (displayName) => {
849
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { displayName });
850
+ return res.workspace;
851
+ }, [bridge]);
852
+ const list = (0, react_1.useCallback)(async () => {
853
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list');
854
+ return res.workspaces;
855
+ }, [bridge]);
856
+ const get = (0, react_1.useCallback)(async (workspaceId) => {
857
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'get', { workspaceId });
858
+ return res.workspace;
859
+ }, [bridge]);
860
+ const archive = (0, react_1.useCallback)(async (workspaceId) => {
861
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'archive', { workspaceId });
862
+ }, [bridge]);
863
+ const listMembers = (0, react_1.useCallback)(async (workspaceId) => {
864
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list_members', { workspaceId });
865
+ return res.members;
866
+ }, [bridge]);
867
+ const addMember = (0, react_1.useCallback)(async (workspaceId, username, role, expiresAt) => {
868
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'add_member', {
869
+ workspaceId,
870
+ username,
871
+ role,
872
+ expiresAt,
873
+ });
874
+ return res.member;
875
+ }, [bridge]);
876
+ const updateMember = (0, react_1.useCallback)(async (workspaceId, username, role) => {
877
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', { workspaceId, username, role });
878
+ return res.member;
879
+ }, [bridge]);
880
+ const removeMember = (0, react_1.useCallback)(async (workspaceId, username) => {
881
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'remove_member', {
882
+ workspaceId,
883
+ username,
884
+ });
885
+ }, [bridge]);
886
+ return (0, react_1.useMemo)(() => ({ create, list, get, archive, listMembers, addMember, updateMember, removeMember }), [create, list, get, archive, listMembers, addMember, updateMember, removeMember]);
631
887
  }
632
888
  /**
633
889
  * Access the in-arche purchase (IAP) store.
@@ -647,8 +903,8 @@ function useFiasStore() {
647
903
  (0, react_1.useEffect)(() => {
648
904
  let mounted = true;
649
905
  Promise.all([
650
- bridge.request('store_get_products', {}),
651
- bridge.request('store_get_entitlements', {}),
906
+ (0, entity_ops_1.invokeStoreOp)(bridge, 'get_products'),
907
+ (0, entity_ops_1.invokeStoreOp)(bridge, 'get_entitlements'),
652
908
  ])
653
909
  .then(([prods, ents]) => {
654
910
  if (mounted) {
@@ -666,7 +922,7 @@ function useFiasStore() {
666
922
  };
667
923
  }, [bridge]);
668
924
  const purchase = (0, react_1.useCallback)(async (productIdentifier, options) => {
669
- const result = await bridge.request('store_purchase', {
925
+ const result = await (0, entity_ops_1.invokeStoreOp)(bridge, 'purchase', {
670
926
  productIdentifier,
671
927
  ...options,
672
928
  });
@@ -688,11 +944,11 @@ function useFiasStore() {
688
944
  return entitlements.some((e) => e.productIdentifier === productIdentifier && e.isActive);
689
945
  }, [entitlements]);
690
946
  const getPurchaseHistory = (0, react_1.useCallback)(async () => {
691
- const res = await bridge.request('store_get_purchase_history', {});
947
+ const res = await (0, entity_ops_1.invokeStoreOp)(bridge, 'get_purchase_history');
692
948
  return res.purchases;
693
949
  }, [bridge]);
694
950
  const restorePurchases = (0, react_1.useCallback)(async () => {
695
- const restored = await bridge.request('store_restore', {});
951
+ const restored = await (0, entity_ops_1.invokeStoreOp)(bridge, 'restore');
696
952
  setEntitlements(restored);
697
953
  return restored;
698
954
  }, [bridge]);
@@ -871,37 +1127,24 @@ function useVaultDocuments() {
871
1127
  });
872
1128
  }, [bridge]);
873
1129
  const upload = (0, react_1.useCallback)(async (bytes, params) => {
874
- // Copy into a freshly-allocated, non-shared ArrayBuffer so the
875
- // SubtleCrypto + fetch overloads resolve cleanly. (Uint8Array's
876
- // generic ArrayBufferLike parameter pessimistically widens to
877
- // ArrayBuffer|SharedArrayBuffer, which neither API accepts.)
1130
+ // The presigned PUT must run in the HOST, not here: the plugin iframe's
1131
+ // CSP is `connect-src 'none'`, which silently blocks an in-iframe
1132
+ // `fetch` PUT in published plugins. The host (parent window, no
1133
+ // connect-src restriction) orchestrates init → PUT → finalize via the
1134
+ // `vault_documents_upload` bridge op; the presigned URL is backend-issued
1135
+ // host-side, so the iframe can't redirect the PUT (no SSRF). The
1136
+ // lower-level `uploadInit`/`uploadFinalize` remain for advanced callers.
878
1137
  const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
879
- const u8 = new Uint8Array(src.byteLength);
880
- u8.set(src);
881
- const sizeBytes = u8.byteLength;
882
- const digest = await crypto.subtle.digest('SHA-256', u8);
883
- const checksumHex = bytesToHex(new Uint8Array(digest));
884
- const init = await uploadInit({ ...params, sizeBytes, checksumSha256: checksumHex });
885
- // PUT the bytes directly to S3 using the headers the server
886
- // signed. Browsers forbid setting `Content-Length` explicitly
887
- // (the Fetch spec lists it as forbidden — the browser computes
888
- // it from `body.byteLength` automatically). Filter it out so the
889
- // fetch doesn't either error in strict mode or silently send a
890
- // different length than what we tried to pass. The signed value
891
- // still matches because S3 sees the auto-set length, which the
892
- // server matched against `sizeBytes` at init time.
893
- const browserHeaders = Object.fromEntries(Object.entries(init.requiredHeaders).filter(([k]) => k.toLowerCase() !== 'content-length'));
894
- const putResp = await fetch(init.uploadUrl, {
895
- method: 'PUT',
896
- body: u8,
897
- headers: browserHeaders,
1138
+ return bridge.request('vault_documents_upload', {
1139
+ name: params.name,
1140
+ mimeType: params.mimeType,
1141
+ bytes: bytesToBase64(src),
1142
+ tags: params.tags,
1143
+ sensitivity: params.sensitivity,
1144
+ folderPath: params.folderPath,
1145
+ replacesDocumentId: params.replacesDocumentId,
898
1146
  });
899
- if (!putResp.ok) {
900
- const text = await putResp.text().catch(() => '');
901
- throw new Error(`Vault upload PUT failed (${putResp.status}): ${text || putResp.statusText}`);
902
- }
903
- return uploadFinalize({ documentId: init.documentId, checksumSha256: checksumHex });
904
- }, [uploadInit, uploadFinalize]);
1147
+ }, [bridge]);
905
1148
  return (0, react_1.useMemo)(() => ({
906
1149
  list,
907
1150
  read,
@@ -930,19 +1173,15 @@ function useVaultDocuments() {
930
1173
  uploadFinalize,
931
1174
  ]);
932
1175
  }
933
- /**
934
- * Render a byte array as lowercase hex. Used by `upload` to convert
935
- * the `crypto.subtle.digest('SHA-256', ...)` output into the
936
- * wire-format (hex) that the bridge expects. S3 still wants base64
937
- * for `x-amz-checksum-sha256` — that conversion happens server-side
938
- * in `generatePresignedUploadUrl`, not here.
939
- */
940
- function bytesToHex(bytes) {
941
- let out = '';
942
- for (let i = 0; i < bytes.length; i++) {
943
- out += bytes[i].toString(16).padStart(2, '0');
1176
+ /** Base64-encode bytes for transport to the host's `vault_documents_upload`
1177
+ * op (the host does the presigned PUT — see PluginBridge.handleVaultUpload). */
1178
+ function bytesToBase64(bytes) {
1179
+ let binary = '';
1180
+ const chunk = 0x8000;
1181
+ for (let i = 0; i < bytes.length; i += chunk) {
1182
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
944
1183
  }
945
- return out;
1184
+ return btoa(binary);
946
1185
  }
947
1186
  /**
948
1187
  * Access the contributor-curated asset library for this arche.
@@ -980,4 +1219,188 @@ function useArcheAssets() {
980
1219
  }, [bridge]);
981
1220
  return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
982
1221
  }
1222
+ /**
1223
+ * Preserve transient UI state across a builder-preview rebuild.
1224
+ *
1225
+ * In the arche builder, editing code (or a background cross-device sync)
1226
+ * rebuilds the preview, which reloads the iframe and wipes the plugin's
1227
+ * in-memory state — losing, for example, a report the user just generated.
1228
+ * This hook lets you opt that state into host-side preservation: just before
1229
+ * the reload the host snapshots `getState()`, and after the reload it hands the
1230
+ * value back to `onRestore`.
1231
+ *
1232
+ * Inert outside the builder preview (the host never requests a capture), so
1233
+ * it's safe to leave in production plugin code.
1234
+ *
1235
+ * @param key Stable namespace for this slice of state (so multiple calls
1236
+ * don't collide). Use a literal like `'report'`.
1237
+ * @param getState Returns the current serializable state. Must be JSON-safe.
1238
+ * @param onRestore Receives the snapshot after a reload. Reads the latest
1239
+ * closure via a ref, so you don't need a stable identity.
1240
+ *
1241
+ * @example
1242
+ * const [report, setReport] = useState<Report | null>(null);
1243
+ * useFiasPreviewState('report', () => report, (saved) => setReport(saved as Report));
1244
+ */
1245
+ function useFiasPreviewState(key, getState, onRestore) {
1246
+ const bridge = useBridge();
1247
+ const getStateRef = (0, react_1.useRef)(getState);
1248
+ getStateRef.current = getState;
1249
+ const onRestoreRef = (0, react_1.useRef)(onRestore);
1250
+ onRestoreRef.current = onRestore;
1251
+ (0, react_1.useEffect)(() => {
1252
+ return bridge.registerPreviewState(key, () => getStateRef.current(), (state) => onRestoreRef.current(state));
1253
+ }, [bridge, key]);
1254
+ }
1255
+ /**
1256
+ * Subscribes to realtime change events for a datastore collection
1257
+ * (ADR-012 Phase M4). Notify-then-refetch: the platform pushes a minimal
1258
+ * "this collection changed" invalidation and `onInvalidate` decides what to
1259
+ * refetch — typically re-running the `useFiasDataStore().query(...)` your
1260
+ * component renders from.
1261
+ *
1262
+ * Requires the `data:store` permission (same as reading the collection —
1263
+ * a live invalidation grants nothing polling couldn't retrieve).
1264
+ *
1265
+ * Semantics and guarantees:
1266
+ * - Best-effort, at-most-once delivery: events can be lost or duplicated;
1267
+ * the initial refetch fires AFTER the subscription is live, so a write
1268
+ * racing the subscribe is never missed.
1269
+ * - `onInvalidate` runs single-flight with one trailing call: a burst of
1270
+ * events during an in-flight refetch coalesces into exactly one more.
1271
+ * - The host renews authorization periodically; when it definitively ends
1272
+ * (`status: 'ended'`), `endedReason` says why — `collection_deleted` and
1273
+ * `authorization_revoked` are terminal, others may warrant resubscribing
1274
+ * (unmount/remount or toggle `enabled`).
1275
+ * - Scope-aware: shared collections notify on any user's writes; user-scoped
1276
+ * collections only on yours; workspace-scoped need `workspaceId` and an
1277
+ * active membership.
1278
+ *
1279
+ * @example
1280
+ * const dataStore = useFiasDataStore();
1281
+ * const [scores, setScores] = useState<DataStoreDocument[]>([]);
1282
+ * const refresh = useCallback(async () => {
1283
+ * const res = await dataStore.query('scores', { limit: 50 });
1284
+ * setScores(res.documents);
1285
+ * }, [dataStore]);
1286
+ * const { status } = useDataSubscription('scores', refresh);
1287
+ */
1288
+ function useDataSubscription(collection, onInvalidate, options) {
1289
+ const bridge = useBridge();
1290
+ const [state, setState] = (0, react_1.useState)({ status: 'subscribing' });
1291
+ const onInvalidateRef = (0, react_1.useRef)(onInvalidate);
1292
+ onInvalidateRef.current = onInvalidate;
1293
+ const enabled = options?.enabled ?? true;
1294
+ const workspaceId = options?.workspaceId;
1295
+ (0, react_1.useEffect)(() => {
1296
+ if (!enabled) {
1297
+ setState({ status: 'ended', endedReason: 'unsubscribed' });
1298
+ return;
1299
+ }
1300
+ let cancelled = false;
1301
+ let subscriptionId = null;
1302
+ let unsubscribeEvent = null;
1303
+ let unsubscribeEnded = null;
1304
+ // Single-flight + exactly one trailing call: events arriving during an
1305
+ // in-flight refetch coalesce into one follow-up, never a parallel run.
1306
+ let refetching = false;
1307
+ let trailing = false;
1308
+ const runInvalidate = async () => {
1309
+ if (refetching) {
1310
+ trailing = true;
1311
+ return;
1312
+ }
1313
+ refetching = true;
1314
+ try {
1315
+ await onInvalidateRef.current();
1316
+ }
1317
+ catch {
1318
+ // The caller's refetch failed — their concern; the subscription
1319
+ // stays live and the next event tries again.
1320
+ }
1321
+ refetching = false;
1322
+ if (trailing && !cancelled) {
1323
+ trailing = false;
1324
+ void runInvalidate();
1325
+ }
1326
+ };
1327
+ setState({ status: 'subscribing' });
1328
+ void (async () => {
1329
+ try {
1330
+ const res = await bridge.request('entity_invoke', {
1331
+ entityId: entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID,
1332
+ payload: { op: 'subscribe', collection, workspaceId },
1333
+ });
1334
+ const subscription = res?.output;
1335
+ if (!subscription ||
1336
+ subscription.kind !== 'subscription' ||
1337
+ typeof subscription.subscriptionId !== 'string') {
1338
+ if (!cancelled)
1339
+ setState({ status: 'ended', endedReason: 'transport_error' });
1340
+ return;
1341
+ }
1342
+ if (cancelled) {
1343
+ bridge.releaseSubscription(subscription.subscriptionId);
1344
+ return;
1345
+ }
1346
+ subscriptionId = subscription.subscriptionId;
1347
+ unsubscribeEvent = bridge.onSubscriptionEvent(subscriptionId, () => void runInvalidate());
1348
+ unsubscribeEnded = bridge.onSubscriptionEnded(subscriptionId, (reason) => {
1349
+ if (!cancelled)
1350
+ setState({ status: 'ended', endedReason: reason });
1351
+ });
1352
+ setState({ status: 'active' });
1353
+ // Subscribe-then-fetch ordering: refetch AFTER the subscription is
1354
+ // live so a write racing the handshake is reflected either in this
1355
+ // fetch or in the event it triggers — never silently missed.
1356
+ void runInvalidate();
1357
+ }
1358
+ catch {
1359
+ if (!cancelled)
1360
+ setState({ status: 'ended', endedReason: 'transport_error' });
1361
+ }
1362
+ })();
1363
+ return () => {
1364
+ cancelled = true;
1365
+ unsubscribeEvent?.();
1366
+ unsubscribeEnded?.();
1367
+ if (subscriptionId)
1368
+ bridge.releaseSubscription(subscriptionId);
1369
+ };
1370
+ }, [bridge, collection, workspaceId, enabled]);
1371
+ return state;
1372
+ }
1373
+ /**
1374
+ * Consented access to documents the USER already owns — the only SDK
1375
+ * surface that reaches data your plugin did not create. Requires the
1376
+ * `vault:user-documents:read` permission. Every access is user-consented
1377
+ * (per-document grants created in the host picker, revocable any time in
1378
+ * Vault → App Access) and audited by the platform. Proprietary-sensitivity
1379
+ * documents are never grantable. For documents your plugin CREATES, use
1380
+ * `useVaultDocuments` instead.
1381
+ */
1382
+ function useVaultUserDocuments() {
1383
+ const bridge = useBridge();
1384
+ const pick = (0, react_1.useCallback)(async (options) => {
1385
+ return bridge.request('vault_documents_pick', {
1386
+ maxDocuments: options?.maxDocuments,
1387
+ });
1388
+ }, [bridge]);
1389
+ const list = (0, react_1.useCallback)(async (options) => {
1390
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'list_consented_documents', { limit: options?.limit, offset: options?.offset });
1391
+ return res.documents;
1392
+ }, [bridge]);
1393
+ const get = (0, react_1.useCallback)(async (documentId, options) => {
1394
+ return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document', { documentId, includeContent: options?.includeContent });
1395
+ }, [bridge]);
1396
+ const getDownloadUrl = (0, react_1.useCallback)(async (documentId) => {
1397
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document_url', { documentId });
1398
+ return {
1399
+ url: res.url,
1400
+ expiresAt: new Date(res.expiresAt).getTime(),
1401
+ contentType: res.contentType,
1402
+ };
1403
+ }, [bridge]);
1404
+ return (0, react_1.useMemo)(() => ({ pick, list, get, getDownloadUrl }), [pick, list, get, getDownloadUrl]);
1405
+ }
983
1406
  //# sourceMappingURL=hooks.js.map