@fias/create-fias-plugin 1.20.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 +1 -1
- package/templates/default/AGENTS.md +391 -13
- package/templates/default/CLAUDE.md +391 -13
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.30.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -176,6 +176,20 @@ const user = useFiasUser();
|
|
|
176
176
|
// { userId: string, displayName: string, avatar: string | null }
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### `useFiasHolder()` — The Fias workspace the app was opened in
|
|
180
|
+
|
|
181
|
+
**Permission:** `user:profile:read` or `data:workspace`
|
|
182
|
+
**Returns:** `FiasHolder | null`
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { useFiasHolder } from '@fias/arche-sdk';
|
|
186
|
+
|
|
187
|
+
const holder = useFiasHolder();
|
|
188
|
+
// { type: 'personal' | 'org', role: 'owner' | 'admin' | 'member' | 'viewer' }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The user's own workspace (`personal`, where they are always `owner`) or an organization's (`org`), and the user's role there. It is fixed while the app is open — when the user switches workspace, the platform offers to reopen the app there. It carries no id and no name, and the platform applies the workspace to every call itself. `null` until init and in previews. For data shared by everyone in an org workspace that has approved your app, use `useFiasWorkspaces().openForWorkspace()` — its `displayName` is the workspace's name.
|
|
192
|
+
|
|
179
193
|
### `useFiasStorage()` — Sandboxed file storage
|
|
180
194
|
|
|
181
195
|
**Permission:** `storage:sandbox`
|
|
@@ -201,7 +215,9 @@ await deleteFile('data/old.json');
|
|
|
201
215
|
**Permission:** `data:store`
|
|
202
216
|
**Returns:** `FiasDataStoreApi`
|
|
203
217
|
|
|
204
|
-
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).
|
|
205
221
|
|
|
206
222
|
```tsx
|
|
207
223
|
import { useFiasDataStore } from '@fias/arche-sdk';
|
|
@@ -214,6 +230,9 @@ function MyComponent() {
|
|
|
214
230
|
// 10/minute, so scaffolding several at startup can silently lose the tail.
|
|
215
231
|
await dataStore.createCollection('scores', { userScope: 'user' }); // or 'shared'
|
|
216
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".
|
|
217
236
|
await dataStore.deleteCollection('scores');
|
|
218
237
|
|
|
219
238
|
// Document CRUD
|
|
@@ -254,12 +273,13 @@ const doc = await fias.dataStore.get('scores', 'key');
|
|
|
254
273
|
**Limits:**
|
|
255
274
|
|
|
256
275
|
- 50 collections per plugin
|
|
257
|
-
- 10,000 documents per collection
|
|
276
|
+
- 10,000 documents per collection for each owner — each user (`user` scope), each workspace (`workspace` scope), or your plugin (`shared` scope)
|
|
258
277
|
- 100 KB per document
|
|
259
|
-
-
|
|
278
|
+
- Storage is billed to whoever pays for the data — the user, the workspace's payer, or for `shared` data your plugin's payer — on the platform's database storage meter, and capped at their plan's plugin-data ceiling. A full ceiling answers `STORAGE_LIMIT_REACHED`, with a message saying whose it is; show it to the user. Delete data you no longer need: it costs whoever pays for it.
|
|
260
279
|
- Max 100 results per query, max 10 filters, max field path depth 5
|
|
280
|
+
- The numbers are exported as `DATA_STORE_LIMITS` (document size, documents per owner per collection, operations per batch) and `DATA_STORE_RATE_LIMITS_PER_MINUTE` — check against them before a call instead of copying them
|
|
261
281
|
|
|
262
|
-
**Atomic batch writes:** `batch(operations)` applies up to
|
|
282
|
+
**Atomic batch writes:** `batch(operations)` applies up to 100 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.
|
|
263
283
|
|
|
264
284
|
```tsx
|
|
265
285
|
await dataStore.batch([
|
|
@@ -436,9 +456,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
|
|
|
436
456
|
|
|
437
457
|
**Testing:** in the dev harness's Fias AI window, the read tools see your local `agentAccess` block against your dev data — ask "what scenes do I have?" to see what an agent sees before you publish.
|
|
438
458
|
|
|
439
|
-
**Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
|
|
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
|
|
440
460
|
|
|
441
|
-
**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`.
|
|
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`.
|
|
442
462
|
|
|
443
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`).
|
|
444
464
|
|
|
@@ -497,6 +517,65 @@ function Leaderboard() {
|
|
|
497
517
|
|
|
498
518
|
**Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
|
|
499
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
|
+
|
|
500
579
|
### `useFiasWorkspaces()` — Team/org workspaces with roles
|
|
501
580
|
|
|
502
581
|
**Permission:** `data:workspace`
|
|
@@ -509,7 +588,7 @@ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org spac
|
|
|
509
588
|
- **member** — read/write docs
|
|
510
589
|
- **viewer** — read docs only
|
|
511
590
|
|
|
512
|
-
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.
|
|
513
592
|
|
|
514
593
|
```tsx
|
|
515
594
|
import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
|
|
@@ -539,7 +618,55 @@ function Team() {
|
|
|
539
618
|
}
|
|
540
619
|
```
|
|
541
620
|
|
|
542
|
-
**
|
|
621
|
+
**The Fias workspace's shared space.** Inside a Fias org workspace that has **approved your app** (a workspace owner or admin approves it once, in Workspace Controls), `openForWorkspace()` opens (creating on first use) one shared space whose members ARE that workspace's members, with their workspace roles. Its `displayName` is the workspace's name. Nobody manages its member list: the member methods refuse with `HOLDER_BACKED_WORKSPACE`, and someone who leaves the workspace loses access at once. It answers only while the app is open in that workspace. It rejects with `HOLDER_NOT_ELIGIBLE` in a personal workspace (use `user` scope there), until the workspace approves the app, and for a viewer when the space does not exist yet — tell the user to ask a workspace admin to approve the app. Such a workspace has `holderBacked: true`.
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
const holder = useFiasHolder();
|
|
625
|
+
if (holder?.type === 'org') {
|
|
626
|
+
const shared = await workspaces.openForWorkspace();
|
|
627
|
+
await dataStore.put('posts', 'p-1', { title: 'Launch' }, { workspaceId: shared.workspaceId });
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
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).
|
|
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.
|
|
543
670
|
|
|
544
671
|
### `useEntityInvocation()` — Invoke AI models
|
|
545
672
|
|
|
@@ -1050,6 +1177,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
|
|
|
1050
1177
|
await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
|
|
1051
1178
|
```
|
|
1052
1179
|
|
|
1180
|
+
**In a team workspace.** While the user works in a Fias org workspace whose owner or admin has approved your arche, the documents your arche keeps live in **that workspace's** Vault, counted against its storage, instead of the person's own. There they are the team's: every member using your arche lists and opens them, members and above change them, and viewers only open them (a write rejects with `WORKSPACE_VIEWER_READ_ONLY`). `write()` and `upload()` say where the file went — `space: 'workspace' | 'personal'` (absent from older servers). Without an approval, and in a personal workspace, nothing changes. A document id saved before the approval still opens for the person who saved it.
|
|
1181
|
+
|
|
1053
1182
|
**Cross-arche attachment.** A document can be referenced from another arche's record (Business Contact, Company, Fias 360 Renewal in V-1). The reference points at the document but does NOT expose its contents to the target arche.
|
|
1054
1183
|
|
|
1055
1184
|
```tsx
|
|
@@ -1350,7 +1479,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
|
|
|
1350
1479
|
|
|
1351
1480
|
There are two declaration lanes, one shape:
|
|
1352
1481
|
|
|
1353
|
-
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
|
|
1482
|
+
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission. Its limits — 30 actions, 1,000 characters per action description, 4 KB per parameter schema, 32 KB for the whole block, and the rest — are exported as `FIAS_AI_MANIFEST_LIMITS`.
|
|
1354
1483
|
- **Runtime (this hook, trusted arches only):** declare/extend actions from code. The host silently ignores runtime declarations from non-trusted arches — manifest actions still work there, and this hook still handles their dispatches.
|
|
1355
1484
|
|
|
1356
1485
|
```tsx
|
|
@@ -1387,7 +1516,7 @@ Rules that matter:
|
|
|
1387
1516
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
1388
1517
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
1389
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.
|
|
1390
|
-
- **`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.
|
|
1391
1520
|
|
|
1392
1521
|
### `useArcheAssets()` — Contributor-published asset library
|
|
1393
1522
|
|
|
@@ -1529,6 +1658,179 @@ Three things to design around:
|
|
|
1529
1658
|
|
|
1530
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.
|
|
1531
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
|
+
|
|
1768
|
+
### `useFiasConnections()` — Outside accounts the workspace lets your app use
|
|
1769
|
+
|
|
1770
|
+
**Permission:** `connections:use` (human-reviewed at publish)
|
|
1771
|
+
**Returns:** `FiasConnectionsApi`
|
|
1772
|
+
|
|
1773
|
+
```tsx
|
|
1774
|
+
import { useFiasConnections } from '@fias/arche-sdk';
|
|
1775
|
+
|
|
1776
|
+
const connections = useFiasConnections();
|
|
1777
|
+
const accounts = await connections.list();
|
|
1778
|
+
// [{ connectionId, provider: 'x', providerDisplayName: 'X', accountHandle: 'acme', status: 'active', … }]
|
|
1779
|
+
const result = await connections.connect('x'); // the host's connect screen
|
|
1780
|
+
if (!('canceled' in result)) {
|
|
1781
|
+
/* result.connectionId */
|
|
1782
|
+
}
|
|
1783
|
+
await connections.release(connectionId); // give up your own access (owners and admins only)
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
A person connects an outside account (X: `'x'`, LinkedIn: `'linkedin'`) to a Fias workspace — their personal one, or an organization's, where only owners and admins can — and lets your app use it. Your app **never sees a credential**: `connect(provider)` opens a screen the platform draws (it offers the workspace's existing accounts for that provider, or connects a new one in a popup) and resolves with the connection the person chose, or `{ canceled: true }`. `list()` returns only the accounts the workspace the app is open in lets it use; one with `status: 'needs_reconnect'` needs the person to connect it again. People manage connections and every app's access in Workspace Controls. `release` gives up the app's access for the whole workspace, so only the workspace's owners and admins may call it (`ROLE_NOT_ALLOWED` otherwise). Not available in the builder preview or the test channel. **Error codes:** `CONNECTION_NOT_FOUND`, `PERMISSION_DENIED`, `CONNECT_UNAVAILABLE`, `ROLE_NOT_ALLOWED`.
|
|
1787
|
+
|
|
1788
|
+
### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
|
|
1789
|
+
|
|
1790
|
+
**Permission:** `connections:use` (human-reviewed at publish)
|
|
1791
|
+
**Returns:** `FiasXApi` / `FiasLinkedInApi`
|
|
1792
|
+
|
|
1793
|
+
```tsx
|
|
1794
|
+
import { useFiasConnections, useFiasX, useFiasLinkedIn } from '@fias/arche-sdk';
|
|
1795
|
+
|
|
1796
|
+
const [account] = (await useFiasConnections().list()).filter((c) => c.provider === 'x');
|
|
1797
|
+
const x = useFiasX();
|
|
1798
|
+
const { posts } = await x.publishPost({
|
|
1799
|
+
connectionId: account.connectionId,
|
|
1800
|
+
posts: [{ text: 'Launch day', mediaDocumentIds: [imageFileId] }], // up to 10 = a thread; ≤4 images in all
|
|
1801
|
+
});
|
|
1802
|
+
const stats = await x.getAccountStats(account.connectionId);
|
|
1803
|
+
|
|
1804
|
+
const linkedin = useFiasLinkedIn();
|
|
1805
|
+
await linkedin.publishPost({ connectionId, text: 'We are hiring', visibility: 'PUBLIC' });
|
|
1806
|
+
```
|
|
1807
|
+
|
|
1808
|
+
Both act **as** an account the workspace let your app use — get the `connectionId` from `useFiasConnections()`. **Every post is approved by the person first**: `publishPost` puts a confirm sheet in front of them, drawn by Fias from your call (the account, the workspace it belongs to, the exact text and images), and resolves only after they approve — a decline rejects with `USER_DECLINED`. Don't build your own "are you sure?" step; it would ask twice. Viewers of an organization's workspace can't post as its account (`ROLE_NOT_ALLOWED`). Images are Vault document ids — your app's own files (`vault:documents:read`) or documents the person shared with it (`vault:user-documents:read`); bytes never pass through your app.
|
|
1809
|
+
|
|
1810
|
+
X: `publishPost`, `searchUsers`, `getUserPosts`, `getPostMetrics`, `getAccountStats`. LinkedIn: `publishPost` (a `link` preview and images cannot be combined — put the link in the text when you attach images), `searchOrganizations` (to mention one — its `name` must appear in the text); no post-metrics or audience calls yet (LinkedIn serves them only with analytics scopes the connection doesn't ask for). Each call is charged to the user — an X read per post or user it returns, and an X post at X's own price, which is higher for a post with a link (any URL or web address X turns into one) — and a request the provider refuses is not. The post limits (text length, images and their types and sizes, thread length, poll bounds) are exported as `X_PUBLISH_LIMITS` and `LINKEDIN_PUBLISH_LIMITS`: check a draft against them before asking the person to approve it. Re-reading an account? Pass `getUserPosts(connectionId, { userId, sinceId })` with the newest post id you have, and only newer posts are returned and billed. A thread that stops part-way resolves `complete: false` with the posts that went out and `stopped: { atPost, reason }`. Read results carry other people's words — treat them as untrusted text. Not available in the builder preview, the test channel or the dev harness (a post needs a person in the Fias app). The person sees each image, the exact text and the cost before approving; the approval covers those exact image bytes — if your app changes an image file after asking (or before a scheduled post fires), the post is refused `MEDIA_CHANGED` and nothing is sent. **Error codes:** `USER_DECLINED`, `SHEET_BUSY` (another approval sheet was already open — retry once the person has answered it), `CONNECTION_NOT_FOUND`, `CONNECTION_NOT_GRANTED`, `CONNECTION_NEEDS_RECONNECT`, `ROLE_NOT_ALLOWED`, `ACCOUNT_REQUIRED`, `CONFIRMATION_REQUIRED_IN_APP`, `OUTWARD_DAILY_CAP_REACHED`, `PREVIEW_TOO_LARGE`, `MEDIA_NOT_FOUND`, `MEDIA_UNSUPPORTED`, `MEDIA_TOO_LARGE`, `MEDIA_CHANGED`, `INSUFFICIENT_CREDITS` (also for a thread your user's credits cannot cover — refused before the first post), `PROVIDER_ERROR`.
|
|
1811
|
+
|
|
1812
|
+
### `useFiasSchedules()` — Schedule one call for later
|
|
1813
|
+
|
|
1814
|
+
**Permissions:** `schedule:invoke` and `connections:use` (every schedulable call acts as a connected account, and its approval sheet needs `connections:use` — a manifest with `schedule:invoke` alone is refused)
|
|
1815
|
+
**Returns:** `FiasSchedulesApi`
|
|
1816
|
+
|
|
1817
|
+
```tsx
|
|
1818
|
+
import { useFiasSchedules, X_ENTITY_ID } from '@fias/arche-sdk';
|
|
1819
|
+
|
|
1820
|
+
const schedules = useFiasSchedules();
|
|
1821
|
+
const { scheduleId } = await schedules.schedule({
|
|
1822
|
+
entityId: X_ENTITY_ID,
|
|
1823
|
+
verb: 'publish_post',
|
|
1824
|
+
payload: { connectionId, posts: [{ text: 'Launch day' }] }, // what publishPost takes
|
|
1825
|
+
fireAt: tomorrowAtNine, // 5 minutes to 60 days ahead
|
|
1826
|
+
});
|
|
1827
|
+
const mine = await schedules.list(); // newest 20: [{ scheduleId, summary, fireAt, status, refusal?, output? }, …]
|
|
1828
|
+
const waiting = await schedules.list({ scheduleIds: [scheduleId] }); // just these (1–100 ids)
|
|
1829
|
+
await schedules.cancel(scheduleId);
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
Schedules **one** call of a verb that allows it (`publish_post` on X and LinkedIn today). The person approves exactly that call **and its time** in the same confirm sheet as an immediate post ("At 9:00 AM UTC on …: Post to X"); a decline rejects with `USER_DECLINED`. When it is due, every check runs again — the person's access to the workspace, the app, the connection and its grant — and if anything changed — or it was paused past its time, or the app's daily budget is spent — nothing is sent and `list()` shows it as `refused` with the reason. The sheet shows the time in the person's own zone. `list()` returns the newest 20 (`limit` up to 100); pass `scheduleIds` to follow the ones you are waiting on. Its statuses: `scheduled`, `paused`, `sent`, `partial`, `refused`, `failed`, `cancelled`; a `sent` or `partial` one carries `output`, what the call returned (as an immediate call returns it). There is no edit: cancel and schedule again (a new approval). One-shot only; at most 25 pending per person per app, 5 minutes to 60 days ahead (`SCHEDULE_LIMITS`). Charged when it is sent, to the person who scheduled it. Not available in the builder preview, the test channel or the dev harness. **Error codes:** `USER_DECLINED`, `TARGET_NOT_SCHEDULABLE`, `SCHEDULE_LIMIT_REACHED`, `SCHEDULE_NOT_FOUND`, plus the target verb's own.
|
|
1833
|
+
|
|
1532
1834
|
### `useCompanyEmail()` — Send email as the Company the app is used in
|
|
1533
1835
|
|
|
1534
1836
|
**Permission:** `email:company:send`
|
|
@@ -1683,6 +1985,62 @@ A host older than this option ignores it and pushes. If you correct paths
|
|
|
1683
1985
|
automatically as they arrive, never repeat the same correction twice in a
|
|
1684
1986
|
row, or a push-only host can trap the Back button between two URLs.
|
|
1685
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
|
+
|
|
1686
2044
|
### Opening external links
|
|
1687
2045
|
|
|
1688
2046
|
To send the user to an external website (a YouTube video, docs, your homepage),
|
|
@@ -1781,7 +2139,7 @@ await fias.dataStore.query('scores', {
|
|
|
1781
2139
|
});
|
|
1782
2140
|
await fias.dataStore.delete('scores', 'doc-key');
|
|
1783
2141
|
await fias.dataStore.listCollections();
|
|
1784
|
-
await fias.dataStore.deleteCollection('scores');
|
|
2142
|
+
await fias.dataStore.deleteCollection('scores'); // collaborators only; live app: only while empty
|
|
1785
2143
|
```
|
|
1786
2144
|
|
|
1787
2145
|
### Panels (dialogs, reward popups, empty states)
|
|
@@ -1912,6 +2270,8 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1912
2270
|
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
1913
2271
|
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
1914
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) |
|
|
1915
2275
|
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
1916
2276
|
| `sdk` | Yes | SDK version range |
|
|
1917
2277
|
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
@@ -1930,10 +2290,12 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1930
2290
|
|
|
1931
2291
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1932
2292
|
|
|
1933
|
-
**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`
|
|
1934
2294
|
|
|
1935
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`.
|
|
1936
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
|
+
|
|
1937
2299
|
**Defining IAP products (for `useFiasStore`):**
|
|
1938
2300
|
|
|
1939
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.
|
|
@@ -1968,6 +2330,12 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
|
|
|
1968
2330
|
|
|
1969
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.
|
|
1970
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
|
+
|
|
1971
2339
|
## Plugin Constraints
|
|
1972
2340
|
|
|
1973
2341
|
These are hard limits enforced by the platform. Code that violates these will fail review or be blocked at runtime.
|
|
@@ -1988,6 +2356,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1988
2356
|
- **No external scripts or stylesheets** — everything must be bundled
|
|
1989
2357
|
- All platform communication goes through the bridge (SDK hooks)
|
|
1990
2358
|
|
|
2359
|
+
### Limits you can read in code
|
|
2360
|
+
|
|
2361
|
+
The numbers the server enforces for the platform's own capabilities ship as constants, each pinned to the server's value, so check against them instead of copying a number into your code: `X_PUBLISH_LIMITS`, `LINKEDIN_PUBLISH_LIMITS`, `SCHEDULE_LIMITS`, `DATA_STORE_LIMITS`, `DATA_STORE_RATE_LIMITS_PER_MINUTE`, `FIAS_AI_MANIFEST_LIMITS` (and, for AI calls, `ENTITY_INVOCATION_HISTORY_LIMITS` and `ENTITY_INVOCATION_REQUEST_LIMITS`). Stop the person before a call would be refused — an over-long post, a schedule too soon, a document too big — rather than after.
|
|
2362
|
+
|
|
2363
|
+
```tsx
|
|
2364
|
+
import { X_PUBLISH_LIMITS } from '@fias/arche-sdk';
|
|
2365
|
+
|
|
2366
|
+
const tooLong = draft.length > X_PUBLISH_LIMITS.MAX_TEXT_CHARACTERS;
|
|
2367
|
+
```
|
|
2368
|
+
|
|
1991
2369
|
### Size and File Limits
|
|
1992
2370
|
|
|
1993
2371
|
- **Bundle size:** Max 5 MB, measured **uncompressed** — the raw bytes of the final single HTML file with your JS and CSS inlined. Files in `public/` don't count toward it (they have their own caps: 4 MB per file, 120 MB and 600 files total)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.30.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -176,6 +176,20 @@ const user = useFiasUser();
|
|
|
176
176
|
// { userId: string, displayName: string, avatar: string | null }
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### `useFiasHolder()` — The Fias workspace the app was opened in
|
|
180
|
+
|
|
181
|
+
**Permission:** `user:profile:read` or `data:workspace`
|
|
182
|
+
**Returns:** `FiasHolder | null`
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { useFiasHolder } from '@fias/arche-sdk';
|
|
186
|
+
|
|
187
|
+
const holder = useFiasHolder();
|
|
188
|
+
// { type: 'personal' | 'org', role: 'owner' | 'admin' | 'member' | 'viewer' }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The user's own workspace (`personal`, where they are always `owner`) or an organization's (`org`), and the user's role there. It is fixed while the app is open — when the user switches workspace, the platform offers to reopen the app there. It carries no id and no name, and the platform applies the workspace to every call itself. `null` until init and in previews. For data shared by everyone in an org workspace that has approved your app, use `useFiasWorkspaces().openForWorkspace()` — its `displayName` is the workspace's name.
|
|
192
|
+
|
|
179
193
|
### `useFiasStorage()` — Sandboxed file storage
|
|
180
194
|
|
|
181
195
|
**Permission:** `storage:sandbox`
|
|
@@ -201,7 +215,9 @@ await deleteFile('data/old.json');
|
|
|
201
215
|
**Permission:** `data:store`
|
|
202
216
|
**Returns:** `FiasDataStoreApi`
|
|
203
217
|
|
|
204
|
-
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).
|
|
205
221
|
|
|
206
222
|
```tsx
|
|
207
223
|
import { useFiasDataStore } from '@fias/arche-sdk';
|
|
@@ -214,6 +230,9 @@ function MyComponent() {
|
|
|
214
230
|
// 10/minute, so scaffolding several at startup can silently lose the tail.
|
|
215
231
|
await dataStore.createCollection('scores', { userScope: 'user' }); // or 'shared'
|
|
216
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".
|
|
217
236
|
await dataStore.deleteCollection('scores');
|
|
218
237
|
|
|
219
238
|
// Document CRUD
|
|
@@ -254,12 +273,13 @@ const doc = await fias.dataStore.get('scores', 'key');
|
|
|
254
273
|
**Limits:**
|
|
255
274
|
|
|
256
275
|
- 50 collections per plugin
|
|
257
|
-
- 10,000 documents per collection
|
|
276
|
+
- 10,000 documents per collection for each owner — each user (`user` scope), each workspace (`workspace` scope), or your plugin (`shared` scope)
|
|
258
277
|
- 100 KB per document
|
|
259
|
-
-
|
|
278
|
+
- Storage is billed to whoever pays for the data — the user, the workspace's payer, or for `shared` data your plugin's payer — on the platform's database storage meter, and capped at their plan's plugin-data ceiling. A full ceiling answers `STORAGE_LIMIT_REACHED`, with a message saying whose it is; show it to the user. Delete data you no longer need: it costs whoever pays for it.
|
|
260
279
|
- Max 100 results per query, max 10 filters, max field path depth 5
|
|
280
|
+
- The numbers are exported as `DATA_STORE_LIMITS` (document size, documents per owner per collection, operations per batch) and `DATA_STORE_RATE_LIMITS_PER_MINUTE` — check against them before a call instead of copying them
|
|
261
281
|
|
|
262
|
-
**Atomic batch writes:** `batch(operations)` applies up to
|
|
282
|
+
**Atomic batch writes:** `batch(operations)` applies up to 100 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.
|
|
263
283
|
|
|
264
284
|
```tsx
|
|
265
285
|
await dataStore.batch([
|
|
@@ -436,9 +456,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
|
|
|
436
456
|
|
|
437
457
|
**Testing:** in the dev harness's Fias AI window, the read tools see your local `agentAccess` block against your dev data — ask "what scenes do I have?" to see what an agent sees before you publish.
|
|
438
458
|
|
|
439
|
-
**Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
|
|
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
|
|
440
460
|
|
|
441
|
-
**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`.
|
|
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`.
|
|
442
462
|
|
|
443
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`).
|
|
444
464
|
|
|
@@ -497,6 +517,65 @@ function Leaderboard() {
|
|
|
497
517
|
|
|
498
518
|
**Rate limit:** subscribe handshakes 60/min (renewals are automatic and budgeted within this). Up to 32 live subscriptions per tab.
|
|
499
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
|
+
|
|
500
579
|
### `useFiasWorkspaces()` — Team/org workspaces with roles
|
|
501
580
|
|
|
502
581
|
**Permission:** `data:workspace`
|
|
@@ -509,7 +588,7 @@ Model **multi-tenant data with roles**: a _workspace_ is a tenant (team/org spac
|
|
|
509
588
|
- **member** — read/write docs
|
|
510
589
|
- **viewer** — read docs only
|
|
511
590
|
|
|
512
|
-
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.
|
|
513
592
|
|
|
514
593
|
```tsx
|
|
515
594
|
import { useFiasWorkspaces, useFiasDataStore } from '@fias/arche-sdk';
|
|
@@ -539,7 +618,55 @@ function Team() {
|
|
|
539
618
|
}
|
|
540
619
|
```
|
|
541
620
|
|
|
542
|
-
**
|
|
621
|
+
**The Fias workspace's shared space.** Inside a Fias org workspace that has **approved your app** (a workspace owner or admin approves it once, in Workspace Controls), `openForWorkspace()` opens (creating on first use) one shared space whose members ARE that workspace's members, with their workspace roles. Its `displayName` is the workspace's name. Nobody manages its member list: the member methods refuse with `HOLDER_BACKED_WORKSPACE`, and someone who leaves the workspace loses access at once. It answers only while the app is open in that workspace. It rejects with `HOLDER_NOT_ELIGIBLE` in a personal workspace (use `user` scope there), until the workspace approves the app, and for a viewer when the space does not exist yet — tell the user to ask a workspace admin to approve the app. Such a workspace has `holderBacked: true`.
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
const holder = useFiasHolder();
|
|
625
|
+
if (holder?.type === 'org') {
|
|
626
|
+
const shared = await workspaces.openForWorkspace();
|
|
627
|
+
await dataStore.put('posts', 'p-1', { title: 'Launch' }, { workspaceId: shared.workspaceId });
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
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).
|
|
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.
|
|
543
670
|
|
|
544
671
|
### `useEntityInvocation()` — Invoke AI models
|
|
545
672
|
|
|
@@ -1050,6 +1177,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
|
|
|
1050
1177
|
await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
|
|
1051
1178
|
```
|
|
1052
1179
|
|
|
1180
|
+
**In a team workspace.** While the user works in a Fias org workspace whose owner or admin has approved your arche, the documents your arche keeps live in **that workspace's** Vault, counted against its storage, instead of the person's own. There they are the team's: every member using your arche lists and opens them, members and above change them, and viewers only open them (a write rejects with `WORKSPACE_VIEWER_READ_ONLY`). `write()` and `upload()` say where the file went — `space: 'workspace' | 'personal'` (absent from older servers). Without an approval, and in a personal workspace, nothing changes. A document id saved before the approval still opens for the person who saved it.
|
|
1181
|
+
|
|
1053
1182
|
**Cross-arche attachment.** A document can be referenced from another arche's record (Business Contact, Company, Fias 360 Renewal in V-1). The reference points at the document but does NOT expose its contents to the target arche.
|
|
1054
1183
|
|
|
1055
1184
|
```tsx
|
|
@@ -1350,7 +1479,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
|
|
|
1350
1479
|
|
|
1351
1480
|
There are two declaration lanes, one shape:
|
|
1352
1481
|
|
|
1353
|
-
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
|
|
1482
|
+
- **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission. Its limits — 30 actions, 1,000 characters per action description, 4 KB per parameter schema, 32 KB for the whole block, and the rest — are exported as `FIAS_AI_MANIFEST_LIMITS`.
|
|
1354
1483
|
- **Runtime (this hook, trusted arches only):** declare/extend actions from code. The host silently ignores runtime declarations from non-trusted arches — manifest actions still work there, and this hook still handles their dispatches.
|
|
1355
1484
|
|
|
1356
1485
|
```tsx
|
|
@@ -1387,7 +1516,7 @@ Rules that matter:
|
|
|
1387
1516
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
1388
1517
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
1389
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.
|
|
1390
|
-
- **`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.
|
|
1391
1520
|
|
|
1392
1521
|
### `useArcheAssets()` — Contributor-published asset library
|
|
1393
1522
|
|
|
@@ -1529,6 +1658,179 @@ Three things to design around:
|
|
|
1529
1658
|
|
|
1530
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.
|
|
1531
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
|
+
|
|
1768
|
+
### `useFiasConnections()` — Outside accounts the workspace lets your app use
|
|
1769
|
+
|
|
1770
|
+
**Permission:** `connections:use` (human-reviewed at publish)
|
|
1771
|
+
**Returns:** `FiasConnectionsApi`
|
|
1772
|
+
|
|
1773
|
+
```tsx
|
|
1774
|
+
import { useFiasConnections } from '@fias/arche-sdk';
|
|
1775
|
+
|
|
1776
|
+
const connections = useFiasConnections();
|
|
1777
|
+
const accounts = await connections.list();
|
|
1778
|
+
// [{ connectionId, provider: 'x', providerDisplayName: 'X', accountHandle: 'acme', status: 'active', … }]
|
|
1779
|
+
const result = await connections.connect('x'); // the host's connect screen
|
|
1780
|
+
if (!('canceled' in result)) {
|
|
1781
|
+
/* result.connectionId */
|
|
1782
|
+
}
|
|
1783
|
+
await connections.release(connectionId); // give up your own access (owners and admins only)
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
A person connects an outside account (X: `'x'`, LinkedIn: `'linkedin'`) to a Fias workspace — their personal one, or an organization's, where only owners and admins can — and lets your app use it. Your app **never sees a credential**: `connect(provider)` opens a screen the platform draws (it offers the workspace's existing accounts for that provider, or connects a new one in a popup) and resolves with the connection the person chose, or `{ canceled: true }`. `list()` returns only the accounts the workspace the app is open in lets it use; one with `status: 'needs_reconnect'` needs the person to connect it again. People manage connections and every app's access in Workspace Controls. `release` gives up the app's access for the whole workspace, so only the workspace's owners and admins may call it (`ROLE_NOT_ALLOWED` otherwise). Not available in the builder preview or the test channel. **Error codes:** `CONNECTION_NOT_FOUND`, `PERMISSION_DENIED`, `CONNECT_UNAVAILABLE`, `ROLE_NOT_ALLOWED`.
|
|
1787
|
+
|
|
1788
|
+
### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
|
|
1789
|
+
|
|
1790
|
+
**Permission:** `connections:use` (human-reviewed at publish)
|
|
1791
|
+
**Returns:** `FiasXApi` / `FiasLinkedInApi`
|
|
1792
|
+
|
|
1793
|
+
```tsx
|
|
1794
|
+
import { useFiasConnections, useFiasX, useFiasLinkedIn } from '@fias/arche-sdk';
|
|
1795
|
+
|
|
1796
|
+
const [account] = (await useFiasConnections().list()).filter((c) => c.provider === 'x');
|
|
1797
|
+
const x = useFiasX();
|
|
1798
|
+
const { posts } = await x.publishPost({
|
|
1799
|
+
connectionId: account.connectionId,
|
|
1800
|
+
posts: [{ text: 'Launch day', mediaDocumentIds: [imageFileId] }], // up to 10 = a thread; ≤4 images in all
|
|
1801
|
+
});
|
|
1802
|
+
const stats = await x.getAccountStats(account.connectionId);
|
|
1803
|
+
|
|
1804
|
+
const linkedin = useFiasLinkedIn();
|
|
1805
|
+
await linkedin.publishPost({ connectionId, text: 'We are hiring', visibility: 'PUBLIC' });
|
|
1806
|
+
```
|
|
1807
|
+
|
|
1808
|
+
Both act **as** an account the workspace let your app use — get the `connectionId` from `useFiasConnections()`. **Every post is approved by the person first**: `publishPost` puts a confirm sheet in front of them, drawn by Fias from your call (the account, the workspace it belongs to, the exact text and images), and resolves only after they approve — a decline rejects with `USER_DECLINED`. Don't build your own "are you sure?" step; it would ask twice. Viewers of an organization's workspace can't post as its account (`ROLE_NOT_ALLOWED`). Images are Vault document ids — your app's own files (`vault:documents:read`) or documents the person shared with it (`vault:user-documents:read`); bytes never pass through your app.
|
|
1809
|
+
|
|
1810
|
+
X: `publishPost`, `searchUsers`, `getUserPosts`, `getPostMetrics`, `getAccountStats`. LinkedIn: `publishPost` (a `link` preview and images cannot be combined — put the link in the text when you attach images), `searchOrganizations` (to mention one — its `name` must appear in the text); no post-metrics or audience calls yet (LinkedIn serves them only with analytics scopes the connection doesn't ask for). Each call is charged to the user — an X read per post or user it returns, and an X post at X's own price, which is higher for a post with a link (any URL or web address X turns into one) — and a request the provider refuses is not. The post limits (text length, images and their types and sizes, thread length, poll bounds) are exported as `X_PUBLISH_LIMITS` and `LINKEDIN_PUBLISH_LIMITS`: check a draft against them before asking the person to approve it. Re-reading an account? Pass `getUserPosts(connectionId, { userId, sinceId })` with the newest post id you have, and only newer posts are returned and billed. A thread that stops part-way resolves `complete: false` with the posts that went out and `stopped: { atPost, reason }`. Read results carry other people's words — treat them as untrusted text. Not available in the builder preview, the test channel or the dev harness (a post needs a person in the Fias app). The person sees each image, the exact text and the cost before approving; the approval covers those exact image bytes — if your app changes an image file after asking (or before a scheduled post fires), the post is refused `MEDIA_CHANGED` and nothing is sent. **Error codes:** `USER_DECLINED`, `SHEET_BUSY` (another approval sheet was already open — retry once the person has answered it), `CONNECTION_NOT_FOUND`, `CONNECTION_NOT_GRANTED`, `CONNECTION_NEEDS_RECONNECT`, `ROLE_NOT_ALLOWED`, `ACCOUNT_REQUIRED`, `CONFIRMATION_REQUIRED_IN_APP`, `OUTWARD_DAILY_CAP_REACHED`, `PREVIEW_TOO_LARGE`, `MEDIA_NOT_FOUND`, `MEDIA_UNSUPPORTED`, `MEDIA_TOO_LARGE`, `MEDIA_CHANGED`, `INSUFFICIENT_CREDITS` (also for a thread your user's credits cannot cover — refused before the first post), `PROVIDER_ERROR`.
|
|
1811
|
+
|
|
1812
|
+
### `useFiasSchedules()` — Schedule one call for later
|
|
1813
|
+
|
|
1814
|
+
**Permissions:** `schedule:invoke` and `connections:use` (every schedulable call acts as a connected account, and its approval sheet needs `connections:use` — a manifest with `schedule:invoke` alone is refused)
|
|
1815
|
+
**Returns:** `FiasSchedulesApi`
|
|
1816
|
+
|
|
1817
|
+
```tsx
|
|
1818
|
+
import { useFiasSchedules, X_ENTITY_ID } from '@fias/arche-sdk';
|
|
1819
|
+
|
|
1820
|
+
const schedules = useFiasSchedules();
|
|
1821
|
+
const { scheduleId } = await schedules.schedule({
|
|
1822
|
+
entityId: X_ENTITY_ID,
|
|
1823
|
+
verb: 'publish_post',
|
|
1824
|
+
payload: { connectionId, posts: [{ text: 'Launch day' }] }, // what publishPost takes
|
|
1825
|
+
fireAt: tomorrowAtNine, // 5 minutes to 60 days ahead
|
|
1826
|
+
});
|
|
1827
|
+
const mine = await schedules.list(); // newest 20: [{ scheduleId, summary, fireAt, status, refusal?, output? }, …]
|
|
1828
|
+
const waiting = await schedules.list({ scheduleIds: [scheduleId] }); // just these (1–100 ids)
|
|
1829
|
+
await schedules.cancel(scheduleId);
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
Schedules **one** call of a verb that allows it (`publish_post` on X and LinkedIn today). The person approves exactly that call **and its time** in the same confirm sheet as an immediate post ("At 9:00 AM UTC on …: Post to X"); a decline rejects with `USER_DECLINED`. When it is due, every check runs again — the person's access to the workspace, the app, the connection and its grant — and if anything changed — or it was paused past its time, or the app's daily budget is spent — nothing is sent and `list()` shows it as `refused` with the reason. The sheet shows the time in the person's own zone. `list()` returns the newest 20 (`limit` up to 100); pass `scheduleIds` to follow the ones you are waiting on. Its statuses: `scheduled`, `paused`, `sent`, `partial`, `refused`, `failed`, `cancelled`; a `sent` or `partial` one carries `output`, what the call returned (as an immediate call returns it). There is no edit: cancel and schedule again (a new approval). One-shot only; at most 25 pending per person per app, 5 minutes to 60 days ahead (`SCHEDULE_LIMITS`). Charged when it is sent, to the person who scheduled it. Not available in the builder preview, the test channel or the dev harness. **Error codes:** `USER_DECLINED`, `TARGET_NOT_SCHEDULABLE`, `SCHEDULE_LIMIT_REACHED`, `SCHEDULE_NOT_FOUND`, plus the target verb's own.
|
|
1833
|
+
|
|
1532
1834
|
### `useCompanyEmail()` — Send email as the Company the app is used in
|
|
1533
1835
|
|
|
1534
1836
|
**Permission:** `email:company:send`
|
|
@@ -1683,6 +1985,62 @@ A host older than this option ignores it and pushes. If you correct paths
|
|
|
1683
1985
|
automatically as they arrive, never repeat the same correction twice in a
|
|
1684
1986
|
row, or a push-only host can trap the Back button between two URLs.
|
|
1685
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
|
+
|
|
1686
2044
|
### Opening external links
|
|
1687
2045
|
|
|
1688
2046
|
To send the user to an external website (a YouTube video, docs, your homepage),
|
|
@@ -1781,7 +2139,7 @@ await fias.dataStore.query('scores', {
|
|
|
1781
2139
|
});
|
|
1782
2140
|
await fias.dataStore.delete('scores', 'doc-key');
|
|
1783
2141
|
await fias.dataStore.listCollections();
|
|
1784
|
-
await fias.dataStore.deleteCollection('scores');
|
|
2142
|
+
await fias.dataStore.deleteCollection('scores'); // collaborators only; live app: only while empty
|
|
1785
2143
|
```
|
|
1786
2144
|
|
|
1787
2145
|
### Panels (dialogs, reward popups, empty states)
|
|
@@ -1912,6 +2270,8 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1912
2270
|
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
1913
2271
|
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
1914
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) |
|
|
1915
2275
|
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
1916
2276
|
| `sdk` | Yes | SDK version range |
|
|
1917
2277
|
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
@@ -1930,10 +2290,12 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1930
2290
|
|
|
1931
2291
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1932
2292
|
|
|
1933
|
-
**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`
|
|
1934
2294
|
|
|
1935
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`.
|
|
1936
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
|
+
|
|
1937
2299
|
**Defining IAP products (for `useFiasStore`):**
|
|
1938
2300
|
|
|
1939
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.
|
|
@@ -1968,6 +2330,12 @@ Add a `products` array to declare what users can buy inside the arche. Each entr
|
|
|
1968
2330
|
|
|
1969
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.
|
|
1970
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
|
+
|
|
1971
2339
|
## Plugin Constraints
|
|
1972
2340
|
|
|
1973
2341
|
These are hard limits enforced by the platform. Code that violates these will fail review or be blocked at runtime.
|
|
@@ -1988,6 +2356,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1988
2356
|
- **No external scripts or stylesheets** — everything must be bundled
|
|
1989
2357
|
- All platform communication goes through the bridge (SDK hooks)
|
|
1990
2358
|
|
|
2359
|
+
### Limits you can read in code
|
|
2360
|
+
|
|
2361
|
+
The numbers the server enforces for the platform's own capabilities ship as constants, each pinned to the server's value, so check against them instead of copying a number into your code: `X_PUBLISH_LIMITS`, `LINKEDIN_PUBLISH_LIMITS`, `SCHEDULE_LIMITS`, `DATA_STORE_LIMITS`, `DATA_STORE_RATE_LIMITS_PER_MINUTE`, `FIAS_AI_MANIFEST_LIMITS` (and, for AI calls, `ENTITY_INVOCATION_HISTORY_LIMITS` and `ENTITY_INVOCATION_REQUEST_LIMITS`). Stop the person before a call would be refused — an over-long post, a schedule too soon, a document too big — rather than after.
|
|
2362
|
+
|
|
2363
|
+
```tsx
|
|
2364
|
+
import { X_PUBLISH_LIMITS } from '@fias/arche-sdk';
|
|
2365
|
+
|
|
2366
|
+
const tooLong = draft.length > X_PUBLISH_LIMITS.MAX_TEXT_CHARACTERS;
|
|
2367
|
+
```
|
|
2368
|
+
|
|
1991
2369
|
### Size and File Limits
|
|
1992
2370
|
|
|
1993
2371
|
- **Bundle size:** Max 5 MB, measured **uncompressed** — the raw bytes of the final single HTML file with your JS and CSS inlined. Files in `public/` don't count toward it (they have their own caps: 4 MB per file, 120 MB and 600 files total)
|