@fias/create-fias-plugin 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fias/create-fias-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -63,6 +63,28 @@ function MyComponent() {
63
63
  - `fonts`: `{ body, heading, mono }` (font-family strings)
64
64
  - `components`: `{ borderRadius, buttonRadius, cardRadius, inputRadius, shadowSm, shadowMd, shadowLg, borderWidth }`
65
65
 
66
+ ### `useFiasVendoredAssets(importSpecifier)` — Self-hosted vendored-library asset URLs
67
+
68
+ **Permission:** `sandbox:vendored-libraries`
69
+ **Returns:** `Record<string, string> | null` (null when the host CDN is unconfigured or the library isn't vendored)
70
+
71
+ Some heavy libraries (pdf.js today; WASM runtimes later) need a Web Worker and large asset files that a sandboxed plugin can't bundle or self-host. The platform vendors a curated set of these — version-pinned, served from its trusted CDN. A plugin opts in by (1) listing the library in its `dependencies` and (2) declaring the `sandbox:vendored-libraries` permission. The platform then widens the plugin CSP for exactly that library (e.g. pdf.js → `worker-src blob:`+CDN, `script-src`/`connect-src` CDN), pins the import specifier to the self-hosted build via the host importmap (so `import * as pdfjsLib from 'pdfjs-dist'` resolves to it), and delivers the library's asset URLs here. For `pdfjs-dist` you typically only need `worker` (feed it to `@fias/pdf-core`'s loader `workerModuleUrl`) plus `cMapUrl` / `standardFontDataUrl` for CID / non-embedded fonts.
72
+
73
+ ```tsx
74
+ import { useFiasVendoredAssets } from '@fias/arche-sdk';
75
+ import { loadPdfJs, configurePdfAssets } from '@fias/pdf-core/loader';
76
+
77
+ function PdfViewer() {
78
+ const assets = useFiasVendoredAssets('pdfjs-dist');
79
+ if (!assets) return <div>PDF rendering unavailable</div>;
80
+ configurePdfAssets({ cMapUrl: assets.cMapUrl, standardFontDataUrl: assets.standardFontDataUrl });
81
+ const pdfjs = await loadPdfJs({ workerModuleUrl: assets.worker }); // off-thread worker
82
+ // ...render with pdfjs
83
+ }
84
+ ```
85
+
86
+ **`pdfjs-dist` asset keys:** `{ mainModule, worker, cMapUrl, standardFontDataUrl }` — all absolute CDN URLs.
87
+
66
88
  ### `useFiasFonts()` — Platform font catalog
67
89
 
68
90
  **Permission:** none
@@ -205,10 +227,120 @@ const doc = await fias.dataStore.get('scores', 'key');
205
227
  - 100 MB total storage per plugin
206
228
  - Max 100 results per query, max 10 filters, max field path depth 5
207
229
 
208
- **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `createCollection` 10
230
+ **Atomic batch writes:** `batch(operations)` applies up to 25 put/delete ops in a **single transaction** — all-or-nothing. Use it when several writes must stay consistent (e.g. append a ledger entry AND update a running total); a sequence of separate `put`/`delete` calls can leave a half-applied state. A batch may span collections and scopes (pass `workspaceId` per op); any failure (bad key, role denial, quota) rolls the whole batch back.
231
+
232
+ ```tsx
233
+ await dataStore.batch([
234
+ { op: 'put', collection: 'ledger', key: 'tx-42', data: { delta: 100 } },
235
+ { op: 'put', collection: 'totals', key: 'balance', data: { value: newBalance } },
236
+ ]);
237
+ ```
238
+
239
+ **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
209
240
 
210
241
  **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `RATE_LIMIT`.
211
242
 
243
+ **Semantic search (`data:search`):** Make a collection searchable to do vector search over a text field. Declare it at creation with `searchable: { field }`, then call `search()`. Embeddings are managed by the platform — you never handle vectors or pick a model. Documents are embedded asynchronously, so a freshly-written doc becomes searchable a moment later (it's always immediately visible to `query`). The query embedding charges the caller's credits. Requires the `data:search` permission (distinct from `data:store`).
244
+
245
+ ```tsx
246
+ // Create a searchable collection (embedding the `text` field) and publish docs.
247
+ await dataStore.createCollection('recipes', { userScope: 'shared', searchable: { field: 'text' } });
248
+ await dataStore.put('recipes', recipeId, { text: `${title} ${description}`, title, cuisine });
249
+
250
+ // Semantic search (charges the caller's credits for the query embedding).
251
+ const matches = await dataStore.search('recipes', 'quick vegetarian weeknight dinner', {
252
+ topK: 10,
253
+ filters: [{ field: 'cuisine', op: 'eq', value: 'italian' }], // vector + JSONB facet, ANDed
254
+ });
255
+ // matches: Array<{ key, data, similarity }>, best first
256
+ ```
257
+
258
+ **Search limits:** `topK` ≤ 50, query ≤ 1024 chars, 50,000 embedded documents per arche; `search` is rate-limited to 20/min (each call embeds the query). Documents in non-searchable collections — and not-yet-embedded docs — are invisible to `search` but always returned by `query`.
259
+
260
+ ### `useDataSubscription()` — Realtime collection change events
261
+
262
+ **Permission:** `data:store` (same as reading — live updates grant nothing polling couldn't)
263
+ **Returns:** `DataSubscriptionState` — `{ status: 'subscribing' | 'active' | 'ended', endedReason? }`
264
+
265
+ Get notified when a datastore collection changes, instead of manually refreshing. **Notify-then-refetch:** the platform pushes a minimal "this collection changed" signal and your `onInvalidate` callback re-runs whatever query your component renders from. Perfect for shared-scope collections where other users' writes should appear live (leaderboards, live scores, registrations).
266
+
267
+ ```tsx
268
+ import { useFiasDataStore, useDataSubscription } from '@fias/arche-sdk';
269
+
270
+ function Leaderboard() {
271
+ const dataStore = useFiasDataStore();
272
+ const [rows, setRows] = useState<ScoreRow[]>([]);
273
+
274
+ const refresh = useCallback(async () => {
275
+ const res = await dataStore.query<ScoreRow>('scores', {
276
+ orderBy: { field: 'score', direction: 'desc' },
277
+ limit: 20,
278
+ });
279
+ setRows(res.documents.map((d) => d.data));
280
+ }, [dataStore]);
281
+
282
+ // Refetches automatically whenever ANY user writes to `scores`.
283
+ const { status } = useDataSubscription('scores', refresh);
284
+ // status: 'subscribing' → 'active'; 'ended' + endedReason on teardown
285
+ }
286
+ ```
287
+
288
+ **Semantics you can rely on:**
289
+
290
+ - The initial refetch fires after the subscription is live, so a write racing your mount is never missed.
291
+ - `onInvalidate` runs single-flight with one trailing call — event bursts coalesce, never parallel refetches.
292
+ - Delivery is best-effort (events can drop or duplicate) — always treat it as "time to refetch", never as data.
293
+ - Scope-aware: `shared` collections notify on any user's writes; `user`-scoped only on yours; `workspace`-scoped need `{ workspaceId }` and an active membership.
294
+ - When the subscription ends (`status: 'ended'`), `endedReason` tells you why: `collection_deleted` and `authorization_revoked` are terminal; `limit`, `transport_error`, and `lease_expired` may be worth resubscribing (remount or toggle the `enabled` option).
295
+
296
+ **Options:** `useDataSubscription(collection, onInvalidate, { workspaceId?, enabled? })`.
297
+
298
+ **Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
299
+
300
+ ### `useFiasWorkspaces()` — Team/org workspaces with roles
301
+
302
+ **Permission:** `data:workspace`
303
+ **Returns:** `FiasWorkspacesApi`
304
+
305
+ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org space) inside your arche, with role-on-edge membership. A collection created with `userScope: 'workspace'` holds documents owned by a workspace and visible to its members, gated by role. Roles are `owner` > `admin` > `member` > `viewer`:
306
+
307
+ - **owner** — manage the workspace + members, read/write docs
308
+ - **admin** — manage non-owner members, read/write docs
309
+ - **member** — read/write docs
310
+ - **viewer** — read docs only
311
+
312
+ The creating user becomes the founding owner, and a workspace always keeps at least one owner (`MUST_KEEP_ONE_OWNER`). To read/write a workspace's documents, use `useFiasDataStore()` with the `{ workspaceId }` option (that still needs `data:store`, plus the server-side role gate).
313
+
314
+ ```tsx
315
+ import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
316
+
317
+ function Team() {
318
+ const workspaces = useFiasWorkspaces();
319
+ const dataStore = useFiasDataStore();
320
+
321
+ async function setup() {
322
+ const ws = await workspaces.create('Acme Corp'); // caller becomes owner
323
+ await workspaces.addMember(ws.workspaceId, 'alice', 'member'); // members are addressed by username
324
+
325
+ // Workspace-scoped collection + document (role-gated server-side).
326
+ await dataStore.createCollection('ledger', { userScope: 'workspace' });
327
+ await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
328
+
329
+ // Read the workspace's ledger back: data store query with the workspace scope.
330
+ const securities = await dataStore.query(
331
+ 'ledger',
332
+ { orderBy: { field: 'shares', direction: 'desc' } },
333
+ { workspaceId: ws.workspaceId },
334
+ );
335
+
336
+ const mine = await workspaces.list(); // workspaces I belong to
337
+ const members = await workspaces.listMembers(ws.workspaceId);
338
+ }
339
+ }
340
+ ```
341
+
342
+ **Methods:** `create`, `list`, `get`, `archive` (owner only), `listMembers`, `addMember`, `updateMember`, `removeMember`. `addMember(workspaceId, username, role, expiresAt?)` accepts an optional ISO-8601 `expiresAt` to grant **time-bounded** access (e.g. a contractor seat) — owners cannot expire; an expired member is treated as no longer a member. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
343
+
212
344
  ### `useEntityInvocation()` — Invoke AI models
213
345
 
214
346
  **Permission:** `entities:invoke`
@@ -412,6 +544,73 @@ const transparentPng = await removeBackground(jpegBlob);
412
544
 
413
545
  **Limits:** input ≤ 25 MiB, PNG/JPEG only, rate-limited to 10/min per arche per user. Throws on size violation rather than returning a result.
414
546
 
547
+ ### Image-editing capability hooks (auto-generated)
548
+
549
+ **Permission:** `entities:image_edit`
550
+ **Returns (per hook):** `{ invoke, isLoading, result, error }`
551
+
552
+ Mask-aware AI image-editing surfaces, generated from the platform's surface registry (same typed-hook pattern as audio). The source image and mask are passed **by handle** — `{ fileId }` (a Vault Document this arche owns), `{ archeAssetId }` (a published asset), or `{ dataUrl }` (inline base64; keep it small — use a handle for large source images). The edited result is saved to the user's Vault and returned as a presigned `imageUrl` you load onto your canvas.
553
+
554
+ ```tsx
555
+ import { useSmartInpaint } from '@fias/arche-sdk';
556
+
557
+ const { invoke } = useSmartInpaint();
558
+ const result = await invoke({
559
+ image: { fileId: sourceDocId }, // or { dataUrl } / { archeAssetId }
560
+ mask: { dataUrl: maskPngDataUrl }, // alpha=255 = region to replace
561
+ prompt: 'a field of sunflowers',
562
+ engine: 'stability', // 'stability' | 'recraft' | 'openai'
563
+ });
564
+ // result: { imageUrl, imageRef, fileId, mimeType, costCredits }
565
+ ```
566
+
567
+ Long-running edits complete over WebSocket transparently — `await invoke(...)` resolves with the final result just like any other hook. Browse the full surface list with `npx fias-dev entities`.
568
+
569
+ Currently shipped image-edit hooks: `useSmartInpaint` (mask replace), `useSmartErase` (mask remove + fill), `useSmartOutpaint` (extend canvas), `useSmartSearchReplace` (prompt-targeted replace, no mask), `useSmartSearchRecolor` (prompt-targeted recolor), `useSmartVectorize` (raster → SVG), `useSmartRemoveBackground` (server-side bg removal), `useSmartImageToImage` (guided regenerate), `useSmartUpscale` (resolution increase), `useSmartControl` (sketch/structure/style control), `useSmartReplaceBackground` (swap + relight background). Mask-based hooks take an `image` + `mask`; search/recolor take an `image` + a text selector; outpaint takes an `image` + edge pixels; vectorize/remove-background/upscale take an `image` (+ mode/engine); control and replace-background accept additional reference images by handle.
570
+
571
+ ### `useClientEntity()` — Invoke client-side (on-device) entities
572
+
573
+ **Permission:** `entities:client_invoke`
574
+ **Returns:** `ClientEntityApi`
575
+
576
+ The generic surface for any client-side entity — capabilities the host runs entirely in the browser (dictionary/WASM engines). Input never leaves the device and there is no per-call cost. The on-device counterpart to `useEntityInvocation()`. Target the entity by ID and supply its documented input/output types:
577
+
578
+ ```tsx
579
+ import {
580
+ useClientEntity,
581
+ GRAMMAR_CHECK_ENTITY_ID,
582
+ NLLB_TRANSLATION_ENTITY_ID,
583
+ NLLB_LANGUAGES,
584
+ } from '@fias/arche-sdk';
585
+ import type {
586
+ GrammarCheckInput,
587
+ GrammarCheckResult,
588
+ TranslateParams,
589
+ TranslationResult,
590
+ } from '@fias/arche-sdk';
591
+
592
+ const { invoke, isLoading, error } = useClientEntity();
593
+
594
+ // Grammar & spell check (non-AI nspell + retext engine):
595
+ const { issues } = await invoke<GrammarCheckInput, GrammarCheckResult>(GRAMMAR_CHECK_ENTITY_ID, {
596
+ text: 'teh quick brown fox',
597
+ categories: ['spelling', 'grammar', 'style', 'readability'], // optional; default = all
598
+ customDictionary: ['Fias'], // optional; words never flagged as misspelled
599
+ });
600
+ // issues: Array<{ start, end, message, category, ruleId, suggestions[] }>
601
+
602
+ // On-device translation (Meta NLLB-200 via transformers.js, free; the source
603
+ // language is auto-detected when omitted, target is a FLORES-200 code — pick
604
+ // from the exported NLLB_LANGUAGES label/code list):
605
+ const { translation, detectedSourceLang } = await invoke<TranslateParams, TranslationResult>(
606
+ NLLB_TRANSLATION_ENTITY_ID,
607
+ { text: 'Hello', targetLang: 'spa_Latn' }, // pass sourceLang to skip detection
608
+ );
609
+ ```
610
+
611
+ **Grammar limits:** text ≤ 100,000 characters, rate-limited to 30/min per arche per user.
612
+ **Translation limits:** text ≤ 2,000 characters, rate-limited to 20/min per arche per user. The first call downloads the model (~600 MB, then browser-cached) and needs a reasonably capable device (~1–2 GB of browser memory); on constrained/mobile devices the load can fail — catch the error and offer `useEntityInvocation` (the AI engine) as a fallback.
613
+
415
614
  ### Audio capability hooks (auto-generated)
416
615
 
417
616
  **Permission:** `entities:audio_generate`
@@ -441,6 +640,23 @@ const { invoke } = useSurface<MyParams, MyResult>('my.surface-key');
441
640
 
442
641
  Default per-request timeout is 180 s (covers TTS for long text and Lyria multi-clip generation).
443
642
 
643
+ ### `useIntegrationWebSearch()` — Live web search
644
+
645
+ **Permission:** `entities:web_search`
646
+ **Returns:** `{ invoke, isLoading, result, error }`
647
+
648
+ Search the live web and get a concise, cited summary. Backed by the swappable
649
+ `web-search` entity (the search provider can change without your code changing). The signed-in user is charged the standard 20% markup over the raw
650
+ search cost. Rate-limited to 10/min per arche per user.
651
+
652
+ ```tsx
653
+ import { useIntegrationWebSearch } from '@fias/arche-sdk';
654
+
655
+ const { invoke, isLoading, result } = useIntegrationWebSearch();
656
+ await invoke({ query: 'latest EU AI Act enforcement dates' });
657
+ // result: { summary, sources: [{ url, title }], searchCount, costCredits }
658
+ ```
659
+
444
660
  ### `useVaultDocuments()` — User-owned Vault documents
445
661
 
446
662
  **Permissions:** `vault:documents:read` (list / read / getDownloadUrl / search), `vault:documents:write` (write / update / delete / attach / detach / upload)
@@ -499,6 +715,41 @@ await vault.detach(documentId, referenceId);
499
715
 
500
716
  **`fetchVaultDocumentDownloadUrl(documentId)`** — non-React function that shares the hook's client-side URL cache. Use from PDF exporters, image preloaders, or other non-component code paths.
501
717
 
718
+ ### `useVaultUserDocuments()` — User-granted access to the user's own documents
719
+
720
+ **Permission:** `vault:user-documents:read`
721
+
722
+ The only SDK surface that reaches data your plugin did NOT create: documents the user already owns in their Vault, shared with your plugin through an explicit, per-document consent flow. `useVaultDocuments` (above) is for documents your plugin creates; this hook is for documents the _user_ brings.
723
+
724
+ ```tsx
725
+ import { useVaultUserDocuments } from '@fias/arche-sdk';
726
+
727
+ const userDocs = useVaultUserDocuments();
728
+
729
+ // 1. Ask the user to share documents. The HOST renders its own Vault picker
730
+ // + consent sheet — your plugin never sees the user's vault listing, only
731
+ // what they hand-pick and explicitly approve. Call from a button click.
732
+ const picked = await userDocs.pick({ maxDocuments: 5 });
733
+ if ('canceled' in picked) {
734
+ // A normal outcome — the user changed their mind. Never treat as an error.
735
+ } else {
736
+ // picked.documents: [{ documentId, name, mimeType, sizeBytes }]
737
+ }
738
+
739
+ // 2. Later (any session): read what was granted.
740
+ const granted = await userDocs.list(); // all granted docs
741
+ const { document, content } = await userDocs.get(id, { includeContent: true }); // text docs
742
+ const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
743
+ ```
744
+
745
+ **The consent model (what to tell your users):**
746
+
747
+ - Grants are **per-document** and **standing**: the plugin can re-read granted documents in later sessions until the user removes access in **Vault → App Access**.
748
+ - After revocation, `get`/`getDownloadUrl` reject with `DOCUMENT_NOT_FOUND` — handle that path gracefully (drop the document from your UI; do not retry in a loop).
749
+ - `proprietary`-sensitivity documents are never grantable (they don't even appear in the picker).
750
+ - Every read is audited by the platform; a new document _version_ is a new documentId, so a re-pick is needed after the user replaces a document.
751
+ - Not available in the builder preview or the dev harness — `pick()` resolves `{ canceled: true }` there. Test the flow in the published plugin with your own documents.
752
+
502
753
  ### `useArcheAssets()` — Contributor-published asset library
503
754
 
504
755
  **Permission:** `assets:read`
@@ -579,10 +830,40 @@ function UnlockButton() {
579
830
  ```tsx
580
831
  import { useFiasNavigation } from '@fias/arche-sdk';
581
832
 
582
- const { navigateTo, currentPath } = useFiasNavigation();
583
- navigateTo('/settings');
833
+ const { navigateTo, openArche, currentPath } = useFiasNavigation();
834
+ navigateTo('/settings'); // within THIS arche's route space
835
+
836
+ // Open ANOTHER arche's page. Requires the `navigation:open_arche` permission.
837
+ // Pass a stable arche id (`arc_…` / `arche_…`), not a slug. `newTab` is
838
+ // best-effort — the browser may block the popup, in which case the host
839
+ // navigates in the same tab instead.
840
+ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
584
841
  ```
585
842
 
843
+ ### Opening external links
844
+
845
+ To send the user to an external website (a YouTube video, docs, your homepage),
846
+ open it in a **new tab** with the standard web API — no SDK hook or permission
847
+ is needed:
848
+
849
+ ```tsx
850
+ <button
851
+ onClick={() => window.open('https://youtube.com/watch?v=…', '_blank', 'noopener,noreferrer')}
852
+ >
853
+ Watch the video
854
+ </button>
855
+ ```
856
+
857
+ Notes:
858
+
859
+ - The plugin iframe grants `allow-popups`, so `window.open(url, '_blank')`
860
+ works. Opening in the **same** tab (or an `<a>` without `target="_blank"`)
861
+ does **not** — that would try to navigate the sandbox itself.
862
+ - There is **no** "leaving Fias" confirmation dialog — the tab opens directly.
863
+ Only send users to URLs you trust, and always pass `'noopener,noreferrer'`.
864
+ - Only `http(s)` URLs make sense here; `javascript:`/`data:` URLs are blocked
865
+ by the sandbox.
866
+
586
867
  ### `useStepNavigation()` — Multi-step workflows
587
868
 
588
869
  **Permission:** None for in-memory use; `storage:sandbox` when `persistKey` is set (it reads/writes `__state/<persistKey>` via the storage bridge).
@@ -616,6 +897,26 @@ const [count, setCount] = usePersistentState<number>('counter', 0);
616
897
 
617
898
  **Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously, but the underlying `storage_write` is coalesced (250 ms trailing-edge debounce, 1 s max-wait, flushed on unmount). Safe to call from `requestAnimationFrame` and other high-frequency handlers. For state that updates every frame (game positions, drag coordinates), still prefer plain `useState` — persisting transient state is wasteful and not useful on reload.
618
899
 
900
+ ### `useFiasPreviewState()` — Survive builder-preview rebuilds
901
+
902
+ **Permission:** none.
903
+
904
+ In the arche builder, editing code (or a background cross-device sync) rebuilds the preview, which reloads the iframe and wipes the plugin's in-memory state — losing, for example, a report the user just generated. Opt that state into host-side preservation: just before the reload the host snapshots `getState()`, and after the reload it hands the value back to `onRestore`.
905
+
906
+ ```tsx
907
+ import { useFiasPreviewState } from '@fias/arche-sdk';
908
+
909
+ const [report, setReport] = useState<Report | null>(null);
910
+ // `key` namespaces this slice; `getState` must return JSON-safe state.
911
+ useFiasPreviewState(
912
+ 'report',
913
+ () => report,
914
+ (saved) => setReport(saved as Report),
915
+ );
916
+ ```
917
+
918
+ **Inert outside the builder preview** (the host never requests a capture), so it's safe to leave in production plugin code. Use it only for expensive-to-recreate transient state — not as a substitute for `usePersistentState` (which persists across full reloads and sessions).
919
+
619
920
  ### `fias` — Imperative utilities
620
921
 
621
922
  Use the `fias` namespace when you need bridge operations from outside a React component (event handlers attached imperatively, utility modules, non-React entry points).
@@ -667,26 +968,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
667
968
 
668
969
  **Fields:**
669
970
 
670
- | Field | Req | Description |
671
- | --------------------- | ---- | ------------------------------------------------------------------------------------------------------- |
672
- | `name` | Yes | Plugin identifier (lowercase, hyphens) |
673
- | `version` | Yes | Semver (e.g., `"1.0.0"`) |
674
- | `description` | Yes | Short marketplace description |
675
- | `expandedDescription` | No | Long-form marketplace copy |
676
- | `main` | Yes | Entry point source file |
677
- | `archeType` | Yes | `"tool"` or `"site"` |
678
- | `categorySlug` | No | Marketplace category for discovery |
679
- | `icon` | No | Relative path to icon asset (shown in marketplace + arche header) |
680
- | `tags` | No | Discovery tags |
681
- | `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
682
- | `permissions` | Yes | Array of permission scopes (see below) |
683
- | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
684
- | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
685
- | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
686
- | `isListed` | No | Default `true`. Visible in marketplace store |
687
- | `sdk` | Yes | SDK version range |
688
- | `dependencies` | No | npm packages with **exact** versions (max 20) |
689
- | `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
971
+ | Field | Req | Description |
972
+ | --------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
973
+ | `name` | Yes | Plugin identifier (lowercase, hyphens) |
974
+ | `version` | Yes | Semver (e.g., `"1.0.0"`) |
975
+ | `description` | Yes | Short marketplace description |
976
+ | `expandedDescription` | No | Long-form marketplace copy |
977
+ | `main` | Yes | Entry point source file |
978
+ | `archeType` | Yes | `"tool"` or `"site"` |
979
+ | `categorySlug` | No | Marketplace category for discovery |
980
+ | `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local-dev submissions only; on staging/prod set the icon via the admin console. |
981
+ | `tags` | No | Discovery tags |
982
+ | `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
983
+ | `permissions` | Yes | Array of permission scopes (see below) |
984
+ | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
985
+ | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
986
+ | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
987
+ | `isListed` | No | Default `true`. Visible in marketplace store |
988
+ | `sdk` | Yes | SDK version range |
989
+ | `dependencies` | No | npm packages with **exact** versions (max 20) |
990
+ | `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
690
991
 
691
992
  **Pricing detail:**
692
993
 
@@ -701,7 +1002,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
701
1002
 
702
1003
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
703
1004
 
704
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `entities:invoke`, `entities:image_generate`, `entities:image_remove_background`, `entities:audio_generate`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `assets:read`
1005
+ **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `data:search`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:image_remove_background`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `assets:read`, `navigation:open_arche`, `sandbox:vendored-libraries`
705
1006
 
706
1007
  **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
707
1008
 
@@ -737,7 +1038,7 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
737
1038
 
738
1039
  **Public arches (`isPublic: true`):**
739
1040
 
740
- Setting `isPublic: true` lets anonymous visitors (no Fias account) view the arche at `/a/<archeId>`. The UI renders without the platform sidebar. **Bridge operations still require auth** — `useFiasDataStore` writes, `useFiasStore.purchase()`, `useFiasStorage` reads, vault operations, etc. all require the visitor to be signed in. Public mode is for read-only landing pages, marketing pages, and shared-content viewers — not for collecting registrations or payments from anonymous users.
1041
+ Setting `isPublic: true` lets anonymous visitors (no Fias account) view the arche at `/a/<archeId>`. The UI renders without the platform sidebar. **Bridge operations still require auth** — `useFiasDataStore` writes, `useFiasStore.purchase()`, `useFiasStorage` reads, vault operations, etc. all require the visitor to be signed in. Public mode is for read-only landing pages, marketing pages, and read-only catalog viewers — not for collecting registrations or payments from anonymous users.
741
1042
 
742
1043
  ## Plugin Constraints
743
1044
 
@@ -751,8 +1052,9 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
751
1052
 
752
1053
  ### Sandboxing
753
1054
 
754
- - Plugins run in an iframe with `sandbox="allow-scripts allow-forms allow-same-origin allow-downloads"`
1055
+ - Plugins run in an iframe with `sandbox="allow-scripts allow-forms allow-downloads allow-popups allow-popups-to-escape-sandbox"` (note: **no** `allow-same-origin` — the plugin has an opaque origin)
755
1056
  - **No `fetch()` or `XMLHttpRequest`** — all network access is blocked
1057
+ - **You _can_ open external links in a new tab** (e.g. `window.open('https://…', '_blank')`) — see "Opening external links" above
756
1058
  - **No access** to parent DOM, cookies, or localStorage
757
1059
  - **No external scripts or stylesheets** — everything must be bundled
758
1060
  - All platform communication goes through the bridge (SDK hooks)
@@ -832,8 +1134,8 @@ You can choose between **Staging** and **Production** environments using the dro
832
1134
  You can also authenticate via the command line:
833
1135
 
834
1136
  ```bash
835
- npx fias-dev login # Authenticate with staging (default)
836
- npx fias-dev login --env production # Authenticate with production
1137
+ npx fias-dev login # Authenticate with prod (default)
1138
+ npx fias-dev login --env staging # Authenticate with staging (internal)
837
1139
  ```
838
1140
 
839
1141
  ### Browsing Available Entities
@@ -869,7 +1171,7 @@ happened either way).
869
1171
 
870
1172
  ### Managing the asset library (for `useArcheAssets`)
871
1173
 
872
- `useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <staging|prod|local>` (default `staging`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
1174
+ `useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <prod|staging|local>` (default `prod`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
873
1175
 
874
1176
  ```bash
875
1177
  npx fias-dev assets enable # Turn on the asset library for this arche
@@ -897,7 +1199,7 @@ npx fias-dev collaborators set <identifier> \ # Change role and/or
897
1199
  npx fias-dev collaborators remove <identifier> # Revoke a collaborator
898
1200
  ```
899
1201
 
900
- All commands accept `--env <staging|production|local>` (default `staging`).
1202
+ All commands accept `--env <prod|staging|local>` (default `prod`).
901
1203
 
902
1204
  ### Other CLI diagnostics
903
1205
 
@@ -63,6 +63,28 @@ function MyComponent() {
63
63
  - `fonts`: `{ body, heading, mono }` (font-family strings)
64
64
  - `components`: `{ borderRadius, buttonRadius, cardRadius, inputRadius, shadowSm, shadowMd, shadowLg, borderWidth }`
65
65
 
66
+ ### `useFiasVendoredAssets(importSpecifier)` — Self-hosted vendored-library asset URLs
67
+
68
+ **Permission:** `sandbox:vendored-libraries`
69
+ **Returns:** `Record<string, string> | null` (null when the host CDN is unconfigured or the library isn't vendored)
70
+
71
+ Some heavy libraries (pdf.js today; WASM runtimes later) need a Web Worker and large asset files that a sandboxed plugin can't bundle or self-host. The platform vendors a curated set of these — version-pinned, served from its trusted CDN. A plugin opts in by (1) listing the library in its `dependencies` and (2) declaring the `sandbox:vendored-libraries` permission. The platform then widens the plugin CSP for exactly that library (e.g. pdf.js → `worker-src blob:`+CDN, `script-src`/`connect-src` CDN), pins the import specifier to the self-hosted build via the host importmap (so `import * as pdfjsLib from 'pdfjs-dist'` resolves to it), and delivers the library's asset URLs here. For `pdfjs-dist` you typically only need `worker` (feed it to `@fias/pdf-core`'s loader `workerModuleUrl`) plus `cMapUrl` / `standardFontDataUrl` for CID / non-embedded fonts.
72
+
73
+ ```tsx
74
+ import { useFiasVendoredAssets } from '@fias/arche-sdk';
75
+ import { loadPdfJs, configurePdfAssets } from '@fias/pdf-core/loader';
76
+
77
+ function PdfViewer() {
78
+ const assets = useFiasVendoredAssets('pdfjs-dist');
79
+ if (!assets) return <div>PDF rendering unavailable</div>;
80
+ configurePdfAssets({ cMapUrl: assets.cMapUrl, standardFontDataUrl: assets.standardFontDataUrl });
81
+ const pdfjs = await loadPdfJs({ workerModuleUrl: assets.worker }); // off-thread worker
82
+ // ...render with pdfjs
83
+ }
84
+ ```
85
+
86
+ **`pdfjs-dist` asset keys:** `{ mainModule, worker, cMapUrl, standardFontDataUrl }` — all absolute CDN URLs.
87
+
66
88
  ### `useFiasFonts()` — Platform font catalog
67
89
 
68
90
  **Permission:** none
@@ -205,10 +227,120 @@ const doc = await fias.dataStore.get('scores', 'key');
205
227
  - 100 MB total storage per plugin
206
228
  - Max 100 results per query, max 10 filters, max field path depth 5
207
229
 
208
- **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `createCollection` 10
230
+ **Atomic batch writes:** `batch(operations)` applies up to 25 put/delete ops in a **single transaction** — all-or-nothing. Use it when several writes must stay consistent (e.g. append a ledger entry AND update a running total); a sequence of separate `put`/`delete` calls can leave a half-applied state. A batch may span collections and scopes (pass `workspaceId` per op); any failure (bad key, role denial, quota) rolls the whole batch back.
231
+
232
+ ```tsx
233
+ await dataStore.batch([
234
+ { op: 'put', collection: 'ledger', key: 'tx-42', data: { delta: 100 } },
235
+ { op: 'put', collection: 'totals', key: 'balance', data: { value: newBalance } },
236
+ ]);
237
+ ```
238
+
239
+ **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
209
240
 
210
241
  **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `RATE_LIMIT`.
211
242
 
243
+ **Semantic search (`data:search`):** Make a collection searchable to do vector search over a text field. Declare it at creation with `searchable: { field }`, then call `search()`. Embeddings are managed by the platform — you never handle vectors or pick a model. Documents are embedded asynchronously, so a freshly-written doc becomes searchable a moment later (it's always immediately visible to `query`). The query embedding charges the caller's credits. Requires the `data:search` permission (distinct from `data:store`).
244
+
245
+ ```tsx
246
+ // Create a searchable collection (embedding the `text` field) and publish docs.
247
+ await dataStore.createCollection('recipes', { userScope: 'shared', searchable: { field: 'text' } });
248
+ await dataStore.put('recipes', recipeId, { text: `${title} ${description}`, title, cuisine });
249
+
250
+ // Semantic search (charges the caller's credits for the query embedding).
251
+ const matches = await dataStore.search('recipes', 'quick vegetarian weeknight dinner', {
252
+ topK: 10,
253
+ filters: [{ field: 'cuisine', op: 'eq', value: 'italian' }], // vector + JSONB facet, ANDed
254
+ });
255
+ // matches: Array<{ key, data, similarity }>, best first
256
+ ```
257
+
258
+ **Search limits:** `topK` ≤ 50, query ≤ 1024 chars, 50,000 embedded documents per arche; `search` is rate-limited to 20/min (each call embeds the query). Documents in non-searchable collections — and not-yet-embedded docs — are invisible to `search` but always returned by `query`.
259
+
260
+ ### `useDataSubscription()` — Realtime collection change events
261
+
262
+ **Permission:** `data:store` (same as reading — live updates grant nothing polling couldn't)
263
+ **Returns:** `DataSubscriptionState` — `{ status: 'subscribing' | 'active' | 'ended', endedReason? }`
264
+
265
+ Get notified when a datastore collection changes, instead of manually refreshing. **Notify-then-refetch:** the platform pushes a minimal "this collection changed" signal and your `onInvalidate` callback re-runs whatever query your component renders from. Perfect for shared-scope collections where other users' writes should appear live (leaderboards, live scores, registrations).
266
+
267
+ ```tsx
268
+ import { useFiasDataStore, useDataSubscription } from '@fias/arche-sdk';
269
+
270
+ function Leaderboard() {
271
+ const dataStore = useFiasDataStore();
272
+ const [rows, setRows] = useState<ScoreRow[]>([]);
273
+
274
+ const refresh = useCallback(async () => {
275
+ const res = await dataStore.query<ScoreRow>('scores', {
276
+ orderBy: { field: 'score', direction: 'desc' },
277
+ limit: 20,
278
+ });
279
+ setRows(res.documents.map((d) => d.data));
280
+ }, [dataStore]);
281
+
282
+ // Refetches automatically whenever ANY user writes to `scores`.
283
+ const { status } = useDataSubscription('scores', refresh);
284
+ // status: 'subscribing' → 'active'; 'ended' + endedReason on teardown
285
+ }
286
+ ```
287
+
288
+ **Semantics you can rely on:**
289
+
290
+ - The initial refetch fires after the subscription is live, so a write racing your mount is never missed.
291
+ - `onInvalidate` runs single-flight with one trailing call — event bursts coalesce, never parallel refetches.
292
+ - Delivery is best-effort (events can drop or duplicate) — always treat it as "time to refetch", never as data.
293
+ - Scope-aware: `shared` collections notify on any user's writes; `user`-scoped only on yours; `workspace`-scoped need `{ workspaceId }` and an active membership.
294
+ - When the subscription ends (`status: 'ended'`), `endedReason` tells you why: `collection_deleted` and `authorization_revoked` are terminal; `limit`, `transport_error`, and `lease_expired` may be worth resubscribing (remount or toggle the `enabled` option).
295
+
296
+ **Options:** `useDataSubscription(collection, onInvalidate, { workspaceId?, enabled? })`.
297
+
298
+ **Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
299
+
300
+ ### `useFiasWorkspaces()` — Team/org workspaces with roles
301
+
302
+ **Permission:** `data:workspace`
303
+ **Returns:** `FiasWorkspacesApi`
304
+
305
+ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org space) inside your arche, with role-on-edge membership. A collection created with `userScope: 'workspace'` holds documents owned by a workspace and visible to its members, gated by role. Roles are `owner` > `admin` > `member` > `viewer`:
306
+
307
+ - **owner** — manage the workspace + members, read/write docs
308
+ - **admin** — manage non-owner members, read/write docs
309
+ - **member** — read/write docs
310
+ - **viewer** — read docs only
311
+
312
+ The creating user becomes the founding owner, and a workspace always keeps at least one owner (`MUST_KEEP_ONE_OWNER`). To read/write a workspace's documents, use `useFiasDataStore()` with the `{ workspaceId }` option (that still needs `data:store`, plus the server-side role gate).
313
+
314
+ ```tsx
315
+ import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
316
+
317
+ function Team() {
318
+ const workspaces = useFiasWorkspaces();
319
+ const dataStore = useFiasDataStore();
320
+
321
+ async function setup() {
322
+ const ws = await workspaces.create('Acme Corp'); // caller becomes owner
323
+ await workspaces.addMember(ws.workspaceId, 'alice', 'member'); // members are addressed by username
324
+
325
+ // Workspace-scoped collection + document (role-gated server-side).
326
+ await dataStore.createCollection('ledger', { userScope: 'workspace' });
327
+ await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
328
+
329
+ // Read the workspace's ledger back: data store query with the workspace scope.
330
+ const securities = await dataStore.query(
331
+ 'ledger',
332
+ { orderBy: { field: 'shares', direction: 'desc' } },
333
+ { workspaceId: ws.workspaceId },
334
+ );
335
+
336
+ const mine = await workspaces.list(); // workspaces I belong to
337
+ const members = await workspaces.listMembers(ws.workspaceId);
338
+ }
339
+ }
340
+ ```
341
+
342
+ **Methods:** `create`, `list`, `get`, `archive` (owner only), `listMembers`, `addMember`, `updateMember`, `removeMember`. `addMember(workspaceId, username, role, expiresAt?)` accepts an optional ISO-8601 `expiresAt` to grant **time-bounded** access (e.g. a contractor seat) — owners cannot expire; an expired member is treated as no longer a member. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
343
+
212
344
  ### `useEntityInvocation()` — Invoke AI models
213
345
 
214
346
  **Permission:** `entities:invoke`
@@ -412,6 +544,73 @@ const transparentPng = await removeBackground(jpegBlob);
412
544
 
413
545
  **Limits:** input ≤ 25 MiB, PNG/JPEG only, rate-limited to 10/min per arche per user. Throws on size violation rather than returning a result.
414
546
 
547
+ ### Image-editing capability hooks (auto-generated)
548
+
549
+ **Permission:** `entities:image_edit`
550
+ **Returns (per hook):** `{ invoke, isLoading, result, error }`
551
+
552
+ Mask-aware AI image-editing surfaces, generated from the platform's surface registry (same typed-hook pattern as audio). The source image and mask are passed **by handle** — `{ fileId }` (a Vault Document this arche owns), `{ archeAssetId }` (a published asset), or `{ dataUrl }` (inline base64; keep it small — use a handle for large source images). The edited result is saved to the user's Vault and returned as a presigned `imageUrl` you load onto your canvas.
553
+
554
+ ```tsx
555
+ import { useSmartInpaint } from '@fias/arche-sdk';
556
+
557
+ const { invoke } = useSmartInpaint();
558
+ const result = await invoke({
559
+ image: { fileId: sourceDocId }, // or { dataUrl } / { archeAssetId }
560
+ mask: { dataUrl: maskPngDataUrl }, // alpha=255 = region to replace
561
+ prompt: 'a field of sunflowers',
562
+ engine: 'stability', // 'stability' | 'recraft' | 'openai'
563
+ });
564
+ // result: { imageUrl, imageRef, fileId, mimeType, costCredits }
565
+ ```
566
+
567
+ Long-running edits complete over WebSocket transparently — `await invoke(...)` resolves with the final result just like any other hook. Browse the full surface list with `npx fias-dev entities`.
568
+
569
+ Currently shipped image-edit hooks: `useSmartInpaint` (mask replace), `useSmartErase` (mask remove + fill), `useSmartOutpaint` (extend canvas), `useSmartSearchReplace` (prompt-targeted replace, no mask), `useSmartSearchRecolor` (prompt-targeted recolor), `useSmartVectorize` (raster → SVG), `useSmartRemoveBackground` (server-side bg removal), `useSmartImageToImage` (guided regenerate), `useSmartUpscale` (resolution increase), `useSmartControl` (sketch/structure/style control), `useSmartReplaceBackground` (swap + relight background). Mask-based hooks take an `image` + `mask`; search/recolor take an `image` + a text selector; outpaint takes an `image` + edge pixels; vectorize/remove-background/upscale take an `image` (+ mode/engine); control and replace-background accept additional reference images by handle.
570
+
571
+ ### `useClientEntity()` — Invoke client-side (on-device) entities
572
+
573
+ **Permission:** `entities:client_invoke`
574
+ **Returns:** `ClientEntityApi`
575
+
576
+ The generic surface for any client-side entity — capabilities the host runs entirely in the browser (dictionary/WASM engines). Input never leaves the device and there is no per-call cost. The on-device counterpart to `useEntityInvocation()`. Target the entity by ID and supply its documented input/output types:
577
+
578
+ ```tsx
579
+ import {
580
+ useClientEntity,
581
+ GRAMMAR_CHECK_ENTITY_ID,
582
+ NLLB_TRANSLATION_ENTITY_ID,
583
+ NLLB_LANGUAGES,
584
+ } from '@fias/arche-sdk';
585
+ import type {
586
+ GrammarCheckInput,
587
+ GrammarCheckResult,
588
+ TranslateParams,
589
+ TranslationResult,
590
+ } from '@fias/arche-sdk';
591
+
592
+ const { invoke, isLoading, error } = useClientEntity();
593
+
594
+ // Grammar & spell check (non-AI nspell + retext engine):
595
+ const { issues } = await invoke<GrammarCheckInput, GrammarCheckResult>(GRAMMAR_CHECK_ENTITY_ID, {
596
+ text: 'teh quick brown fox',
597
+ categories: ['spelling', 'grammar', 'style', 'readability'], // optional; default = all
598
+ customDictionary: ['Fias'], // optional; words never flagged as misspelled
599
+ });
600
+ // issues: Array<{ start, end, message, category, ruleId, suggestions[] }>
601
+
602
+ // On-device translation (Meta NLLB-200 via transformers.js, free; the source
603
+ // language is auto-detected when omitted, target is a FLORES-200 code — pick
604
+ // from the exported NLLB_LANGUAGES label/code list):
605
+ const { translation, detectedSourceLang } = await invoke<TranslateParams, TranslationResult>(
606
+ NLLB_TRANSLATION_ENTITY_ID,
607
+ { text: 'Hello', targetLang: 'spa_Latn' }, // pass sourceLang to skip detection
608
+ );
609
+ ```
610
+
611
+ **Grammar limits:** text ≤ 100,000 characters, rate-limited to 30/min per arche per user.
612
+ **Translation limits:** text ≤ 2,000 characters, rate-limited to 20/min per arche per user. The first call downloads the model (~600 MB, then browser-cached) and needs a reasonably capable device (~1–2 GB of browser memory); on constrained/mobile devices the load can fail — catch the error and offer `useEntityInvocation` (the AI engine) as a fallback.
613
+
415
614
  ### Audio capability hooks (auto-generated)
416
615
 
417
616
  **Permission:** `entities:audio_generate`
@@ -441,6 +640,23 @@ const { invoke } = useSurface<MyParams, MyResult>('my.surface-key');
441
640
 
442
641
  Default per-request timeout is 180 s (covers TTS for long text and Lyria multi-clip generation).
443
642
 
643
+ ### `useIntegrationWebSearch()` — Live web search
644
+
645
+ **Permission:** `entities:web_search`
646
+ **Returns:** `{ invoke, isLoading, result, error }`
647
+
648
+ Search the live web and get a concise, cited summary. Backed by the swappable
649
+ `web-search` entity (the search provider can change without your code changing). The signed-in user is charged the standard 20% markup over the raw
650
+ search cost. Rate-limited to 10/min per arche per user.
651
+
652
+ ```tsx
653
+ import { useIntegrationWebSearch } from '@fias/arche-sdk';
654
+
655
+ const { invoke, isLoading, result } = useIntegrationWebSearch();
656
+ await invoke({ query: 'latest EU AI Act enforcement dates' });
657
+ // result: { summary, sources: [{ url, title }], searchCount, costCredits }
658
+ ```
659
+
444
660
  ### `useVaultDocuments()` — User-owned Vault documents
445
661
 
446
662
  **Permissions:** `vault:documents:read` (list / read / getDownloadUrl / search), `vault:documents:write` (write / update / delete / attach / detach / upload)
@@ -499,6 +715,41 @@ await vault.detach(documentId, referenceId);
499
715
 
500
716
  **`fetchVaultDocumentDownloadUrl(documentId)`** — non-React function that shares the hook's client-side URL cache. Use from PDF exporters, image preloaders, or other non-component code paths.
501
717
 
718
+ ### `useVaultUserDocuments()` — User-granted access to the user's own documents
719
+
720
+ **Permission:** `vault:user-documents:read`
721
+
722
+ The only SDK surface that reaches data your plugin did NOT create: documents the user already owns in their Vault, shared with your plugin through an explicit, per-document consent flow. `useVaultDocuments` (above) is for documents your plugin creates; this hook is for documents the _user_ brings.
723
+
724
+ ```tsx
725
+ import { useVaultUserDocuments } from '@fias/arche-sdk';
726
+
727
+ const userDocs = useVaultUserDocuments();
728
+
729
+ // 1. Ask the user to share documents. The HOST renders its own Vault picker
730
+ // + consent sheet — your plugin never sees the user's vault listing, only
731
+ // what they hand-pick and explicitly approve. Call from a button click.
732
+ const picked = await userDocs.pick({ maxDocuments: 5 });
733
+ if ('canceled' in picked) {
734
+ // A normal outcome — the user changed their mind. Never treat as an error.
735
+ } else {
736
+ // picked.documents: [{ documentId, name, mimeType, sizeBytes }]
737
+ }
738
+
739
+ // 2. Later (any session): read what was granted.
740
+ const granted = await userDocs.list(); // all granted docs
741
+ const { document, content } = await userDocs.get(id, { includeContent: true }); // text docs
742
+ const { url } = await userDocs.getDownloadUrl(id); // binary docs (images, PDFs)
743
+ ```
744
+
745
+ **The consent model (what to tell your users):**
746
+
747
+ - Grants are **per-document** and **standing**: the plugin can re-read granted documents in later sessions until the user removes access in **Vault → App Access**.
748
+ - After revocation, `get`/`getDownloadUrl` reject with `DOCUMENT_NOT_FOUND` — handle that path gracefully (drop the document from your UI; do not retry in a loop).
749
+ - `proprietary`-sensitivity documents are never grantable (they don't even appear in the picker).
750
+ - Every read is audited by the platform; a new document _version_ is a new documentId, so a re-pick is needed after the user replaces a document.
751
+ - Not available in the builder preview or the dev harness — `pick()` resolves `{ canceled: true }` there. Test the flow in the published plugin with your own documents.
752
+
502
753
  ### `useArcheAssets()` — Contributor-published asset library
503
754
 
504
755
  **Permission:** `assets:read`
@@ -579,10 +830,40 @@ function UnlockButton() {
579
830
  ```tsx
580
831
  import { useFiasNavigation } from '@fias/arche-sdk';
581
832
 
582
- const { navigateTo, currentPath } = useFiasNavigation();
583
- navigateTo('/settings');
833
+ const { navigateTo, openArche, currentPath } = useFiasNavigation();
834
+ navigateTo('/settings'); // within THIS arche's route space
835
+
836
+ // Open ANOTHER arche's page. Requires the `navigation:open_arche` permission.
837
+ // Pass a stable arche id (`arc_…` / `arche_…`), not a slug. `newTab` is
838
+ // best-effort — the browser may block the popup, in which case the host
839
+ // navigates in the same tab instead.
840
+ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
584
841
  ```
585
842
 
843
+ ### Opening external links
844
+
845
+ To send the user to an external website (a YouTube video, docs, your homepage),
846
+ open it in a **new tab** with the standard web API — no SDK hook or permission
847
+ is needed:
848
+
849
+ ```tsx
850
+ <button
851
+ onClick={() => window.open('https://youtube.com/watch?v=…', '_blank', 'noopener,noreferrer')}
852
+ >
853
+ Watch the video
854
+ </button>
855
+ ```
856
+
857
+ Notes:
858
+
859
+ - The plugin iframe grants `allow-popups`, so `window.open(url, '_blank')`
860
+ works. Opening in the **same** tab (or an `<a>` without `target="_blank"`)
861
+ does **not** — that would try to navigate the sandbox itself.
862
+ - There is **no** "leaving Fias" confirmation dialog — the tab opens directly.
863
+ Only send users to URLs you trust, and always pass `'noopener,noreferrer'`.
864
+ - Only `http(s)` URLs make sense here; `javascript:`/`data:` URLs are blocked
865
+ by the sandbox.
866
+
586
867
  ### `useStepNavigation()` — Multi-step workflows
587
868
 
588
869
  **Permission:** None for in-memory use; `storage:sandbox` when `persistKey` is set (it reads/writes `__state/<persistKey>` via the storage bridge).
@@ -616,6 +897,26 @@ const [count, setCount] = usePersistentState<number>('counter', 0);
616
897
 
617
898
  **Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously, but the underlying `storage_write` is coalesced (250 ms trailing-edge debounce, 1 s max-wait, flushed on unmount). Safe to call from `requestAnimationFrame` and other high-frequency handlers. For state that updates every frame (game positions, drag coordinates), still prefer plain `useState` — persisting transient state is wasteful and not useful on reload.
618
899
 
900
+ ### `useFiasPreviewState()` — Survive builder-preview rebuilds
901
+
902
+ **Permission:** none.
903
+
904
+ In the arche builder, editing code (or a background cross-device sync) rebuilds the preview, which reloads the iframe and wipes the plugin's in-memory state — losing, for example, a report the user just generated. Opt that state into host-side preservation: just before the reload the host snapshots `getState()`, and after the reload it hands the value back to `onRestore`.
905
+
906
+ ```tsx
907
+ import { useFiasPreviewState } from '@fias/arche-sdk';
908
+
909
+ const [report, setReport] = useState<Report | null>(null);
910
+ // `key` namespaces this slice; `getState` must return JSON-safe state.
911
+ useFiasPreviewState(
912
+ 'report',
913
+ () => report,
914
+ (saved) => setReport(saved as Report),
915
+ );
916
+ ```
917
+
918
+ **Inert outside the builder preview** (the host never requests a capture), so it's safe to leave in production plugin code. Use it only for expensive-to-recreate transient state — not as a substitute for `usePersistentState` (which persists across full reloads and sessions).
919
+
619
920
  ### `fias` — Imperative utilities
620
921
 
621
922
  Use the `fias` namespace when you need bridge operations from outside a React component (event handlers attached imperatively, utility modules, non-React entry points).
@@ -667,26 +968,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
667
968
 
668
969
  **Fields:**
669
970
 
670
- | Field | Req | Description |
671
- | --------------------- | ---- | ------------------------------------------------------------------------------------------------------- |
672
- | `name` | Yes | Plugin identifier (lowercase, hyphens) |
673
- | `version` | Yes | Semver (e.g., `"1.0.0"`) |
674
- | `description` | Yes | Short marketplace description |
675
- | `expandedDescription` | No | Long-form marketplace copy |
676
- | `main` | Yes | Entry point source file |
677
- | `archeType` | Yes | `"tool"` or `"site"` |
678
- | `categorySlug` | No | Marketplace category for discovery |
679
- | `icon` | No | Relative path to icon asset (shown in marketplace + arche header) |
680
- | `tags` | No | Discovery tags |
681
- | `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
682
- | `permissions` | Yes | Array of permission scopes (see below) |
683
- | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
684
- | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
685
- | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
686
- | `isListed` | No | Default `true`. Visible in marketplace store |
687
- | `sdk` | Yes | SDK version range |
688
- | `dependencies` | No | npm packages with **exact** versions (max 20) |
689
- | `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
971
+ | Field | Req | Description |
972
+ | --------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
973
+ | `name` | Yes | Plugin identifier (lowercase, hyphens) |
974
+ | `version` | Yes | Semver (e.g., `"1.0.0"`) |
975
+ | `description` | Yes | Short marketplace description |
976
+ | `expandedDescription` | No | Long-form marketplace copy |
977
+ | `main` | Yes | Entry point source file |
978
+ | `archeType` | Yes | `"tool"` or `"site"` |
979
+ | `categorySlug` | No | Marketplace category for discovery |
980
+ | `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local-dev submissions only; on staging/prod set the icon via the admin console. |
981
+ | `tags` | No | Discovery tags |
982
+ | `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
983
+ | `permissions` | Yes | Array of permission scopes (see below) |
984
+ | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
985
+ | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
986
+ | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
987
+ | `isListed` | No | Default `true`. Visible in marketplace store |
988
+ | `sdk` | Yes | SDK version range |
989
+ | `dependencies` | No | npm packages with **exact** versions (max 20) |
990
+ | `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
690
991
 
691
992
  **Pricing detail:**
692
993
 
@@ -701,7 +1002,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
701
1002
 
702
1003
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
703
1004
 
704
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `entities:invoke`, `entities:image_generate`, `entities:image_remove_background`, `entities:audio_generate`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `assets:read`
1005
+ **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `data:search`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:image_remove_background`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `assets:read`, `navigation:open_arche`, `sandbox:vendored-libraries`
705
1006
 
706
1007
  **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
707
1008
 
@@ -737,7 +1038,7 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
737
1038
 
738
1039
  **Public arches (`isPublic: true`):**
739
1040
 
740
- Setting `isPublic: true` lets anonymous visitors (no Fias account) view the arche at `/a/<archeId>`. The UI renders without the platform sidebar. **Bridge operations still require auth** — `useFiasDataStore` writes, `useFiasStore.purchase()`, `useFiasStorage` reads, vault operations, etc. all require the visitor to be signed in. Public mode is for read-only landing pages, marketing pages, and shared-content viewers — not for collecting registrations or payments from anonymous users.
1041
+ Setting `isPublic: true` lets anonymous visitors (no Fias account) view the arche at `/a/<archeId>`. The UI renders without the platform sidebar. **Bridge operations still require auth** — `useFiasDataStore` writes, `useFiasStore.purchase()`, `useFiasStorage` reads, vault operations, etc. all require the visitor to be signed in. Public mode is for read-only landing pages, marketing pages, and read-only catalog viewers — not for collecting registrations or payments from anonymous users.
741
1042
 
742
1043
  ## Plugin Constraints
743
1044
 
@@ -751,8 +1052,9 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
751
1052
 
752
1053
  ### Sandboxing
753
1054
 
754
- - Plugins run in an iframe with `sandbox="allow-scripts allow-forms allow-same-origin allow-downloads"`
1055
+ - Plugins run in an iframe with `sandbox="allow-scripts allow-forms allow-downloads allow-popups allow-popups-to-escape-sandbox"` (note: **no** `allow-same-origin` — the plugin has an opaque origin)
755
1056
  - **No `fetch()` or `XMLHttpRequest`** — all network access is blocked
1057
+ - **You _can_ open external links in a new tab** (e.g. `window.open('https://…', '_blank')`) — see "Opening external links" above
756
1058
  - **No access** to parent DOM, cookies, or localStorage
757
1059
  - **No external scripts or stylesheets** — everything must be bundled
758
1060
  - All platform communication goes through the bridge (SDK hooks)
@@ -832,8 +1134,8 @@ You can choose between **Staging** and **Production** environments using the dro
832
1134
  You can also authenticate via the command line:
833
1135
 
834
1136
  ```bash
835
- npx fias-dev login # Authenticate with staging (default)
836
- npx fias-dev login --env production # Authenticate with production
1137
+ npx fias-dev login # Authenticate with prod (default)
1138
+ npx fias-dev login --env staging # Authenticate with staging (internal)
837
1139
  ```
838
1140
 
839
1141
  ### Browsing Available Entities
@@ -869,7 +1171,7 @@ happened either way).
869
1171
 
870
1172
  ### Managing the asset library (for `useArcheAssets`)
871
1173
 
872
- `useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <staging|prod|local>` (default `staging`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
1174
+ `useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <prod|staging|local>` (default `prod`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
873
1175
 
874
1176
  ```bash
875
1177
  npx fias-dev assets enable # Turn on the asset library for this arche
@@ -897,7 +1199,7 @@ npx fias-dev collaborators set <identifier> \ # Change role and/or
897
1199
  npx fias-dev collaborators remove <identifier> # Revoke a collaborator
898
1200
  ```
899
1201
 
900
- All commands accept `--env <staging|production|local>` (default `staging`).
1202
+ All commands accept `--env <prod|staging|local>` (default `prod`).
901
1203
 
902
1204
  ### Other CLI diagnostics
903
1205