@fias/create-fias-plugin 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fias/create-fias-plugin",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.28.0 -->
1
+ <!-- fias-sdk-guide-version: 2.29.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`
@@ -254,12 +268,13 @@ const doc = await fias.dataStore.get('scores', 'key');
254
268
  **Limits:**
255
269
 
256
270
  - 50 collections per plugin
257
- - 10,000 documents per collection
271
+ - 10,000 documents per collection for each owner — each user (`user` scope), each workspace (`workspace` scope), or your plugin (`shared` scope)
258
272
  - 100 KB per document
259
- - 100 MB total storage per plugin
273
+ - 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
274
  - Max 100 results per query, max 10 filters, max field path depth 5
275
+ - 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
276
 
262
- **Atomic batch writes:** `batch(operations)` applies up to 25 put/delete ops in a **single transaction** — all-or-nothing. Use it when several writes must stay consistent (e.g. append a ledger entry AND update a running total); a sequence of separate `put`/`delete` calls can leave a half-applied state. A batch may span collections and scopes (pass `workspaceId` per op); any failure (bad key, role denial, quota) rolls the whole batch back.
277
+ **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
278
 
264
279
  ```tsx
265
280
  await dataStore.batch([
@@ -436,9 +451,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
436
451
 
437
452
  **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
453
 
439
- **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
454
+ **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
455
 
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`.
456
+ **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
442
457
 
443
458
  **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
459
 
@@ -539,7 +554,17 @@ function Team() {
539
554
  }
540
555
  ```
541
556
 
542
- **Methods:** `create`, `list`, `get`, `archive` (owner only), `listMembers`, `addMember`, `updateMember`, `removeMember`. `addMember(workspaceId, username, role, expiresAt?)` accepts an optional ISO-8601 `expiresAt` to grant **time-bounded** access (e.g. a contractor seat) — owners cannot expire; an expired member is treated as no longer a member. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
557
+ **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`.
558
+
559
+ ```tsx
560
+ const holder = useFiasHolder();
561
+ if (holder?.type === 'org') {
562
+ const shared = await workspaces.openForWorkspace();
563
+ await dataStore.put('posts', 'p-1', { title: 'Launch' }, { workspaceId: shared.workspaceId });
564
+ }
565
+ ```
566
+
567
+ **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).
543
568
 
544
569
  ### `useEntityInvocation()` — Invoke AI models
545
570
 
@@ -1050,6 +1075,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
1050
1075
  await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
1051
1076
  ```
1052
1077
 
1078
+ **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.
1079
+
1053
1080
  **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
1081
 
1055
1082
  ```tsx
@@ -1350,7 +1377,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
1350
1377
 
1351
1378
  There are two declaration lanes, one shape:
1352
1379
 
1353
- - **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
1380
+ - **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
1381
  - **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
1382
 
1356
1383
  ```tsx
@@ -1529,6 +1556,72 @@ Three things to design around:
1529
1556
 
1530
1557
  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
1558
 
1559
+ ### `useFiasConnections()` — Outside accounts the workspace lets your app use
1560
+
1561
+ **Permission:** `connections:use` (human-reviewed at publish)
1562
+ **Returns:** `FiasConnectionsApi`
1563
+
1564
+ ```tsx
1565
+ import { useFiasConnections } from '@fias/arche-sdk';
1566
+
1567
+ const connections = useFiasConnections();
1568
+ const accounts = await connections.list();
1569
+ // [{ connectionId, provider: 'x', providerDisplayName: 'X', accountHandle: 'acme', status: 'active', … }]
1570
+ const result = await connections.connect('x'); // the host's connect screen
1571
+ if (!('canceled' in result)) {
1572
+ /* result.connectionId */
1573
+ }
1574
+ await connections.release(connectionId); // give up your own access (owners and admins only)
1575
+ ```
1576
+
1577
+ 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`.
1578
+
1579
+ ### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
1580
+
1581
+ **Permission:** `connections:use` (human-reviewed at publish)
1582
+ **Returns:** `FiasXApi` / `FiasLinkedInApi`
1583
+
1584
+ ```tsx
1585
+ import { useFiasConnections, useFiasX, useFiasLinkedIn } from '@fias/arche-sdk';
1586
+
1587
+ const [account] = (await useFiasConnections().list()).filter((c) => c.provider === 'x');
1588
+ const x = useFiasX();
1589
+ const { posts } = await x.publishPost({
1590
+ connectionId: account.connectionId,
1591
+ posts: [{ text: 'Launch day', mediaDocumentIds: [imageFileId] }], // up to 10 = a thread; ≤4 images in all
1592
+ });
1593
+ const stats = await x.getAccountStats(account.connectionId);
1594
+
1595
+ const linkedin = useFiasLinkedIn();
1596
+ await linkedin.publishPost({ connectionId, text: 'We are hiring', visibility: 'PUBLIC' });
1597
+ ```
1598
+
1599
+ 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.
1600
+
1601
+ 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`.
1602
+
1603
+ ### `useFiasSchedules()` — Schedule one call for later
1604
+
1605
+ **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)
1606
+ **Returns:** `FiasSchedulesApi`
1607
+
1608
+ ```tsx
1609
+ import { useFiasSchedules, X_ENTITY_ID } from '@fias/arche-sdk';
1610
+
1611
+ const schedules = useFiasSchedules();
1612
+ const { scheduleId } = await schedules.schedule({
1613
+ entityId: X_ENTITY_ID,
1614
+ verb: 'publish_post',
1615
+ payload: { connectionId, posts: [{ text: 'Launch day' }] }, // what publishPost takes
1616
+ fireAt: tomorrowAtNine, // 5 minutes to 60 days ahead
1617
+ });
1618
+ const mine = await schedules.list(); // newest 20: [{ scheduleId, summary, fireAt, status, refusal?, output? }, …]
1619
+ const waiting = await schedules.list({ scheduleIds: [scheduleId] }); // just these (1–100 ids)
1620
+ await schedules.cancel(scheduleId);
1621
+ ```
1622
+
1623
+ 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.
1624
+
1532
1625
  ### `useCompanyEmail()` — Send email as the Company the app is used in
1533
1626
 
1534
1627
  **Permission:** `email:company:send`
@@ -1988,6 +2081,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
1988
2081
  - **No external scripts or stylesheets** — everything must be bundled
1989
2082
  - All platform communication goes through the bridge (SDK hooks)
1990
2083
 
2084
+ ### Limits you can read in code
2085
+
2086
+ 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.
2087
+
2088
+ ```tsx
2089
+ import { X_PUBLISH_LIMITS } from '@fias/arche-sdk';
2090
+
2091
+ const tooLong = draft.length > X_PUBLISH_LIMITS.MAX_TEXT_CHARACTERS;
2092
+ ```
2093
+
1991
2094
  ### Size and File Limits
1992
2095
 
1993
2096
  - **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.28.0 -->
1
+ <!-- fias-sdk-guide-version: 2.29.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`
@@ -254,12 +268,13 @@ const doc = await fias.dataStore.get('scores', 'key');
254
268
  **Limits:**
255
269
 
256
270
  - 50 collections per plugin
257
- - 10,000 documents per collection
271
+ - 10,000 documents per collection for each owner — each user (`user` scope), each workspace (`workspace` scope), or your plugin (`shared` scope)
258
272
  - 100 KB per document
259
- - 100 MB total storage per plugin
273
+ - 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
274
  - Max 100 results per query, max 10 filters, max field path depth 5
275
+ - 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
276
 
262
- **Atomic batch writes:** `batch(operations)` applies up to 25 put/delete ops in a **single transaction** — all-or-nothing. Use it when several writes must stay consistent (e.g. append a ledger entry AND update a running total); a sequence of separate `put`/`delete` calls can leave a half-applied state. A batch may span collections and scopes (pass `workspaceId` per op); any failure (bad key, role denial, quota) rolls the whole batch back.
277
+ **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
278
 
264
279
  ```tsx
265
280
  await dataStore.batch([
@@ -436,9 +451,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
436
451
 
437
452
  **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
453
 
439
- **Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
454
+ **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
455
 
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`.
456
+ **Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `STORAGE_PAYER_UNAVAILABLE` (nobody is set up to pay for the data — for `shared` data, your plugin's billing; show the message), `RATE_LIMIT`.
442
457
 
443
458
  **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
459
 
@@ -539,7 +554,17 @@ function Team() {
539
554
  }
540
555
  ```
541
556
 
542
- **Methods:** `create`, `list`, `get`, `archive` (owner only), `listMembers`, `addMember`, `updateMember`, `removeMember`. `addMember(workspaceId, username, role, expiresAt?)` accepts an optional ISO-8601 `expiresAt` to grant **time-bounded** access (e.g. a contractor seat) — owners cannot expire; an expired member is treated as no longer a member. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
557
+ **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`.
558
+
559
+ ```tsx
560
+ const holder = useFiasHolder();
561
+ if (holder?.type === 'org') {
562
+ const shared = await workspaces.openForWorkspace();
563
+ await dataStore.put('posts', 'p-1', { title: 'Launch' }, { workspaceId: shared.workspaceId });
564
+ }
565
+ ```
566
+
567
+ **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).
543
568
 
544
569
  ### `useEntityInvocation()` — Invoke AI models
545
570
 
@@ -1050,6 +1075,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
1050
1075
  await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
1051
1076
  ```
1052
1077
 
1078
+ **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.
1079
+
1053
1080
  **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
1081
 
1055
1082
  ```tsx
@@ -1350,7 +1377,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
1350
1377
 
1351
1378
  There are two declaration lanes, one shape:
1352
1379
 
1353
- - **Manifest (static, every plugin):** a `fiasAI: { description, actions: [...] }` block in `fias-plugin.json`. Fixed at publish and reviewed with your submission.
1380
+ - **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
1381
  - **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
1382
 
1356
1383
  ```tsx
@@ -1529,6 +1556,72 @@ Three things to design around:
1529
1556
 
1530
1557
  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
1558
 
1559
+ ### `useFiasConnections()` — Outside accounts the workspace lets your app use
1560
+
1561
+ **Permission:** `connections:use` (human-reviewed at publish)
1562
+ **Returns:** `FiasConnectionsApi`
1563
+
1564
+ ```tsx
1565
+ import { useFiasConnections } from '@fias/arche-sdk';
1566
+
1567
+ const connections = useFiasConnections();
1568
+ const accounts = await connections.list();
1569
+ // [{ connectionId, provider: 'x', providerDisplayName: 'X', accountHandle: 'acme', status: 'active', … }]
1570
+ const result = await connections.connect('x'); // the host's connect screen
1571
+ if (!('canceled' in result)) {
1572
+ /* result.connectionId */
1573
+ }
1574
+ await connections.release(connectionId); // give up your own access (owners and admins only)
1575
+ ```
1576
+
1577
+ 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`.
1578
+
1579
+ ### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
1580
+
1581
+ **Permission:** `connections:use` (human-reviewed at publish)
1582
+ **Returns:** `FiasXApi` / `FiasLinkedInApi`
1583
+
1584
+ ```tsx
1585
+ import { useFiasConnections, useFiasX, useFiasLinkedIn } from '@fias/arche-sdk';
1586
+
1587
+ const [account] = (await useFiasConnections().list()).filter((c) => c.provider === 'x');
1588
+ const x = useFiasX();
1589
+ const { posts } = await x.publishPost({
1590
+ connectionId: account.connectionId,
1591
+ posts: [{ text: 'Launch day', mediaDocumentIds: [imageFileId] }], // up to 10 = a thread; ≤4 images in all
1592
+ });
1593
+ const stats = await x.getAccountStats(account.connectionId);
1594
+
1595
+ const linkedin = useFiasLinkedIn();
1596
+ await linkedin.publishPost({ connectionId, text: 'We are hiring', visibility: 'PUBLIC' });
1597
+ ```
1598
+
1599
+ 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.
1600
+
1601
+ 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`.
1602
+
1603
+ ### `useFiasSchedules()` — Schedule one call for later
1604
+
1605
+ **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)
1606
+ **Returns:** `FiasSchedulesApi`
1607
+
1608
+ ```tsx
1609
+ import { useFiasSchedules, X_ENTITY_ID } from '@fias/arche-sdk';
1610
+
1611
+ const schedules = useFiasSchedules();
1612
+ const { scheduleId } = await schedules.schedule({
1613
+ entityId: X_ENTITY_ID,
1614
+ verb: 'publish_post',
1615
+ payload: { connectionId, posts: [{ text: 'Launch day' }] }, // what publishPost takes
1616
+ fireAt: tomorrowAtNine, // 5 minutes to 60 days ahead
1617
+ });
1618
+ const mine = await schedules.list(); // newest 20: [{ scheduleId, summary, fireAt, status, refusal?, output? }, …]
1619
+ const waiting = await schedules.list({ scheduleIds: [scheduleId] }); // just these (1–100 ids)
1620
+ await schedules.cancel(scheduleId);
1621
+ ```
1622
+
1623
+ 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.
1624
+
1532
1625
  ### `useCompanyEmail()` — Send email as the Company the app is used in
1533
1626
 
1534
1627
  **Permission:** `email:company:send`
@@ -1988,6 +2081,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
1988
2081
  - **No external scripts or stylesheets** — everything must be bundled
1989
2082
  - All platform communication goes through the bridge (SDK hooks)
1990
2083
 
2084
+ ### Limits you can read in code
2085
+
2086
+ 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.
2087
+
2088
+ ```tsx
2089
+ import { X_PUBLISH_LIMITS } from '@fias/arche-sdk';
2090
+
2091
+ const tooLong = draft.length > X_PUBLISH_LIMITS.MAX_TEXT_CHARACTERS;
2092
+ ```
2093
+
1991
2094
  ### Size and File Limits
1992
2095
 
1993
2096
  - **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)