@fias/arche-sdk 2.3.0 → 2.10.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 +7 -1
  14. package/dist/fias.d.ts.map +1 -1
  15. package/dist/fias.js +19 -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 +36 -1
  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 +230 -3
  26. package/dist/hooks.d.ts.map +1 -1
  27. package/dist/hooks.js +519 -72
  28. package/dist/hooks.js.map +1 -1
  29. package/dist/hooks.test.js +322 -219
  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 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +966 -97
  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 +339 -13
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +2 -1
  53. package/templates/default/AGENTS.md +328 -26
  54. package/templates/default/CLAUDE.md +328 -26
package/dist/hooks.js CHANGED
@@ -1,26 +1,34 @@
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
11
  exports.useBackgroundRemoval = useBackgroundRemoval;
12
+ exports.useClientEntity = useClientEntity;
10
13
  exports.useAudioGeneration = useAudioGeneration;
11
14
  exports.useSurface = useSurface;
12
15
  exports.useFiasNavigation = useFiasNavigation;
13
16
  exports.usePersistentState = usePersistentState;
14
17
  exports.useStepNavigation = useStepNavigation;
15
18
  exports.useFiasDataStore = useFiasDataStore;
19
+ exports.useFiasWorkspaces = useFiasWorkspaces;
16
20
  exports.useFiasStore = useFiasStore;
17
21
  exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
18
22
  exports.useVaultDocuments = useVaultDocuments;
19
23
  exports.useArcheAssets = useArcheAssets;
24
+ exports.useFiasPreviewState = useFiasPreviewState;
25
+ exports.useDataSubscription = useDataSubscription;
26
+ exports.useVaultUserDocuments = useVaultUserDocuments;
20
27
  const react_1 = require("react");
21
28
  const react_dom_1 = require("react-dom");
22
29
  const context_1 = require("./context");
23
30
  const bridge_1 = require("./bridge");
31
+ const entity_ops_1 = require("./entity-ops");
24
32
  /**
25
33
  * Internal helper to get bridge from context, throws if not wrapped in FiasProvider.
26
34
  */
@@ -58,7 +66,7 @@ function createDebouncedWriter(bridge) {
58
66
  if (timerId !== null)
59
67
  clearTimeout(timerId);
60
68
  pending = null;
61
- bridge.request('storage_write', { path, content }).catch(() => { });
69
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', { path, content }).catch(() => { });
62
70
  }
