@fias/create-fias-plugin 1.19.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 +1 -1
- package/templates/default/AGENTS.md +252 -42
- package/templates/default/CLAUDE.md +252 -42
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.29.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -21,24 +21,27 @@ vite.config.ts # Vite dev server config (port 3100)
|
|
|
21
21
|
|
|
22
22
|
Decide by who writes the data and when, before you write the code: moving data to a different home after you publish means a migration.
|
|
23
23
|
|
|
24
|
-
| What it is | Where it goes
|
|
25
|
-
| ------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| Your code and small constants (config, a short word list) | The bundle (`src/`)
|
|
27
|
-
| A few static images, fonts or
|
|
28
|
-
| Content you write and ship with each version: lessons, articles, levels, reference data, large image or audio sets, fonts | A content pack: `content/` + `useFiasContent()`
|
|
29
|
-
| Images you want to change without republishing code (logos, maps, photos with alt text) | The asset library: `useArcheAssets()` + `fias-dev assets`
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
24
|
+
| What it is | Where it goes | What you get |
|
|
25
|
+
| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| Your code and small constants (config, a short word list) | The bundle (`src/`) | One file, max 5 MB uncompressed. Every user downloads all of it each time the plugin opens. |
|
|
27
|
+
| A few static images, fonts or music tracks your UI names directly | `public/`, referenced as literal `'/name.png'` strings | Served from a CDN. Binary only (png, jpg, webp, gif, woff, woff2, ogg), top-level files only, 4 MB each, 600 files and 120 MB in total. |
|
|
28
|
+
| Content you write and ship with each version: lessons, articles, levels, reference data, large image or audio sets, fonts | A content pack: `content/` + `useFiasContent()` | Not bundled, so users download only what they open. Reviewed on every publish and read-only at runtime. You pay its storage and bandwidth; reads are free to users. |
|
|
29
|
+
| Images you want to change without republishing code (logos, maps, photos with alt text) | The asset library: `useArcheAssets()` + `fias-dev assets` | Managed from the CLI and refreshed without a code publish. |
|
|
30
|
+
| Sound effects that must play on cue (hits, jumps, UI clicks) | The bundle: base64 in `src/`, decoded once at startup with `decodeAudioData` | Ready the moment the plugin opens; nothing to download per session. Keep the set small (a few hundred KB). Not `public/`: a `public/` sound can only play through an `<audio>` element, which lags its cue (badly on iOS). |
|
|
31
|
+
| The same sound effects shared by several arches (optional) | A workspace asset library: `useArcheAssetBytes().getMany()` | One copy for every linked arche; a new version reaches them with a pin refresh, no code republish. Downloaded each session — wait for it before the first cue. MP3 or WAV, ≤ 1 MB and 30 s each. |
|
|
32
|
+
| Records your app writes while it runs: progress, things users create, shared catalogues, leaderboards | `useFiasDataStore()` collections | Queryable, `user` or `shared` scope, write policies, optional semantic search. |
|
|
33
|
+
| Files only your app uses, for one user: drafts, caches, working data | `useFiasStorage()` | Free-form paths, private to the plugin and the user. Not shown in their Vault. |
|
|
34
|
+
| A small piece of UI state that should survive a reload (a setting, the last tab) | `usePersistentState()` | Debounced writes to `useFiasStorage()`. |
|
|
35
|
+
| Images one user shares with the app's other users | `useCommunityAssets()` | Moderated before anyone else sees them. |
|
|
36
|
+
| Documents the user should keep: files your app makes for them, or files they already have | `useVaultDocuments()` / `useVaultUserDocuments()` | In the user's My Data, where they outlive your plugin. Reading files they already had needs their consent. |
|
|
35
37
|
|
|
36
38
|
**Common mistakes:**
|
|
37
39
|
|
|
38
|
-
- **Inlining content in `src/`** — a JSON file of lessons, a level pack, base64 images. It counts against the 5 MB cap, and every user downloads all of it to use a little. Put it in `content/`.
|
|
40
|
+
- **Inlining content in `src/`** — a JSON file of lessons, a level pack, base64 images. It counts against the 5 MB cap, and every user downloads all of it to use a little. Put it in `content/`. (A small set of sound effects is the exception — see the table.)
|
|
39
41
|
- **Seeding content you wrote into the DataStore from the plugin.** Every user's session tries to write it, the first writer of a `shared` document owns it, and nothing reviews it. Ship it in `content/`. If it must also be searchable, keep a searchable copy in a `shared` collection, but display the pack's version.
|
|
40
42
|
- **Using the DataStore or `useFiasStorage()` for data that is the same for every user.** That is content.
|
|
41
43
|
- **Building `public/` paths at runtime**, e.g. `` `/music-${name}.ogg` ``. Only literal paths are rewritten to the CDN when you publish, so a path built in code 404s in production. Use a lookup table of literal strings, or move the files to `content/` and read them by name.
|
|
44
|
+
- **Putting anything confidential in `public/`.** Files in `public/` are always public: anyone with a file's link can fetch it, even when your arche is restricted to chosen people. Put confidential material in a content pack (`content/`) — its access follows who may use the arche.
|
|
42
45
|
|
|
43
46
|
## SDK API Reference
|
|
44
47
|
|
|
@@ -173,6 +176,20 @@ const user = useFiasUser();
|
|
|
173
176
|
// { userId: string, displayName: string, avatar: string | null }
|
|
174
177
|
```
|
|
175
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
|
+
|
|
176
193
|
### `useFiasStorage()` — Sandboxed file storage
|
|
177
194
|
|
|
178
195
|
**Permission:** `storage:sandbox`
|
|
@@ -251,12 +268,13 @@ const doc = await fias.dataStore.get('scores', 'key');
|
|
|
251
268
|
**Limits:**
|
|
252
269
|
|
|
253
270
|
- 50 collections per plugin
|
|
254
|
-
- 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)
|
|
255
272
|
- 100 KB per document
|
|
256
|
-
-
|
|
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.
|
|
257
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
|
|
258
276
|
|
|
259
|
-
**Atomic batch writes:** `batch(operations)` applies up to
|
|
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.
|
|
260
278
|
|
|
261
279
|
```tsx
|
|
262
280
|
await dataStore.batch([
|
|
@@ -283,6 +301,41 @@ Why this and not `createCollection` at startup: the runtime op is capped at **10
|
|
|
283
301
|
|
|
284
302
|
Re-publishing is safe: existing collections are left alone, a stricter role floor is applied, a looser one is refused, and a collection you stop declaring is kept (never deleted — it may hold user data). Keep using `createCollection` for collections whose names you only know at runtime.
|
|
285
303
|
|
|
304
|
+
### Integrity rules (`constraints`)
|
|
305
|
+
|
|
306
|
+
A collection can carry rules the platform checks on **every** write, so a buggy or tampered client cannot break them. Declare them as `constraints` on the collection in `fias-plugin.json` (or in `createCollection`'s options):
|
|
307
|
+
|
|
308
|
+
```jsonc
|
|
309
|
+
"collections": [
|
|
310
|
+
{ "name": "stakeholders", "scope": "workspace" },
|
|
311
|
+
{ "name": "transactions", "scope": "workspace", "constraints": { "appendOnly": true } },
|
|
312
|
+
{
|
|
313
|
+
"name": "securities",
|
|
314
|
+
"scope": "workspace",
|
|
315
|
+
"constraints": {
|
|
316
|
+
"nonNegative": ["quantityOutstanding"],
|
|
317
|
+
"fieldBounds": [{ "field": "quantityIssued", "op": "lte", "valueField": "quantityAuthorized" }],
|
|
318
|
+
"immutableFields": ["issuedAt"],
|
|
319
|
+
"references": [{ "field": "stakeholderId", "collection": "stakeholders" }],
|
|
320
|
+
"ledgerSums": [{
|
|
321
|
+
"field": "quantityOutstanding",
|
|
322
|
+
"ledger": "transactions",
|
|
323
|
+
"terms": [{ "key": "securityId", "amount": "quantityDelta" }]
|
|
324
|
+
}]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- **`appendOnly`**: the collection is a ledger. Documents can be added, never overwritten or deleted.
|
|
331
|
+
- **`nonNegative`**: these numeric fields must be ≥ 0 when present.
|
|
332
|
+
- **`fieldBounds`**: `field <op> bound`, where `op` is `eq` / `ne` / `lt` / `lte` / `gt` / `gte` and the bound is a constant `value` or a sibling field `valueField` (exactly one).
|
|
333
|
+
- **`immutableFields`**: set once; an overwrite must carry the same value.
|
|
334
|
+
- **`references`**: `field` holds the key of a document that must exist in `collection`, in the same scope (same workspace, same user, or shared). An absent or null field is allowed. The writer must be able to read the target collection. Deleting a document that something still references is refused; there is no cascade.
|
|
335
|
+
- **`ledgerSums`**: a counter `field` may change only by exactly the sum of the rows appended to `ledger` in the **same write**. A ledger row counts toward the counter whose key its term's `key` field holds, adding its `amount` (subtracted with `negate: true`, filtered with `when: { field, in: [...] }`). A counter that moves without rows, and rows whose counter did not move by their sum, are both refused. The ledger must be `appendOnly` and have the counter's scope; an absent counter counts as 0. At most 4 terms per rule.
|
|
336
|
+
|
|
337
|
+
Rules that relate two documents (`references`, `ledgerSums`) are checked at the **end** of the write, so write the ledger row and the counter, or the target and the document pointing at it, together in one `batch` in any order. A violation rejects the whole write with `CONSTRAINT_VIOLATION`. Rules are **tighten-only**: you can add rules (at most 10 per kind), never remove or change one.
|
|
338
|
+
|
|
286
339
|
### Letting agents use your data (`agentAccess`)
|
|
287
340
|
|
|
288
341
|
**Permission:** `data:agents` (requires `data:store`)
|
|
@@ -398,9 +451,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
|
|
|
398
451
|
|
|
399
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.
|
|
400
453
|
|
|
401
|
-
**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
|
|
402
455
|
|
|
403
|
-
**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`.
|
|
404
457
|
|
|
405
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`).
|
|
406
459
|
|
|
@@ -501,7 +554,17 @@ function Team() {
|
|
|
501
554
|
}
|
|
502
555
|
```
|
|
503
556
|
|
|
504
|
-
**
|
|
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).
|
|
505
568
|
|
|
506
569
|
### `useEntityInvocation()` — Invoke AI models
|
|
507
570
|
|
|
@@ -1012,6 +1075,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
|
|
|
1012
1075
|
await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
|
|
1013
1076
|
```
|
|
1014
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
|
+
|
|
1015
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.
|
|
1016
1081
|
|
|
1017
1082
|
```tsx
|
|
@@ -1302,6 +1367,7 @@ useArcheHandoff((payload) => {
|
|
|
1302
1367
|
- **You are the type gate.** If you cannot open this file, say so plainly in your UI. Half-opening it is worse than refusing.
|
|
1303
1368
|
- Fires **once per handoff**, and one that arrives before your hook mounts is buffered and delivered on registration — so the first handoff of a session is never lost.
|
|
1304
1369
|
- Nothing hands documents to a preview, so the handler only fires in the published plugin.
|
|
1370
|
+
- **Being offered at all takes two steps.** Declare what you open in `fias-plugin.json` — `"fileIntake": { "kinds": ["document"], "types": [".docx"] }` — and ask Fias to approve it. `kinds` are `image`, `pdf`, `document`, `spreadsheet`, `presentation`, `audio`, `video`, `archive`, `code`, `other`; `types` (optional, lowercase extensions or MIME types) narrows them — list them, or your app is offered for every file of the kind (`document` includes `.txt`, `.md` and `.html`). Your own format goes under `other` with its extension in `types` (`"kinds": ["document", "other"], "types": [".docx", ".myformat"]`). The build fails if you declare `fileIntake` but never call `useArcheHandoff`. Once approved, your app appears under "Open in…" in My Data (and is the double-click default for documents, spreadsheets and presentations), and Fias AI can open files in it — when the user didn't pick your app themselves, Fias asks them first. A kind or type you add later stays off until it is approved too.
|
|
1305
1371
|
|
|
1306
1372
|
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
1307
1373
|
|
|
@@ -1311,7 +1377,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
|
|
|
1311
1377
|
|
|
1312
1378
|
There are two declaration lanes, one shape:
|
|
1313
1379
|
|
|
1314
|
-
- **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`.
|
|
1315
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.
|
|
1316
1382
|
|
|
1317
1383
|
```tsx
|
|
@@ -1355,7 +1421,7 @@ Rules that matter:
|
|
|
1355
1421
|
**Permission:** `assets:read`
|
|
1356
1422
|
**Returns:** `ArcheAssetsApi`
|
|
1357
1423
|
|
|
1358
|
-
A frozen set of images the arche's contributor uploaded and pinned to the published version
|
|
1424
|
+
A frozen set of images — and short sound effects — the arche's contributor uploaded and pinned to the published version: logos, brand images, hole maps, reference photos, UI sounds. Each entry has an immutable `assetId` and a short-lived `signedUrl` (~5 min) suitable for `<img src>` / `<audio src>`. `list` and `index` return images only unless you pass `kinds: ['audio']` (or `['image', 'audio']`); with `kinds` the entries are typed `ArcheLibraryAsset`, so narrow on `contentType` before using an image's size (a sound's `width`/`height` are `null`). Keeping sounds here is optional (see the table at the top for where sound effects belong), and music is not accepted.
|
|
1359
1425
|
|
|
1360
1426
|
```tsx
|
|
1361
1427
|
import { useArcheAssets } from '@fias/arche-sdk';
|
|
@@ -1372,6 +1438,37 @@ const fresh = await getUrl(assetId);
|
|
|
1372
1438
|
|
|
1373
1439
|
Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
|
|
1374
1440
|
|
|
1441
|
+
### `useArcheAssetBytes()` — Asset bytes for Web Audio and canvas
|
|
1442
|
+
|
|
1443
|
+
**Permissions:** `entities:client_invoke` and `assets:read`
|
|
1444
|
+
**Returns:** `ArcheAssetBytesApi`
|
|
1445
|
+
|
|
1446
|
+
For sounds (or images) kept in the asset library. The plugin sandbox cannot fetch, so a signed URL is only good for `<img>`/`<audio>`. When you need the bytes — `AudioContext.decodeAudioData` for overlapping, precisely timed sound effects, or canvas pixel work — the host reads them for you. Load a set of sounds with **one `getMany` call** and wait for it before the first cue:
|
|
1447
|
+
|
|
1448
|
+
```tsx
|
|
1449
|
+
import { useArcheAssets, useArcheAssetBytes } from '@fias/arche-sdk';
|
|
1450
|
+
|
|
1451
|
+
const { list } = useArcheAssets();
|
|
1452
|
+
const { getMany } = useArcheAssetBytes();
|
|
1453
|
+
|
|
1454
|
+
const ctx = new AudioContext(); // resume() it on the first user gesture
|
|
1455
|
+
const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
|
|
1456
|
+
const loaded = await getMany(entries); // { [assetId]: { contentType, bytes, sizeBytes } | ArcheAssetError }
|
|
1457
|
+
const buffers = new Map<string, AudioBuffer>();
|
|
1458
|
+
for (const sound of entries) {
|
|
1459
|
+
const result = loaded[sound.assetId];
|
|
1460
|
+
if (result instanceof Error) continue; // result.code says why
|
|
1461
|
+
buffers.set(sound.name ?? sound.assetId, await ctx.decodeAudioData(result.bytes));
|
|
1462
|
+
}
|
|
1463
|
+
// From here a sound plays on cue: const src = ctx.createBufferSource(); src.buffer = …; src.connect(ctx.destination); src.start();
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
- **Never one call per sound.** The host blocks any capability called more than 50 times in 5 seconds, for 10 seconds — the 51st sound and every one after it would stay silent. `getMany` reads up to 100 per call. `getBytes(entry)` reads one; `getBytes` calls made together (a `Promise.all`) are batched the same way, but an `await` loop over many sounds is not.
|
|
1467
|
+
- **Pass entries straight from `list()`.** Their signed URLs (5 minutes) are reused, so each read is one round trip; a lapsed URL is replaced for a few entries, past that `ASSET_URL_EXPIRED` asks you to list again.
|
|
1468
|
+
- **Read once per session and keep the decoded buffers.** Every read is one billed asset load, and signed URLs change each session, so nothing carries over from the last one.
|
|
1469
|
+
- `getMany` resolves to each asset's bytes or its `ArcheAssetError` (`code`: `ASSET_URL_EXPIRED`, `ASSET_UNAVAILABLE`, `ASSET_TOO_LARGE`, …) and rejects only when the read is refused as a whole (`PERMISSION_DENIED`, `RATE_LIMIT`, `ASSETS_UNAVAILABLE_IN_PREVIEW`). Not available in the builder preview.
|
|
1470
|
+
- Library sounds are MP3 or WAV, which `decodeAudioData` accepts in every browser, iOS Safari included.
|
|
1471
|
+
|
|
1375
1472
|
### `useFiasContent()` — This plugin's content pack
|
|
1376
1473
|
|
|
1377
1474
|
**Permission:** `entities:client_invoke`
|
|
@@ -1398,6 +1495,33 @@ const { url } = await content.getUrl('audio/theme.ogg'); // signed URL, for long
|
|
|
1398
1495
|
- There is no published pack in the Arche Builder preview, so calls there fail with `CONTENT_UNAVAILABLE_IN_PREVIEW`; the dev harness serves your local `content/` directory.
|
|
1399
1496
|
- Every publish reviews all of the content again. Reads are free for your users; the pack's storage and bandwidth are billed to you, the contributor.
|
|
1400
1497
|
|
|
1498
|
+
### `useLocalStore()` — Working data kept on the user's device
|
|
1499
|
+
|
|
1500
|
+
**Permission:** `storage:local`
|
|
1501
|
+
**Returns:** `FiasLocalStoreApi`
|
|
1502
|
+
|
|
1503
|
+
A recovery journal, drafts, a cache: anything you would write on every change so a refresh, a crash or a closed tab loses nothing. Writes take milliseconds and never touch the network — the host page keeps the data in its own browser storage, in a space only your plugin can read, for the signed-in user in the workspace they are working in. Personal and each org workspace keep separate data: when the user switches workspace, your plugin sees that workspace's store, so nothing kept while working in one workspace leaks into another. It is cleared when the user signs out, or when someone else signs in on the device; an expired session keeps it, so the same user picks up where they left off.
|
|
1504
|
+
|
|
1505
|
+
```tsx
|
|
1506
|
+
import { useLocalStore, FiasLocalStoreError } from '@fias/arche-sdk';
|
|
1507
|
+
|
|
1508
|
+
const store = useLocalStore();
|
|
1509
|
+
|
|
1510
|
+
await store.put(`session/${docId}`, snapshot); // JSON, a string, or bytes — on every change
|
|
1511
|
+
const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load; null if absent
|
|
1512
|
+
const open = await store.list('session/'); // [{ key, sizeBytes, updatedAt }], sorted by key
|
|
1513
|
+
const restored = await store.getMany(open.map((e) => e.key)); // ≤ 100 entries in one call
|
|
1514
|
+
await store.delete(`session/${docId}`); // on close
|
|
1515
|
+
const { usedBytes, budgetBytes } = await store.estimate();
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
- This is **not** where the user's work is kept. It is best-effort device storage: a browser may clear site data, and nothing here reaches another device. Journal here; save the user's files through the Vault (`useVaultUserDocuments().saveContent`, `upload`, or the Save dialog).
|
|
1519
|
+
- Values come back as the kind they were put: a string is text, an `ArrayBuffer` / `Uint8Array` is bytes (read back as an `ArrayBuffer`), anything else is plain JSON (no `Date`, `Map`, `Blob` or other typed arrays — those are refused). `getText`, `getBytes` and `getJson` check the kind and throw `LOCAL_STORE_WRONG_KIND` on a mismatch; `getJson` also parses a text entry, so `put(key, JSON.stringify(x))` reads back like `put(key, x)`. `get` returns the raw entry; `getMany(keys)` returns up to 100 raw entries in one call, in the order asked, `null` for a missing key — restore a journal that way rather than with a `get` per key, which the bridge's runaway-loop guard (50 calls in 5 seconds) would stop.
|
|
1520
|
+
- Put a large snapshot (an image, a big document) as **bytes**. A JSON value is validated and serialized on the host page on every write, which takes time in proportion to its size; bytes are only copied.
|
|
1521
|
+
- Limits: a key ≤ 256 characters; one value ≤ 64 MB, and one `getMany` returns ≤ 64 MB in all (`LOCAL_STORE_VALUE_TOO_LARGE` — read fewer keys at a time); your whole store for this user in one workspace ≤ 256 MB (or a quarter of the browser's quota, if smaller) and ≤ 10,000 entries. Each entry is charged its value, its key and a small fixed overhead. Over a limit, `put` throws `LOCAL_STORE_QUOTA_EXCEEDED` instead of evicting anything — decide what to keep with `estimate()` and `list()`.
|
|
1522
|
+
- Failures throw `FiasLocalStoreError` with a `code`: `LOCAL_STORE_UNAVAILABLE` (the browser refused storage — degrade to "nothing persisted"), `LOCAL_STORE_KEY_INVALID`, `LOCAL_STORE_VALUE_TOO_LARGE`, `LOCAL_STORE_QUOTA_EXCEEDED`, `LOCAL_STORE_WRONG_KIND`, `INVALID_INPUT`, `PERMISSION_DENIED`, `RATE_LIMIT`.
|
|
1523
|
+
- Works in the dev harness (mock and live modes) and in the Arche Builder preview, each under its own namespace.
|
|
1524
|
+
|
|
1401
1525
|
### `useCommunityAssets()` — User-published images, visible to this arche's users
|
|
1402
1526
|
|
|
1403
1527
|
**Permissions:** `assets:community:read` (browse/view) · `assets:community:publish` (publish/unpublish/listMine)
|
|
@@ -1432,6 +1556,72 @@ Three things to design around:
|
|
|
1432
1556
|
|
|
1433
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.
|
|
1434
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
|
+
|
|
1435
1625
|
### `useCompanyEmail()` — Send email as the Company the app is used in
|
|
1436
1626
|
|
|
1437
1627
|
**Permission:** `email:company:send`
|
|
@@ -1535,8 +1725,17 @@ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
|
|
|
1535
1725
|
// `arche_…` arche is rejected, as is a malformed one (you get an error, never
|
|
1536
1726
|
// a silent landing on the home screen).
|
|
1537
1727
|
openArche('arc_0123456789abcdef0123456789abcdef', { path: 'map' });
|
|
1728
|
+
|
|
1729
|
+
// Open a chat channel in the Chat arche.
|
|
1730
|
+
openArche('arche_chat', { payload: { kind: 'open_channel', channelId } });
|
|
1538
1731
|
```
|
|
1539
1732
|
|
|
1733
|
+
**Opening a chat channel.** The `open_channel` payload is a pointer, never
|
|
1734
|
+
access: Chat re-reads the channel under the user's own membership, and a
|
|
1735
|
+
non-member lands on the Chat home with a notice instead. Make the user a member
|
|
1736
|
+
first (LitCraft: `join_guild_chat` on the LitCraft Player entity). Code that
|
|
1737
|
+
switches exhaustively on `ArcheHandoffPayloadInput`'s `kind` needs the new case.
|
|
1738
|
+
|
|
1540
1739
|
**Receiving a deep link.** There is nothing extra to opt into: `currentPath` is
|
|
1541
1740
|
the host's path expressed in YOUR arche's route space — the same space
|
|
1542
1741
|
`navigateTo` accepts — so an arche that wants to be a deep-link target just
|
|
@@ -1790,26 +1989,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1790
1989
|
|
|
1791
1990
|
**Fields:**
|
|
1792
1991
|
|
|
1793
|
-
| Field | Req | Description
|
|
1794
|
-
| --------------------- | ---- |
|
|
1795
|
-
| `name` | Yes | Plugin identifier (lowercase, hyphens)
|
|
1796
|
-
| `version` | Yes | Semver (e.g., `"1.0.0"`)
|
|
1797
|
-
| `description` | Yes | Short marketplace description
|
|
1798
|
-
| `expandedDescription` | No | Long-form marketplace copy
|
|
1799
|
-
| `main` | Yes | Entry point source file
|
|
1800
|
-
| `archeType` | Yes | `"tool"` or `"site"`
|
|
1801
|
-
| `categorySlug` | No | Marketplace category for discovery
|
|
1802
|
-
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local
|
|
1803
|
-
| `tags` | No | Discovery tags
|
|
1804
|
-
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products`
|
|
1805
|
-
| `permissions` | Yes | Array of permission scopes (see below)
|
|
1806
|
-
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below)
|
|
1807
|
-
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below)
|
|
1808
|
-
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"`
|
|
1809
|
-
| `isListed` | No | Default `true`. Visible in marketplace store
|
|
1810
|
-
| `sdk` | Yes | SDK version range
|
|
1811
|
-
| `dependencies` | No | npm packages with **exact** versions (max 20)
|
|
1812
|
-
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit
|
|
1992
|
+
| Field | Req | Description |
|
|
1993
|
+
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1994
|
+
| `name` | Yes | Plugin identifier (lowercase, hyphens) |
|
|
1995
|
+
| `version` | Yes | Semver (e.g., `"1.0.0"`) |
|
|
1996
|
+
| `description` | Yes | Short marketplace description |
|
|
1997
|
+
| `expandedDescription` | No | Long-form marketplace copy |
|
|
1998
|
+
| `main` | Yes | Entry point source file |
|
|
1999
|
+
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
2000
|
+
| `categorySlug` | No | Marketplace category for discovery |
|
|
2001
|
+
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on every publish (local, staging and prod) and rehosted as your marketplace/store icon. PNG, JPEG, WebP or SVG, ≤5 MB; raster images must also be ≤1024×1024 px (SVG has no pixel limit). An invalid icon rejects the submission: a first-publish listing fee is refunded, but the AI review that already ran is still billed. To change it, replace the file (or the path) and republish; removing the field keeps the current icon. |
|
|
2002
|
+
| `tags` | No | Discovery tags |
|
|
2003
|
+
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
2004
|
+
| `permissions` | Yes | Array of permission scopes (see below) |
|
|
2005
|
+
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
2006
|
+
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
2007
|
+
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
|
|
2008
|
+
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
2009
|
+
| `sdk` | Yes | SDK version range |
|
|
2010
|
+
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
2011
|
+
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
1813
2012
|
|
|
1814
2013
|
**Pricing detail:**
|
|
1815
2014
|
|
|
@@ -1824,7 +2023,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1824
2023
|
|
|
1825
2024
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1826
2025
|
|
|
1827
|
-
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `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`
|
|
2026
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
|
|
1828
2027
|
|
|
1829
2028
|
**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`.
|
|
1830
2029
|
|
|
@@ -1882,6 +2081,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1882
2081
|
- **No external scripts or stylesheets** — everything must be bundled
|
|
1883
2082
|
- All platform communication goes through the bridge (SDK hooks)
|
|
1884
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
|
+
|
|
1885
2094
|
### Size and File Limits
|
|
1886
2095
|
|
|
1887
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)
|
|
@@ -1896,6 +2105,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1896
2105
|
- `storage_read`: 300/minute
|
|
1897
2106
|
- `storage_list`, `storage_delete`: 60/minute
|
|
1898
2107
|
- Content reads (`useFiasContent()`): 120/minute — read related files together with `getMany` (up to 100 per call)
|
|
2108
|
+
- Asset byte reads (`useArcheAssetBytes()`): 120/minute — load a set of sounds with one `getMany` (up to 100 per call)
|
|
1899
2109
|
|
|
1900
2110
|
A separate **runaway-loop detector** fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) …` — the path tells you where to debounce at the source.
|
|
1901
2111
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.29.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -21,24 +21,27 @@ vite.config.ts # Vite dev server config (port 3100)
|
|
|
21
21
|
|
|
22
22
|
Decide by who writes the data and when, before you write the code: moving data to a different home after you publish means a migration.
|
|
23
23
|
|
|
24
|
-
| What it is | Where it goes
|
|
25
|
-
| ------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| Your code and small constants (config, a short word list) | The bundle (`src/`)
|
|
27
|
-
| A few static images, fonts or
|
|
28
|
-
| Content you write and ship with each version: lessons, articles, levels, reference data, large image or audio sets, fonts | A content pack: `content/` + `useFiasContent()`
|
|
29
|
-
| Images you want to change without republishing code (logos, maps, photos with alt text) | The asset library: `useArcheAssets()` + `fias-dev assets`
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
24
|
+
| What it is | Where it goes | What you get |
|
|
25
|
+
| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| Your code and small constants (config, a short word list) | The bundle (`src/`) | One file, max 5 MB uncompressed. Every user downloads all of it each time the plugin opens. |
|
|
27
|
+
| A few static images, fonts or music tracks your UI names directly | `public/`, referenced as literal `'/name.png'` strings | Served from a CDN. Binary only (png, jpg, webp, gif, woff, woff2, ogg), top-level files only, 4 MB each, 600 files and 120 MB in total. |
|
|
28
|
+
| Content you write and ship with each version: lessons, articles, levels, reference data, large image or audio sets, fonts | A content pack: `content/` + `useFiasContent()` | Not bundled, so users download only what they open. Reviewed on every publish and read-only at runtime. You pay its storage and bandwidth; reads are free to users. |
|
|
29
|
+
| Images you want to change without republishing code (logos, maps, photos with alt text) | The asset library: `useArcheAssets()` + `fias-dev assets` | Managed from the CLI and refreshed without a code publish. |
|
|
30
|
+
| Sound effects that must play on cue (hits, jumps, UI clicks) | The bundle: base64 in `src/`, decoded once at startup with `decodeAudioData` | Ready the moment the plugin opens; nothing to download per session. Keep the set small (a few hundred KB). Not `public/`: a `public/` sound can only play through an `<audio>` element, which lags its cue (badly on iOS). |
|
|
31
|
+
| The same sound effects shared by several arches (optional) | A workspace asset library: `useArcheAssetBytes().getMany()` | One copy for every linked arche; a new version reaches them with a pin refresh, no code republish. Downloaded each session — wait for it before the first cue. MP3 or WAV, ≤ 1 MB and 30 s each. |
|
|
32
|
+
| Records your app writes while it runs: progress, things users create, shared catalogues, leaderboards | `useFiasDataStore()` collections | Queryable, `user` or `shared` scope, write policies, optional semantic search. |
|
|
33
|
+
| Files only your app uses, for one user: drafts, caches, working data | `useFiasStorage()` | Free-form paths, private to the plugin and the user. Not shown in their Vault. |
|
|
34
|
+
| A small piece of UI state that should survive a reload (a setting, the last tab) | `usePersistentState()` | Debounced writes to `useFiasStorage()`. |
|
|
35
|
+
| Images one user shares with the app's other users | `useCommunityAssets()` | Moderated before anyone else sees them. |
|
|
36
|
+
| Documents the user should keep: files your app makes for them, or files they already have | `useVaultDocuments()` / `useVaultUserDocuments()` | In the user's My Data, where they outlive your plugin. Reading files they already had needs their consent. |
|
|
35
37
|
|
|
36
38
|
**Common mistakes:**
|
|
37
39
|
|
|
38
|
-
- **Inlining content in `src/`** — a JSON file of lessons, a level pack, base64 images. It counts against the 5 MB cap, and every user downloads all of it to use a little. Put it in `content/`.
|
|
40
|
+
- **Inlining content in `src/`** — a JSON file of lessons, a level pack, base64 images. It counts against the 5 MB cap, and every user downloads all of it to use a little. Put it in `content/`. (A small set of sound effects is the exception — see the table.)
|
|
39
41
|
- **Seeding content you wrote into the DataStore from the plugin.** Every user's session tries to write it, the first writer of a `shared` document owns it, and nothing reviews it. Ship it in `content/`. If it must also be searchable, keep a searchable copy in a `shared` collection, but display the pack's version.
|
|
40
42
|
- **Using the DataStore or `useFiasStorage()` for data that is the same for every user.** That is content.
|
|
41
43
|
- **Building `public/` paths at runtime**, e.g. `` `/music-${name}.ogg` ``. Only literal paths are rewritten to the CDN when you publish, so a path built in code 404s in production. Use a lookup table of literal strings, or move the files to `content/` and read them by name.
|
|
44
|
+
- **Putting anything confidential in `public/`.** Files in `public/` are always public: anyone with a file's link can fetch it, even when your arche is restricted to chosen people. Put confidential material in a content pack (`content/`) — its access follows who may use the arche.
|
|
42
45
|
|
|
43
46
|
## SDK API Reference
|
|
44
47
|
|
|
@@ -173,6 +176,20 @@ const user = useFiasUser();
|
|
|
173
176
|
// { userId: string, displayName: string, avatar: string | null }
|
|
174
177
|
```
|
|
175
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
|
+
|
|
176
193
|
### `useFiasStorage()` — Sandboxed file storage
|
|
177
194
|
|
|
178
195
|
**Permission:** `storage:sandbox`
|
|
@@ -251,12 +268,13 @@ const doc = await fias.dataStore.get('scores', 'key');
|
|
|
251
268
|
**Limits:**
|
|
252
269
|
|
|
253
270
|
- 50 collections per plugin
|
|
254
|
-
- 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)
|
|
255
272
|
- 100 KB per document
|
|
256
|
-
-
|
|
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.
|
|
257
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
|
|
258
276
|
|
|
259
|
-
**Atomic batch writes:** `batch(operations)` applies up to
|
|
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.
|
|
260
278
|
|
|
261
279
|
```tsx
|
|
262
280
|
await dataStore.batch([
|
|
@@ -283,6 +301,41 @@ Why this and not `createCollection` at startup: the runtime op is capped at **10
|
|
|
283
301
|
|
|
284
302
|
Re-publishing is safe: existing collections are left alone, a stricter role floor is applied, a looser one is refused, and a collection you stop declaring is kept (never deleted — it may hold user data). Keep using `createCollection` for collections whose names you only know at runtime.
|
|
285
303
|
|
|
304
|
+
### Integrity rules (`constraints`)
|
|
305
|
+
|
|
306
|
+
A collection can carry rules the platform checks on **every** write, so a buggy or tampered client cannot break them. Declare them as `constraints` on the collection in `fias-plugin.json` (or in `createCollection`'s options):
|
|
307
|
+
|
|
308
|
+
```jsonc
|
|
309
|
+
"collections": [
|
|
310
|
+
{ "name": "stakeholders", "scope": "workspace" },
|
|
311
|
+
{ "name": "transactions", "scope": "workspace", "constraints": { "appendOnly": true } },
|
|
312
|
+
{
|
|
313
|
+
"name": "securities",
|
|
314
|
+
"scope": "workspace",
|
|
315
|
+
"constraints": {
|
|
316
|
+
"nonNegative": ["quantityOutstanding"],
|
|
317
|
+
"fieldBounds": [{ "field": "quantityIssued", "op": "lte", "valueField": "quantityAuthorized" }],
|
|
318
|
+
"immutableFields": ["issuedAt"],
|
|
319
|
+
"references": [{ "field": "stakeholderId", "collection": "stakeholders" }],
|
|
320
|
+
"ledgerSums": [{
|
|
321
|
+
"field": "quantityOutstanding",
|
|
322
|
+
"ledger": "transactions",
|
|
323
|
+
"terms": [{ "key": "securityId", "amount": "quantityDelta" }]
|
|
324
|
+
}]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- **`appendOnly`**: the collection is a ledger. Documents can be added, never overwritten or deleted.
|
|
331
|
+
- **`nonNegative`**: these numeric fields must be ≥ 0 when present.
|
|
332
|
+
- **`fieldBounds`**: `field <op> bound`, where `op` is `eq` / `ne` / `lt` / `lte` / `gt` / `gte` and the bound is a constant `value` or a sibling field `valueField` (exactly one).
|
|
333
|
+
- **`immutableFields`**: set once; an overwrite must carry the same value.
|
|
334
|
+
- **`references`**: `field` holds the key of a document that must exist in `collection`, in the same scope (same workspace, same user, or shared). An absent or null field is allowed. The writer must be able to read the target collection. Deleting a document that something still references is refused; there is no cascade.
|
|
335
|
+
- **`ledgerSums`**: a counter `field` may change only by exactly the sum of the rows appended to `ledger` in the **same write**. A ledger row counts toward the counter whose key its term's `key` field holds, adding its `amount` (subtracted with `negate: true`, filtered with `when: { field, in: [...] }`). A counter that moves without rows, and rows whose counter did not move by their sum, are both refused. The ledger must be `appendOnly` and have the counter's scope; an absent counter counts as 0. At most 4 terms per rule.
|
|
336
|
+
|
|
337
|
+
Rules that relate two documents (`references`, `ledgerSums`) are checked at the **end** of the write, so write the ledger row and the counter, or the target and the document pointing at it, together in one `batch` in any order. A violation rejects the whole write with `CONSTRAINT_VIOLATION`. Rules are **tighten-only**: you can add rules (at most 10 per kind), never remove or change one.
|
|
338
|
+
|
|
286
339
|
### Letting agents use your data (`agentAccess`)
|
|
287
340
|
|
|
288
341
|
**Permission:** `data:agents` (requires `data:store`)
|
|
@@ -398,9 +451,9 @@ The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` +
|
|
|
398
451
|
|
|
399
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.
|
|
400
453
|
|
|
401
|
-
**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
|
|
402
455
|
|
|
403
|
-
**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`.
|
|
404
457
|
|
|
405
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`).
|
|
406
459
|
|
|
@@ -501,7 +554,17 @@ function Team() {
|
|
|
501
554
|
}
|
|
502
555
|
```
|
|
503
556
|
|
|
504
|
-
**
|
|
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).
|
|
505
568
|
|
|
506
569
|
### `useEntityInvocation()` — Invoke AI models
|
|
507
570
|
|
|
@@ -1012,6 +1075,8 @@ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
|
|
|
1012
1075
|
await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
|
|
1013
1076
|
```
|
|
1014
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
|
+
|
|
1015
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.
|
|
1016
1081
|
|
|
1017
1082
|
```tsx
|
|
@@ -1302,6 +1367,7 @@ useArcheHandoff((payload) => {
|
|
|
1302
1367
|
- **You are the type gate.** If you cannot open this file, say so plainly in your UI. Half-opening it is worse than refusing.
|
|
1303
1368
|
- Fires **once per handoff**, and one that arrives before your hook mounts is buffered and delivered on registration — so the first handoff of a session is never lost.
|
|
1304
1369
|
- Nothing hands documents to a preview, so the handler only fires in the published plugin.
|
|
1370
|
+
- **Being offered at all takes two steps.** Declare what you open in `fias-plugin.json` — `"fileIntake": { "kinds": ["document"], "types": [".docx"] }` — and ask Fias to approve it. `kinds` are `image`, `pdf`, `document`, `spreadsheet`, `presentation`, `audio`, `video`, `archive`, `code`, `other`; `types` (optional, lowercase extensions or MIME types) narrows them — list them, or your app is offered for every file of the kind (`document` includes `.txt`, `.md` and `.html`). Your own format goes under `other` with its extension in `types` (`"kinds": ["document", "other"], "types": [".docx", ".myformat"]`). The build fails if you declare `fileIntake` but never call `useArcheHandoff`. Once approved, your app appears under "Open in…" in My Data (and is the double-click default for documents, spreadsheets and presentations), and Fias AI can open files in it — when the user didn't pick your app themselves, Fias asks them first. A kind or type you add later stays off until it is approved too.
|
|
1305
1371
|
|
|
1306
1372
|
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
1307
1373
|
|
|
@@ -1311,7 +1377,7 @@ The platform assistant (the Fias AI button in the host header) can perform actio
|
|
|
1311
1377
|
|
|
1312
1378
|
There are two declaration lanes, one shape:
|
|
1313
1379
|
|
|
1314
|
-
- **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`.
|
|
1315
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.
|
|
1316
1382
|
|
|
1317
1383
|
```tsx
|
|
@@ -1355,7 +1421,7 @@ Rules that matter:
|
|
|
1355
1421
|
**Permission:** `assets:read`
|
|
1356
1422
|
**Returns:** `ArcheAssetsApi`
|
|
1357
1423
|
|
|
1358
|
-
A frozen set of images the arche's contributor uploaded and pinned to the published version
|
|
1424
|
+
A frozen set of images — and short sound effects — the arche's contributor uploaded and pinned to the published version: logos, brand images, hole maps, reference photos, UI sounds. Each entry has an immutable `assetId` and a short-lived `signedUrl` (~5 min) suitable for `<img src>` / `<audio src>`. `list` and `index` return images only unless you pass `kinds: ['audio']` (or `['image', 'audio']`); with `kinds` the entries are typed `ArcheLibraryAsset`, so narrow on `contentType` before using an image's size (a sound's `width`/`height` are `null`). Keeping sounds here is optional (see the table at the top for where sound effects belong), and music is not accepted.
|
|
1359
1425
|
|
|
1360
1426
|
```tsx
|
|
1361
1427
|
import { useArcheAssets } from '@fias/arche-sdk';
|
|
@@ -1372,6 +1438,37 @@ const fresh = await getUrl(assetId);
|
|
|
1372
1438
|
|
|
1373
1439
|
Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
|
|
1374
1440
|
|
|
1441
|
+
### `useArcheAssetBytes()` — Asset bytes for Web Audio and canvas
|
|
1442
|
+
|
|
1443
|
+
**Permissions:** `entities:client_invoke` and `assets:read`
|
|
1444
|
+
**Returns:** `ArcheAssetBytesApi`
|
|
1445
|
+
|
|
1446
|
+
For sounds (or images) kept in the asset library. The plugin sandbox cannot fetch, so a signed URL is only good for `<img>`/`<audio>`. When you need the bytes — `AudioContext.decodeAudioData` for overlapping, precisely timed sound effects, or canvas pixel work — the host reads them for you. Load a set of sounds with **one `getMany` call** and wait for it before the first cue:
|
|
1447
|
+
|
|
1448
|
+
```tsx
|
|
1449
|
+
import { useArcheAssets, useArcheAssetBytes } from '@fias/arche-sdk';
|
|
1450
|
+
|
|
1451
|
+
const { list } = useArcheAssets();
|
|
1452
|
+
const { getMany } = useArcheAssetBytes();
|
|
1453
|
+
|
|
1454
|
+
const ctx = new AudioContext(); // resume() it on the first user gesture
|
|
1455
|
+
const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
|
|
1456
|
+
const loaded = await getMany(entries); // { [assetId]: { contentType, bytes, sizeBytes } | ArcheAssetError }
|
|
1457
|
+
const buffers = new Map<string, AudioBuffer>();
|
|
1458
|
+
for (const sound of entries) {
|
|
1459
|
+
const result = loaded[sound.assetId];
|
|
1460
|
+
if (result instanceof Error) continue; // result.code says why
|
|
1461
|
+
buffers.set(sound.name ?? sound.assetId, await ctx.decodeAudioData(result.bytes));
|
|
1462
|
+
}
|
|
1463
|
+
// From here a sound plays on cue: const src = ctx.createBufferSource(); src.buffer = …; src.connect(ctx.destination); src.start();
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
- **Never one call per sound.** The host blocks any capability called more than 50 times in 5 seconds, for 10 seconds — the 51st sound and every one after it would stay silent. `getMany` reads up to 100 per call. `getBytes(entry)` reads one; `getBytes` calls made together (a `Promise.all`) are batched the same way, but an `await` loop over many sounds is not.
|
|
1467
|
+
- **Pass entries straight from `list()`.** Their signed URLs (5 minutes) are reused, so each read is one round trip; a lapsed URL is replaced for a few entries, past that `ASSET_URL_EXPIRED` asks you to list again.
|
|
1468
|
+
- **Read once per session and keep the decoded buffers.** Every read is one billed asset load, and signed URLs change each session, so nothing carries over from the last one.
|
|
1469
|
+
- `getMany` resolves to each asset's bytes or its `ArcheAssetError` (`code`: `ASSET_URL_EXPIRED`, `ASSET_UNAVAILABLE`, `ASSET_TOO_LARGE`, …) and rejects only when the read is refused as a whole (`PERMISSION_DENIED`, `RATE_LIMIT`, `ASSETS_UNAVAILABLE_IN_PREVIEW`). Not available in the builder preview.
|
|
1470
|
+
- Library sounds are MP3 or WAV, which `decodeAudioData` accepts in every browser, iOS Safari included.
|
|
1471
|
+
|
|
1375
1472
|
### `useFiasContent()` — This plugin's content pack
|
|
1376
1473
|
|
|
1377
1474
|
**Permission:** `entities:client_invoke`
|
|
@@ -1398,6 +1495,33 @@ const { url } = await content.getUrl('audio/theme.ogg'); // signed URL, for long
|
|
|
1398
1495
|
- There is no published pack in the Arche Builder preview, so calls there fail with `CONTENT_UNAVAILABLE_IN_PREVIEW`; the dev harness serves your local `content/` directory.
|
|
1399
1496
|
- Every publish reviews all of the content again. Reads are free for your users; the pack's storage and bandwidth are billed to you, the contributor.
|
|
1400
1497
|
|
|
1498
|
+
### `useLocalStore()` — Working data kept on the user's device
|
|
1499
|
+
|
|
1500
|
+
**Permission:** `storage:local`
|
|
1501
|
+
**Returns:** `FiasLocalStoreApi`
|
|
1502
|
+
|
|
1503
|
+
A recovery journal, drafts, a cache: anything you would write on every change so a refresh, a crash or a closed tab loses nothing. Writes take milliseconds and never touch the network — the host page keeps the data in its own browser storage, in a space only your plugin can read, for the signed-in user in the workspace they are working in. Personal and each org workspace keep separate data: when the user switches workspace, your plugin sees that workspace's store, so nothing kept while working in one workspace leaks into another. It is cleared when the user signs out, or when someone else signs in on the device; an expired session keeps it, so the same user picks up where they left off.
|
|
1504
|
+
|
|
1505
|
+
```tsx
|
|
1506
|
+
import { useLocalStore, FiasLocalStoreError } from '@fias/arche-sdk';
|
|
1507
|
+
|
|
1508
|
+
const store = useLocalStore();
|
|
1509
|
+
|
|
1510
|
+
await store.put(`session/${docId}`, snapshot); // JSON, a string, or bytes — on every change
|
|
1511
|
+
const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load; null if absent
|
|
1512
|
+
const open = await store.list('session/'); // [{ key, sizeBytes, updatedAt }], sorted by key
|
|
1513
|
+
const restored = await store.getMany(open.map((e) => e.key)); // ≤ 100 entries in one call
|
|
1514
|
+
await store.delete(`session/${docId}`); // on close
|
|
1515
|
+
const { usedBytes, budgetBytes } = await store.estimate();
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
- This is **not** where the user's work is kept. It is best-effort device storage: a browser may clear site data, and nothing here reaches another device. Journal here; save the user's files through the Vault (`useVaultUserDocuments().saveContent`, `upload`, or the Save dialog).
|
|
1519
|
+
- Values come back as the kind they were put: a string is text, an `ArrayBuffer` / `Uint8Array` is bytes (read back as an `ArrayBuffer`), anything else is plain JSON (no `Date`, `Map`, `Blob` or other typed arrays — those are refused). `getText`, `getBytes` and `getJson` check the kind and throw `LOCAL_STORE_WRONG_KIND` on a mismatch; `getJson` also parses a text entry, so `put(key, JSON.stringify(x))` reads back like `put(key, x)`. `get` returns the raw entry; `getMany(keys)` returns up to 100 raw entries in one call, in the order asked, `null` for a missing key — restore a journal that way rather than with a `get` per key, which the bridge's runaway-loop guard (50 calls in 5 seconds) would stop.
|
|
1520
|
+
- Put a large snapshot (an image, a big document) as **bytes**. A JSON value is validated and serialized on the host page on every write, which takes time in proportion to its size; bytes are only copied.
|
|
1521
|
+
- Limits: a key ≤ 256 characters; one value ≤ 64 MB, and one `getMany` returns ≤ 64 MB in all (`LOCAL_STORE_VALUE_TOO_LARGE` — read fewer keys at a time); your whole store for this user in one workspace ≤ 256 MB (or a quarter of the browser's quota, if smaller) and ≤ 10,000 entries. Each entry is charged its value, its key and a small fixed overhead. Over a limit, `put` throws `LOCAL_STORE_QUOTA_EXCEEDED` instead of evicting anything — decide what to keep with `estimate()` and `list()`.
|
|
1522
|
+
- Failures throw `FiasLocalStoreError` with a `code`: `LOCAL_STORE_UNAVAILABLE` (the browser refused storage — degrade to "nothing persisted"), `LOCAL_STORE_KEY_INVALID`, `LOCAL_STORE_VALUE_TOO_LARGE`, `LOCAL_STORE_QUOTA_EXCEEDED`, `LOCAL_STORE_WRONG_KIND`, `INVALID_INPUT`, `PERMISSION_DENIED`, `RATE_LIMIT`.
|
|
1523
|
+
- Works in the dev harness (mock and live modes) and in the Arche Builder preview, each under its own namespace.
|
|
1524
|
+
|
|
1401
1525
|
### `useCommunityAssets()` — User-published images, visible to this arche's users
|
|
1402
1526
|
|
|
1403
1527
|
**Permissions:** `assets:community:read` (browse/view) · `assets:community:publish` (publish/unpublish/listMine)
|
|
@@ -1432,6 +1556,72 @@ Three things to design around:
|
|
|
1432
1556
|
|
|
1433
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.
|
|
1434
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
|
+
|
|
1435
1625
|
### `useCompanyEmail()` — Send email as the Company the app is used in
|
|
1436
1626
|
|
|
1437
1627
|
**Permission:** `email:company:send`
|
|
@@ -1535,8 +1725,17 @@ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
|
|
|
1535
1725
|
// `arche_…` arche is rejected, as is a malformed one (you get an error, never
|
|
1536
1726
|
// a silent landing on the home screen).
|
|
1537
1727
|
openArche('arc_0123456789abcdef0123456789abcdef', { path: 'map' });
|
|
1728
|
+
|
|
1729
|
+
// Open a chat channel in the Chat arche.
|
|
1730
|
+
openArche('arche_chat', { payload: { kind: 'open_channel', channelId } });
|
|
1538
1731
|
```
|
|
1539
1732
|
|
|
1733
|
+
**Opening a chat channel.** The `open_channel` payload is a pointer, never
|
|
1734
|
+
access: Chat re-reads the channel under the user's own membership, and a
|
|
1735
|
+
non-member lands on the Chat home with a notice instead. Make the user a member
|
|
1736
|
+
first (LitCraft: `join_guild_chat` on the LitCraft Player entity). Code that
|
|
1737
|
+
switches exhaustively on `ArcheHandoffPayloadInput`'s `kind` needs the new case.
|
|
1738
|
+
|
|
1540
1739
|
**Receiving a deep link.** There is nothing extra to opt into: `currentPath` is
|
|
1541
1740
|
the host's path expressed in YOUR arche's route space — the same space
|
|
1542
1741
|
`navigateTo` accepts — so an arche that wants to be a deep-link target just
|
|
@@ -1790,26 +1989,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1790
1989
|
|
|
1791
1990
|
**Fields:**
|
|
1792
1991
|
|
|
1793
|
-
| Field | Req | Description
|
|
1794
|
-
| --------------------- | ---- |
|
|
1795
|
-
| `name` | Yes | Plugin identifier (lowercase, hyphens)
|
|
1796
|
-
| `version` | Yes | Semver (e.g., `"1.0.0"`)
|
|
1797
|
-
| `description` | Yes | Short marketplace description
|
|
1798
|
-
| `expandedDescription` | No | Long-form marketplace copy
|
|
1799
|
-
| `main` | Yes | Entry point source file
|
|
1800
|
-
| `archeType` | Yes | `"tool"` or `"site"`
|
|
1801
|
-
| `categorySlug` | No | Marketplace category for discovery
|
|
1802
|
-
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local
|
|
1803
|
-
| `tags` | No | Discovery tags
|
|
1804
|
-
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products`
|
|
1805
|
-
| `permissions` | Yes | Array of permission scopes (see below)
|
|
1806
|
-
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below)
|
|
1807
|
-
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below)
|
|
1808
|
-
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"`
|
|
1809
|
-
| `isListed` | No | Default `true`. Visible in marketplace store
|
|
1810
|
-
| `sdk` | Yes | SDK version range
|
|
1811
|
-
| `dependencies` | No | npm packages with **exact** versions (max 20)
|
|
1812
|
-
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit
|
|
1992
|
+
| Field | Req | Description |
|
|
1993
|
+
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1994
|
+
| `name` | Yes | Plugin identifier (lowercase, hyphens) |
|
|
1995
|
+
| `version` | Yes | Semver (e.g., `"1.0.0"`) |
|
|
1996
|
+
| `description` | Yes | Short marketplace description |
|
|
1997
|
+
| `expandedDescription` | No | Long-form marketplace copy |
|
|
1998
|
+
| `main` | Yes | Entry point source file |
|
|
1999
|
+
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
2000
|
+
| `categorySlug` | No | Marketplace category for discovery |
|
|
2001
|
+
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on every publish (local, staging and prod) and rehosted as your marketplace/store icon. PNG, JPEG, WebP or SVG, ≤5 MB; raster images must also be ≤1024×1024 px (SVG has no pixel limit). An invalid icon rejects the submission: a first-publish listing fee is refunded, but the AI review that already ran is still billed. To change it, replace the file (or the path) and republish; removing the field keeps the current icon. |
|
|
2002
|
+
| `tags` | No | Discovery tags |
|
|
2003
|
+
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
2004
|
+
| `permissions` | Yes | Array of permission scopes (see below) |
|
|
2005
|
+
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
2006
|
+
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
2007
|
+
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
|
|
2008
|
+
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
2009
|
+
| `sdk` | Yes | SDK version range |
|
|
2010
|
+
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
2011
|
+
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
1813
2012
|
|
|
1814
2013
|
**Pricing detail:**
|
|
1815
2014
|
|
|
@@ -1824,7 +2023,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1824
2023
|
|
|
1825
2024
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1826
2025
|
|
|
1827
|
-
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `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`
|
|
2026
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
|
|
1828
2027
|
|
|
1829
2028
|
**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`.
|
|
1830
2029
|
|
|
@@ -1882,6 +2081,16 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1882
2081
|
- **No external scripts or stylesheets** — everything must be bundled
|
|
1883
2082
|
- All platform communication goes through the bridge (SDK hooks)
|
|
1884
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
|
+
|
|
1885
2094
|
### Size and File Limits
|
|
1886
2095
|
|
|
1887
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)
|
|
@@ -1896,6 +2105,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1896
2105
|
- `storage_read`: 300/minute
|
|
1897
2106
|
- `storage_list`, `storage_delete`: 60/minute
|
|
1898
2107
|
- Content reads (`useFiasContent()`): 120/minute — read related files together with `getMany` (up to 100 per call)
|
|
2108
|
+
- Asset byte reads (`useArcheAssetBytes()`): 120/minute — load a set of sounds with one `getMany` (up to 100 per call)
|
|
1899
2109
|
|
|
1900
2110
|
A separate **runaway-loop detector** fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) …` — the path tells you where to debounce at the source.
|
|
1901
2111
|
|