@fias/arche-sdk 2.19.2 → 2.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.
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.19.0 -->
1
+ <!-- fias-sdk-guide-version: 2.21.0 -->
2
2
 
3
3
  # FIAS Plugin Development Guide
4
4
 
@@ -12,9 +12,34 @@ package.json # Dependencies and scripts
12
12
  src/
13
13
  index.tsx # Entry point — must render into #root with <FiasProvider> wrapper
14
14
  App.tsx # Main component
15
+ public/ # Optional — static images, fonts, sounds referenced as '/name.png'
16
+ content/ # Optional — a content pack read with useFiasContent() (lessons, data, media)
15
17
  vite.config.ts # Vite dev server config (port 3100)
16
18
  ```
17
19
 
20
+ ## Where does each kind of data go?
21
+
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
+
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 sounds 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
+ | 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. |
31
+ | 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. |
32
+ | A small piece of UI state that should survive a reload (a setting, the last tab) | `usePersistentState()` | Debounced writes to `useFiasStorage()`. |
33
+ | Images one user shares with the app's other users | `useCommunityAssets()` | Moderated before anyone else sees them. |
34
+ | 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
+
36
+ **Common mistakes:**
37
+
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/`.
39
+ - **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
+ - **Using the DataStore or `useFiasStorage()` for data that is the same for every user.** That is content.
41
+ - **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.
42
+
18
43
  ## SDK API Reference
19
44
 
20
45
  All hooks require the app to be wrapped in `<FiasProvider>`:
