@fias/create-fias-plugin 1.18.0 → 1.20.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 +257 -35
- package/templates/default/CLAUDE.md +257 -35
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.28.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
|
|
|
@@ -283,6 +286,156 @@ Why this and not `createCollection` at startup: the runtime op is capped at **10
|
|
|
283
286
|
|
|
284
287
|
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
288
|
|
|
289
|
+
### Integrity rules (`constraints`)
|
|
290
|
+
|
|
291
|
+
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):
|
|
292
|
+
|
|
293
|
+
```jsonc
|
|
294
|
+
"collections": [
|
|
295
|
+
{ "name": "stakeholders", "scope": "workspace" },
|
|
296
|
+
{ "name": "transactions", "scope": "workspace", "constraints": { "appendOnly": true } },
|
|
297
|
+
{
|
|
298
|
+
"name": "securities",
|
|
299
|
+
"scope": "workspace",
|
|
300
|
+
"constraints": {
|
|
301
|
+
"nonNegative": ["quantityOutstanding"],
|
|
302
|
+
"fieldBounds": [{ "field": "quantityIssued", "op": "lte", "valueField": "quantityAuthorized" }],
|
|
303
|
+
"immutableFields": ["issuedAt"],
|
|
304
|
+
"references": [{ "field": "stakeholderId", "collection": "stakeholders" }],
|
|
305
|
+
"ledgerSums": [{
|
|
306
|
+
"field": "quantityOutstanding",
|
|
307
|
+
"ledger": "transactions",
|
|
308
|
+
"terms": [{ "key": "securityId", "amount": "quantityDelta" }]
|
|
309
|
+
}]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- **`appendOnly`**: the collection is a ledger. Documents can be added, never overwritten or deleted.
|
|
316
|
+
- **`nonNegative`**: these numeric fields must be ≥ 0 when present.
|
|
317
|
+
- **`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).
|
|
318
|
+
- **`immutableFields`**: set once; an overwrite must carry the same value.
|
|
319
|
+
- **`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.
|
|
320
|
+
- **`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.
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
### Letting agents use your data (`agentAccess`)
|
|
325
|
+
|
|
326
|
+
**Permission:** `data:agents` (requires `data:store`)
|
|
327
|
+
|
|
328
|
+
Your plugin can share some of its collections with AI agents — the platform's Fias AI assistant, and agents a user connects over MCP (Claude Code and others) — **while your plugin is closed**. Agents never run your code: they read and change documents through a fixed set of platform tools (`plugin_apps_list`, `plugin_app_describe`, `plugin_data_query`, `plugin_data_get`, `plugin_data_put`, `plugin_data_patch`, `plugin_data_delete`). Declare what you share in `fias-plugin.json`:
|
|
329
|
+
|
|
330
|
+
```jsonc
|
|
331
|
+
"permissions": ["data:store", "data:agents"],
|
|
332
|
+
"collections": [
|
|
333
|
+
{ "name": "scenes", "scope": "user" },
|
|
334
|
+
{ "name": "render_requests", "scope": "user" },
|
|
335
|
+
{ "name": "previews", "scope": "user" }
|
|
336
|
+
],
|
|
337
|
+
"agentAccess": {
|
|
338
|
+
"description": "Puppet cut scenes: a cast, scene-frames and spoken lines.",
|
|
339
|
+
"collections": {
|
|
340
|
+
"scenes": {
|
|
341
|
+
"access": "read_write",
|
|
342
|
+
"description": "One cut scene per document, keyed by scene id.",
|
|
343
|
+
"schema": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] }
|
|
344
|
+
},
|
|
345
|
+
"render_requests": {
|
|
346
|
+
"access": "read_write",
|
|
347
|
+
"description": "Ask the open app for stills of a scene at chosen times.",
|
|
348
|
+
"schema": {
|
|
349
|
+
"type": "object",
|
|
350
|
+
"properties": {
|
|
351
|
+
"scene": { "type": "string", "maxLength": 100 },
|
|
352
|
+
"times": { "type": "array", "items": { "type": "number", "minimum": 0 }, "minItems": 1, "maxItems": 12 },
|
|
353
|
+
"width": { "type": "integer", "minimum": 64, "maximum": 800 },
|
|
354
|
+
"status": { "enum": ["pending", "working", "done", "failed"] },
|
|
355
|
+
"error": { "type": "string", "maxLength": 2000 },
|
|
356
|
+
"previews": { "type": "integer", "minimum": 0, "maximum": 12 }
|
|
357
|
+
},
|
|
358
|
+
"required": ["scene", "times", "status"],
|
|
359
|
+
"additionalProperties": false
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"previews": { "access": "read", "description": "Stills the open app rendered.", "images": ["/image"] }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Rules (checked by `fias-dev validate` and at publish):
|
|
368
|
+
|
|
369
|
+
- `data:agents` and the `agentAccess` block go together: declaring the permission without a block is an error.
|
|
370
|
+
- Only collections you declare in `collections`, with `user` or `shared` scope. `read_write` only on `user` scope (a write to a `shared` collection would land in every user's copy), and it needs a `schema` — every agent write is validated against it. `workspace` collections can't be shared with agents yet.
|
|
371
|
+
- Schemas use a restricted JSON Schema 2020-12: `type`, `properties`, `required`, `additionalProperties`, `items`, `prefixItems`, `enum`, `const`, min/max bounds, `anyOf`/`oneOf`/`allOf`/`not`, `title`, `description`, `default`, `examples`, `format` (annotation only), and `$ref` to your own `#/$defs/…`. **No `pattern`** (regular expressions), no `propertyNames`, and no remote `$ref`. `uniqueItems` only on arrays whose `items` have a scalar `type`. A `$ref` may recurse only through `properties` / `items` / `additionalProperties` (a tree), never straight back to itself. ≤ 16 KB per schema, depth ≤ 12. If you generate schemas with Zod, note that `z.record()` emits `propertyNames` and `z.email()` / `z.uuid()` / `z.regex()` emit `pattern` — neither is accepted.
|
|
372
|
+
- Descriptions describe your data — they are not instructions to an AI. The publish review flags text that tells an agent what to do, links, and mentions of other apps.
|
|
373
|
+
- `images` names fields holding `{ "mimeType": "image/png" | "image/jpeg" | "image/webp", "data": "<base64>" }` (≤ 96 KB, ≤ 2000 px a side; every image is fully decoded before an agent sees it, and one that doesn't decode is replaced by an error). Agents receive them as pictures and can never write them — so in a `read_write` collection an image field must be optional, never `required`.
|
|
374
|
+
|
|
375
|
+
What agents can do: **Fias AI** acts as the signed-in user on what you declare. An **external agent** reaches your app only through an API key the user allowed to use it, collection by collection, read or read and change — and if you later share more, nothing new is reachable until the user allows it again. Every agent write is conditional on the version it read, so it can't silently overwrite a change made in your open UI; deletes always need the user's confirmation in Fias.
|
|
376
|
+
|
|
377
|
+
**Your obligation for `read_write` collections: validate what you load.** An agent — or text an agent was shown — may have written it. Treat those documents like any untrusted input; never evaluate or render a field as code or HTML.
|
|
378
|
+
|
|
379
|
+
**Answering an agent while open (previews).** Agents can't run your code, but your open plugin can do work for them through data. Watch a requests collection, do the work, and write the answer back:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
const dataStore = useFiasDataStore();
|
|
383
|
+
const answerPending = useCallback(async () => {
|
|
384
|
+
const { documents } = await dataStore.query<RenderRequest>('render_requests', {
|
|
385
|
+
filters: [{ field: 'status', op: 'eq', value: 'pending' }],
|
|
386
|
+
limit: 5,
|
|
387
|
+
});
|
|
388
|
+
for (const req of documents) {
|
|
389
|
+
// Claim it first, so two open tabs never both do it.
|
|
390
|
+
const claimed = await dataStore
|
|
391
|
+
.batch(
|
|
392
|
+
[
|
|
393
|
+
{
|
|
394
|
+
op: 'put',
|
|
395
|
+
collection: 'render_requests',
|
|
396
|
+
key: req.key,
|
|
397
|
+
data: { ...req.data, status: 'working' },
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
{
|
|
401
|
+
preconditions: [
|
|
402
|
+
{ kind: 'version', collection: 'render_requests', key: req.key, version: req.version },
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
.then(
|
|
407
|
+
() => true,
|
|
408
|
+
() => false,
|
|
409
|
+
);
|
|
410
|
+
if (!claimed) continue;
|
|
411
|
+
const stills = await renderStills(req.data); // your own code, bounded by your schema
|
|
412
|
+
for (const [i, image] of stills.entries()) {
|
|
413
|
+
await dataStore.put('previews', `${req.key}-${i}`, {
|
|
414
|
+
request: req.key,
|
|
415
|
+
t: req.data.times[i],
|
|
416
|
+
image,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
await dataStore.put('render_requests', req.key, {
|
|
420
|
+
...req.data,
|
|
421
|
+
status: 'done',
|
|
422
|
+
previews: stills.length,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}, [dataStore]);
|
|
426
|
+
useDataSubscription('render_requests', answerPending); // changes arrive at most once —
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
// so also check on open and every ~10 s
|
|
429
|
+
void answerPending();
|
|
430
|
+
const timer = setInterval(answerPending, 10_000);
|
|
431
|
+
return () => clearInterval(timer);
|
|
432
|
+
}, [answerPending]);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` + `waitSeconds`), and reads the stills back as images. Keep requests bounded in your schema (e.g. `"times": { "maxItems": 12 }`), prune old previews, and report failures in the request (e.g. `status: "failed"` with errors) so the agent can fix what it wrote. **Give both the requests and the results collections `user` scope** — in a `shared` collection any user of your app could plant a "result" that another user's agent then reads.
|
|
436
|
+
|
|
437
|
+
**Testing:** in the dev harness's Fias AI window, the read tools see your local `agentAccess` block against your dev data — ask "what scenes do I have?" to see what an agent sees before you publish.
|
|
438
|
+
|
|
286
439
|
**Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
|
|
287
440
|
|
|
288
441
|
**Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `RATE_LIMIT`.
|
|
@@ -1187,6 +1340,7 @@ useArcheHandoff((payload) => {
|
|
|
1187
1340
|
- **You are the type gate.** If you cannot open this file, say so plainly in your UI. Half-opening it is worse than refusing.
|
|
1188
1341
|
- 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.
|
|
1189
1342
|
- Nothing hands documents to a preview, so the handler only fires in the published plugin.
|
|
1343
|
+
- **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.
|
|
1190
1344
|
|
|
1191
1345
|
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
1192
1346
|
|
|
@@ -1240,7 +1394,7 @@ Rules that matter:
|
|
|
1240
1394
|
**Permission:** `assets:read`
|
|
1241
1395
|
**Returns:** `ArcheAssetsApi`
|
|
1242
1396
|
|
|
1243
|
-
A frozen set of images the arche's contributor uploaded and pinned to the published version
|
|
1397
|
+
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.
|
|
1244
1398
|
|
|
1245
1399
|
```tsx
|
|
1246
1400
|
import { useArcheAssets } from '@fias/arche-sdk';
|
|
@@ -1257,6 +1411,37 @@ const fresh = await getUrl(assetId);
|
|
|
1257
1411
|
|
|
1258
1412
|
Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
|
|
1259
1413
|
|
|
1414
|
+
### `useArcheAssetBytes()` — Asset bytes for Web Audio and canvas
|
|
1415
|
+
|
|
1416
|
+
**Permissions:** `entities:client_invoke` and `assets:read`
|
|
1417
|
+
**Returns:** `ArcheAssetBytesApi`
|
|
1418
|
+
|
|
1419
|
+
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:
|
|
1420
|
+
|
|
1421
|
+
```tsx
|
|
1422
|
+
import { useArcheAssets, useArcheAssetBytes } from '@fias/arche-sdk';
|
|
1423
|
+
|
|
1424
|
+
const { list } = useArcheAssets();
|
|
1425
|
+
const { getMany } = useArcheAssetBytes();
|
|
1426
|
+
|
|
1427
|
+
const ctx = new AudioContext(); // resume() it on the first user gesture
|
|
1428
|
+
const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
|
|
1429
|
+
const loaded = await getMany(entries); // { [assetId]: { contentType, bytes, sizeBytes } | ArcheAssetError }
|
|
1430
|
+
const buffers = new Map<string, AudioBuffer>();
|
|
1431
|
+
for (const sound of entries) {
|
|
1432
|
+
const result = loaded[sound.assetId];
|
|
1433
|
+
if (result instanceof Error) continue; // result.code says why
|
|
1434
|
+
buffers.set(sound.name ?? sound.assetId, await ctx.decodeAudioData(result.bytes));
|
|
1435
|
+
}
|
|
1436
|
+
// From here a sound plays on cue: const src = ctx.createBufferSource(); src.buffer = …; src.connect(ctx.destination); src.start();
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
- **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.
|
|
1440
|
+
- **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.
|
|
1441
|
+
- **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.
|
|
1442
|
+
- `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.
|
|
1443
|
+
- Library sounds are MP3 or WAV, which `decodeAudioData` accepts in every browser, iOS Safari included.
|
|
1444
|
+
|
|
1260
1445
|
### `useFiasContent()` — This plugin's content pack
|
|
1261
1446
|
|
|
1262
1447
|
**Permission:** `entities:client_invoke`
|
|
@@ -1283,6 +1468,33 @@ const { url } = await content.getUrl('audio/theme.ogg'); // signed URL, for long
|
|
|
1283
1468
|
- 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.
|
|
1284
1469
|
- 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.
|
|
1285
1470
|
|
|
1471
|
+
### `useLocalStore()` — Working data kept on the user's device
|
|
1472
|
+
|
|
1473
|
+
**Permission:** `storage:local`
|
|
1474
|
+
**Returns:** `FiasLocalStoreApi`
|
|
1475
|
+
|
|
1476
|
+
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.
|
|
1477
|
+
|
|
1478
|
+
```tsx
|
|
1479
|
+
import { useLocalStore, FiasLocalStoreError } from '@fias/arche-sdk';
|
|
1480
|
+
|
|
1481
|
+
const store = useLocalStore();
|
|
1482
|
+
|
|
1483
|
+
await store.put(`session/${docId}`, snapshot); // JSON, a string, or bytes — on every change
|
|
1484
|
+
const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load; null if absent
|
|
1485
|
+
const open = await store.list('session/'); // [{ key, sizeBytes, updatedAt }], sorted by key
|
|
1486
|
+
const restored = await store.getMany(open.map((e) => e.key)); // ≤ 100 entries in one call
|
|
1487
|
+
await store.delete(`session/${docId}`); // on close
|
|
1488
|
+
const { usedBytes, budgetBytes } = await store.estimate();
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
- 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).
|
|
1492
|
+
- 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.
|
|
1493
|
+
- 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.
|
|
1494
|
+
- 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()`.
|
|
1495
|
+
- 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`.
|
|
1496
|
+
- Works in the dev harness (mock and live modes) and in the Arche Builder preview, each under its own namespace.
|
|
1497
|
+
|
|
1286
1498
|
### `useCommunityAssets()` — User-published images, visible to this arche's users
|
|
1287
1499
|
|
|
1288
1500
|
**Permissions:** `assets:community:read` (browse/view) · `assets:community:publish` (publish/unpublish/listMine)
|
|
@@ -1420,8 +1632,17 @@ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
|
|
|
1420
1632
|
// `arche_…` arche is rejected, as is a malformed one (you get an error, never
|
|
1421
1633
|
// a silent landing on the home screen).
|
|
1422
1634
|
openArche('arc_0123456789abcdef0123456789abcdef', { path: 'map' });
|
|
1635
|
+
|
|
1636
|
+
// Open a chat channel in the Chat arche.
|
|
1637
|
+
openArche('arche_chat', { payload: { kind: 'open_channel', channelId } });
|
|
1423
1638
|
```
|
|
1424
1639
|
|
|
1640
|
+
**Opening a chat channel.** The `open_channel` payload is a pointer, never
|
|
1641
|
+
access: Chat re-reads the channel under the user's own membership, and a
|
|
1642
|
+
non-member lands on the Chat home with a notice instead. Make the user a member
|
|
1643
|
+
first (LitCraft: `join_guild_chat` on the LitCraft Player entity). Code that
|
|
1644
|
+
switches exhaustively on `ArcheHandoffPayloadInput`'s `kind` needs the new case.
|
|
1645
|
+
|
|
1425
1646
|
**Receiving a deep link.** There is nothing extra to opt into: `currentPath` is
|
|
1426
1647
|
the host's path expressed in YOUR arche's route space — the same space
|
|
1427
1648
|
`navigateTo` accepts — so an arche that wants to be a deep-link target just
|
|
@@ -1675,26 +1896,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1675
1896
|
|
|
1676
1897
|
**Fields:**
|
|
1677
1898
|
|
|
1678
|
-
| Field | Req | Description
|
|
1679
|
-
| --------------------- | ---- |
|
|
1680
|
-
| `name` | Yes | Plugin identifier (lowercase, hyphens)
|
|
1681
|
-
| `version` | Yes | Semver (e.g., `"1.0.0"`)
|
|
1682
|
-
| `description` | Yes | Short marketplace description
|
|
1683
|
-
| `expandedDescription` | No | Long-form marketplace copy
|
|
1684
|
-
| `main` | Yes | Entry point source file
|
|
1685
|
-
| `archeType` | Yes | `"tool"` or `"site"`
|
|
1686
|
-
| `categorySlug` | No | Marketplace category for discovery
|
|
1687
|
-
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local
|
|
1688
|
-
| `tags` | No | Discovery tags
|
|
1689
|
-
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products`
|
|
1690
|
-
| `permissions` | Yes | Array of permission scopes (see below)
|
|
1691
|
-
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below)
|
|
1692
|
-
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below)
|
|
1693
|
-
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"`
|
|
1694
|
-
| `isListed` | No | Default `true`. Visible in marketplace store
|
|
1695
|
-
| `sdk` | Yes | SDK version range
|
|
1696
|
-
| `dependencies` | No | npm packages with **exact** versions (max 20)
|
|
1697
|
-
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit
|
|
1899
|
+
| Field | Req | Description |
|
|
1900
|
+
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1901
|
+
| `name` | Yes | Plugin identifier (lowercase, hyphens) |
|
|
1902
|
+
| `version` | Yes | Semver (e.g., `"1.0.0"`) |
|
|
1903
|
+
| `description` | Yes | Short marketplace description |
|
|
1904
|
+
| `expandedDescription` | No | Long-form marketplace copy |
|
|
1905
|
+
| `main` | Yes | Entry point source file |
|
|
1906
|
+
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
1907
|
+
| `categorySlug` | No | Marketplace category for discovery |
|
|
1908
|
+
| `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. |
|
|
1909
|
+
| `tags` | No | Discovery tags |
|
|
1910
|
+
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
1911
|
+
| `permissions` | Yes | Array of permission scopes (see below) |
|
|
1912
|
+
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
1913
|
+
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
1914
|
+
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
|
|
1915
|
+
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
1916
|
+
| `sdk` | Yes | SDK version range |
|
|
1917
|
+
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
1918
|
+
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
1698
1919
|
|
|
1699
1920
|
**Pricing detail:**
|
|
1700
1921
|
|
|
@@ -1709,7 +1930,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1709
1930
|
|
|
1710
1931
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1711
1932
|
|
|
1712
|
-
**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`
|
|
1933
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
|
|
1713
1934
|
|
|
1714
1935
|
**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`.
|
|
1715
1936
|
|
|
@@ -1781,6 +2002,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1781
2002
|
- `storage_read`: 300/minute
|
|
1782
2003
|
- `storage_list`, `storage_delete`: 60/minute
|
|
1783
2004
|
- Content reads (`useFiasContent()`): 120/minute — read related files together with `getMany` (up to 100 per call)
|
|
2005
|
+
- Asset byte reads (`useArcheAssetBytes()`): 120/minute — load a set of sounds with one `getMany` (up to 100 per call)
|
|
1784
2006
|
|
|
1785
2007
|
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.
|
|
1786
2008
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.28.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
|
|
|
@@ -283,6 +286,156 @@ Why this and not `createCollection` at startup: the runtime op is capped at **10
|
|
|
283
286
|
|
|
284
287
|
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
288
|
|
|
289
|
+
### Integrity rules (`constraints`)
|
|
290
|
+
|
|
291
|
+
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):
|
|
292
|
+
|
|
293
|
+
```jsonc
|
|
294
|
+
"collections": [
|
|
295
|
+
{ "name": "stakeholders", "scope": "workspace" },
|
|
296
|
+
{ "name": "transactions", "scope": "workspace", "constraints": { "appendOnly": true } },
|
|
297
|
+
{
|
|
298
|
+
"name": "securities",
|
|
299
|
+
"scope": "workspace",
|
|
300
|
+
"constraints": {
|
|
301
|
+
"nonNegative": ["quantityOutstanding"],
|
|
302
|
+
"fieldBounds": [{ "field": "quantityIssued", "op": "lte", "valueField": "quantityAuthorized" }],
|
|
303
|
+
"immutableFields": ["issuedAt"],
|
|
304
|
+
"references": [{ "field": "stakeholderId", "collection": "stakeholders" }],
|
|
305
|
+
"ledgerSums": [{
|
|
306
|
+
"field": "quantityOutstanding",
|
|
307
|
+
"ledger": "transactions",
|
|
308
|
+
"terms": [{ "key": "securityId", "amount": "quantityDelta" }]
|
|
309
|
+
}]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- **`appendOnly`**: the collection is a ledger. Documents can be added, never overwritten or deleted.
|
|
316
|
+
- **`nonNegative`**: these numeric fields must be ≥ 0 when present.
|
|
317
|
+
- **`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).
|
|
318
|
+
- **`immutableFields`**: set once; an overwrite must carry the same value.
|
|
319
|
+
- **`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.
|
|
320
|
+
- **`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.
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
### Letting agents use your data (`agentAccess`)
|
|
325
|
+
|
|
326
|
+
**Permission:** `data:agents` (requires `data:store`)
|
|
327
|
+
|
|
328
|
+
Your plugin can share some of its collections with AI agents — the platform's Fias AI assistant, and agents a user connects over MCP (Claude Code and others) — **while your plugin is closed**. Agents never run your code: they read and change documents through a fixed set of platform tools (`plugin_apps_list`, `plugin_app_describe`, `plugin_data_query`, `plugin_data_get`, `plugin_data_put`, `plugin_data_patch`, `plugin_data_delete`). Declare what you share in `fias-plugin.json`:
|
|
329
|
+
|
|
330
|
+
```jsonc
|
|
331
|
+
"permissions": ["data:store", "data:agents"],
|
|
332
|
+
"collections": [
|
|
333
|
+
{ "name": "scenes", "scope": "user" },
|
|
334
|
+
{ "name": "render_requests", "scope": "user" },
|
|
335
|
+
{ "name": "previews", "scope": "user" }
|
|
336
|
+
],
|
|
337
|
+
"agentAccess": {
|
|
338
|
+
"description": "Puppet cut scenes: a cast, scene-frames and spoken lines.",
|
|
339
|
+
"collections": {
|
|
340
|
+
"scenes": {
|
|
341
|
+
"access": "read_write",
|
|
342
|
+
"description": "One cut scene per document, keyed by scene id.",
|
|
343
|
+
"schema": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] }
|
|
344
|
+
},
|
|
345
|
+
"render_requests": {
|
|
346
|
+
"access": "read_write",
|
|
347
|
+
"description": "Ask the open app for stills of a scene at chosen times.",
|
|
348
|
+
"schema": {
|
|
349
|
+
"type": "object",
|
|
350
|
+
"properties": {
|
|
351
|
+
"scene": { "type": "string", "maxLength": 100 },
|
|
352
|
+
"times": { "type": "array", "items": { "type": "number", "minimum": 0 }, "minItems": 1, "maxItems": 12 },
|
|
353
|
+
"width": { "type": "integer", "minimum": 64, "maximum": 800 },
|
|
354
|
+
"status": { "enum": ["pending", "working", "done", "failed"] },
|
|
355
|
+
"error": { "type": "string", "maxLength": 2000 },
|
|
356
|
+
"previews": { "type": "integer", "minimum": 0, "maximum": 12 }
|
|
357
|
+
},
|
|
358
|
+
"required": ["scene", "times", "status"],
|
|
359
|
+
"additionalProperties": false
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"previews": { "access": "read", "description": "Stills the open app rendered.", "images": ["/image"] }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Rules (checked by `fias-dev validate` and at publish):
|
|
368
|
+
|
|
369
|
+
- `data:agents` and the `agentAccess` block go together: declaring the permission without a block is an error.
|
|
370
|
+
- Only collections you declare in `collections`, with `user` or `shared` scope. `read_write` only on `user` scope (a write to a `shared` collection would land in every user's copy), and it needs a `schema` — every agent write is validated against it. `workspace` collections can't be shared with agents yet.
|
|
371
|
+
- Schemas use a restricted JSON Schema 2020-12: `type`, `properties`, `required`, `additionalProperties`, `items`, `prefixItems`, `enum`, `const`, min/max bounds, `anyOf`/`oneOf`/`allOf`/`not`, `title`, `description`, `default`, `examples`, `format` (annotation only), and `$ref` to your own `#/$defs/…`. **No `pattern`** (regular expressions), no `propertyNames`, and no remote `$ref`. `uniqueItems` only on arrays whose `items` have a scalar `type`. A `$ref` may recurse only through `properties` / `items` / `additionalProperties` (a tree), never straight back to itself. ≤ 16 KB per schema, depth ≤ 12. If you generate schemas with Zod, note that `z.record()` emits `propertyNames` and `z.email()` / `z.uuid()` / `z.regex()` emit `pattern` — neither is accepted.
|
|
372
|
+
- Descriptions describe your data — they are not instructions to an AI. The publish review flags text that tells an agent what to do, links, and mentions of other apps.
|
|
373
|
+
- `images` names fields holding `{ "mimeType": "image/png" | "image/jpeg" | "image/webp", "data": "<base64>" }` (≤ 96 KB, ≤ 2000 px a side; every image is fully decoded before an agent sees it, and one that doesn't decode is replaced by an error). Agents receive them as pictures and can never write them — so in a `read_write` collection an image field must be optional, never `required`.
|
|
374
|
+
|
|
375
|
+
What agents can do: **Fias AI** acts as the signed-in user on what you declare. An **external agent** reaches your app only through an API key the user allowed to use it, collection by collection, read or read and change — and if you later share more, nothing new is reachable until the user allows it again. Every agent write is conditional on the version it read, so it can't silently overwrite a change made in your open UI; deletes always need the user's confirmation in Fias.
|
|
376
|
+
|
|
377
|
+
**Your obligation for `read_write` collections: validate what you load.** An agent — or text an agent was shown — may have written it. Treat those documents like any untrusted input; never evaluate or render a field as code or HTML.
|
|
378
|
+
|
|
379
|
+
**Answering an agent while open (previews).** Agents can't run your code, but your open plugin can do work for them through data. Watch a requests collection, do the work, and write the answer back:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
const dataStore = useFiasDataStore();
|
|
383
|
+
const answerPending = useCallback(async () => {
|
|
384
|
+
const { documents } = await dataStore.query<RenderRequest>('render_requests', {
|
|
385
|
+
filters: [{ field: 'status', op: 'eq', value: 'pending' }],
|
|
386
|
+
limit: 5,
|
|
387
|
+
});
|
|
388
|
+
for (const req of documents) {
|
|
389
|
+
// Claim it first, so two open tabs never both do it.
|
|
390
|
+
const claimed = await dataStore
|
|
391
|
+
.batch(
|
|
392
|
+
[
|
|
393
|
+
{
|
|
394
|
+
op: 'put',
|
|
395
|
+
collection: 'render_requests',
|
|
396
|
+
key: req.key,
|
|
397
|
+
data: { ...req.data, status: 'working' },
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
{
|
|
401
|
+
preconditions: [
|
|
402
|
+
{ kind: 'version', collection: 'render_requests', key: req.key, version: req.version },
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
.then(
|
|
407
|
+
() => true,
|
|
408
|
+
() => false,
|
|
409
|
+
);
|
|
410
|
+
if (!claimed) continue;
|
|
411
|
+
const stills = await renderStills(req.data); // your own code, bounded by your schema
|
|
412
|
+
for (const [i, image] of stills.entries()) {
|
|
413
|
+
await dataStore.put('previews', `${req.key}-${i}`, {
|
|
414
|
+
request: req.key,
|
|
415
|
+
t: req.data.times[i],
|
|
416
|
+
image,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
await dataStore.put('render_requests', req.key, {
|
|
420
|
+
...req.data,
|
|
421
|
+
status: 'done',
|
|
422
|
+
previews: stills.length,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}, [dataStore]);
|
|
426
|
+
useDataSubscription('render_requests', answerPending); // changes arrive at most once —
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
// so also check on open and every ~10 s
|
|
429
|
+
void answerPending();
|
|
430
|
+
const timer = setInterval(answerPending, 10_000);
|
|
431
|
+
return () => clearInterval(timer);
|
|
432
|
+
}, [answerPending]);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The agent writes a request, waits on it with `plugin_data_get` (`afterVersion` + `waitSeconds`), and reads the stills back as images. Keep requests bounded in your schema (e.g. `"times": { "maxItems": 12 }`), prune old previews, and report failures in the request (e.g. `status: "failed"` with errors) so the agent can fix what it wrote. **Give both the requests and the results collections `user` scope** — in a `shared` collection any user of your app could plant a "result" that another user's agent then reads.
|
|
436
|
+
|
|
437
|
+
**Testing:** in the dev harness's Fias AI window, the read tools see your local `agentAccess` block against your dev data — ask "what scenes do I have?" to see what an agent sees before you publish.
|
|
438
|
+
|
|
286
439
|
**Rate limits (per minute):** `put` 120, `get` 300, `query` 60, `delete` 60, `batch` 30, `createCollection` 10
|
|
287
440
|
|
|
288
441
|
**Error handling:** Data store calls can reject on infrastructure errors or limit violations. Always use `try/catch`. Common error codes: `COLLECTION_NOT_FOUND`, `DOCUMENT_TOO_LARGE`, `STORAGE_LIMIT_REACHED`, `RATE_LIMIT`.
|
|
@@ -1187,6 +1340,7 @@ useArcheHandoff((payload) => {
|
|
|
1187
1340
|
- **You are the type gate.** If you cannot open this file, say so plainly in your UI. Half-opening it is worse than refusing.
|
|
1188
1341
|
- 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.
|
|
1189
1342
|
- Nothing hands documents to a preview, so the handler only fires in the published plugin.
|
|
1343
|
+
- **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.
|
|
1190
1344
|
|
|
1191
1345
|
### `useFiasAIActions()` — Let the platform's Fias AI assistant operate your plugin
|
|
1192
1346
|
|
|
@@ -1240,7 +1394,7 @@ Rules that matter:
|
|
|
1240
1394
|
**Permission:** `assets:read`
|
|
1241
1395
|
**Returns:** `ArcheAssetsApi`
|
|
1242
1396
|
|
|
1243
|
-
A frozen set of images the arche's contributor uploaded and pinned to the published version
|
|
1397
|
+
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.
|
|
1244
1398
|
|
|
1245
1399
|
```tsx
|
|
1246
1400
|
import { useArcheAssets } from '@fias/arche-sdk';
|
|
@@ -1257,6 +1411,37 @@ const fresh = await getUrl(assetId);
|
|
|
1257
1411
|
|
|
1258
1412
|
Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
|
|
1259
1413
|
|
|
1414
|
+
### `useArcheAssetBytes()` — Asset bytes for Web Audio and canvas
|
|
1415
|
+
|
|
1416
|
+
**Permissions:** `entities:client_invoke` and `assets:read`
|
|
1417
|
+
**Returns:** `ArcheAssetBytesApi`
|
|
1418
|
+
|
|
1419
|
+
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:
|
|
1420
|
+
|
|
1421
|
+
```tsx
|
|
1422
|
+
import { useArcheAssets, useArcheAssetBytes } from '@fias/arche-sdk';
|
|
1423
|
+
|
|
1424
|
+
const { list } = useArcheAssets();
|
|
1425
|
+
const { getMany } = useArcheAssetBytes();
|
|
1426
|
+
|
|
1427
|
+
const ctx = new AudioContext(); // resume() it on the first user gesture
|
|
1428
|
+
const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
|
|
1429
|
+
const loaded = await getMany(entries); // { [assetId]: { contentType, bytes, sizeBytes } | ArcheAssetError }
|
|
1430
|
+
const buffers = new Map<string, AudioBuffer>();
|
|
1431
|
+
for (const sound of entries) {
|
|
1432
|
+
const result = loaded[sound.assetId];
|
|
1433
|
+
if (result instanceof Error) continue; // result.code says why
|
|
1434
|
+
buffers.set(sound.name ?? sound.assetId, await ctx.decodeAudioData(result.bytes));
|
|
1435
|
+
}
|
|
1436
|
+
// From here a sound plays on cue: const src = ctx.createBufferSource(); src.buffer = …; src.connect(ctx.destination); src.start();
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
- **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.
|
|
1440
|
+
- **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.
|
|
1441
|
+
- **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.
|
|
1442
|
+
- `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.
|
|
1443
|
+
- Library sounds are MP3 or WAV, which `decodeAudioData` accepts in every browser, iOS Safari included.
|
|
1444
|
+
|
|
1260
1445
|
### `useFiasContent()` — This plugin's content pack
|
|
1261
1446
|
|
|
1262
1447
|
**Permission:** `entities:client_invoke`
|
|
@@ -1283,6 +1468,33 @@ const { url } = await content.getUrl('audio/theme.ogg'); // signed URL, for long
|
|
|
1283
1468
|
- 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.
|
|
1284
1469
|
- 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.
|
|
1285
1470
|
|
|
1471
|
+
### `useLocalStore()` — Working data kept on the user's device
|
|
1472
|
+
|
|
1473
|
+
**Permission:** `storage:local`
|
|
1474
|
+
**Returns:** `FiasLocalStoreApi`
|
|
1475
|
+
|
|
1476
|
+
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.
|
|
1477
|
+
|
|
1478
|
+
```tsx
|
|
1479
|
+
import { useLocalStore, FiasLocalStoreError } from '@fias/arche-sdk';
|
|
1480
|
+
|
|
1481
|
+
const store = useLocalStore();
|
|
1482
|
+
|
|
1483
|
+
await store.put(`session/${docId}`, snapshot); // JSON, a string, or bytes — on every change
|
|
1484
|
+
const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load; null if absent
|
|
1485
|
+
const open = await store.list('session/'); // [{ key, sizeBytes, updatedAt }], sorted by key
|
|
1486
|
+
const restored = await store.getMany(open.map((e) => e.key)); // ≤ 100 entries in one call
|
|
1487
|
+
await store.delete(`session/${docId}`); // on close
|
|
1488
|
+
const { usedBytes, budgetBytes } = await store.estimate();
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
- 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).
|
|
1492
|
+
- 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.
|
|
1493
|
+
- 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.
|
|
1494
|
+
- 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()`.
|
|
1495
|
+
- 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`.
|
|
1496
|
+
- Works in the dev harness (mock and live modes) and in the Arche Builder preview, each under its own namespace.
|
|
1497
|
+
|
|
1286
1498
|
### `useCommunityAssets()` — User-published images, visible to this arche's users
|
|
1287
1499
|
|
|
1288
1500
|
**Permissions:** `assets:community:read` (browse/view) · `assets:community:publish` (publish/unpublish/listMine)
|
|
@@ -1420,8 +1632,17 @@ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
|
|
|
1420
1632
|
// `arche_…` arche is rejected, as is a malformed one (you get an error, never
|
|
1421
1633
|
// a silent landing on the home screen).
|
|
1422
1634
|
openArche('arc_0123456789abcdef0123456789abcdef', { path: 'map' });
|
|
1635
|
+
|
|
1636
|
+
// Open a chat channel in the Chat arche.
|
|
1637
|
+
openArche('arche_chat', { payload: { kind: 'open_channel', channelId } });
|
|
1423
1638
|
```
|
|
1424
1639
|
|
|
1640
|
+
**Opening a chat channel.** The `open_channel` payload is a pointer, never
|
|
1641
|
+
access: Chat re-reads the channel under the user's own membership, and a
|
|
1642
|
+
non-member lands on the Chat home with a notice instead. Make the user a member
|
|
1643
|
+
first (LitCraft: `join_guild_chat` on the LitCraft Player entity). Code that
|
|
1644
|
+
switches exhaustively on `ArcheHandoffPayloadInput`'s `kind` needs the new case.
|
|
1645
|
+
|
|
1425
1646
|
**Receiving a deep link.** There is nothing extra to opt into: `currentPath` is
|
|
1426
1647
|
the host's path expressed in YOUR arche's route space — the same space
|
|
1427
1648
|
`navigateTo` accepts — so an arche that wants to be a deep-link target just
|
|
@@ -1675,26 +1896,26 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1675
1896
|
|
|
1676
1897
|
**Fields:**
|
|
1677
1898
|
|
|
1678
|
-
| Field | Req | Description
|
|
1679
|
-
| --------------------- | ---- |
|
|
1680
|
-
| `name` | Yes | Plugin identifier (lowercase, hyphens)
|
|
1681
|
-
| `version` | Yes | Semver (e.g., `"1.0.0"`)
|
|
1682
|
-
| `description` | Yes | Short marketplace description
|
|
1683
|
-
| `expandedDescription` | No | Long-form marketplace copy
|
|
1684
|
-
| `main` | Yes | Entry point source file
|
|
1685
|
-
| `archeType` | Yes | `"tool"` or `"site"`
|
|
1686
|
-
| `categorySlug` | No | Marketplace category for discovery
|
|
1687
|
-
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on local
|
|
1688
|
-
| `tags` | No | Discovery tags
|
|
1689
|
-
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products`
|
|
1690
|
-
| `permissions` | Yes | Array of permission scopes (see below)
|
|
1691
|
-
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below)
|
|
1692
|
-
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below)
|
|
1693
|
-
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"`
|
|
1694
|
-
| `isListed` | No | Default `true`. Visible in marketplace store
|
|
1695
|
-
| `sdk` | Yes | SDK version range
|
|
1696
|
-
| `dependencies` | No | npm packages with **exact** versions (max 20)
|
|
1697
|
-
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit
|
|
1899
|
+
| Field | Req | Description |
|
|
1900
|
+
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1901
|
+
| `name` | Yes | Plugin identifier (lowercase, hyphens) |
|
|
1902
|
+
| `version` | Yes | Semver (e.g., `"1.0.0"`) |
|
|
1903
|
+
| `description` | Yes | Short marketplace description |
|
|
1904
|
+
| `expandedDescription` | No | Long-form marketplace copy |
|
|
1905
|
+
| `main` | Yes | Entry point source file |
|
|
1906
|
+
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
1907
|
+
| `categorySlug` | No | Marketplace category for discovery |
|
|
1908
|
+
| `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. |
|
|
1909
|
+
| `tags` | No | Discovery tags |
|
|
1910
|
+
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
1911
|
+
| `permissions` | Yes | Array of permission scopes (see below) |
|
|
1912
|
+
| `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
|
|
1913
|
+
| `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
|
|
1914
|
+
| `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
|
|
1915
|
+
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
1916
|
+
| `sdk` | Yes | SDK version range |
|
|
1917
|
+
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
1918
|
+
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
1698
1919
|
|
|
1699
1920
|
**Pricing detail:**
|
|
1700
1921
|
|
|
@@ -1709,7 +1930,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
1709
1930
|
|
|
1710
1931
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
1711
1932
|
|
|
1712
|
-
**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`
|
|
1933
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `entities:invoke`, `entities:client_invoke`, `entities:image_generate`, `entities:image_edit`, `entities:audio_generate`, `entities:web_search`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `vault:user-documents:read`, `vault:user-documents:write`, `vault:user-documents:edit`, `vault:workspace-documents:read`, `assets:read`, `assets:community:read`, `assets:community:publish`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`
|
|
1713
1934
|
|
|
1714
1935
|
**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`.
|
|
1715
1936
|
|
|
@@ -1781,6 +2002,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1781
2002
|
- `storage_read`: 300/minute
|
|
1782
2003
|
- `storage_list`, `storage_delete`: 60/minute
|
|
1783
2004
|
- Content reads (`useFiasContent()`): 120/minute — read related files together with `getMany` (up to 100 per call)
|
|
2005
|
+
- Asset byte reads (`useArcheAssetBytes()`): 120/minute — load a set of sounds with one `getMany` (up to 100 per call)
|
|
1784
2006
|
|
|
1785
2007
|
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.
|
|
1786
2008
|
|