@fias/arche-sdk 2.21.0 → 2.23.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.
Files changed (46) hide show
  1. package/dist/bridge.d.ts +33 -3
  2. package/dist/bridge.d.ts.map +1 -1
  3. package/dist/bridge.js +98 -15
  4. package/dist/bridge.js.map +1 -1
  5. package/dist/bridge.test.js +173 -0
  6. package/dist/bridge.test.js.map +1 -1
  7. package/dist/entity-ops.d.ts +3 -0
  8. package/dist/entity-ops.d.ts.map +1 -1
  9. package/dist/entity-ops.js +4 -1
  10. package/dist/entity-ops.js.map +1 -1
  11. package/dist/generated/permissions.d.ts +1 -1
  12. package/dist/generated/permissions.d.ts.map +1 -1
  13. package/dist/generated/permissions.js +4 -0
  14. package/dist/generated/permissions.js.map +1 -1
  15. package/dist/hooks.d.ts +82 -1
  16. package/dist/hooks.d.ts.map +1 -1
  17. package/dist/hooks.js +143 -20
  18. package/dist/hooks.js.map +1 -1
  19. package/dist/hooks.test.js +93 -1
  20. package/dist/hooks.test.js.map +1 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +14 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +329 -36
  26. package/dist/invocation-params.d.ts +92 -0
  27. package/dist/invocation-params.d.ts.map +1 -0
  28. package/dist/invocation-params.js +180 -0
  29. package/dist/invocation-params.js.map +1 -0
  30. package/dist/invocation-params.test.d.ts +2 -0
  31. package/dist/invocation-params.test.d.ts.map +1 -0
  32. package/dist/invocation-params.test.js +140 -0
  33. package/dist/invocation-params.test.js.map +1 -0
  34. package/dist/panels.d.ts.map +1 -1
  35. package/dist/panels.js.map +1 -1
  36. package/dist/panels.test.js +1 -3
  37. package/dist/panels.test.js.map +1 -1
  38. package/dist/protocol.d.ts +8 -0
  39. package/dist/protocol.d.ts.map +1 -1
  40. package/dist/protocol.js +8 -0
  41. package/dist/protocol.js.map +1 -1
  42. package/dist/types.d.ts +219 -4
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/templates/default/AGENTS.md +140 -4
  46. package/templates/default/CLAUDE.md +140 -4
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.21.0 -->
1
+ <!-- fias-sdk-guide-version: 2.23.0 -->
2
2
 
3
3
  # FIAS Plugin Development Guide
4
4
 
@@ -465,6 +465,85 @@ The text capabilities:
465
465
 
466
466
  Browse specific models with `npx fias-dev entities`.
467
467
 