@@ -479,6 +504,25 @@ generate({
479
504
 
480
505
  Mode values: `'text-to-image'` (baseline), `'image-to-image'` (requires `referenceImage`), `'vector'` (SVG output). Style values: `'illustration'` (stylized / digital-art) or `'photoreal'`.
481
506
 
507
+ **Several reference images, and output resolution.** A model whose `maxReferenceImages` (from `useImageEntities`) is more than one takes them together in `referenceImages` — Nano Banana 2 (`ent_modeldef_nano_banana_2`) takes up to 14. There is no role per image: the model reads them in order, so the prompt says which is which. `resolution` picks one of the model's `supportedResolutions`; higher costs more (`creditsPerImageByResolution`):
508
+
509
+ ```tsx
510
+ generate({
511
+ entityId: 'ent_modeldef_nano_banana_2',
512
+ prompt:
513
+ 'The knight from the first image, standing on a cliff at dawn, painted in the style of the second and third images',
514
+ referenceImages: [
515
+ { fileId: knight.fileId },
516
+ { fileId: styleA.fileId },
517
+ { fileId: styleB.fileId },
518
+ ],
519
+ size: '21:9',
520
+ resolution: '2K', // '512px' | '1K' (default) | '2K' | '4K'
521
+ });
522
+ ```
523
+
524
+ Use `referenceImage` for one picture and `referenceImages` for several — not both. More than the model takes returns `TOO_MANY_REFERENCE_IMAGES`; a resolution it does not list returns `INVALID_RESOLUTION`. Prefer `fileId` / `archeAssetId` over `dataUrl` when sending several: each `dataUrl` counts against the bridge message size. The result's `costCents` is what the call actually cost, markup included.
525
+
482
526
  Use `npx fias-dev entities --detail <entityId>` to check supported sizes, qualities, and styles for each model.
483
527
 
484
528
  #### Full example
@@ -545,14 +589,15 @@ const { entities, isLoading } = useImageEntities({
545
589
  supportsReferenceImage: true,
546
590
  });
547
591
  // entities[i]: { entityId, displayName, provider, modes, supportedSizes,
548
- // supportsReferenceImage, isPlatformRecommended,
549
- // creditsPerImage, estimatedDurationMs, company, strengths,
550
- // providerParams, ... }
592
+ // supportsReferenceImage, maxReferenceImages,
593
+ // supportedResolutions, defaultResolution, isPlatformRecommended,
594
+ // creditsPerImage, creditsPerImageByResolution,
595
+ // estimatedDurationMs, company, strengths, providerParams, ... }
551
596
  ```
552
597
 
553
598
  Each entry is scrubbed — only fields a UI legitimately needs (no provider model strings, no endpoints, no execution config).
554
599
 
555
- `creditsPerImage` is what one image costs the user in credits, markup already included — it matches what the ledger deducts, so it is the figure to show. `estimatedDurationMs` sizes a progress indicator; `company` and `strengths` are model-info copy. All optional: absent means the model has nothing to declare, not that the data is missing.
600
+ `creditsPerImage` is what one image costs the user in credits, markup already included — it matches what the ledger deducts, so it is the figure to show. For a model priced by resolution it is the price at `defaultResolution`, and `creditsPerImageByResolution` has each resolution's; Nano Banana 2 also bills reference images and thinking as tokens on top (a fraction of a credit to a few credits). `estimatedDurationMs` sizes a progress indicator; `company` and `strengths` are model-info copy. All optional: absent means the model has nothing to declare, not that the data is missing.
556
601
 
557
602
  **Tunable engine parameters.** `providerParams` describes what a model exposes for tuning — enough to render a settings panel without hardcoding a thing:
558
603
 
@@ -564,6 +609,8 @@ const seed = entity.providerParams?.find((p) => p.key === 'seed');
564
609
  await generate({ entityId, prompt, providerParams: { seed: 42 } });
565
610
  ```
566
611
 
612
+ Nano Banana 2 exposes `seed`, `temperature` (0–2) and `thinking_level` (`'minimal'` default, or `'high'` — the model plans the picture first: better at layouts and directions like "seen from above", slower, and its thinking tokens are billed).
613
+
567
614
  **The `category` trap.** A param carrying `category: 'quality'` or `'style'` does NOT belong in `providerParams` — send it on the matching top-level field. Providers read those two from the top level only, and an unrecognized `providerParams` key is dropped without an error, so the image generates at the model's default **and you are billed for it anyway**:
568
615
 
569
616
  ```tsx
@@ -772,30 +819,26 @@ const { referenceId } = await vault.attach(documentId, {
772
819
  await vault.detach(documentId, referenceId);
773
820
  ```
774
821
 
775
- **Saving into the user's OWN files** — `upload(bytes, { ..., userVisible: true })`. By default everything your app writes here is _arche-private_: yours to read and write, invisible in the user's Fias files, unusable by other arches. That is the right home for your app's own state (caches, session snapshots, working files). It is the wrong home for the artifact the user made and asked you to keep — an edited image, an exported document — because they will go looking for it and it will not be there.
822
+ **Saving into the user's Documents** — `upload(bytes, { ..., destination: 'documents' })` (`userVisible: true` is the same thing). By default everything your app writes here is _arche-private_: yours to read and write, invisible in the user's Fias files, unusable by other arches. That is the right home for your app's own state (caches, session snapshots, working files). It is the wrong home for the artifact the user made and asked you to keep — an edited image, an exported document — because they will go looking for it and it will not be there.
776
823
 
777
824
  ```tsx
778
825
  const saved = await vault.upload(bytes, {
779
- name: 'sunset.png',
826
+ name: 'sunset.png', // a suggestion — the user can change it
780
827
  mimeType: 'image/png',
781
- userVisible: true,
828
+ destination: 'documents',
782
829
  });
830
+ // saved.documentId, saved.name (what the user kept), saved.access ('none' | 'read' | 'write')
783
831
  ```
784
832
 
785
- Requires the `vault:user-documents:write` permission, and the HOST asks the user to confirm each save by name — you cannot script it, batch it, or pre-approve it. A decline rejects with `SAVE_DECLINED`; that is a normal outcome, not an error to retry. The file lands in `/My-Fias/<your arche's folder>/`, which the platform assigns — you cannot choose it, and `folderPath` is rejected alongside `userVisible`. Your arche must be registered for a user-visible folder platform-side (`ARCHE_NOT_REGISTERED_FOR_USER_VAULT` if not). Passing `replacesDocumentId` saves a new version in place; a version keeps its predecessor's visibility, so you cannot flip a user's visible file to private or vice versa (`VISIBILITY_MISMATCH`). Not available in preview or the dev harness — test it in the published plugin.
833
+ The host opens its **Save dialog**: the user picks a folder in their Documents, can rename the file, and settles a name clash (keep both, or replace a file of the same type). You cannot script it, batch it, or pre-approve it, and you never learn the folder. Requires `vault:user-documents:write`. Cancelling rejects with `SAVE_DECLINED`; that is a normal outcome, not an error to retry. `folderPath`, `replacesDocumentId`, `tags` and `sensitivity` are rejected alongside it (the user decides those). Not available in preview or the dev harness — test it in the published plugin.
786
834
 
787
- **Handing over a file you already made** — `promoteToUserFiles(documentId)`. If the artifact already exists as one of your private documents, do NOT re-upload it with `userVisible` — that creates a second copy, doubles the user's storage, and orphans anything pointing at the original. Promote it instead: the same document moves into the user's files, keeping its `documentId`.
835
+ **Afterwards the file is the user's.** `useVaultDocuments()` no longer lists it. What you keep is reported as `access`: with `vault:user-documents:read` you can open it again (`'read'`) through `useVaultUserDocuments()`, and with `vault:user-documents:edit` the dialog offers the user "keep editing" (`'write'`) so you can keep saving it with `useVaultUserDocuments().saveContent`. The user can change either in Arche Access.
788
836
 
789
- ```typescript
790
- try {
791
- await vault.promoteToUserFiles(documentId); // the HOST asks the user; you cannot script it
792
- } catch (err) {
793
- if (err instanceof FiasBridgeError && err.code === 'SAVE_DECLINED') return; // a normal outcome
794
- throw err;
795
- }
796
- ```
837
+ **Arche Saves, automatically** — `upload(bytes, { ..., destination: 'arche-saves' })` saves into the user's `Arche Saves/<your label>/` with no dialog. Only for an arche a Fias admin has designated (that issues its label, which is yours for good) and that declares `vault:arche-saves:write` (human-reviewed). Otherwise it rejects with `ARCHE_NOT_DESIGNATED` or `PERMISSION_DENIED`. The files are the user's to keep, move or delete; you can keep reading them while they stay in Arche Saves.
797
838
 
798
- Same requirements as a user-visible upload (`vault:user-documents:write`, a registered folder — the platform picks the destination). It is **one-way and it changes what you can do**: once the file is the user's, this hook's `saveContent`, `update` and `delete` reject, and there is no way to take a file back out of the user's files. To keep saving it, declare `vault:user-documents:edit` — the host's confirmation then offers the user "let this app keep editing", the result carries `editAccess`, and when it is `true` you carry on with `useVaultUserDocuments().saveContent` (below). Without it, promote when the user is DONE — never while your editor still needs to save. After a `SAVE_DECLINED`, re-`list()` before assuming nothing moved: the host performs the move itself, and if its page is torn down mid-move you are told "declined" for a promote that completed — the document's `visibility` is the truth. Re-asking is capped at 5 a minute, like the picker. Not available in preview or the dev harness.
839
+ **`saveCopy(documentId, { suggestedName? })`** — hand the user a copy of one of your own documents through the same dialog, without re-sending the bytes (needs `vault:documents:read` too). Your working file is unchanged; delete it afterwards if you no longer need it.
840
+
841
+ **A document's visibility never changes.** Your private documents stay private, and a file in the user's files stays theirs — there is no call that moves one into the other.
799
842
 
800
843
  **Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
801
844
 
@@ -834,6 +877,7 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
834
877
  - Grants are **per-document** and **standing**: the plugin can re-read granted documents in later sessions until the user removes access in **Vault → App Access**.
835
878
  - After revocation, `get`/`getDownloadUrl` reject with `DOCUMENT_NOT_FOUND` — handle that path gracefully (drop the document from your UI; do not retry in a loop).
836
879
  - `proprietary`-sensitivity documents are never grantable (they don't even appear in the picker).
880
+ - **Say what you can open:** `pick({ accept: ['application/pdf', 'image/*', '.docx'] })` takes MIME types, MIME families or extensions, up to 20. The host's Open dialog offers the matching files; the user can still reveal the rest, shown disabled. A malformed list rejects with `INVALID_PARAMS`. Omitted, every type is offered.
837
881
  - Every read is audited by the platform; a new document _version_ is a new documentId, so a re-pick is needed after the user replaces a document.
838
882
  - `getDownloadUrl` is for DISPLAY (`<img src>`); your iframe cannot fetch the URL (CSP). To process binary content (parse a PDF, transform an image), use `getBytes(id)` — size-capped at 15 MB (`CONSENTED_DOCUMENT_TOO_LARGE` names the cap; ask the user to open the file manually instead). `getBytes` has a tighter per-minute budget than the rest of the family because it can move megabytes: read sequentially, and surface a rate-limit error rather than retrying in a loop.
839
883
  - Revocation behaves differently per method: `getBytes` and `get` re-check the grant on every call, so they start failing immediately. A URL already handed out by `getDownloadUrl` keeps working until it expires (1 hour) — that is the documented contract, not a bug.
@@ -880,7 +924,10 @@ const saved = await userDocs.saveContent({
880
924
 
881
925
  ### Sending a document for signature — Document Sign handoff
882
926
 
883
- **Permission:** `navigation:open_arche`.
927
+ **Permission:** `navigation:open_arche` to hand the document over. Reading the
928
+ envelope's status afterwards is a separate act (`docsign:envelopes:read`) —
929
+ your app only ever sees envelopes it started this way, and never their
930
+ document bytes.
884
931
 
885
932
  Hand one of the user's Vault PDFs to Document Sign with recipients pre-filled.
886
933
  They place the signature fields and press Send there — a handoff never sends
@@ -924,7 +971,9 @@ created, which is why the outcome is there.
924
971
  **Treat the result as a pointer, never as a record.** It tells you which
925
972
  envelope to look at; it is not evidence of what happened to it. Match it to an
926
973
  outstanding request by `requestId`, consume it once, and never re-point an
927
- existing association because a `correlationId` matched.
974
+ existing association because a `correlationId` matched. Anything you would act
975
+ on — who signed, what is outstanding — comes from reading the envelope under
976
+ `docsign:envelopes:read`, not from the payload.
928
977
 
929
978
  **A handoff ENDS YOUR APP'S PROCESS. Anything you will need afterwards has to
930
979
  be written down before you call `openArche`.** The host navigates away and
@@ -953,6 +1002,65 @@ local dev. Judging on arrival therefore rejects every genuine reply, not an
953
1002
  occasional one. Hold the result until you know what was outstanding, then
954
1003
  evaluate it.
955
1004
 
1005
+ ### `useDocumentSign()` — Follow an envelope you handed over
1006
+
1007
+ **Permission:** `docsign:envelopes:read`
1008
+
1009
+ The other half of the handoff above. Once the user has pressed Send in
1010
+ Document Sign, this is how your app learns what happened — status, who the
1011
+ recipients are, where each of them has got to, and the audit timeline.
1012
+
1013
+ ```tsx
1014
+ import { useDocumentSign } from '@fias/arche-sdk';
1015
+
1016
+ const sign = useDocumentSign();
1017
+
1018
+ const envelope = await sign.getEnvelope(approval.envelopeId);
1019
+ if (envelope.status === 'completed') markApproved(approval.id);
1020
+
1021
+ // The audit timeline, oldest first.
1022
+ const { events } = await sign.listEvents(approval.envelopeId);
1023
+ ```
1024
+
1025
+ **The contract:**
1026
+
1027
+ - **Only envelopes YOUR app started**, via a `sign_document` handoff. Not one
1028
+ the user created in Document Sign directly, not another app's. Two
1029
+ independent gates, both the user's: they grant access by pressing Send on an
1030
+ envelope you handed over (named to them on the send screen, revocable in
1031
+ Vault → App Access), and the platform separately requires the envelope to
1032
+ record your app as its origin. Either one missing means no read.
1033
+ - **This is where truth comes from.** The `sign_document_result` handoff is a
1034
+ pointer; it rides navigation state, which is forgeable. Write "signed" into
1035
+ an approval record only on the strength of a read.
1036
+ - **No list verb.** Keep the `envelopeId` on your own record and read by id —
1037
+ the same reason the result has to be matched to an outstanding `requestId`
1038
+ and consumed once.
1039
+ - **Drafts are not readable.** Send is the moment the user grants access, so a
1040
+ `saved_draft` result gives you an id to keep, and `getEnvelope` on it rejects
1041
+ until the envelope is actually sent. Do not poll a draft.
1042
+ - **One error for every refusal.** `ENVELOPE_NOT_FOUND_OR_NOT_GRANTED` covers
1043
+ an unknown id, an envelope you did not start, one on a workspace this user
1044
+ cannot act for, a draft, and one whose access the user has removed. Deliberately
1045
+ indistinguishable — a specific "it exists but you cannot see it" would let
1046
+ anyone probe for envelope ids. Treat it as "we can no longer follow this",
1047
+ not as a bug, and stop asking.
1048
+ - **You never see the document.** No bytes, no download URL — and no recipient
1049
+ email addresses either. Name, role, status and timestamps only. If your app
1050
+ supplied the recipients, it already has their addresses.
1051
+ - **The timeline is a projection, not the stored rows.** `type`, `actorKind`,
1052
+ `recipientId`, `createdAt`. Signers' IP addresses and user agents, the event
1053
+ payloads, and the hash-chain columns stay on the platform — they belong to
1054
+ the signing certificate, not to your app.
1055
+ - **There are no webhooks.** Nothing calls you when a recipient signs. Read
1056
+ when your UI needs to know — on mount, on a refresh the user asked for — not
1057
+ on a timer.
1058
+ - Terminal envelopes (`voided`, `expired`, `declined`) stay readable. A
1059
+ terminal outcome is part of the trail you came for; your UI should render it
1060
+ rather than leaving the record stuck on "in progress".
1061
+ - Not available in the builder preview: a preview tenant has no arche row, so
1062
+ it can never be an envelope's origin.
1063
+
956
1064
  ### `useArcheHandoff()` — Receive a document the user asked to open in your app
957
1065
 
958
1066
  **Permission:** none to receive. Reading the document is a separate act (`vault:user-documents:read`, via `useVaultUserDocuments().getBytes`).
@@ -1050,6 +1158,32 @@ const fresh = await getUrl(assetId);
1050
1158
 
1051
1159
  Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
1052
1160
 
1161
+ ### `useFiasContent()` — This plugin's content pack
1162
+
1163
+ **Permission:** `entities:client_invoke`
1164
+ **Returns:** `FiasContentApi`
1165
+
1166
+ Ship large or numerous files — lessons, articles, data, images, audio, fonts — in a `content/` directory beside `src/`. They are not bundled: `fias-dev submit` uploads them separately (only the files that changed), the review reads every one, and the published plugin reads them at runtime through this hook. The host page fetches, verifies and caches them; your plugin never touches the network.
1167
+
1168
+ ```tsx
1169
+ import { useFiasContent, FiasContentError } from '@fias/arche-sdk';
1170
+
1171
+ const content = useFiasContent();
1172
+
1173
+ const lessons = await content.list('lessons/'); // [{ path, sizeBytes, mimeType }]
1174
+ const markdown = await content.getText('lessons/01-intro.md');
1175
+ const course = await content.getJson<Course>('course.json');
1176
+ const batch = await content.getMany(['a.md', 'b.md'], 'text'); // up to 100 files per call
1177
+ const cover = await content.getObjectUrl('img/cover.webp'); // blob: URL for <img src>
1178
+ const { url } = await content.getUrl('audio/theme.ogg'); // signed URL, for long streaming audio
1179
+ ```
1180
+
1181
+ - Paths are relative to `content/`. Formats: `.md`, `.txt`, `.json` (text, ≤ 5 MB each); `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.avif` (static, single-frame images, ≤ 20 MB and 50 megapixels); `.ogg` (audio, ≤ 50 MB); `.woff2` (fonts, ≤ 5 MB). A pack holds at most 10,000 files, 2,000 images, 20 MB of text and 500 MB in total. Run `npx fias-dev content check` to validate without submitting.
1182
+ - Prefer `getObjectUrl` for images and fonts: repeat views come from the host cache. `getUrl` is for media that must stream or seek; it bypasses the cache, and a URL held past its `expiresAt` must be requested again.
1183
+ - Failures throw `FiasContentError` with a `code`: `CONTENT_NOT_FOUND`, `CONTENT_NOT_TEXT`, `CONTENT_TOO_LARGE`, `CONTENT_UNAVAILABLE`, `CONTENT_THROTTLED`, `CONTENT_UNAVAILABLE_IN_PREVIEW`, `PERMISSION_DENIED`, `RATE_LIMIT`. `getMany` returns the error for a failed path instead of throwing.
1184
+ - 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.
1185
+ - 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.
1186
+
1053
1187
  ### `useCommunityAssets()` — User-published images, visible to this arche's users
1054
1188
 
1055
1189
  **Permissions:** `assets:community:read` (browse/view) · `assets:community:publish` (publish/unpublish/listMine)
@@ -1159,18 +1293,29 @@ openArche('arc_0123456789abcdef0123456789abcdef', { newTab: true });
1159
1293
  openArche('arc_0123456789abcdef0123456789abcdef', { path: 'map' });
1160
1294
  ```
1161
1295
 
1162
- **Receiving a deep link.** There is nothing extra to opt into: `currentPath` in
1163
- the init payload is the full platform path the host loaded you at, so an arche
1164
- that wants to be a deep-link target just reads it once at boot.
1296
+ **Receiving a deep link.** There is nothing extra to opt into: `currentPath` is
1297
+ the host's path expressed in YOUR arche's route space — the same space
1298
+ `navigateTo` accepts — so an arche that wants to be a deep-link target just
1299
+ reads it at boot. The `/a/<arche>` prefix is stripped for you, and your home
1300
+ screen reads `/`.
1165
1301
 
1166
1302
  ```tsx
1167
1303
  const { currentPath } = useFiasNavigation();
1168
- // e.g. '/a/arc_0123…/map' → 'map'
1169
- const page = currentPath.split('/').slice(3).join('/') || 'home';
1304
+ // a deep link to '/a/arc_0123…/map' arrives here as '/map'
1305
+ const page = currentPath.replace(/^\//, '') || 'home';
1170
1306
  ```
1171
1307
 
1172
- It is set once, when your app mounts. A deep link from another arche always
1173
- mounts you fresh, so that is exactly when you need it.
1308
+ It is `''` until the host's `init` message lands — the bridge handshake is
1309
+ async, so your first render usually beats it. Treat `''` as "not known yet"
1310
+ rather than as your home screen; that distinction is what keeps a direct entry
1311
+ such as a share link from being lost to the race.
1312
+
1313
+ After that it TRACKS the host: it updates when you call `navigateTo`, and when
1314
+ the user moves the host themselves with browser back/forward. So a router
1315
+ driven off `currentPath` stays in step with the address bar, and the back
1316
+ button works the way your users expect — you do not have to mirror the path in
1317
+ your own state. The dev harness echoes navigations the same way, so what you
1318
+ see locally is what ships.
1174
1319
 
1175
1320
  ### Opening external links
1176
1321
 
@@ -1480,6 +1625,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
1480
1625
  ### Size and File Limits
1481
1626
 
1482
1627
  - **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)
1628
+ - **Content packs:** files in `content/` don't count toward the bundle either. If content (lessons, data, media) is what is pushing you toward the cap, move it there — see "Where does each kind of data go?" above. A pack holds up to 10,000 files and 500 MB (20 MB of text, 2,000 images)
1483
1629
  - **Dependencies:** Max 20, exact semver versions only (e.g., `"4.4.7"`, not `"^4.4.7"`)
1484
1630
  - Platform packages (`react`, `react-dom`, `@fias/arche-sdk`) are provided — do not include in `dependencies`
1485
1631
 
@@ -1489,6 +1635,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
1489
1635
  - `storage_write`: 120/minute
1490
1636
  - `storage_read`: 300/minute
1491
1637
  - `storage_list`, `storage_delete`: 60/minute
1638
+ - Content reads (`useFiasContent()`): 120/minute — read related files together with `getMany` (up to 100 per call)
1492
1639
 
1493
1640
  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.
1494
1641
 
@@ -1578,6 +1725,8 @@ npm run submit
1578
1725
  # Builds, validates, packages, uploads, submits for AI review
1579
1726
  ```
1580
1727
 
1728
+ If the plugin has a `content/` folder, submit uploads it too — only the files that changed — and the review reads the whole pack on every publish. `npx fias-dev content check` validates it without submitting.
1729
+
1581
1730
  The first time you list a plugin in the marketplace there is a one-time
1582
1731
  listing fee; subsequent submissions of the same plugin only pay a small
1583
1732
  per-review fee. The harness fetches the exact amount from the server and