@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.
- package/dist/bridge.d.ts +33 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +98 -15
- package/dist/bridge.js.map +1 -1
- package/dist/bridge.test.js +173 -0
- package/dist/bridge.test.js.map +1 -1
- package/dist/entity-ops.d.ts +3 -0
- package/dist/entity-ops.d.ts.map +1 -1
- package/dist/entity-ops.js +4 -1
- package/dist/entity-ops.js.map +1 -1
- package/dist/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +4 -0
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +82 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +143 -20
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +93 -1
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +329 -36
- package/dist/invocation-params.d.ts +92 -0
- package/dist/invocation-params.d.ts.map +1 -0
- package/dist/invocation-params.js +180 -0
- package/dist/invocation-params.js.map +1 -0
- package/dist/invocation-params.test.d.ts +2 -0
- package/dist/invocation-params.test.d.ts.map +1 -0
- package/dist/invocation-params.test.js +140 -0
- package/dist/invocation-params.test.js.map +1 -0
- package/dist/panels.d.ts.map +1 -1
- package/dist/panels.js.map +1 -1
- package/dist/panels.test.js +1 -3
- package/dist/panels.test.js.map +1 -1
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +8 -0
- package/dist/protocol.js.map +1 -1
- package/dist/types.d.ts +219 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +140 -4
- package/templates/default/CLAUDE.md +140 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
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.
|
|
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
|
|