468
+ #### Multi-turn conversations — `history`
469
+
470
+ `input` is the NEW user message. Send the earlier turns, oldest first, in `history`:
471
+
472
+ ```tsx
473
+ import { fitHistoryToLimits } from '@fias/arche-sdk';
474
+
475
+ const request = {
476
+ entityId: model.entityId,
477
+ systemPrompt: 'You are a helpful assistant.',
478
+ input: newMessage,
479
+ };
480
+ await invoke({
481
+ ...request,
482
+ // [{ role: 'user' | 'assistant', content }] — pass `request` so the history
483
+ // gets only the bytes the rest of the call leaves.
484
+ history: fitHistoryToLimits(messages, { maxTurns: 20, request }),
485
+ });
486
+ ```
487
+
488
+ Every turn is billed as input tokens on EVERY call, so send a window, not the whole transcript. Limits: 40 turns, 100,000 characters per turn, 150,000 in total (`ENTITY_INVOCATION_HISTORY_LIMITS`); a call over them, or a turn with a role other than `user` / `assistant`, is rejected with `INVALID_PARAMS`. The whole call must ALSO fit the bridge: 256 KB serialized for a text call, 1.5 MB with images (`ENTITY_INVOCATION_REQUEST_LIMITS`). A character can take up to 4 bytes (CJK 3, emoji 4, a newline or quote 2), so a history within the character limits can still be too large. `fitHistoryToLimits(turns, { request })` keeps the newest turns that fit both; `invoke()` refuses an oversized call with `REQUEST_TOO_LARGE` before sending it.
489
+
490
+ #### Reasoning, thinking, and several streams at once
491
+
492
+ ```tsx
493
+ const controller = new AbortController();
494
+ const result = await invoke(
495
+ {
496
+ entityId: model.entityId,
497
+ input,
498
+ history,
499
+ // Only for models with capabilities.reasoning; ignored otherwise.
500
+ parameters: { reasoning: { effort: 'medium', includeThinking: true } }, // effort: 'off' | 'low' | 'medium' | 'high'
501
+ },
502
+ {
503
+ onToken: (text) => appendAnswer(text), // this call's answer chunks
504
+ onThinking: (text) => appendThinking(text), // this call's reasoning text
505
+ signal: controller.signal, // controller.abort() → rejects with code 'ABORTED'
506
+ },
507
+ );
508
+ const cost = result.metadata?.usage?.costCredits; // what this reply cost the user, for a cost label
509
+ ```
510
+
511
+ - **Per-call callbacks** (`onToken` / `onThinking`) are scoped to that one call, so you can run several streams side by side — e.g. the same prompt against three models. A call that passes `onToken` leaves the hook's shared `streamingText` / `result` / `error` alone — its failure arrives only as the rejected promise.
512
+ - **Reasoning** uses more output tokens (billed) but always fits inside the model's output ceiling, so it never raises the credit hold. Set `includeThinking` only if you show the thinking — on some providers it raises the cost of a `'low'` request.
513
+ - **Aborting stops your UI, not the charge.** The platform finishes and bills the generation.
514
+ - A stream fails with `TIMEOUT` only after 2 minutes with no sign of life (the host sends keepalives during silent reasoning) or 15 minutes in total.
515
+ - `result.metadata.usage` has `inputTokens`, `outputTokens`, and `costCredits` when the platform reported them. Display only — the charge is already settled.
516
+ - A failed call rejects with a `FiasBridgeError`: branch on `err.code` (e.g. `INSUFFICIENT_CREDITS`, `PROVIDER_OVERLOADED`), and show `err.requestId` as a support reference when present.
517
+
518
+ ### `useTextModels()` — Let the user pick a text model
519
+
520
+ **Permission:** `entities:invoke` · **Returns:** `FiasTextModel[]`
521
+
522
+ For an app whose USER chooses the model (a chat app, a model comparison), read the platform's live catalog instead of hardcoding ids. Each entry's `entityId` goes straight into `invoke`. The list is `[]` until the host delivers it (usually immediately), and retired models drop out, so fall back to a `recommended` entry when a saved choice disappears.
523
+
524
+ ```tsx
525
+ import { useTextModels } from '@fias/arche-sdk';
526
+
527
+ function ModelPicker({ value, onChange }: { value: string; onChange: (id: string) => void }) {
528
+ const models = useTextModels();
529
+ const selected = models.find((m) => m.entityId === value) ?? models.find((m) => m.recommended);
530
+ return (
531
+ <select value={selected?.entityId ?? ''} onChange={(e) => onChange(e.target.value)}>
532
+ {models.map((m) => (
533
+ <option key={m.entityId} value={m.entityId}>
534
+ {m.providerDisplayName} · {m.displayName}
535
+ {m.pricing
536
+ ? ` — ${m.pricing.inputCreditsPerMillionTokens}/${m.pricing.outputCreditsPerMillionTokens} cr per 1M tok`
537
+ : ''}
538
+ </option>
539
+ ))}
540
+ </select>
541
+ );
542
+ }
543
+ ```
544
+
545
+ `capabilities.vision` says the model accepts `images`; `capabilities.reasoning` says it honours `parameters.reasoning`. `pricing` is what the user pays per 1M tokens in credits (1 credit = 1¢), markup included — for display only.
546
+
468
547
  ### `useImageGeneration()` — Generate images via AI models