63
71
  function schedule(path, content) {
64
72
  const now = Date.now();
@@ -84,21 +92,34 @@ function createDebouncedWriter(bridge) {
84
92
  * Access the current user's profile information.
85
93
  * Requires the `user:profile:read` permission in fias-plugin.json.
86
94
  *
95
+ * If the `get_user` bridge request fails — most commonly because the plugin
96
+ * called this hook without declaring `user:profile:read` — the error is
97
+ * thrown during render so it surfaces to the nearest error boundary
98
+ * (`FiasProvider` installs one). Previously a failed/denied request left the
99
+ * hook permanently returning `null`, indistinguishable from "still loading",
100
+ * so a UI that gated on `if (!user)` hung on an infinite spinner forever.
101
+ *
87
102
  * @returns The current user's profile or null while loading.
88
103
  */
89
104
  function useFiasUser() {
90
105
  const bridge = useBridge();
91
106
  const [user, setUser] = (0, react_1.useState)(null);
107
+ const [error, setError] = (0, react_1.useState)(null);
92
108
  (0, react_1.useEffect)(() => {
93
109
  let mounted = true;
94
110
  bridge.request('get_user', {}).then((data) => {
95
111
  if (mounted)
96
112
  setUser(data);
113
+ }, (err) => {
114
+ if (mounted)
115
+ setError(err instanceof Error ? err : new Error(String(err)));
97
116
  });
98
117
  return () => {
99
118
  mounted = false;
100
119
  };
101
120
  }, [bridge]);
121
+ if (error)
122
+ throw error;
102
123
  return user;
103
124
  }
104
125
  /**
@@ -116,6 +137,25 @@ function useFiasTheme() {
116
137
  }, [bridge]);
117
138
  return theme;
118
139
  }
140
+ /**
141
+ * Self-hosted asset URLs for a platform-vendored library, for plugins that
142
+ * declare the `sandbox:vendored-libraries` permission. Returns a file-key → URL
143
+ * map (for `pdfjs-dist`: `{ mainModule, worker, cMapUrl, standardFontDataUrl }`),
144
+ * or `null` when the host's CDN is unconfigured or the library isn't vendored
145
+ * (the plugin should then degrade gracefully).
146
+ *
147
+ * Delivered in the init payload (no gated request); values are static for the
148
+ * session. The import specifier is already pinned to its main module via the
149
+ * host importmap, so a plugin typically only needs `worker` (pass to the
150
+ * pdf-core loader's `workerModuleUrl`) and the CMap/font directories.
151
+ *
152
+ * @param importSpecifier The vendored library's bare module name (e.g. `'pdfjs-dist'`).
153
+ * @returns The library's asset URLs, or null if unavailable.
154
+ */
155
+ function useFiasVendoredAssets(importSpecifier) {
156
+ const bridge = useBridge();
157
+ return bridge.getVendoredAssets(importSpecifier);
158
+ }
119
159
  /**
120
160
  * Access the plugin's sandboxed storage.
121
161
  * Requires the `storage:sandbox` permission in fias-plugin.json.
@@ -125,18 +165,23 @@ function useFiasTheme() {
125
165
  function useFiasStorage() {
126
166
  const bridge = useBridge();
127
167
  const readFile = (0, react_1.useCallback)(async (path) => {
128
- const res = await bridge.request('storage_read', { path });
168
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path });
129
169
  return res?.content ?? null;
130
170
  }, [bridge]);
131
171
  const writeFile = (0, react_1.useCallback)(async (path, content) => {
132
- await bridge.request('storage_write', { path, content });
172
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', {
173
+ path,
174
+ content,
175
+ });
133
176
  }, [bridge]);
134
177
  const listFiles = (0, react_1.useCallback)(async (prefix) => {
135
- const res = await bridge.request('storage_list', { prefix });
178
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'list', { prefix });
136
179
  return res?.files ?? [];
137
180
  }, [bridge]);
138
181
  const deleteFile = (0, react_1.useCallback)(async (path) => {
139
- await bridge.request('storage_delete', { path });
182
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'delete', {
183
+ path,
184
+ });
140
185
  }, [bridge]);
141
186
  return { readFile, writeFile, listFiles, deleteFile };
142
187
  }
@@ -330,6 +375,132 @@ function useBackgroundRemoval() {
330
375
  }, [bridge]);
331
376
  return { removeBackground, isLoading, result, error };
332
377
  }
378
+ /**
379
+ * Default timeout for a client-side entity invocation. Client-side entities
380
+ * (WASM models, dictionary engines) can take many seconds to warm up and
381
+ * run, so we use the same 2-minute ceiling as the image/audio surfaces.
382
+ */
383
+ const CLIENT_ENTITY_INVOKE_TIMEOUT_MS = 120000;
384
+ /**
385
+ * Invoke a client-side ("on-device") entity through the bridge's generic
386
+ * `client_entity_invoke` op. Client-side entities execute entirely in the
387
+ * browser (e.g. the nspell + retext grammar checker, the imgly WASM
388
+ * background remover) — input never leaves the device and there is no
389
+ * per-call credit cost.
390
+ *
391
+ * This is the client-side counterpart to {@link useEntityInvocation}, which
392
+ * invokes server-side, credit-billed AI entities. Target an entity by its ID
393
+ * and supply the input shape that entity documents; the typed result flows
394
+ * back through the returned promise:
395
+ *
396
+ * ```tsx
397
+ * const { invoke, isLoading, error } = useClientEntity();
398
+ * const { issues } = await invoke<GrammarCheckInput, GrammarCheckResult>(
399
+ * GRAMMAR_CHECK_ENTITY_ID,
400
+ * { text },
401
+ * );
402
+ * ```
403
+ *
404
+ * Requires the `entities:client_invoke` permission in `fias-plugin.json`.
405
+ * Per-entity rate limits (each entity's `pluginInvocable.rateLimitPerMinute`)
406
+ * are enforced by the host.
407
+ *
408
+ * Input limits are enforced **host-side by each entity's executor**, not by
409
+ * this hook — being generic, it cannot know a given entity's caps (e.g. the
410
+ * grammar entity's 100,000-character limit). Oversized/invalid input is
411
+ * rejected after it crosses the bridge, surfacing as a rejected promise /
412
+ * `error`. If you want to fail fast before the round-trip, pre-validate in
413
+ * your own code against the limits the entity documents.
414
+ */
415
+ function useClientEntity() {
416
+ const bridge = useBridge();
417
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
418
+ const [error, setError] = (0, react_1.useState)(null);
419
+ const invoke = (0, react_1.useCallback)(async (entityId, input) => {
420
+ setIsLoading(true);
421
+ setError(null);
422
+ try {
423
+ return await bridge.request('client_entity_invoke', { entityId, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
424
+ }
425
+ catch (err) {
426
+ const e = err instanceof Error ? err : new Error(String(err));
427
+ setError(e);
428
+ throw e;
429
+ }
430
+ finally {
431
+ setIsLoading(false);
432
+ }
433
+ }, [bridge]);
434
+ return { invoke, isLoading, error };
435
+ }
436
+ /**
437
+ * Entity ID of the NLLB-200 on-device translation entity. Exported so plugins
438
+ * can target it via the generic {@link useClientEntity} hook, with
439
+ * `TranslateParams` / `TranslationResult` as the I/O types.
440
+ *
441
+ * SYNC NOTE: must match `NLLB_TRANSLATION_ENTITY_ID` in
442
+ * `packages/entities/src/integrations/nllb-translation/config.ts`. The
443
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this —
444
+ * drift fails CI.
445
+ */
446
+ exports.NLLB_TRANSLATION_ENTITY_ID = 'ent_intg_nllb_xlate_000000000000';
447
+ /**
448
+ * Curated list of target languages for the NLLB translator, as
449
+ * label/FLORES-200-code pairs. Not exhaustive (NLLB supports 200 languages) —
450
+ * a convenient default set for building a picker. Sorted by label.
451
+ */
452
+ exports.NLLB_LANGUAGES = [
453
+ { label: 'Arabic', code: 'arb_Arab' },
454
+ { label: 'Bengali', code: 'ben_Beng' },
455
+ { label: 'Bulgarian', code: 'bul_Cyrl' },
456
+ { label: 'Catalan', code: 'cat_Latn' },
457
+ { label: 'Chinese (Simplified)', code: 'zho_Hans' },
458
+ { label: 'Chinese (Traditional)', code: 'zho_Hant' },
459
+ { label: 'Croatian', code: 'hrv_Latn' },
460
+ { label: 'Czech', code: 'ces_Latn' },
461
+ { label: 'Danish', code: 'dan_Latn' },
462
+ { label: 'Dutch', code: 'nld_Latn' },
463
+ { label: 'English', code: 'eng_Latn' },
464
+ { label: 'Finnish', code: 'fin_Latn' },
465
+ { label: 'French', code: 'fra_Latn' },
466
+ { label: 'German', code: 'deu_Latn' },
467
+ { label: 'Greek', code: 'ell_Grek' },
468
+ { label: 'Hebrew', code: 'heb_Hebr' },
469
+ { label: 'Hindi', code: 'hin_Deva' },
470
+ { label: 'Hungarian', code: 'hun_Latn' },
471
+ { label: 'Indonesian', code: 'ind_Latn' },
472
+ { label: 'Italian', code: 'ita_Latn' },
473
+ { label: 'Japanese', code: 'jpn_Jpan' },
474
+ { label: 'Korean', code: 'kor_Hang' },
475
+ { label: 'Norwegian', code: 'nob_Latn' },
476
+ { label: 'Persian', code: 'pes_Arab' },
477
+ { label: 'Polish', code: 'pol_Latn' },
478
+ { label: 'Portuguese', code: 'por_Latn' },
479
+ { label: 'Romanian', code: 'ron_Latn' },
480
+ { label: 'Russian', code: 'rus_Cyrl' },
481
+ { label: 'Serbian', code: 'srp_Cyrl' },
482
+ { label: 'Slovak', code: 'slk_Latn' },
483
+ { label: 'Spanish', code: 'spa_Latn' },
484
+ { label: 'Swedish', code: 'swe_Latn' },
485
+ { label: 'Tamil', code: 'tam_Taml' },
486
+ { label: 'Telugu', code: 'tel_Telu' },
487
+ { label: 'Thai', code: 'tha_Thai' },
488
+ { label: 'Turkish', code: 'tur_Latn' },
489
+ { label: 'Ukrainian', code: 'ukr_Cyrl' },
490
+ { label: 'Urdu', code: 'urd_Arab' },
491
+ { label: 'Vietnamese', code: 'vie_Latn' },
492
+ ];
493
+ /**
494
+ * Entity ID of the grammar_check client-side entity. Exported so plugins can
495
+ * target it via the generic {@link useClientEntity} hook. Literal here so the
496
+ * SDK stays bundle-clean (no `@fias/entities` runtime dep).
497
+ *
498
+ * SYNC NOTE: must match `GRAMMAR_CHECK_ENTITY_ID` in
499
+ * `packages/entities/src/integrations/grammar-check/config.ts`. The
500
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this —
501
+ * drift fails CI.
502
+ */
503
+ exports.GRAMMAR_CHECK_ENTITY_ID = 'ent_intg_grammar_check_000000000';
333
504
  /**
334
505
  * Generate audio (music today; future speech/SFX sub-types via the same
335
506
  * hook) via the bridge's `audio_generate` op. Returns an array of clips
@@ -414,9 +585,10 @@ function useSurface(surfaceKey, timeoutMs = 180000) {
414
585
  return { invoke, isLoading, result, error };
415
586
  }
416
587
  /**
417
- * Navigate within the plugin's route space.
588
+ * Navigate within the plugin's route space (`navigateTo`), or open another
589
+ * arche's page (`openArche`, requires the `navigation:open_arche` permission).
418
590
  *
419
- * @returns Navigation API (navigateTo, currentPath).
591
+ * @returns Navigation API (navigateTo, openArche, currentPath).
420
592
  */
421
593
  function useFiasNavigation() {
422
594
  const bridge = useBridge();
@@ -441,7 +613,10 @@ function useFiasNavigation() {
441
613
  const navigateTo = (0, react_1.useCallback)((path) => {
442
614
  bridge.request('navigate', { path });
443
615
  }, [bridge]);
444
- return { navigateTo, currentPath };
616
+ const openArche = (0, react_1.useCallback)((archeId, opts) => {
617
+ bridge.request('open_arche', { archeId, newTab: opts?.newTab === true });
618
+ }, [bridge]);
619
+ return { navigateTo, openArche, currentPath };
445
620
  }
446
621
  /**
447
622
  * Like useState, but auto-persists to bridge storage.
@@ -459,8 +634,7 @@ function usePersistentState(key, initialValue) {
459
634
  // Load from storage on mount; flush any pending write on unmount/key change
460
635
  // so rapid updates in a game loop never lose their last value.
461
636
  (0, react_1.useEffect)(() => {
462
- bridge
463
- .request('storage_read', { path: `__state/${key}` })
637
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${key}` })
464
638
  .then((result) => {
465
639
  if (result.exists && result.content !== null) {
466
640
  try {
@@ -473,6 +647,13 @@ function usePersistentState(key, initialValue) {
473
647
  }
474
648
  }
475
649
  initializedRef.current = true;
650
+ })
651
+ .catch(() => {
652
+ // A failed/denied storage_read (e.g. the plugin didn't declare
653
+ // `storage:sandbox`) must not become a silent unhandled rejection.
654
+ // Persistence is best-effort — fall back to the initial value and
655
+ // let subsequent writes fail on their own rather than hanging here.
656
+ initializedRef.current = true;
476
657
  });
477
658
  return () => {
478
659
  writer.flush();
@@ -514,10 +695,7 @@ function useStepNavigation(initialStep, options) {
514
695
  return;
515
696
  }
516
697
  let cancelled = false;
517
- bridge
518
- .request('storage_read', {
519
- path: `__state/${persistKey}`,
520
- })
698
+ (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${persistKey}` })
521
699
  .then((result) => {
522
700
  // Don't clobber a user-initiated setCurrentStep that landed before this resolved.
523
701
  if (cancelled || initializedRef.current)
@@ -590,34 +768,63 @@ function useStepNavigation(initialStep, options) {
590
768
  function useFiasDataStore() {
591
769
  const bridge = useBridge();
592
770
  const createCollection = (0, react_1.useCallback)(async (name, options) => {
593
- const res = await bridge.request('data_create_collection', { name, userScope: options?.userScope ?? 'user' });
771
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'create_collection', {
772
+ name,
773
+ userScope: options?.userScope ?? 'user',
774
+ searchable: options?.searchable,
775
+ writeMinRole: options?.writeMinRole,
776
+ readMinRole: options?.readMinRole,
777
+ });
594
778
  return res.collection;
595
779
  }, [bridge]);
596
780
  const listCollections = (0, react_1.useCallback)(async () => {
597
- const res = await bridge.request('data_list_collections', {});
781
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'list_collections');
598
782
  return res.collections;
599
783
  }, [bridge]);
600
784
  const deleteCollection = (0, react_1.useCallback)(async (name) => {
601
- await bridge.request('data_delete_collection', { name });
785
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete_collection', { name });
602
786
  }, [bridge]);
603
- const put = (0, react_1.useCallback)(async (collection, key, data) => {
604
- await bridge.request('data_put', { collection, key, data });
787
+ const put = (0, react_1.useCallback)(async (collection, key, data, options) => {
788
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'put', {
789
+ collection,
790
+ key,
791
+ data,
792
+ workspaceId: options?.workspaceId,
793
+ });
605
794
  }, [bridge]);
606
- const get = (0, react_1.useCallback)(async (collection, key) => {
607
- const res = await bridge.request('data_get', {
795
+ const get = (0, react_1.useCallback)(async (collection, key, options) => {
796
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'get', {
608
797
  collection,
609
798
  key,
799
+ workspaceId: options?.workspaceId,
610
800
  });
611
801
  return res.data;
612
802
  }, [bridge]);
613
- const query = (0, react_1.useCallback)(async (collection, options) => {
614
- return bridge.request('data_query', {
803
+ const query = (0, react_1.useCallback)(async (collection, options, scope) => {
804
+ return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'query', {
615
805
  collection,
616
806
  ...options,
807
+ workspaceId: scope?.workspaceId,
617
808
  });
618
809
  }, [bridge]);
619
- const deleteDoc = (0, react_1.useCallback)(async (collection, key) => {
620
- await bridge.request('data_delete', { collection, key });
810
+ const search = (0, react_1.useCallback)(async (collection, queryText, options, scope) => {
811
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'search', {
812
+ collection,
813
+ query: queryText,
814
+ ...options,
815
+ workspaceId: scope?.workspaceId,
816
+ });
817
+ return res.matches;
818
+ }, [bridge]);
819
+ const deleteDoc = (0, react_1.useCallback)(async (collection, key, options) => {
820
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete', {
821
+ collection,
822
+ key,
823
+ workspaceId: options?.workspaceId,
824
+ });
825
+ }, [bridge]);
826
+ const batch = (0, react_1.useCallback)(async (operations) => {
827
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'batch', { operations });
621
828
  }, [bridge]);
622
829
  return (0, react_1.useMemo)(() => ({
623
830
  createCollection,
@@ -626,8 +833,81 @@ function useFiasDataStore() {
626
833
  put,
627
834
  get,
628
835
  query,
836
+ search,
629
837
  delete: deleteDoc,
630
- }), [createCollection, listCollections, deleteCollection, put, get, query, deleteDoc]);
838
+ batch,
839
+ }), [
840
+ createCollection,
841
+ listCollections,
842
+ deleteCollection,
843
+ put,
844
+ get,
845
+ query,
846
+ search,
847
+ deleteDoc,
848
+ batch,
849
+ ]);
850
+ }
851
+ /**
852
+ * Access workspace tenancy — team/org workspaces with role-gated membership
853
+ * (owner/admin/member/viewer) over `workspace`-scoped Data Store collections
854
+ * (ADR-010). The creating user becomes the founding owner; a workspace always
855
+ * keeps at least one owner.
856
+ *
857
+ * Requires the `data:workspace` permission in fias-plugin.json. To read/write a
858
+ * workspace's documents, use `useFiasDataStore()` with `{ workspaceId }` on a
859
+ * collection created with `userScope: 'workspace'`.
860
+ *
861
+ * @example
862
+ * const workspaces = useFiasWorkspaces();
863
+ * const ws = await workspaces.create('Acme Corp');
864
+ * await workspaces.addMember(ws.workspaceId, otherUserId, 'member');
865
+ * // then, with useFiasDataStore():
866
+ * await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
867
+ *
868
+ * @returns Workspace API with lifecycle + membership methods.
869
+ */
870
+ function useFiasWorkspaces() {
871
+ const bridge = useBridge();
872
+ const create = (0, react_1.useCallback)(async (displayName) => {
873
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { displayName });
874
+ return res.workspace;
875
+ }, [bridge]);
876
+ const list = (0, react_1.useCallback)(async () => {
877
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list');
878
+ return res.workspaces;
879
+ }, [bridge]);
880
+ const get = (0, react_1.useCallback)(async (workspaceId) => {
881
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'get', { workspaceId });
882
+ return res.workspace;
883
+ }, [bridge]);
884
+ const archive = (0, react_1.useCallback)(async (workspaceId) => {
885
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'archive', { workspaceId });
886
+ }, [bridge]);
887
+ const listMembers = (0, react_1.useCallback)(async (workspaceId) => {
888
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list_members', { workspaceId });
889
+ return res.members;
890
+ }, [bridge]);
891
+ const addMember = (0, react_1.useCallback)(async (workspaceId, username, role, expiresAt) => {
892
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'add_member', {
893
+ workspaceId,
894
+ username,
895
+ role,
896
+ expiresAt,
897
+ });
898
+ return res.member;
899
+ }, [bridge]);
900
+ const updateMember = (0, react_1.useCallback)(async (workspaceId, username, role) => {
901
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', { workspaceId, username, role });
902
+ return res.member;
903
+ }, [bridge]);
904
+ const removeMember = (0, react_1.useCallback)(async (workspaceId, username) => {
905
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'remove_member', {
906
+ workspaceId,
907
+ username,
908
+ });
909
+ }, [bridge]);
910
+ return (0, react_1.useMemo)(() => ({ create, list, get, archive, listMembers, addMember, updateMember, removeMember }), [create, list, get, archive, listMembers, addMember, updateMember, removeMember]);
631
911
  }
