@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.
- package/dist/bridge.d.ts +76 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +219 -6
- package/dist/bridge.js.map +1 -1
- package/dist/bridge.test.js +155 -6
- package/dist/bridge.test.js.map +1 -1
- package/dist/coverage-fills.test.js +26 -32
- package/dist/coverage-fills.test.js.map +1 -1
- package/dist/entity-ops.d.ts +55 -0
- package/dist/entity-ops.d.ts.map +1 -0
- package/dist/entity-ops.js +48 -0
- package/dist/entity-ops.js.map +1 -0
- package/dist/fias.d.ts +7 -1
- package/dist/fias.d.ts.map +1 -1
- package/dist/fias.js +19 -12
- package/dist/fias.js.map +1 -1
- package/dist/generated/permissions.d.ts +7 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +36 -1
- package/dist/generated/permissions.js.map +1 -1
- package/dist/generated/surfaces.d.ts +424 -0
- package/dist/generated/surfaces.d.ts.map +1 -1
- package/dist/generated/surfaces.js +97 -1
- package/dist/generated/surfaces.js.map +1 -1
- package/dist/hooks.d.ts +230 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +519 -72
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +322 -219
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +966 -97
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -0
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +50 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +187 -2
- package/dist/provider.js.map +1 -1
- package/dist/provider.render.test.d.ts +17 -0
- package/dist/provider.render.test.d.ts.map +1 -0
- package/dist/provider.render.test.js +191 -0
- package/dist/provider.render.test.js.map +1 -0
- package/dist/provider.test.js +64 -8
- package/dist/provider.test.js.map +1 -1
- package/dist/types.d.ts +339 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/templates/default/AGENTS.md +328 -26
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
620
|
-
await bridge.
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
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
|