469
548
 
470
549
  **Permission:** `entities:image_generate`
@@ -884,6 +963,17 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
884
963
  - **Workspaces.** The picker offers the files of the workspace the user is acting in — the workspace switcher in Fias: their Personal vault, or an org workspace they belong to — and `list()` returns the grants made in that workspace, so switching workspace changes what `list()` shows. A document you already hold a grant on stays readable by id (`get`, `getBytes`, `getDownloadUrl`, `saveContent`) whichever workspace the user is in: a grant belongs to the workspace the document lives in. A plugin never names a workspace itself. A member's role caps what they can share and read: a viewer can only share standard-sensitivity documents, and a document above a member's tier reads as `DOCUMENT_NOT_FOUND` even if another member granted it.
885
964
  - **Testable in the dev harness (mock mode).** `pick()` grants a small fixture set — a text file, a real PDF, and one deliberately over the 15 MB cap — and `list` / `get` / `getDownloadUrl` / `getBytes` all resolve against it, including the refusals (`BINARY_DOCUMENT` on a text read of a PDF, `CONSENTED_DOCUMENT_TOO_LARGE`, `DOCUMENT_NOT_FOUND`). Run the harness with `FIAS_HARNESS_VAULT_PICK=canceled` to exercise the cancel branch. Not available in the builder preview — `pick()` resolves `{ canceled: true }` there.
886
965
 
966
+ **Workspace-approved reads** — `userDocs.workspace.*`, permission `vault:workspace-documents:read`, declared TOGETHER WITH `vault:user-documents:read` (escalates to human review). When the user is acting in an **org workspace** they belong to, whose admin (or its Company) has **approved your arche**, you can read that workspace's documents without a per-document pick — every document the acting member can see in Documents and Arche Saves, at their role, never proprietary:
967
+
968
+ ```tsx
969
+ const docs = await userDocs.workspace.list({ search: 'invoice' }); // the acting workspace's documents
970
+ const { document, content } = await userDocs.workspace.get(id, { includeContent: true });
971
+ const { url } = await userDocs.workspace.getDownloadUrl(id);
972
+ const { bytes } = await userDocs.workspace.getBytes(id);
973
+ ```
974
+
975
+ Two refusals to handle: `WORKSPACE_REQUIRED` (the user is in their Personal vault, or not a member of the workspace — offer `pick()` instead) and `ARCHE_NOT_APPROVED` (this workspace has not approved your arche, or approved it before it asked for this access — say so; a workspace or Company admin approves apps under Workspace Controls). **An approval covers exactly what the admin was shown:** if you add this permission, or start reading a new kind of workspace data, after being approved, those reads are refused with `ARCHE_NOT_APPROVED` until an admin approves again — plan releases that widen access with that in mind. The workspace is fixed when your arche opens (the user reopens it to switch), approval is revocable at once, and every read is logged where the admin can see it. Not available in the builder preview. In the dev harness, `mockWorkspace` in `fias-dev.config.json` (`"approved"` default, `"unapproved"`, `"personal"`) lets you exercise both refusals.
976
+
887
977
  **Editing the user's files** — permission `vault:user-documents:edit`, declared TOGETHER WITH `vault:user-documents:read` (escalates to human review). `:edit` does not include `:read`: the grants you save under are created and listed through the read surface (`pick`, `list`, `get`), so a manifest with `:edit` alone can ask for nothing and open nothing.
888
978
 