632
912
  /**
633
913
  * Access the in-arche purchase (IAP) store.
@@ -647,8 +927,8 @@ function useFiasStore() {
647
927
  (0, react_1.useEffect)(() => {
648
928
  let mounted = true;
649
929
  Promise.all([
650
- bridge.request('store_get_products', {}),
651
- bridge.request('store_get_entitlements', {}),
930
+ (0, entity_ops_1.invokeStoreOp)(bridge, 'get_products'),
931
+ (0, entity_ops_1.invokeStoreOp)(bridge, 'get_entitlements'),
652
932
  ])
653
933
  .then(([prods, ents]) => {
654
934
  if (mounted) {
@@ -666,7 +946,7 @@ function useFiasStore() {
666
946
  };
667
947
  }, [bridge]);
668
948
  const purchase = (0, react_1.useCallback)(async (productIdentifier, options) => {
669
- const result = await bridge.request('store_purchase', {
949
+ const result = await (0, entity_ops_1.invokeStoreOp)(bridge, 'purchase', {
670
950
  productIdentifier,
671
951
  ...options,
672
952
  });
@@ -688,11 +968,11 @@ function useFiasStore() {
688
968
  return entitlements.some((e) => e.productIdentifier === productIdentifier && e.isActive);
689
969
  }, [entitlements]);
690
970
  const getPurchaseHistory = (0, react_1.useCallback)(async () => {
691
- const res = await bridge.request('store_get_purchase_history', {});
971
+ const res = await (0, entity_ops_1.invokeStoreOp)(bridge, 'get_purchase_history');
692
972
  return res.purchases;
693
973
  }, [bridge]);
694
974
  const restorePurchases = (0, react_1.useCallback)(async () => {
695
- const restored = await bridge.request('store_restore', {});
975
+ const restored = await (0, entity_ops_1.invokeStoreOp)(bridge, 'restore');
696
976
  setEntitlements(restored);
697
977
  return restored;
698
978
  }, [bridge]);
@@ -871,37 +1151,24 @@ function useVaultDocuments() {
871
1151
  });
872
1152
  }, [bridge]);
873
1153
  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.)
1154
+ // The presigned PUT must run in the HOST, not here: the plugin iframe's
1155
+ // CSP is `connect-src 'none'`, which silently blocks an in-iframe
1156
+ // `fetch` PUT in published plugins. The host (parent window, no
1157
+ // connect-src restriction) orchestrates init → PUT → finalize via the
1158
+ // `vault_documents_upload` bridge op; the presigned URL is backend-issued
1159
+ // host-side, so the iframe can't redirect the PUT (no SSRF). The
1160
+ // lower-level `uploadInit`/`uploadFinalize` remain for advanced callers.
878
1161
  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,
1162
+ return bridge.request('vault_documents_upload', {
1163
+ name: params.name,
1164
+ mimeType: params.mimeType,
1165
+ bytes: bytesToBase64(src),
1166
+ tags: params.tags,
1167
+ sensitivity: params.sensitivity,
1168
+ folderPath: params.folderPath,
1169
+ replacesDocumentId: params.replacesDocumentId,
898
1170
  });
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]);
1171
+ }, [bridge]);
905
1172
  return (0, react_1.useMemo)(() => ({
906
1173
  list,
907
1174
  read,
@@ -930,19 +1197,15 @@ function useVaultDocuments() {
930
1197
  uploadFinalize,
931
1198
  ]);
932
1199
  }
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');
1200
+ /** Base64-encode bytes for transport to the host's `vault_documents_upload`
1201
+ * op (the host does the presigned PUT — see PluginBridge.handleVaultUpload). */
1202
+ function bytesToBase64(bytes) {
1203
+ let binary = '';
1204
+ const chunk = 0x8000;
1205
+ for (let i = 0; i < bytes.length; i += chunk) {
1206
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
944
1207
  }
945
- return out;
1208
+ return btoa(binary);
946
1209
  }
