@fias/create-fias-plugin 1.21.0 → 1.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fias/create-fias-plugin",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.29.0 -->
1
+ <!-- fias-sdk-guide-version: 2.30.0 -->
2
2
 
3
3
  # FIAS Plugin Development Guide
4
4
 
@@ -215,7 +215,9 @@ await deleteFile('data/old.json');
215
215
  **Permission:** `data:store`
216
216
  **Returns:** `FiasDataStoreApi`
217
217
 
218
- A document database with collections, queries, and filtering. Data persists across sessions in live mode. Each collection can be `user`-scoped (private to each user) or `shared` (visible to all users of the plugin).
218
+ A document database with collections, queries, and filtering. Data persists across sessions in live mode. Each collection can be `user`-scoped (private to each user) or `shared` (visible to all users of the plugin) — or `workspace`-scoped, for a team's data (see `useFiasWorkspaces()`).
219
+
220
+ **`shared` is one pool across every user and every organization using your app.** Use it only for content meant for everyone (a public catalog, a global leaderboard). Data that belongs to a team or a customer goes in a `workspace`-scoped collection, where the platform enforces each member's role and the collection's role floors — see [Access control](#access-control--the-platform-enforces-it-your-app-never-does).
219
221
 
220
222
  ```tsx
221
223
  import { useFiasDataStore } from '@fias/arche-sdk';
@@ -228,6 +230,9 @@ function MyComponent() {
228
230
  // 10/minute, so scaffolding several at startup can silently lose the tail.
229
231
  await dataStore.createCollection('scores', { userScope: 'user' }); // or 'shared'
230
232
  const collections = await dataStore.listCollections();
233
+ // Only a collaborator (owner/publisher) may delete a collection (FORBIDDEN otherwise),
234
+ // and on a live app only an EMPTY one (COLLECTION_NOT_EMPTY); sandboxes (fias-dev,
235
+ // builder preview, test channel) can delete any. Never use it as an end-user "reset".
231
236
  await dataStore.deleteCollection('scores');
232
237
 
233
238
  // Document CRUD
@@ -453,7 +458,7 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
453
458
 
454
459
  **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30 (counted in units of 25 operations — a full 100-op batch uses 4), `createCollection` 10
455
460
 
456
- **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`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
461
+ **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `COLLECTION_NOT_EMPTY` (`deleteCollection` on a published app's collection that still holds documents — it would delete every user's data, so it is refused; `deleteCollection` is also `FORBIDDEN` to anyone but a collaborator, so to clear one user's data, `query` their documents and `delete` or `batch`-delete them; to change something fixed at creation, such as `searchable` or `userScope`, create a collection under a new name and migrate to it; to remove a live collection that holds data, contact Fias support), `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
457
462
 
458
463
  **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`).
459
464
 
@@ -512,6 +517,65 @@ function Leaderboard() {
512
517
 
513
518
  **Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
514
519
 
520
+ ### `useEntitySubscription()` — Realtime events from any subscribe-kind verb
521
+
522
+ **Returns:** `EntitySubscriptionState` — the same `{ status, endedReason? }` as `useDataSubscription`
523
+
524
+ `useDataSubscription` is this hook bound to the datastore. Any catalog verb declared with `kind: 'subscribe'` works the same way: pass the entity id and the verb's payload (both from the capability catalog of the entity you are linked to), and `onInvalidate` runs once the subscription is live and again on every event. The permission is the verb's own. Events are a refetch signal, never data.
525
+
526
+ ```tsx
527
+ import { useEntitySubscription } from '@fias/arche-sdk';
528
+
529
+ // Re-read the HEAD of the resource on each event and merge by id — an event
530
+ // also fires for an edit or a deletion, which a "newer than X" read never sees.
531
+ const { status } = useEntitySubscription(
532
+ ENTITY_ID, // from the catalog entry of the entity your arche is linked to
533
+ { op: 'subscribe_feed' },
534
+ refetchHead,
535
+ { enabled: member, minRefetchIntervalMs: 2_000 },
536
+ );
537
+ ```
538
+
539
+ **Options:** `{ enabled?, minRefetchIntervalMs? }`. `minRefetchIntervalMs` caps refetches to one per interval (a trailing one still follows the last event) — every refetch is a bridge call against the entity's per-minute budget, so set it on a busy resource. The payload is compared by content (key order ignored), so a fresh object literal each render does not re-subscribe. Same semantics as above: subscribe-then-fetch, single-flight with one trailing call, best-effort delivery, and the same `endedReason` values (`collection_deleted` means the observed resource is gone, whatever its kind).
540
+
541
+ **Dev harness:** MOCK mode has executors only for the Data Store, so a subscribe verb on another entity works in CLOUD mode (the dev bridge against staging), not in MOCK mode.
542
+
543
+ ### Access control — the platform enforces it, your app never does
544
+
545
+ On **every** read and write, the platform decides who may do what with your app's data: a workspace member's role (`owner` > `admin` > `member` > `viewer`) and each workspace collection's role floors (`readMinRole` / `writeMinRole`). Nothing that runs in the browser is access control:
546
+
547
+ - **Never build your own role system** — no role picker, no role-to-permission matrix, no "switch to Administrator" control, no role kept in your own data and checked in your code. Anyone can change what runs in their own browser, so such checks protect nothing. When part of your app must be limited to some people, put that data in a workspace collection with a raised role floor: the server refuses everyone below it, whatever your UI shows.
548
+ - **Never put business data in `shared` collections.** `shared` is one pool across every user and every organization using your app. A team's or a customer's data goes in `workspace`-scoped collections.
549
+ - **`useFiasAccess()` is a UI hint** — show or hide controls by the user's role. It is not a gate; the server already is one.
550
+
551
+ **When four workspace roles are not enough, declare app roles** (ADR-034) — never build your own. An `access` block in `fias-plugin.json` declares your app's permissions and the roles that bundle them; every `workspace` collection then names a `readPermission` and a `writePermission`. Workspace owners choose who holds which role in Fias's own Access screen, and the platform enforces it on every read and write, deny by default:
552
+
553
+ ```jsonc
554
+ "access": {
555
+ "permissions": [
556
+ { "id": "ledger.read", "label": "View the ledger" },
557
+ { "id": "ledger.write", "label": "Edit the ledger" },
558
+ { "id": "ledger.approve", "label": "Approve entries", "sensitive": true }
559
+ ],
560
+ "roles": [
561
+ { "id": "bookkeeper", "label": "Bookkeeper", "permissions": ["ledger.read", "ledger.write"], "minTier": "member" },
562
+ { "id": "controller", "label": "Controller", "permissions": ["ledger.read", "ledger.approve"], "minTier": "admin" }
563
+ ],
564
+ "suggestions": { "member": ["bookkeeper"] }
565
+ },
566
+ "collections": [
567
+ { "name": "ledger", "scope": "workspace", "readPermission": "ledger.read", "writePermission": "ledger.write" }
568
+ ]
569
+ ```
570
+
571
+ - **Ids are permanent.** Never rename or reuse a permission, role or audience id; retire one with `"deprecated": true`, and remove it only in a later publish (it stays reserved forever). `sensitive` and `deprecated` cannot be undone, a published permission can never be marked `sensitive`, and a role's `minTier` only rises. Policy changes only ever narrow what people can do; anything that widens reaches a workspace only when a person there grants or accepts it.
572
+ - **Collection permissions are set once** (`COLLECTION_PERMISSION_IMMUTABLE`) — choose them when you first declare the collection. The one change allowed later is pointing a collection at a permission that is new in that publish and that no existing role grants (so nobody holds it until a workspace grants it).
573
+ - **Per-record confidentiality — audiences.** `access.audiences` declares readerships (`{ "id": "board", "readers": ["board.read"], "disclosedBy": "board.file" }`) and a collection lists the ones its records may go to (`"audiences": ["board", "staff"]`). Each record sits in one audience; the platform returns only records of audiences the user reads. In such collections keys are generated by the platform: create with `create(collection, data, { workspaceId, audience })` (or `put(collection, null, …)`), and a `put` with a key and another `audience` moves the record.
574
+ - **Write-only roles add, never change.** A role with a collection's `writePermission` but not its `readPermission` may only add documents, with keys the platform generates (`create()`); a key you choose is refused the same way whether or not it exists, so a write-only role can never probe what exists. Use `create()` for submissions, complaints and anything filed by people who must not read the rest.
575
+ - **Where it applies:** only in an org workspace that approved your app and turned its roles on; elsewhere tier rules apply, and a collection created already protected stays closed (`ACCESS_SETUP_REQUIRED`) until an owner turns roles on. An app's first `access` block is accepted only where Fias has made app roles available ("app roles are not available on this platform yet" otherwise — publish without the block for now).
576
+
577
+ Full rules (limits, publish order, references and counters across audiences, activation): `docs/api-plugins.md` § App roles and permissions.
578
+
515
579
  ### `useFiasWorkspaces()` — Team/org workspaces with roles
516
580
 
517
581
  **Permission:** `data:workspace`
@@ -524,7 +588,7 @@ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org spac
524
588
  - **member** — read/write docs
525
589
  - **viewer** — read docs only
526
590
 
527
- 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).
591
+ 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). The roles are enforced by the platform on every call; to show the current user's own role in your UI, use `useFiasAccess(workspaceId)` — and read [Access control](#access-control--the-platform-enforces-it-your-app-never-does) before you design anything role-shaped.
528
592
 
529
593
  ```tsx
530
594
  import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
@@ -566,6 +630,44 @@ if (holder?.type === 'org') {
566
630
 
567
631
  **Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives), `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`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. 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).
568
632
 
633
+ ### `useFiasAccess()` — The current user's own access in a workspace
634
+
635
+ **Permission:** `data:workspace`
636
+ **Returns:** `FiasAccessState` — `{ access, isLoading, error, refetch, can, canReadAudience, canDiscloseTo, openAccessDialog }`
637
+
638
+ What the signed-in user can do in one workspace, so your UI can show the controls that apply to them. `access.mode` says which model answered:
639
+
640
+ - `'tier'` — an app without roles (or a workspace outside an organization): `{ mode, workspaceId, tier, expiresAt }`, their workspace role (`owner` / `admin` / `member` / `viewer`), or `tier: null` when they have none there, and when their membership expires (`null` = never).
641
+ - `'legacy'` — your app declares roles but this workspace has not turned them on yet: tier rules apply.
642
+ - `'roles'` — roles are on: also `roles` (the ones that count at the user's current tier, each with its expiry), `permissions`, and `audiences: { readable, disclosable }`. `can(permission)`, `canReadAudience(id)` and `canDiscloseTo(id)` are true only here.
643
+
644
+ Pass `null` or `undefined` while there is no workspace yet; nothing is fetched. Mount it in as many components as you like: instances for one workspace share one request, and it refreshes itself when Fias says the user's access changed and when the window regains focus. `openAccessDialog()` opens Fias's own sheet where the user sees why they have the access they have and can ask for more (and an access manager can manage roles); your app passes nothing to it and changes nothing through it.
645
+
646
+ **A UI hint, never security.** The platform checks every read and write on arrival against the user's role and the collection's role floors. Hiding a button does not protect anything, and showing one does not grant anything. Never build your own role check on top of this, and never offer a role picker as a way to grant access.
647
+
648
+ ```tsx
649
+ import { useFiasAccess } from '@fias/arche-sdk';
650
+
651
+ function Toolbar({ workspaceId }: { workspaceId: string }) {
652
+ const { access, isLoading } = useFiasAccess(workspaceId);
653
+ if (isLoading || !access) return null;
654
+ const canManage = access.tier === 'owner' || access.tier === 'admin';
655
+ return canManage ? <InviteButton /> : null; // the server still decides every call
656
+ }
657
+
658
+ function ApproveButton({ workspaceId }: { workspaceId: string }) {
659
+ const { access, can, openAccessDialog } = useFiasAccess(workspaceId);
660
+ if (access?.mode !== 'roles') return null; // tier / legacy: the permission is not in force
661
+ return can('ledger.approve') ? (
662
+ <button onClick={approve}>Approve</button>
663
+ ) : (
664
+ <button onClick={() => openAccessDialog()}>Request access</button>
665
+ );
666
+ }
667
+ ```
668
+
669
+ Treat a `mode` you do not recognise as "hide what it would gate". It answers only about the signed-in user: no user ids, nothing about other members. **Rate limit:** 120/min.
670
+
569
671
  ### `useEntityInvocation()` — Invoke AI models
570
672
 
571
673
  **Permission:** `entities:invoke`
@@ -1414,7 +1516,7 @@ Rules that matter:
1414
1516
  - `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
1415
1517
  - Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
1416
1518
  - **Test it in the dev harness:** the **✦ Fias AI** toolbar button opens the real assistant over your plugin (Live mode; turns are real and use your credits). It describes your plugin exactly as the platform would: manifest actions always, runtime (`useFiasAIActions`) actions and `state` only for a trusted arche, and the window names any runtime action it is withholding. In the harness it can use your plugin's actions plus read-only Fias tools, never writes or sends. The builder preview has no assistant.
1417
- - **`openAssistant()`** (the hook's return) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
1519
+ - **`openAssistant()`** (the hook's return, also on `useFiasChrome()`) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). Opening needs no permission: any plugin may do it, actions declared or not, and the user can still use the assistant's own tools. The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
1418
1520
 
1419
1521
  ### `useArcheAssets()` — Contributor-published asset library
1420
1522
 
@@ -1556,6 +1658,113 @@ Three things to design around:
1556
1658
 
1557
1659
  Only images the user made or uploaded in **their own** Fias files can be published (pass the `fileId` from a `useImageGeneration()` result). PNG/JPEG/WebP only; every image is re-encoded to a canonical PNG with metadata stripped. Publishers can `unpublish()` their own assets, and any user can `report()` one for review.
1558
1660
 
1661
+ ### `useCreativeLibrary()` — The user's styles and characters
1662
+
1663
+ **Permission:** `vault:creative:read`; creating and changing also `vault:creative:write` (both human-reviewed at publish)
1664
+ **Returns:** `FiasCreativeLibraryApi`
1665
+
1666
+ ```tsx
1667
+ import { useCreativeLibrary, useImageGeneration } from '@fias/arche-sdk';
1668
+
1669
+ const library = useCreativeLibrary();
1670
+ const styles = await library.list({ kind: 'style' });
1671
+ // [{ itemId: 'vci_…', kind: 'style', name: 'Flappyland', words: '…', referenceDocumentIds: [...], version: 3, extension: null, … }]
1672
+ const style = await library.get(styles[0].itemId); // + previews: [{ documentId, url, expiresAt }] (5-minute URLs)
1673
+
1674
+ // Or let the person choose, in a picker Fias draws with the pictures:
1675
+ const picked = await library.pick({ kind: 'style' }); // { item: { itemId, kind, name, version } } | { canceled: true }
1676
+
1677
+ // Draw in a style — the platform sends its pictures and words for you:
1678
+ await useImageGeneration().generate({
1679
+ entityId,
1680
+ prompt: 'a red fox',
1681
+ creativeStyle: { styleId: style.itemId },
1682
+ });
1683
+ // …or choose which of its pictures go (1–3 of its referenceDocumentIds, in order):
1684
+ // creativeStyle: { styleId: style.itemId, pictures: [style.referenceDocumentIds[2]] }
1685
+
1686
+ // Make one (needs vault:creative:write). Pictures must be images YOUR arche created:
1687
+ const { documentId } = await useVaultDocuments().upload(pngBytes, {
1688
+ name: 'sample.png',
1689
+ mimeType: 'image/png',
1690
+ });
1691
+ await library.create({
1692
+ kind: 'style',
1693
+ name: 'Moonlit',
1694
+ words: 'inky blues, soft glow',
1695
+ referenceDocumentIds: [documentId],
1696
+ });
1697
+ await library.update(style.itemId, { words: 'brighter', expectedVersion: style.version });
1698
+ ```
1699
+
1700
+ The Creative Library holds the user's **styles** (a look: words and 1–12 reference pictures) and **characters** (one reference picture, an appearance, and a style). They belong to the user — or, in a team workspace that approved your arche, to the workspace — not to the arche that made them: FiasWorks Studio makes most of them, and any arche the user allows can use them. Reading them is seeing their pictures, so the permission is disclosed that way. Every change writes a new version (`get(id, version)` reads an old one); pass `expectedVersion` to `update` so a change made elsewhere is never overwritten. An update may keep pictures the item already has, whoever added them; only pictures you add must be your arche's own. Keep your own fields in `extension` — you see and change only yours. In a workspace, viewers can read but not change (`WORKSPACE_VIEWER_READ_ONLY`). `pick()` shows exactly what `list()` would return, so prefer it to drawing your own chooser; it rejects with `PICKER_UNAVAILABLE` in the dev harness (use `list()` there) and `SHEET_BUSY` while another Fias sheet is open. Not available in the builder preview or the test channel. **Error codes:** `CREATIVE_ITEM_NOT_FOUND`, `CREATIVE_REFERENCE_NOT_USABLE`, `CREATIVE_STYLE_REF_INVALID`, `CREATIVE_VERSION_CONFLICT`, `CREATIVE_LIBRARY_FULL`, `CREATIVE_INVALID`; on `generate`, `STYLE_NOT_FOUND`, `STYLE_NEEDS_DESCRIPTION` and `STYLE_PICTURE_NOT_IN_STYLE`.
1701
+
1702
+ ### `useCreateAsset()` — Game art the FiasWorks way
1703
+
1704
+ **Permission:** `entities:image_generate`; a library style or character also needs `vault:creative:read`
1705
+ **Returns:** `{ invoke(params): Promise<CreateAssetResult> }` (generated surface hook, `image.create-asset`)
1706
+
1707
+ ```tsx
1708
+ import { useCreateAsset } from '@fias/arche-sdk';
1709
+
1710
+ const { invoke } = useCreateAsset();
1711
+ const chest = await invoke({
1712
+ kind: 'object',
1713
+ description: 'a treasure chest',
1714
+ style: { builtIn: 'pixel-art' },
1715
+ });
1716
+ // chest.image — the painting; chest.cutout — a transparent PNG (null + cutoutError if removal failed)
1717
+
1718
+ // A library style, sending the pictures that suit this scene (1–3; default its first):
1719
+ await invoke({
1720
+ kind: 'image',
1721
+ description: 'a cellar lit by one candle',
1722
+ style: { library: { id: styleId, pictures: [indoorPictureId] } },
1723
+ });
1724
+
1725
+ // A later picture of a library character, in their own style:
1726
+ await invoke({
1727
+ kind: 'character',
1728
+ character: { library: { id: characterId }, pose: 'running', view: 'side' },
1729
+ });
1730
+
1731
+ // A games-list card with the name lettered in, finished at its export sizes:
1732
+ const card = await invoke({
1733
+ kind: 'game',
1734
+ description: 'a glowing mine shaft at night',
1735
+ gameArt: { format: 'list-card', title: 'Lit Mining', titleInArt: true },
1736
+ exports: true,
1737
+ }); // card.exports: [{ name, fileId, imageUrl, width, height }, …]
1738
+ ```
1739
+
1740
+ One call makes a scene (`image`, empty of people by default), an `object` (or an n×n `series`, split into its objects with `exports: true`), an `icon` (on a `plate`, or matching another icon of a set with `matchIcon`), a `character` (the base from words, or a pose / `turnaround` of a Creative Library character) or `game` art (`list-card`, `front-screen`, `game-icon`, `title`) with FiasWorks Studio's recipes: the framing each kind needs, a built-in style (`cartoon` by default, `pixel-art`, `watercolor`, …) or a library style, and the cutout. `restyle` redraws a picture in the style; `variation` pushes a new take away from the last; `seed` (Nano Banana 2, FLUX 2 Pro) aims for the same picture again. Models: Nano Banana 2 (default, takes a style's pictures), Nano Banana Pro, FLUX 2 Pro, GPT Image 2, Nano Banana 2 Lite (no reference pictures, so no restyle, character poses or library pictures).
1741
+
1742
+ Billing is per step, as if you had called each one: the generation, then AI background removal for the cut-out kinds. Keying a title's flat backdrop and every export size cost nothing. The painting lands in the user's Arche Saves like `useImageGeneration`; the cutout and export sizes are your arche's own working files. In the builder preview only built-in styles work. **Error codes:** `INVALID_PARAMS`, `MODEL_UNAVAILABLE`, `REFERENCE_IMAGE_NOT_SUPPORTED`, `STYLE_NEEDS_DESCRIPTION`, `STYLE_NOT_FOUND`, `CHARACTER_NOT_FOUND`, `PERMISSION_DENIED`, `STORAGE_FAILED`, and the `useImageGeneration` codes (`INSUFFICIENT_CREDITS`, `INVALID_RESOLUTION`, …).
1743
+
1744
+ ### Styles your arche ships for its players
1745
+
1746
+ **Permission:** `entities:image_generate` only — no Creative Library access
1747
+
1748
+ ```jsonc
1749
+ // fias-plugin.json
1750
+ "styles": [{ "key": "lit-mining", "name": "Lit Mining", "words": "glossy cartoon, warm lamplight" }]
1751
+ ```
1752
+
1753
+ ```tsx
1754
+ await useImageGeneration().generate({
1755
+ entityId,
1756
+ prompt: 'a gem cart',
1757
+ creativeStyle: { shipped: 'lit-mining' },
1758
+ });
1759
+ await useCreateAsset().invoke({
1760
+ kind: 'object',
1761
+ description: 'a pickaxe',
1762
+ style: { shipped: 'lit-mining' },
1763
+ });
1764
+ ```
1765
+
1766
+ A style can ship **with** your arche so every player draws in it — no library of their own, no `vault:creative:read`. The developer ships one of their Creative Library styles from the Fias Forge ("Styles your players can use") — signed in, never with an API key: its pictures are copied into the arche's asset library tagged `style:<key>`, and the answer is the `styles` entry to add here (up to 8; `key` is lowercase letters, digits and dashes, ≤ 40). Both are frozen with each published version, so a shipped style is read from the arche's **published** version — publish once with it before relying on it; the dev harness has none (`SHIPPED_STYLE_UNAVAILABLE`). Players can see the pictures, so a proprietary picture never ships. The player pays for what they make. **Error codes:** `STYLE_NOT_FOUND` (this version ships no such key), `STYLE_NEEDS_DESCRIPTION`, `SHIPPED_STYLE_UNAVAILABLE`.
1767
+
1559
1768
  ### `useFiasConnections()` — Outside accounts the workspace lets your app use
1560
1769
 
1561
1770
  **Permission:** `connections:use` (human-reviewed at publish)
@@ -1776,6 +1985,62 @@ A host older than this option ignores it and pushes. If you correct paths
1776
1985
  automatically as they arrive, never repeat the same correction twice in a
1777
1986
  row, or a push-only host can trap the Back button between two URLs.
1778
1987
 
1988
+ ### `useFiasChrome()` — Expand, contract and the host chrome
1989
+
1990
+ **Permission:** None required
1991
+
1992
+ Fias's standard expand/contract — what the internal arches call focus mode — plus the four controls the platform's focus-mode bar carries, so you can put them where your design wants them. Three levels, your choice:
1993
+
1994
+ 1. **Do nothing.** The host draws a small expand button over your app. Expanding hides the platform header and the sidebar; the host then shows a thin bar with Fias home, Worktop, Fias AI and Collapse.
1995
+ 2. **Your own buttons, the host's bar.** Mount the hook, draw your own expand button and sidebar toggle, keep the host's bar for the way back.
1996
+ 3. **Your own bar.** Mount the hook, `setHostExpandedBar(false)`, and draw your own bar from the four controls.
1997
+
1998
+ ```tsx
1999
+ import { useFiasChrome } from '@fias/arche-sdk';
2000
+
2001
+ const chrome = useFiasChrome();
2002
+
2003
+ // Level 2: your own expand button, where it fits your toolbar.
2004
+ {
2005
+ chrome.focusModeAvailable && !chrome.focusMode && (
2006
+ <button onClick={() => chrome.setFocusMode(true)}>Expand</button>
2007
+ );
2008
+ }
2009
+ {
2010
+ chrome.sidebarAvailable && (
2011
+ <button onClick={() => chrome.setSidebarCollapsed(!chrome.sidebarCollapsed)}>
2012
+ {chrome.sidebarCollapsed ? 'Show' : 'Hide'} sidebar
2013
+ </button>
2014
+ );
2015
+ }
2016
+
2017
+ // Level 3: your own way back while expanded.
2018
+ useEffect(() => {
2019
+ chrome.setHostExpandedBar(false); // only because the bar below exists
2020
+ }, [chrome.setHostExpandedBar]);
2021
+ {
2022
+ chrome.focusMode && (
2023
+ <nav>
2024
+ <button onClick={chrome.goToFiasHome}>Fias</button>
2025
+ <button onClick={chrome.goToWorktop}>Worktop</button>
2026
+ <button onClick={chrome.openAssistant}>Fias AI</button>
2027
+ <button onClick={() => chrome.setFocusMode(false)}>Collapse</button>
2028
+ </nav>
2029
+ );
2030
+ }
2031
+ ```
2032
+
2033
+ Rules that matter:
2034
+
2035
+ - **Mounting the hook takes the chrome over.** The host's floating controls over your app — the default expand button, and the "show sidebar" control it offers while the sidebar is collapsed — go away the moment your plugin reads chrome state, because a plugin that does is expected to draw its own affordances. Draw them (`setFocusMode`, `setSidebarCollapsed`), or set `showExpandButton: false` in the manifest if you want no host expand button and no hook.
2036
+ - **Gate your controls on what the host offers.** `focusModeAvailable` is false outside the normal signed-in layout (public, full-screen and embedded views have no chrome to hide) and `sidebarAvailable` is false where your manifest declined the sidebar. A setter for something unavailable is ignored, not faked.
2037
+ - **Suppress the host's bar only if you draw your own way back.** Once the chrome is hidden that bar is the only one. Focus mode you set persists across reloads only while the bar is kept; with it suppressed a reload restores the chrome, so a broken collapse control of yours is never a reload-proof trap.
2038
+ - **The sidebar you collapse is your page's, not the user's setting.** `setSidebarCollapsed` overrides the suggested-arches sidebar beside your arche; it is never written to the user's preference and is dropped when your plugin leaves, so other arches start the way the user left them.
2039
+ - **The state is the host's, and it tells you.** A user clicking the host's own expand or Collapse reaches you as a change to `focusMode`, so a plugin drawing its own button stays in step. Read state from the hook; never mirror it in your own state.
2040
+ - **`goToFiasHome` / `goToWorktop` leave your arche** for the two destinations the platform's own bar links to. They are a closed set, never a URL; the host rate-limits them. To open another arche, use `useFiasNavigation().openArche`.
2041
+ - **`openAssistant` needs no permission** and carries no text (the assistant's input belongs to the user). The host debounces it and honours the user's last dismissal — never call it on mount or in a loop.
2042
+ - `supported` is false before the host's init lands and on a host older than this API; the state is then inert and the setters are no-ops, so you can render unconditionally. The dev harness stands in for the platform layout: its expand button and focus-mode bar behave as the platform's do, and Fias home / Worktop log instead of leaving.
2043
+
1779
2044
  ### Opening external links
1780
2045
 
1781
2046
  To send the user to an external website (a YouTube video, docs, your homepage),
@@ -1874,7 +2139,7 @@ await fias.dataStore.query('scores', {
1874
2139
  });
1875
2140
  await fias.dataStore.delete('scores', 'doc-key');
1876
2141
  await fias.dataStore.listCollections();
1877
- await fias.dataStore.deleteCollection('scores');
2142
+ await fias.dataStore.deleteCollection('scores'); // collaborators only; live app: only while empty
1878
2143
  ```
1879
2144
 
1880
2145
  ### Panels (dialogs, reward popups, empty states)
@@ -2005,6 +2270,8 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
2005
2270
  | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
2006
2271
  | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
2007
2272
  | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
2273
+ | `showSidebar` | No | Default `true`. Set `false` to keep the platform top nav and drop the suggested-arches sidebar (see "Declining the sidebar" below) |
2274
+ | `showExpandButton` | No | Default `true`. Set `false` if you want neither the host's floating expand button nor your own (see "Controlling the host chrome" below) |
2008
2275
  | `isListed` | No | Default `true`. Visible in marketplace store |
2009
2276
  | `sdk` | Yes | SDK version range |
2010
2277
  | `dependencies` | No | npm packages with **exact** versions (max 20) |
@@ -2023,10 +2290,12 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
2023
2290
 
2024
2291
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
2025
2292
 
2026
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
2293
+ **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `vault:creative:read`, `vault:creative:write`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
2027
2294
 
2028
2295
  **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID (or a `{ capability }` selector, or an entry from `useTextModels()`) and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
2029
2296
 
2297
+ **LitCraft companion apps:** Add `"litcraft:hub"` to read the LitCraft Hub entity (`ent_intg_litcraft_hub_0000000000`): the player's own LitCraft profile, stats, currency and inventory, and the community's boards — every game's leaderboards with the player's rank, play time, top earners and activity counters. No game link is needed. Board rows carry no user ids (`isMe` marks the player's own row), and `enable_incognito` can hide the player's name on leaderboards but never show it again — the player does that on the Game Rewards page. `npx fias-dev entities --detail ent_intg_litcraft_hub_0000000000` lists the verbs.
2298
+
2030
2299
  **Defining IAP products (for `useFiasStore`):**
2031
2300
 
2032
2301
  Add a `products` array to declare what users can buy inside the arche. Each entry needs a stable `identifier` your code passes to `purchase()`. Prices are in Fias credits.
@@ -2061,6 +2330,12 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
2061
2330
 
2062
2331
  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.
2063
2332
 
2333
+ **Controlling the host chrome (`showSidebar`, `showExpandButton`, `useFiasChrome`):**
2334
+
2335
+ Most plugins want the platform's top nav and nothing else. By default the signed-in layout draws the suggested-arches sidebar beside your arche; `showSidebar: false` keeps the top nav (Worktop, marketplace, Fias AI) and drops just the sidebar. It is a manifest field, not a runtime call, because the layout has to be right before your iframe paints. There is no user override, and it only matters where a sidebar would otherwise appear: public (signed-out), full-screen and embedded views never had one.
2336
+
2337
+ Every plugin also gets Fias's standard expand/contract with no code: the host draws a small expand button over your app, and expanding hides the platform header and sidebar and shows a thin bar with the way back — Fias home, Worktop, Fias AI, Collapse. Mounting `useFiasChrome()` takes that over (the host button disappears and you draw your own; see the hook). `showExpandButton: false` is for a plugin that wants neither the host button nor its own.
2338
+
2064
2339
  ## Plugin Constraints
2065
2340
 
2066
2341
  These are hard limits enforced by the platform. Code that violates these will fail review or be blocked at runtime.
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.29.0 -->
1
+ <!-- fias-sdk-guide-version: 2.30.0 -->
2
2
 
3
3
  # FIAS Plugin Development Guide
4
4
 
@@ -215,7 +215,9 @@ await deleteFile('data/old.json');
215
215
  **Permission:** `data:store`
216
216
  **Returns:** `FiasDataStoreApi`
217
217
 
218
- A document database with collections, queries, and filtering. Data persists across sessions in live mode. Each collection can be `user`-scoped (private to each user) or `shared` (visible to all users of the plugin).
218
+ A document database with collections, queries, and filtering. Data persists across sessions in live mode. Each collection can be `user`-scoped (private to each user) or `shared` (visible to all users of the plugin) — or `workspace`-scoped, for a team's data (see `useFiasWorkspaces()`).
219
+
220
+ **`shared` is one pool across every user and every organization using your app.** Use it only for content meant for everyone (a public catalog, a global leaderboard). Data that belongs to a team or a customer goes in a `workspace`-scoped collection, where the platform enforces each member's role and the collection's role floors — see [Access control](#access-control--the-platform-enforces-it-your-app-never-does).
219
221
 
220
222
  ```tsx
221
223
  import { useFiasDataStore } from '@fias/arche-sdk';
@@ -228,6 +230,9 @@ function MyComponent() {
228
230
  // 10/minute, so scaffolding several at startup can silently lose the tail.
229
231
  await dataStore.createCollection('scores', { userScope: 'user' }); // or 'shared'
230
232
  const collections = await dataStore.listCollections();
233
+ // Only a collaborator (owner/publisher) may delete a collection (FORBIDDEN otherwise),
234
+ // and on a live app only an EMPTY one (COLLECTION_NOT_EMPTY); sandboxes (fias-dev,
235
+ // builder preview, test channel) can delete any. Never use it as an end-user "reset".
231
236
  await dataStore.deleteCollection('scores');
232
237
 
233
238
  // Document CRUD
@@ -453,7 +458,7 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
453
458
 
454
459
  **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30 (counted in units of 25 operations — a full 100-op batch uses 4), `createCollection` 10
455
460
 
456
- **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`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
461
+ **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `COLLECTION_NOT_EMPTY` (`deleteCollection` on a published app's collection that still holds documents — it would delete every user's data, so it is refused; `deleteCollection` is also `FORBIDDEN` to anyone but a collaborator, so to clear one user's data, `query` their documents and `delete` or `batch`-delete them; to change something fixed at creation, such as `searchable` or `userScope`, create a collection under a new name and migrate to it; to remove a live collection that holds data, contact Fias support), `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
457
462
 
458
463
  **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`).
459
464
 
@@ -512,6 +517,65 @@ function Leaderboard() {
512
517
 
513
518
  **Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
514
519
 
520
+ ### `useEntitySubscription()` — Realtime events from any subscribe-kind verb
521
+
522
+ **Returns:** `EntitySubscriptionState` — the same `{ status, endedReason? }` as `useDataSubscription`
523
+
524
+ `useDataSubscription` is this hook bound to the datastore. Any catalog verb declared with `kind: 'subscribe'` works the same way: pass the entity id and the verb's payload (both from the capability catalog of the entity you are linked to), and `onInvalidate` runs once the subscription is live and again on every event. The permission is the verb's own. Events are a refetch signal, never data.
525
+
526
+ ```tsx
527
+ import { useEntitySubscription } from '@fias/arche-sdk';
528
+
529
+ // Re-read the HEAD of the resource on each event and merge by id — an event
530
+ // also fires for an edit or a deletion, which a "newer than X" read never sees.
531
+ const { status } = useEntitySubscription(
532
+ ENTITY_ID, // from the catalog entry of the entity your arche is linked to
533
+ { op: 'subscribe_feed' },
534
+ refetchHead,
535
+ { enabled: member, minRefetchIntervalMs: 2_000 },
536
+ );
537
+ ```
538
+
539
+ **Options:** `{ enabled?, minRefetchIntervalMs? }`. `minRefetchIntervalMs` caps refetches to one per interval (a trailing one still follows the last event) — every refetch is a bridge call against the entity's per-minute budget, so set it on a busy resource. The payload is compared by content (key order ignored), so a fresh object literal each render does not re-subscribe. Same semantics as above: subscribe-then-fetch, single-flight with one trailing call, best-effort delivery, and the same `endedReason` values (`collection_deleted` means the observed resource is gone, whatever its kind).
540
+
541
+ **Dev harness:** MOCK mode has executors only for the Data Store, so a subscribe verb on another entity works in CLOUD mode (the dev bridge against staging), not in MOCK mode.
542
+
543
+ ### Access control — the platform enforces it, your app never does
544
+
545
+ On **every** read and write, the platform decides who may do what with your app's data: a workspace member's role (`owner` > `admin` > `member` > `viewer`) and each workspace collection's role floors (`readMinRole` / `writeMinRole`). Nothing that runs in the browser is access control:
546
+
547
+ - **Never build your own role system** — no role picker, no role-to-permission matrix, no "switch to Administrator" control, no role kept in your own data and checked in your code. Anyone can change what runs in their own browser, so such checks protect nothing. When part of your app must be limited to some people, put that data in a workspace collection with a raised role floor: the server refuses everyone below it, whatever your UI shows.
548
+ - **Never put business data in `shared` collections.** `shared` is one pool across every user and every organization using your app. A team's or a customer's data goes in `workspace`-scoped collections.
549
+ - **`useFiasAccess()` is a UI hint** — show or hide controls by the user's role. It is not a gate; the server already is one.
550
+
551
+ **When four workspace roles are not enough, declare app roles** (ADR-034) — never build your own. An `access` block in `fias-plugin.json` declares your app's permissions and the roles that bundle them; every `workspace` collection then names a `readPermission` and a `writePermission`. Workspace owners choose who holds which role in Fias's own Access screen, and the platform enforces it on every read and write, deny by default:
552
+
553
+ ```jsonc
554
+ "access": {
555
+ "permissions": [
556
+ { "id": "ledger.read", "label": "View the ledger" },
557
+ { "id": "ledger.write", "label": "Edit the ledger" },
558
+ { "id": "ledger.approve", "label": "Approve entries", "sensitive": true }
559
+ ],
560
+ "roles": [
561
+ { "id": "bookkeeper", "label": "Bookkeeper", "permissions": ["ledger.read", "ledger.write"], "minTier": "member" },
562
+ { "id": "controller", "label": "Controller", "permissions": ["ledger.read", "ledger.approve"], "minTier": "admin" }
563
+ ],
564
+ "suggestions": { "member": ["bookkeeper"] }
565
+ },
566
+ "collections": [
567
+ { "name": "ledger", "scope": "workspace", "readPermission": "ledger.read", "writePermission": "ledger.write" }
568
+ ]
569
+ ```
570
+
571
+ - **Ids are permanent.** Never rename or reuse a permission, role or audience id; retire one with `"deprecated": true`, and remove it only in a later publish (it stays reserved forever). `sensitive` and `deprecated` cannot be undone, a published permission can never be marked `sensitive`, and a role's `minTier` only rises. Policy changes only ever narrow what people can do; anything that widens reaches a workspace only when a person there grants or accepts it.
572
+ - **Collection permissions are set once** (`COLLECTION_PERMISSION_IMMUTABLE`) — choose them when you first declare the collection. The one change allowed later is pointing a collection at a permission that is new in that publish and that no existing role grants (so nobody holds it until a workspace grants it).
573
+ - **Per-record confidentiality — audiences.** `access.audiences` declares readerships (`{ "id": "board", "readers": ["board.read"], "disclosedBy": "board.file" }`) and a collection lists the ones its records may go to (`"audiences": ["board", "staff"]`). Each record sits in one audience; the platform returns only records of audiences the user reads. In such collections keys are generated by the platform: create with `create(collection, data, { workspaceId, audience })` (or `put(collection, null, …)`), and a `put` with a key and another `audience` moves the record.
574
+ - **Write-only roles add, never change.** A role with a collection's `writePermission` but not its `readPermission` may only add documents, with keys the platform generates (`create()`); a key you choose is refused the same way whether or not it exists, so a write-only role can never probe what exists. Use `create()` for submissions, complaints and anything filed by people who must not read the rest.
575
+ - **Where it applies:** only in an org workspace that approved your app and turned its roles on; elsewhere tier rules apply, and a collection created already protected stays closed (`ACCESS_SETUP_REQUIRED`) until an owner turns roles on. An app's first `access` block is accepted only where Fias has made app roles available ("app roles are not available on this platform yet" otherwise — publish without the block for now).
576
+
577
+ Full rules (limits, publish order, references and counters across audiences, activation): `docs/api-plugins.md` § App roles and permissions.
578
+
515
579
  ### `useFiasWorkspaces()` — Team/org workspaces with roles
516
580
 
517
581
  **Permission:** `data:workspace`
@@ -524,7 +588,7 @@ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org spac
524
588
  - **member** — read/write docs
525
589
  - **viewer** — read docs only
526
590
 
527
- 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).
591
+ 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). The roles are enforced by the platform on every call; to show the current user's own role in your UI, use `useFiasAccess(workspaceId)` — and read [Access control](#access-control--the-platform-enforces-it-your-app-never-does) before you design anything role-shaped.
528
592
 
529
593
  ```tsx
530
594
  import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
@@ -566,6 +630,44 @@ if (holder?.type === 'org') {
566
630
 
567
631
  **Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives), `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`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. 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).
568
632
 
633
+ ### `useFiasAccess()` — The current user's own access in a workspace
634
+
635
+ **Permission:** `data:workspace`
636
+ **Returns:** `FiasAccessState` — `{ access, isLoading, error, refetch, can, canReadAudience, canDiscloseTo, openAccessDialog }`
637
+
638
+ What the signed-in user can do in one workspace, so your UI can show the controls that apply to them. `access.mode` says which model answered:
639
+
640
+ - `'tier'` — an app without roles (or a workspace outside an organization): `{ mode, workspaceId, tier, expiresAt }`, their workspace role (`owner` / `admin` / `member` / `viewer`), or `tier: null` when they have none there, and when their membership expires (`null` = never).
641
+ - `'legacy'` — your app declares roles but this workspace has not turned them on yet: tier rules apply.
642
+ - `'roles'` — roles are on: also `roles` (the ones that count at the user's current tier, each with its expiry), `permissions`, and `audiences: { readable, disclosable }`. `can(permission)`, `canReadAudience(id)` and `canDiscloseTo(id)` are true only here.
643
+
644
+ Pass `null` or `undefined` while there is no workspace yet; nothing is fetched. Mount it in as many components as you like: instances for one workspace share one request, and it refreshes itself when Fias says the user's access changed and when the window regains focus. `openAccessDialog()` opens Fias's own sheet where the user sees why they have the access they have and can ask for more (and an access manager can manage roles); your app passes nothing to it and changes nothing through it.
645
+
646
+ **A UI hint, never security.** The platform checks every read and write on arrival against the user's role and the collection's role floors. Hiding a button does not protect anything, and showing one does not grant anything. Never build your own role check on top of this, and never offer a role picker as a way to grant access.
647
+
648
+ ```tsx
649
+ import { useFiasAccess } from '@fias/arche-sdk';
650
+
651
+ function Toolbar({ workspaceId }: { workspaceId: string }) {
652
+ const { access, isLoading } = useFiasAccess(workspaceId);
653
+ if (isLoading || !access) return null;
654
+ const canManage = access.tier === 'owner' || access.tier === 'admin';
655
+ return canManage ? <InviteButton /> : null; // the server still decides every call
656
+ }
657
+
658
+ function ApproveButton({ workspaceId }: { workspaceId: string }) {
659
+ const { access, can, openAccessDialog } = useFiasAccess(workspaceId);
660
+ if (access?.mode !== 'roles') return null; // tier / legacy: the permission is not in force
661
+ return can('ledger.approve') ? (
662
+ <button onClick={approve}>Approve</button>
663
+ ) : (
664
+ <button onClick={() => openAccessDialog()}>Request access</button>
665
+ );
666
+ }
667
+ ```
668
+
669
+ Treat a `mode` you do not recognise as "hide what it would gate". It answers only about the signed-in user: no user ids, nothing about other members. **Rate limit:** 120/min.
670
+
569
671
  ### `useEntityInvocation()` — Invoke AI models
570
672
 
571
673
  **Permission:** `entities:invoke`
@@ -1414,7 +1516,7 @@ Rules that matter:
1414
1516
  - `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
1415
1517
  - Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
1416
1518
  - **Test it in the dev harness:** the **✦ Fias AI** toolbar button opens the real assistant over your plugin (Live mode; turns are real and use your credits). It describes your plugin exactly as the platform would: manifest actions always, runtime (`useFiasAIActions`) actions and `state` only for a trusted arche, and the window names any runtime action it is withholding. In the harness it can use your plugin's actions plus read-only Fias tools, never writes or sends. The builder preview has no assistant.
1417
- - **`openAssistant()`** (the hook's return) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
1519
+ - **`openAssistant()`** (the hook's return, also on `useFiasChrome()`) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). Opening needs no permission: any plugin may do it, actions declared or not, and the user can still use the assistant's own tools. The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
1418
1520
 
1419
1521
  ### `useArcheAssets()` — Contributor-published asset library
1420
1522
 
@@ -1556,6 +1658,113 @@ Three things to design around:
1556
1658
 
1557
1659
  Only images the user made or uploaded in **their own** Fias files can be published (pass the `fileId` from a `useImageGeneration()` result). PNG/JPEG/WebP only; every image is re-encoded to a canonical PNG with metadata stripped. Publishers can `unpublish()` their own assets, and any user can `report()` one for review.
1558
1660
 
1661
+ ### `useCreativeLibrary()` — The user's styles and characters
1662
+
1663
+ **Permission:** `vault:creative:read`; creating and changing also `vault:creative:write` (both human-reviewed at publish)
1664
+ **Returns:** `FiasCreativeLibraryApi`
1665
+
1666
+ ```tsx
1667
+ import { useCreativeLibrary, useImageGeneration } from '@fias/arche-sdk';
1668
+
1669
+ const library = useCreativeLibrary();
1670
+ const styles = await library.list({ kind: 'style' });
1671
+ // [{ itemId: 'vci_…', kind: 'style', name: 'Flappyland', words: '…', referenceDocumentIds: [...], version: 3, extension: null, … }]
1672
+ const style = await library.get(styles[0].itemId); // + previews: [{ documentId, url, expiresAt }] (5-minute URLs)
1673
+
1674
+ // Or let the person choose, in a picker Fias draws with the pictures:
1675
+ const picked = await library.pick({ kind: 'style' }); // { item: { itemId, kind, name, version } } | { canceled: true }
1676
+
1677
+ // Draw in a style — the platform sends its pictures and words for you:
1678
+ await useImageGeneration().generate({
1679
+ entityId,
1680
+ prompt: 'a red fox',
1681
+ creativeStyle: { styleId: style.itemId },
1682
+ });
1683
+ // …or choose which of its pictures go (1–3 of its referenceDocumentIds, in order):
1684
+ // creativeStyle: { styleId: style.itemId, pictures: [style.referenceDocumentIds[2]] }
1685
+
1686
+ // Make one (needs vault:creative:write). Pictures must be images YOUR arche created:
1687
+ const { documentId } = await useVaultDocuments().upload(pngBytes, {
1688
+ name: 'sample.png',
1689
+ mimeType: 'image/png',
1690
+ });
1691
+ await library.create({
1692
+ kind: 'style',
1693
+ name: 'Moonlit',
1694
+ words: 'inky blues, soft glow',
1695
+ referenceDocumentIds: [documentId],
1696
+ });
1697
+ await library.update(style.itemId, { words: 'brighter', expectedVersion: style.version });
1698
+ ```
1699
+
1700
+ The Creative Library holds the user's **styles** (a look: words and 1–12 reference pictures) and **characters** (one reference picture, an appearance, and a style). They belong to the user — or, in a team workspace that approved your arche, to the workspace — not to the arche that made them: FiasWorks Studio makes most of them, and any arche the user allows can use them. Reading them is seeing their pictures, so the permission is disclosed that way. Every change writes a new version (`get(id, version)` reads an old one); pass `expectedVersion` to `update` so a change made elsewhere is never overwritten. An update may keep pictures the item already has, whoever added them; only pictures you add must be your arche's own. Keep your own fields in `extension` — you see and change only yours. In a workspace, viewers can read but not change (`WORKSPACE_VIEWER_READ_ONLY`). `pick()` shows exactly what `list()` would return, so prefer it to drawing your own chooser; it rejects with `PICKER_UNAVAILABLE` in the dev harness (use `list()` there) and `SHEET_BUSY` while another Fias sheet is open. Not available in the builder preview or the test channel. **Error codes:** `CREATIVE_ITEM_NOT_FOUND`, `CREATIVE_REFERENCE_NOT_USABLE`, `CREATIVE_STYLE_REF_INVALID`, `CREATIVE_VERSION_CONFLICT`, `CREATIVE_LIBRARY_FULL`, `CREATIVE_INVALID`; on `generate`, `STYLE_NOT_FOUND`, `STYLE_NEEDS_DESCRIPTION` and `STYLE_PICTURE_NOT_IN_STYLE`.
1701
+
1702
+ ### `useCreateAsset()` — Game art the FiasWorks way
1703
+
1704
+ **Permission:** `entities:image_generate`; a library style or character also needs `vault:creative:read`
1705
+ **Returns:** `{ invoke(params): Promise<CreateAssetResult> }` (generated surface hook, `image.create-asset`)
1706
+
1707
+ ```tsx
1708
+ import { useCreateAsset } from '@fias/arche-sdk';
1709
+
1710
+ const { invoke } = useCreateAsset();
1711
+ const chest = await invoke({
1712
+ kind: 'object',
1713
+ description: 'a treasure chest',
1714
+ style: { builtIn: 'pixel-art' },
1715
+ });
1716
+ // chest.image — the painting; chest.cutout — a transparent PNG (null + cutoutError if removal failed)
1717
+
1718
+ // A library style, sending the pictures that suit this scene (1–3; default its first):
1719
+ await invoke({
1720
+ kind: 'image',
1721
+ description: 'a cellar lit by one candle',
1722
+ style: { library: { id: styleId, pictures: [indoorPictureId] } },
1723
+ });
1724
+
1725
+ // A later picture of a library character, in their own style:
1726
+ await invoke({
1727
+ kind: 'character',
1728
+ character: { library: { id: characterId }, pose: 'running', view: 'side' },
1729
+ });
1730
+
1731
+ // A games-list card with the name lettered in, finished at its export sizes:
1732
+ const card = await invoke({
1733
+ kind: 'game',
1734
+ description: 'a glowing mine shaft at night',
1735
+ gameArt: { format: 'list-card', title: 'Lit Mining', titleInArt: true },
1736
+ exports: true,
1737
+ }); // card.exports: [{ name, fileId, imageUrl, width, height }, …]
1738
+ ```
1739
+
1740
+ One call makes a scene (`image`, empty of people by default), an `object` (or an n×n `series`, split into its objects with `exports: true`), an `icon` (on a `plate`, or matching another icon of a set with `matchIcon`), a `character` (the base from words, or a pose / `turnaround` of a Creative Library character) or `game` art (`list-card`, `front-screen`, `game-icon`, `title`) with FiasWorks Studio's recipes: the framing each kind needs, a built-in style (`cartoon` by default, `pixel-art`, `watercolor`, …) or a library style, and the cutout. `restyle` redraws a picture in the style; `variation` pushes a new take away from the last; `seed` (Nano Banana 2, FLUX 2 Pro) aims for the same picture again. Models: Nano Banana 2 (default, takes a style's pictures), Nano Banana Pro, FLUX 2 Pro, GPT Image 2, Nano Banana 2 Lite (no reference pictures, so no restyle, character poses or library pictures).
1741
+
1742
+ Billing is per step, as if you had called each one: the generation, then AI background removal for the cut-out kinds. Keying a title's flat backdrop and every export size cost nothing. The painting lands in the user's Arche Saves like `useImageGeneration`; the cutout and export sizes are your arche's own working files. In the builder preview only built-in styles work. **Error codes:** `INVALID_PARAMS`, `MODEL_UNAVAILABLE`, `REFERENCE_IMAGE_NOT_SUPPORTED`, `STYLE_NEEDS_DESCRIPTION`, `STYLE_NOT_FOUND`, `CHARACTER_NOT_FOUND`, `PERMISSION_DENIED`, `STORAGE_FAILED`, and the `useImageGeneration` codes (`INSUFFICIENT_CREDITS`, `INVALID_RESOLUTION`, …).
1743
+
1744
+ ### Styles your arche ships for its players
1745
+
1746
+ **Permission:** `entities:image_generate` only — no Creative Library access
1747
+
1748
+ ```jsonc
1749
+ // fias-plugin.json
1750
+ "styles": [{ "key": "lit-mining", "name": "Lit Mining", "words": "glossy cartoon, warm lamplight" }]
1751
+ ```
1752
+
1753
+ ```tsx
1754
+ await useImageGeneration().generate({
1755
+ entityId,
1756
+ prompt: 'a gem cart',
1757
+ creativeStyle: { shipped: 'lit-mining' },
1758
+ });
1759
+ await useCreateAsset().invoke({
1760
+ kind: 'object',
1761
+ description: 'a pickaxe',
1762
+ style: { shipped: 'lit-mining' },
1763
+ });
1764
+ ```
1765
+
1766
+ A style can ship **with** your arche so every player draws in it — no library of their own, no `vault:creative:read`. The developer ships one of their Creative Library styles from the Fias Forge ("Styles your players can use") — signed in, never with an API key: its pictures are copied into the arche's asset library tagged `style:<key>`, and the answer is the `styles` entry to add here (up to 8; `key` is lowercase letters, digits and dashes, ≤ 40). Both are frozen with each published version, so a shipped style is read from the arche's **published** version — publish once with it before relying on it; the dev harness has none (`SHIPPED_STYLE_UNAVAILABLE`). Players can see the pictures, so a proprietary picture never ships. The player pays for what they make. **Error codes:** `STYLE_NOT_FOUND` (this version ships no such key), `STYLE_NEEDS_DESCRIPTION`, `SHIPPED_STYLE_UNAVAILABLE`.
1767
+
1559
1768
  ### `useFiasConnections()` — Outside accounts the workspace lets your app use
1560
1769
 
1561
1770
  **Permission:** `connections:use` (human-reviewed at publish)
@@ -1776,6 +1985,62 @@ A host older than this option ignores it and pushes. If you correct paths
1776
1985
  automatically as they arrive, never repeat the same correction twice in a
1777
1986
  row, or a push-only host can trap the Back button between two URLs.
1778
1987
 
1988
+ ### `useFiasChrome()` — Expand, contract and the host chrome
1989
+
1990
+ **Permission:** None required
1991
+
1992
+ Fias's standard expand/contract — what the internal arches call focus mode — plus the four controls the platform's focus-mode bar carries, so you can put them where your design wants them. Three levels, your choice:
1993
+
1994
+ 1. **Do nothing.** The host draws a small expand button over your app. Expanding hides the platform header and the sidebar; the host then shows a thin bar with Fias home, Worktop, Fias AI and Collapse.
1995
+ 2. **Your own buttons, the host's bar.** Mount the hook, draw your own expand button and sidebar toggle, keep the host's bar for the way back.
1996
+ 3. **Your own bar.** Mount the hook, `setHostExpandedBar(false)`, and draw your own bar from the four controls.
1997
+
1998
+ ```tsx
1999
+ import { useFiasChrome } from '@fias/arche-sdk';
2000
+
2001
+ const chrome = useFiasChrome();
2002
+
2003
+ // Level 2: your own expand button, where it fits your toolbar.
2004
+ {
2005
+ chrome.focusModeAvailable && !chrome.focusMode && (
2006
+ <button onClick={() => chrome.setFocusMode(true)}>Expand</button>
2007
+ );
2008
+ }
2009
+ {
2010
+ chrome.sidebarAvailable && (
2011
+ <button onClick={() => chrome.setSidebarCollapsed(!chrome.sidebarCollapsed)}>
2012
+ {chrome.sidebarCollapsed ? 'Show' : 'Hide'} sidebar
2013
+ </button>
2014
+ );
2015
+ }
2016
+
2017
+ // Level 3: your own way back while expanded.
2018
+ useEffect(() => {
2019
+ chrome.setHostExpandedBar(false); // only because the bar below exists
2020
+ }, [chrome.setHostExpandedBar]);
2021
+ {
2022
+ chrome.focusMode && (
2023
+ <nav>
2024
+ <button onClick={chrome.goToFiasHome}>Fias</button>
2025
+ <button onClick={chrome.goToWorktop}>Worktop</button>
2026
+ <button onClick={chrome.openAssistant}>Fias AI</button>
2027
+ <button onClick={() => chrome.setFocusMode(false)}>Collapse</button>
2028
+ </nav>
2029
+ );
2030
+ }
2031
+ ```
2032
+
2033
+ Rules that matter:
2034
+
2035
+ - **Mounting the hook takes the chrome over.** The host's floating controls over your app — the default expand button, and the "show sidebar" control it offers while the sidebar is collapsed — go away the moment your plugin reads chrome state, because a plugin that does is expected to draw its own affordances. Draw them (`setFocusMode`, `setSidebarCollapsed`), or set `showExpandButton: false` in the manifest if you want no host expand button and no hook.
2036
+ - **Gate your controls on what the host offers.** `focusModeAvailable` is false outside the normal signed-in layout (public, full-screen and embedded views have no chrome to hide) and `sidebarAvailable` is false where your manifest declined the sidebar. A setter for something unavailable is ignored, not faked.
2037
+ - **Suppress the host's bar only if you draw your own way back.** Once the chrome is hidden that bar is the only one. Focus mode you set persists across reloads only while the bar is kept; with it suppressed a reload restores the chrome, so a broken collapse control of yours is never a reload-proof trap.
2038
+ - **The sidebar you collapse is your page's, not the user's setting.** `setSidebarCollapsed` overrides the suggested-arches sidebar beside your arche; it is never written to the user's preference and is dropped when your plugin leaves, so other arches start the way the user left them.
2039
+ - **The state is the host's, and it tells you.** A user clicking the host's own expand or Collapse reaches you as a change to `focusMode`, so a plugin drawing its own button stays in step. Read state from the hook; never mirror it in your own state.
2040
+ - **`goToFiasHome` / `goToWorktop` leave your arche** for the two destinations the platform's own bar links to. They are a closed set, never a URL; the host rate-limits them. To open another arche, use `useFiasNavigation().openArche`.
2041
+ - **`openAssistant` needs no permission** and carries no text (the assistant's input belongs to the user). The host debounces it and honours the user's last dismissal — never call it on mount or in a loop.
2042
+ - `supported` is false before the host's init lands and on a host older than this API; the state is then inert and the setters are no-ops, so you can render unconditionally. The dev harness stands in for the platform layout: its expand button and focus-mode bar behave as the platform's do, and Fias home / Worktop log instead of leaving.
2043
+
1779
2044
  ### Opening external links
1780
2045
 
1781
2046
  To send the user to an external website (a YouTube video, docs, your homepage),
@@ -1874,7 +2139,7 @@ await fias.dataStore.query('scores', {
1874
2139
  });
1875
2140
  await fias.dataStore.delete('scores', 'doc-key');
1876
2141
  await fias.dataStore.listCollections();
1877
- await fias.dataStore.deleteCollection('scores');
2142
+ await fias.dataStore.deleteCollection('scores'); // collaborators only; live app: only while empty
1878
2143
  ```
1879
2144
 
1880
2145
  ### Panels (dialogs, reward popups, empty states)
@@ -2005,6 +2270,8 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
2005
2270
  | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
2006
2271
  | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
2007
2272
  | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
2273
+ | `showSidebar` | No | Default `true`. Set `false` to keep the platform top nav and drop the suggested-arches sidebar (see "Declining the sidebar" below) |
2274
+ | `showExpandButton` | No | Default `true`. Set `false` if you want neither the host's floating expand button nor your own (see "Controlling the host chrome" below) |
2008
2275
  | `isListed` | No | Default `true`. Visible in marketplace store |
2009
2276
  | `sdk` | Yes | SDK version range |
2010
2277
  | `dependencies` | No | npm packages with **exact** versions (max 20) |
@@ -2023,10 +2290,12 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
2023
2290
 
2024
2291
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
2025
2292
 
2026
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
2293
+ **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `vault:creative:read`, `vault:creative:write`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
2027
2294
 
2028
2295
  **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID (or a `{ capability }` selector, or an entry from `useTextModels()`) and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
2029
2296
 
2297
+ **LitCraft companion apps:** Add `"litcraft:hub"` to read the LitCraft Hub entity (`ent_intg_litcraft_hub_0000000000`): the player's own LitCraft profile, stats, currency and inventory, and the community's boards — every game's leaderboards with the player's rank, play time, top earners and activity counters. No game link is needed. Board rows carry no user ids (`isMe` marks the player's own row), and `enable_incognito` can hide the player's name on leaderboards but never show it again — the player does that on the Game Rewards page. `npx fias-dev entities --detail ent_intg_litcraft_hub_0000000000` lists the verbs.
2298
+
2030
2299
  **Defining IAP products (for `useFiasStore`):**
2031
2300
 
2032
2301
  Add a `products` array to declare what users can buy inside the arche. Each entry needs a stable `identifier` your code passes to `purchase()`. Prices are in Fias credits.
@@ -2061,6 +2330,12 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
2061
2330
 
2062
2331
  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.
2063
2332
 
2333
+ **Controlling the host chrome (`showSidebar`, `showExpandButton`, `useFiasChrome`):**
2334
+
2335
+ Most plugins want the platform's top nav and nothing else. By default the signed-in layout draws the suggested-arches sidebar beside your arche; `showSidebar: false` keeps the top nav (Worktop, marketplace, Fias AI) and drops just the sidebar. It is a manifest field, not a runtime call, because the layout has to be right before your iframe paints. There is no user override, and it only matters where a sidebar would otherwise appear: public (signed-out), full-screen and embedded views never had one.
2336
+
2337
+ Every plugin also gets Fias's standard expand/contract with no code: the host draws a small expand button over your app, and expanding hides the platform header and sidebar and shows a thin bar with the way back — Fias home, Worktop, Fias AI, Collapse. Mounting `useFiasChrome()` takes that over (the host button disappears and you draw your own; see the hook). `showExpandButton: false` is for a plugin that wants neither the host button nor its own.
2338
+
2064
2339
  ## Plugin Constraints
2065
2340
 
2066
2341
  These are hard limits enforced by the platform. Code that violates these will fail review or be blocked at runtime.