889
979
  ```tsx
@@ -1218,6 +1308,36 @@ Three things to design around:
1218
1308
 
1219
1309
  Only images the user made or uploaded in **their own** Fias files can be published (pass the `fileId` from a `useImageGeneration()` result). PNG/JPEG/WebP only; every image is re-encoded to a canonical PNG with metadata stripped. Publishers can `unpublish()` their own assets, and any user can `report()` one for review.
1220
1310
 
1311
+ ### `useCompanyEmail()` — Send email as the Company the app is used in
1312
+
1313
+ **Permission:** `email:company:send`
1314
+ **Returns:** `CompanyEmailApi`
1315
+
1316
+ For apps built for an approved partner Company: send email **from the Company's own verified domain** (e.g. `team@example.com`), not from Fias. Declaring the permission is not enough — a Company admin must also authorize your app to send, and chooses who it may email: members of the workspace it is used in, members of the whole Company, or anyone. Until then every call is refused with `COMPANY_EMAIL_NOT_AUTHORIZED`.
1317
+
1318
+ ```tsx
1319
+ import { useCompanyEmail } from '@fias/arche-sdk';
1320
+
1321
+ const email = useCompanyEmail();
1322
+ const { results, sent } = await email.send({
1323
+ requestKey: `digest-${weekId}`, // reuse on retry — never double-sends
1324
+ to: members.map((m) => ({ userId: m.userId })),
1325
+ subject: 'Your weekly digest',
1326
+ text: digestText, // required
1327
+ html: digestHtml, // optional, sanitized
1328
+ });
1329
+ // results[i]: { index, status: 'accepted' | 'unknown' | 'failed' | 'muted' | 'not_sent' }
1330
+ ```
1331
+
1332
+ - **You never choose the From address** — it is the address the Company assigned your app. You choose recipients and content.
1333
+ - **Name members by `userId`**; you never see their addresses. Raw `{ email }` recipients work only if the Company let your app email anyone. One recipient outside what the Company allowed refuses the whole call (without saying which).
1334
+ - **The user must be working in one of the Company's workspaces, with edit access.** Each recipient is charged to that user, like any paid call; up to 50 recipients per call, and a daily limit per app per Company.
1335
+ - **Retry with the same `requestKey`.** A recipient already sent under that key is not sent again. `unknown` means it may have gone out — do not resend it under a new key.
1336
+ - **HTML is sanitized** to the email allow-list: tables, inline presentational styles, `https` images and `https`/`mailto` links are kept; `<script>`, `<form>`, `<iframe>` and similar refuse the send (`EMAIL_CONTENT_REJECTED`).
1337
+ - **Attachments:** up to 5 files, 5 MB total, and only `.pdf`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.csv`, `.txt`, `.ics`, `.docx`, `.xlsx`. `contentType` must match the extension (e.g. `application/pdf`), and the bytes must be that format; anything else refuses the send (`EMAIL_CONTENT_REJECTED`).
1338
+ - **Every email carries one-click unsubscribe.** Recipients who used it come back as `muted`; the Company may also show a "Stop emails from this arche" line, which is always shown for `kind: 'marketing'`.
1339
+ - Not available in the builder preview or the local dev harness — it sends real email. Other refusals: `WORKSPACE_REQUIRED`, `RECIPIENT_NOT_ALLOWED`, `COMPANY_EMAIL_UNAVAILABLE` (the Company's domain is not ready or paused), `DAILY_EMAIL_LIMIT_REACHED`, `INSUFFICIENT_CREDITS`.
1340
+
1221
1341
  ### `useFiasStore()` — In-app purchases (IAP)
1222
1342
 
1223
1343
  **Permission:** `store:purchase`
@@ -1315,7 +1435,23 @@ the user moves the host themselves with browser back/forward. So a router
1315
1435
  driven off `currentPath` stays in step with the address bar, and the back
1316
1436
  button works the way your users expect — you do not have to mirror the path in
1317
1437
  your own state. The dev harness echoes navigations the same way, so what you
1318
- see locally is what ships.
1438
+ see locally is what ships. To test a deep link locally, open the harness with
1439
+ `?path=/map` (a root-relative path in your route space) and `currentPath`
1440
+ starts there instead of at `/`.
1441
+
1442
+ **Correcting the URL.** Every `navigateTo` pushes a history entry by default,
1443
+ which is right for navigation the user asked for. When you are CORRECTING the
1444
+ address bar instead — normalizing a link you were opened at (`/Map/` →
1445
+ `/map`), or putting it back after refusing a route — pass `{ replace: true }`
1446
+ so Back does not land on the URL you just corrected:
1447
+
1448
+ ```tsx
1449
+ navigateTo('/map', { replace: true });
1450
+ ```
1451
+
1452
+ A host older than this option ignores it and pushes. If you correct paths
1453
+ automatically as they arrive, never repeat the same correction twice in a
1454
+ row, or a push-only host can trap the Back button between two URLs.
1319
1455
 
1320
1456
  ### Opening external links
1321
1457
 
@@ -1564,9 +1700,9 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
1564
1700
 
1565
1701
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
1566
1702
 
1567
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `data:search`, `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`, `assets:read`, `navigation:open_arche`, `sandbox:vendored-libraries`
1703
+ **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`
1568
1704
 
1569
- **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
1705
+ **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`.
1570
1706
 
1571
1707
  **Defining IAP products (for `useFiasStore`):**
1572
1708
 
@@ -1,4 +1,4 @@
1
- <!-- fias-sdk-guide-version: 2.21.0 -->
1
+ <!-- fias-sdk-guide-version: 2.23.0 -->
2
2
 
3
3
  # FIAS Plugin Development Guide
4
4
 
@@ -465,6 +465,85 @@ The text capabilities:
465
465
 
466
466
  Browse specific models with `npx fias-dev entities`.
467
467
 
468
+ #### Multi-turn conversations — `history`
469
+
470
+ `input` is the NEW user message. Send the earlier turns, oldest first, in `history`:
471
+
472
+ ```tsx
473
+ import { fitHistoryToLimits } from '@fias/arche-sdk';
474
+
475
+ const request = {
476
+ entityId: model.entityId,
477
+ systemPrompt: 'You are a helpful assistant.',
478
+ input: newMessage,
479
+ };
480
+ await invoke({
481
+ ...request,
482
+ // [{ role: 'user' | 'assistant', content }] — pass `request` so the history
483
+ // gets only the bytes the rest of the call leaves.
484
+ history: fitHistoryToLimits(messages, { maxTurns: 20, request }),
485
+ });
486
+ ```
487
+
488
+ Every turn is billed as input tokens on EVERY call, so send a window, not the whole transcript. Limits: 40 turns, 100,000 characters per turn, 150,000 in total (`ENTITY_INVOCATION_HISTORY_LIMITS`); a call over them, or a turn with a role other than `user` / `assistant`, is rejected with `INVALID_PARAMS`. The whole call must ALSO fit the bridge: 256 KB serialized for a text call, 1.5 MB with images (`ENTITY_INVOCATION_REQUEST_LIMITS`). A character can take up to 4 bytes (CJK 3, emoji 4, a newline or quote 2), so a history within the character limits can still be too large. `fitHistoryToLimits(turns, { request })` keeps the newest turns that fit both; `invoke()` refuses an oversized call with `REQUEST_TOO_LARGE` before sending it.
489
+
490
+ #### Reasoning, thinking, and several streams at once
491
+
492
+ ```tsx
493
+ const controller = new AbortController();
494
+ const result = await invoke(
495
+ {
496
+ entityId: model.entityId,
497
+ input,
498
+ history,
499
+ // Only for models with capabilities.reasoning; ignored otherwise.
500
+ parameters: { reasoning: { effort: 'medium', includeThinking: true } }, // effort: 'off' | 'low' | 'medium' | 'high'
501
+ },
502
+ {
503
+ onToken: (text) => appendAnswer(text), // this call's answer chunks
504
+ onThinking: (text) => appendThinking(text), // this call's reasoning text
505
+ signal: controller.signal, // controller.abort() → rejects with code 'ABORTED'
506
+ },
507
+ );
508
+ const cost = result.metadata?.usage?.costCredits; // what this reply cost the user, for a cost label
509
+ ```
510
+
511
+ - **Per-call callbacks** (`onToken` / `onThinking`) are scoped to that one call, so you can run several streams side by side — e.g. the same prompt against three models. A call that passes `onToken` leaves the hook's shared `streamingText` / `result` / `error` alone — its failure arrives only as the rejected promise.
512
+ - **Reasoning** uses more output tokens (billed) but always fits inside the model's output ceiling, so it never raises the credit hold. Set `includeThinking` only if you show the thinking — on some providers it raises the cost of a `'low'` request.
513
+ - **Aborting stops your UI, not the charge.** The platform finishes and bills the generation.
514
+ - A stream fails with `TIMEOUT` only after 2 minutes with no sign of life (the host sends keepalives during silent reasoning) or 15 minutes in total.
515
+ - `result.metadata.usage` has `inputTokens`, `outputTokens`, and `costCredits` when the platform reported them. Display only — the charge is already settled.
516
+ - A failed call rejects with a `FiasBridgeError`: branch on `err.code` (e.g. `INSUFFICIENT_CREDITS`, `PROVIDER_OVERLOADED`), and show `err.requestId` as a support reference when present.
517
+
518
+ ### `useTextModels()` — Let the user pick a text model
519
+
520
+ **Permission:** `entities:invoke` · **Returns:** `FiasTextModel[]`
521
+
522
+ For an app whose USER chooses the model (a chat app, a model comparison), read the platform's live catalog instead of hardcoding ids. Each entry's `entityId` goes straight into `invoke`. The list is `[]` until the host delivers it (usually immediately), and retired models drop out, so fall back to a `recommended` entry when a saved choice disappears.
523
+
524
+ ```tsx
525
+ import { useTextModels } from '@fias/arche-sdk';
526
+
527
+ function ModelPicker({ value, onChange }: { value: string; onChange: (id: string) => void }) {
528
+ const models = useTextModels();
529
+ const selected = models.find((m) => m.entityId === value) ?? models.find((m) => m.recommended);
530
+ return (
531
+ <select value={selected?.entityId ?? ''} onChange={(e) => onChange(e.target.value)}>
532
+ {models.map((m) => (
533
+ <option key={m.entityId} value={m.entityId}>
534
+ {m.providerDisplayName} · {m.displayName}
535
+ {m.pricing
536
+ ? ` — ${m.pricing.inputCreditsPerMillionTokens}/${m.pricing.outputCreditsPerMillionTokens} cr per 1M tok`
537
+ : ''}
538
+ </option>
539
+ ))}
540
+ </select>
541
+ );
542
+ }
543
+ ```
544
+
545
+ `capabilities.vision` says the model accepts `images`; `capabilities.reasoning` says it honours `parameters.reasoning`. `pricing` is what the user pays per 1M tokens in credits (1 credit = 1¢), markup included — for display only.
546
+
468
547
  ### `useImageGeneration()` — Generate images via AI models
469
548
 
470
549
  **Permission:** `entities:image_generate`
@@ -884,6 +963,17 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
884
963
  - **Workspaces.** The picker offers the files of the workspace the user is acting in — the workspace switcher in Fias: their Personal vault, or an org workspace they belong to — and `list()` returns the grants made in that workspace, so switching workspace changes what `list()` shows. A document you already hold a grant on stays readable by id (`get`, `getBytes`, `getDownloadUrl`, `saveContent`) whichever workspace the user is in: a grant belongs to the workspace the document lives in. A plugin never names a workspace itself. A member's role caps what they can share and read: a viewer can only share standard-sensitivity documents, and a document above a member's tier reads as `DOCUMENT_NOT_FOUND` even if another member granted it.
885
964
  - **Testable in the dev harness (mock mode).** `pick()` grants a small fixture set — a text file, a real PDF, and one deliberately over the 15 MB cap — and `list` / `get` / `getDownloadUrl` / `getBytes` all resolve against it, including the refusals (`BINARY_DOCUMENT` on a text read of a PDF, `CONSENTED_DOCUMENT_TOO_LARGE`, `DOCUMENT_NOT_FOUND`). Run the harness with `FIAS_HARNESS_VAULT_PICK=canceled` to exercise the cancel branch. Not available in the builder preview — `pick()` resolves `{ canceled: true }` there.
886
965
 
966
+ **Workspace-approved reads** — `userDocs.workspace.*`, permission `vault:workspace-documents:read`, declared TOGETHER WITH `vault:user-documents:read` (escalates to human review). When the user is acting in an **org workspace** they belong to, whose admin (or its Company) has **approved your arche**, you can read that workspace's documents without a per-document pick — every document the acting member can see in Documents and Arche Saves, at their role, never proprietary:
967
+
968
+ ```tsx
969
+ const docs = await userDocs.workspace.list({ search: 'invoice' }); // the acting workspace's documents
970
+ const { document, content } = await userDocs.workspace.get(id, { includeContent: true });
971
+ const { url } = await userDocs.workspace.getDownloadUrl(id);
972
+ const { bytes } = await userDocs.workspace.getBytes(id);
973
+ ```
974
+
975
+ Two refusals to handle: `WORKSPACE_REQUIRED` (the user is in their Personal vault, or not a member of the workspace — offer `pick()` instead) and `ARCHE_NOT_APPROVED` (this workspace has not approved your arche, or approved it before it asked for this access — say so; a workspace or Company admin approves apps under Workspace Controls). **An approval covers exactly what the admin was shown:** if you add this permission, or start reading a new kind of workspace data, after being approved, those reads are refused with `ARCHE_NOT_APPROVED` until an admin approves again — plan releases that widen access with that in mind. The workspace is fixed when your arche opens (the user reopens it to switch), approval is revocable at once, and every read is logged where the admin can see it. Not available in the builder preview. In the dev harness, `mockWorkspace` in `fias-dev.config.json` (`"approved"` default, `"unapproved"`, `"personal"`) lets you exercise both refusals.
976
+
887
977
  **Editing the user's files** — permission `vault:user-documents:edit`, declared TOGETHER WITH `vault:user-documents:read` (escalates to human review). `:edit` does not include `:read`: the grants you save under are created and listed through the read surface (`pick`, `list`, `get`), so a manifest with `:edit` alone can ask for nothing and open nothing.
888
978
 
889
979
  ```tsx
@@ -1218,6 +1308,36 @@ Three things to design around:
1218
1308
 
1219
1309
  Only images the user made or uploaded in **their own** Fias files can be published (pass the `fileId` from a `useImageGeneration()` result). PNG/JPEG/WebP only; every image is re-encoded to a canonical PNG with metadata stripped. Publishers can `unpublish()` their own assets, and any user can `report()` one for review.
1220
1310
 
1311
+ ### `useCompanyEmail()` — Send email as the Company the app is used in
1312
+
1313
+ **Permission:** `email:company:send`
1314
+ **Returns:** `CompanyEmailApi`
1315
+
1316
+ For apps built for an approved partner Company: send email **from the Company's own verified domain** (e.g. `team@example.com`), not from Fias. Declaring the permission is not enough — a Company admin must also authorize your app to send, and chooses who it may email: members of the workspace it is used in, members of the whole Company, or anyone. Until then every call is refused with `COMPANY_EMAIL_NOT_AUTHORIZED`.
1317
+
1318
+ ```tsx
1319
+ import { useCompanyEmail } from '@fias/arche-sdk';
1320
+
1321
+ const email = useCompanyEmail();
1322
+ const { results, sent } = await email.send({
1323
+ requestKey: `digest-${weekId}`, // reuse on retry — never double-sends
1324
+ to: members.map((m) => ({ userId: m.userId })),
1325
+ subject: 'Your weekly digest',
1326
+ text: digestText, // required
1327
+ html: digestHtml, // optional, sanitized
1328
+ });
1329
+ // results[i]: { index, status: 'accepted' | 'unknown' | 'failed' | 'muted' | 'not_sent' }
1330
+ ```
1331
+
1332
+ - **You never choose the From address** — it is the address the Company assigned your app. You choose recipients and content.
1333
+ - **Name members by `userId`**; you never see their addresses. Raw `{ email }` recipients work only if the Company let your app email anyone. One recipient outside what the Company allowed refuses the whole call (without saying which).
1334
+ - **The user must be working in one of the Company's workspaces, with edit access.** Each recipient is charged to that user, like any paid call; up to 50 recipients per call, and a daily limit per app per Company.
1335
+ - **Retry with the same `requestKey`.** A recipient already sent under that key is not sent again. `unknown` means it may have gone out — do not resend it under a new key.
1336
+ - **HTML is sanitized** to the email allow-list: tables, inline presentational styles, `https` images and `https`/`mailto` links are kept; `<script>`, `<form>`, `<iframe>` and similar refuse the send (`EMAIL_CONTENT_REJECTED`).
1337
+ - **Attachments:** up to 5 files, 5 MB total, and only `.pdf`, `.png`, `.jpg`/`.jpeg`, `.gif`, `.csv`, `.txt`, `.ics`, `.docx`, `.xlsx`. `contentType` must match the extension (e.g. `application/pdf`), and the bytes must be that format; anything else refuses the send (`EMAIL_CONTENT_REJECTED`).
1338
+ - **Every email carries one-click unsubscribe.** Recipients who used it come back as `muted`; the Company may also show a "Stop emails from this arche" line, which is always shown for `kind: 'marketing'`.
1339
+ - Not available in the builder preview or the local dev harness — it sends real email. Other refusals: `WORKSPACE_REQUIRED`, `RECIPIENT_NOT_ALLOWED`, `COMPANY_EMAIL_UNAVAILABLE` (the Company's domain is not ready or paused), `DAILY_EMAIL_LIMIT_REACHED`, `INSUFFICIENT_CREDITS`.
1340
+
1221
1341
  ### `useFiasStore()` — In-app purchases (IAP)
1222
1342
 
1223
1343
  **Permission:** `store:purchase`
@@ -1315,7 +1435,23 @@ the user moves the host themselves with browser back/forward. So a router
1315
1435
  driven off `currentPath` stays in step with the address bar, and the back
1316
1436
  button works the way your users expect — you do not have to mirror the path in
1317
1437
  your own state. The dev harness echoes navigations the same way, so what you
1318
- see locally is what ships.
1438
+ see locally is what ships. To test a deep link locally, open the harness with
1439
+ `?path=/map` (a root-relative path in your route space) and `currentPath`
1440
+ starts there instead of at `/`.
1441
+
1442
+ **Correcting the URL.** Every `navigateTo` pushes a history entry by default,
1443
+ which is right for navigation the user asked for. When you are CORRECTING the
1444
+ address bar instead — normalizing a link you were opened at (`/Map/` →
1445
+ `/map`), or putting it back after refusing a route — pass `{ replace: true }`
1446
+ so Back does not land on the URL you just corrected:
1447
+
1448
+ ```tsx
1449
+ navigateTo('/map', { replace: true });
1450
+ ```
1451
+
1452
+ A host older than this option ignores it and pushes. If you correct paths
1453
+ automatically as they arrive, never repeat the same correction twice in a
1454
+ row, or a push-only host can trap the Back button between two URLs.
1319
1455
 
1320
1456
  ### Opening external links
1321
1457
 
@@ -1564,9 +1700,9 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
1564
1700
 
1565
1701
  Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
1566
1702
 
1567
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `data:search`, `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`, `assets:read`, `navigation:open_arche`, `sandbox:vendored-libraries`
1703
+ **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`
1568
1704
 
1569
- **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
1705
+ **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`.
1570
1706
 
1571
1707
  **Defining IAP products (for `useFiasStore`):**
1572
1708