947
1210
  /**
948
1211
  * Access the contributor-curated asset library for this arche.
@@ -980,4 +1243,188 @@ function useArcheAssets() {
980
1243
  }, [bridge]);
981
1244
  return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
982
1245
  }
1246
+ /**
1247
+ * Preserve transient UI state across a builder-preview rebuild.
1248
+ *
1249
+ * In the arche builder, editing code (or a background cross-device sync)
1250
+ * rebuilds the preview, which reloads the iframe and wipes the plugin's
1251
+ * in-memory state — losing, for example, a report the user just generated.
1252
+ * This hook lets you opt that state into host-side preservation: just before
1253
+ * the reload the host snapshots `getState()`, and after the reload it hands the
1254
+ * value back to `onRestore`.
1255
+ *
1256
+ * Inert outside the builder preview (the host never requests a capture), so
1257
+ * it's safe to leave in production plugin code.
1258
+ *
1259
+ * @param key Stable namespace for this slice of state (so multiple calls
1260
+ * don't collide). Use a literal like `'report'`.
1261
+ * @param getState Returns the current serializable state. Must be JSON-safe.
1262
+ * @param onRestore Receives the snapshot after a reload. Reads the latest
1263
+ * closure via a ref, so you don't need a stable identity.
1264
+ *
1265
+ * @example
1266
+ * const [report, setReport] = useState<Report | null>(null);
1267
+ * useFiasPreviewState('report', () => report, (saved) => setReport(saved as Report));
1268
+ */
1269
+ function useFiasPreviewState(key, getState, onRestore) {
1270
+ const bridge = useBridge();
1271
+ const getStateRef = (0, react_1.useRef)(getState);
1272
+ getStateRef.current = getState;
1273
+ const onRestoreRef = (0, react_1.useRef)(onRestore);
1274
+ onRestoreRef.current = onRestore;
1275
+ (0, react_1.useEffect)(() => {
1276
+ return bridge.registerPreviewState(key, () => getStateRef.current(), (state) => onRestoreRef.current(state));
1277
+ }, [bridge, key]);
1278
+ }
1279
+ /**
1280
+ * Subscribes to realtime change events for a datastore collection
1281
+ * (ADR-012 Phase M4). Notify-then-refetch: the platform pushes a minimal
1282
+ * "this collection changed" invalidation and `onInvalidate` decides what to
1283
+ * refetch — typically re-running the `useFiasDataStore().query(...)` your
1284
+ * component renders from.
1285
+ *
1286
+ * Requires the `data:store` permission (same as reading the collection —
1287
+ * a live invalidation grants nothing polling couldn't retrieve).
1288
+ *
1289
+ * Semantics and guarantees:
1290
+ * - Best-effort, at-most-once delivery: events can be lost or duplicated;
1291
+ * the initial refetch fires AFTER the subscription is live, so a write
1292
+ * racing the subscribe is never missed.
1293
+ * - `onInvalidate` runs single-flight with one trailing call: a burst of
1294
+ * events during an in-flight refetch coalesces into exactly one more.
1295
+ * - The host renews authorization periodically; when it definitively ends
1296
+ * (`status: 'ended'`), `endedReason` says why — `collection_deleted` and
1297
+ * `authorization_revoked` are terminal, others may warrant resubscribing
1298
+ * (unmount/remount or toggle `enabled`).
1299
+ * - Scope-aware: shared collections notify on any user's writes; user-scoped
1300
+ * collections only on yours; workspace-scoped need `workspaceId` and an
1301
+ * active membership.
1302
+ *
1303
+ * @example
1304
+ * const dataStore = useFiasDataStore();
1305
+ * const [scores, setScores] = useState<DataStoreDocument[]>([]);
1306
+ * const refresh = useCallback(async () => {
1307
+ * const res = await dataStore.query('scores', { limit: 50 });
1308
+ * setScores(res.documents);
1309
+ * }, [dataStore]);
1310
+ * const { status } = useDataSubscription('scores', refresh);
1311
+ */
1312
+ function useDataSubscription(collection, onInvalidate, options) {
1313
+ const bridge = useBridge();
1314
+ const [state, setState] = (0, react_1.useState)({ status: 'subscribing' });
1315
+ const onInvalidateRef = (0, react_1.useRef)(onInvalidate);
1316
+ onInvalidateRef.current = onInvalidate;
1317
+ const enabled = options?.enabled ?? true;
1318
+ const workspaceId = options?.workspaceId;
1319
+ (0, react_1.useEffect)(() => {
1320
+ if (!enabled) {
1321
+ setState({ status: 'ended', endedReason: 'unsubscribed' });
1322
+ return;
1323
+ }
1324
+ let cancelled = false;
1325
+ let subscriptionId = null;
1326
+ let unsubscribeEvent = null;
1327
+ let unsubscribeEnded = null;
1328
+ // Single-flight + exactly one trailing call: events arriving during an
1329
+ // in-flight refetch coalesce into one follow-up, never a parallel run.
1330
+ let refetching = false;
1331
+ let trailing = false;
1332
+ const runInvalidate = async () => {
1333
+ if (refetching) {
1334
+ trailing = true;
1335
+ return;
1336
+ }
1337
+ refetching = true;
1338
+ try {
1339
+ await onInvalidateRef.current();
1340
+ }
1341
+ catch {
1342
+ // The caller's refetch failed — their concern; the subscription
1343
+ // stays live and the next event tries again.
1344
+ }
1345
+ refetching = false;
1346
+ if (trailing && !cancelled) {
1347
+ trailing = false;
1348
+ void runInvalidate();
1349
+ }
1350
+ };
1351
+ setState({ status: 'subscribing' });
1352
+ void (async () => {
1353
+ try {
1354
+ const res = await bridge.request('entity_invoke', {
1355
+ entityId: entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID,
1356
+ payload: { op: 'subscribe', collection, workspaceId },
1357
+ });
1358
+ const subscription = res?.output;
1359
+ if (!subscription ||
1360
+ subscription.kind !== 'subscription' ||
1361
+ typeof subscription.subscriptionId !== 'string') {
1362
+ if (!cancelled)
1363
+ setState({ status: 'ended', endedReason: 'transport_error' });
1364
+ return;
1365
+ }
1366
+ if (cancelled) {
1367
+ bridge.releaseSubscription(subscription.subscriptionId);
1368
+ return;
1369
+ }
1370
+ subscriptionId = subscription.subscriptionId;
1371
+ unsubscribeEvent = bridge.onSubscriptionEvent(subscriptionId, () => void runInvalidate());
1372
+ unsubscribeEnded = bridge.onSubscriptionEnded(subscriptionId, (reason) => {
1373
+ if (!cancelled)
1374
+ setState({ status: 'ended', endedReason: reason });
1375
+ });
1376
+ setState({ status: 'active' });
1377
+ // Subscribe-then-fetch ordering: refetch AFTER the subscription is
1378
+ // live so a write racing the handshake is reflected either in this
1379
+ // fetch or in the event it triggers — never silently missed.
1380
+ void runInvalidate();
1381
+ }
1382
+ catch {
1383
+ if (!cancelled)
1384
+ setState({ status: 'ended', endedReason: 'transport_error' });
1385
+ }
1386
+ })();
1387
+ return () => {
1388
+ cancelled = true;
1389
+ unsubscribeEvent?.();
1390
+ unsubscribeEnded?.();
1391
+ if (subscriptionId)
1392
+ bridge.releaseSubscription(subscriptionId);
1393
+ };
1394
+ }, [bridge, collection, workspaceId, enabled]);
1395
+ return state;
1396
+ }
1397
+ /**
1398
+ * Consented access to documents the USER already owns — the only SDK
1399
+ * surface that reaches data your plugin did not create. Requires the
1400
+ * `vault:user-documents:read` permission. Every access is user-consented
1401
+ * (per-document grants created in the host picker, revocable any time in
1402
+ * Vault → App Access) and audited by the platform. Proprietary-sensitivity
1403
+ * documents are never grantable. For documents your plugin CREATES, use
1404
+ * `useVaultDocuments` instead.
1405
+ */
1406
+ function useVaultUserDocuments() {
1407
+ const bridge = useBridge();
1408
+ const pick = (0, react_1.useCallback)(async (options) => {
1409
+ return bridge.request('vault_documents_pick', {
1410
+ maxDocuments: options?.maxDocuments,
1411
+ });
1412
+ }, [bridge]);
1413
+ const list = (0, react_1.useCallback)(async (options) => {
1414
+ 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 });
1415
+ return res.documents;
1416
+ }, [bridge]);
1417
+ const get = (0, react_1.useCallback)(async (documentId, options) => {
1418
+ return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document', { documentId, includeContent: options?.includeContent });
1419
+ }, [bridge]);
1420
+ const getDownloadUrl = (0, react_1.useCallback)(async (documentId) => {
1421
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document_url', { documentId });
1422
+ return {
1423
+ url: res.url,
1424
+ expiresAt: new Date(res.expiresAt).getTime(),
1425
+ contentType: res.contentType,
1426
+ };
1427
+ }, [bridge]);
1428
+ return (0, react_1.useMemo)(() => ({ pick, list, get, getDownloadUrl }), [pick, list, get, getDownloadUrl]);
1429
+ }
983
1430
  //# sourceMappingURL=hooks.js.map