@fias/create-fias-plugin 1.11.0 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/default/AGENTS.md +170 -8
- package/templates/default/CLAUDE.md +170 -8
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.19.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -545,11 +545,34 @@ const { entities, isLoading } = useImageEntities({
|
|
|
545
545
|
supportsReferenceImage: true,
|
|
546
546
|
});
|
|
547
547
|
// entities[i]: { entityId, displayName, provider, modes, supportedSizes,
|
|
548
|
-
// supportsReferenceImage, isPlatformRecommended,
|
|
548
|
+
// supportsReferenceImage, isPlatformRecommended,
|
|
549
|
+
// creditsPerImage, estimatedDurationMs, company, strengths,
|
|
550
|
+
// providerParams, ... }
|
|
549
551
|
```
|
|
550
552
|
|
|
551
553
|
Each entry is scrubbed — only fields a UI legitimately needs (no provider model strings, no endpoints, no execution config).
|
|
552
554
|
|
|
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.
|
|
556
|
+
|
|
557
|
+
**Tunable engine parameters.** `providerParams` describes what a model exposes for tuning — enough to render a settings panel without hardcoding a thing:
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
// param: { key, label, description, controlType, defaultValue,
|
|
561
|
+
// min?, max?, step?, options?, category? }
|
|
562
|
+
// controlType: 'slider' | 'number' | 'select' | 'checkbox' | 'textarea'
|
|
563
|
+
const seed = entity.providerParams?.find((p) => p.key === 'seed');
|
|
564
|
+
await generate({ entityId, prompt, providerParams: { seed: 42 } });
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**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
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
generate({ entityId, prompt, quality: 'high' }); // honoured
|
|
571
|
+
generate({ entityId, prompt, providerParams: { gpt_quality: 'high' } }); // SILENTLY IGNORED
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Its `options` are still how you build the control — only the destination differs.
|
|
575
|
+
|
|
553
576
|
### Image-editing capability hooks (auto-generated)
|
|
554
577
|
|
|
555
578
|
**Permission:** `entities:image_edit`
|
|
@@ -683,7 +706,7 @@ const { url, expiresAt, contentType } = await vault.getDownloadUrl(documentId);
|
|
|
683
706
|
// List documents this arche owns
|
|
684
707
|
const { documents, nextCursor } = await vault.list({ search: 'invoice', limit: 50 });
|
|
685
708
|
|
|
686
|
-
// Write a small text/JSON document (UTF-8, ≤
|
|
709
|
+
// Write a small text/JSON document (UTF-8, ≤ 200 KB — use upload() for anything larger)
|
|
687
710
|
const { documentId } = await vault.write({
|
|
688
711
|
name: 'settings.json',
|
|
689
712
|
content: JSON.stringify(settings),
|
|
@@ -712,11 +735,23 @@ const { bytes: saved } = await vault.readBytes(documentId);
|
|
|
712
735
|
// Overwrite a document THIS arche created IN PLACE — same documentId, bytes
|
|
713
736
|
// replaced, no version history. The autosave path (a spreadsheet saving as
|
|
714
737
|
// the user works); to publish a distinct new version use write({
|
|
715
|
-
// replacesDocumentId }) instead.
|
|
716
|
-
// names the cap
|
|
717
|
-
//
|
|
738
|
+
// replacesDocumentId }) instead. ONE method for any content up to 15 MB
|
|
739
|
+
// (OWN_DOCUMENT_SAVE_TOO_LARGE names the cap — the same one readBytes serves,
|
|
740
|
+
// so anything you save you can reopen): a string is saved as UTF-8 text (the
|
|
741
|
+
// document must be a text type); a Uint8Array / ArrayBuffer / Blob is saved
|
|
742
|
+
// as-is, for any type. Transport is automatic — never split, chunk or re-upload
|
|
743
|
+
// to work around size. Debounce: a few seconds of idle plus blur/close — never
|
|
744
|
+
// per keystroke — and surface RATE_LIMIT verbatim rather than retrying. A save
|
|
718
745
|
// drops the document's search index and does not re-run extraction.
|
|
719
746
|
await vault.saveContent({ documentId, content: JSON.stringify(sheet) });
|
|
747
|
+
await vault.saveContent({ documentId, content: pdfBytes }); // a PDF editor saving its PDF
|
|
748
|
+
|
|
749
|
+
// Don't let two tabs clobber each other: pass the `revision` you last saw
|
|
750
|
+
// (list() items, read(), readBytes().document and every saveContent result
|
|
751
|
+
// carry one — treat it as opaque). A stale one rejects with VERSION_CONFLICT
|
|
752
|
+
// and the document is untouched: re-read, merge or ask, retry with the new one.
|
|
753
|
+
let { revision } = (await vault.readBytes(documentId)).document;
|
|
754
|
+
({ revision } = await vault.saveContent({ documentId, content, expectedRevision: revision }));
|
|
720
755
|
|
|
721
756
|
// Semantic search across documents this arche owns
|
|
722
757
|
// (burns user credits per the AI Markup Invariant — call sparingly)
|
|
@@ -749,6 +784,19 @@ const saved = await vault.upload(bytes, {
|
|
|
749
784
|
|
|
750
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.
|
|
751
786
|
|
|
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`.
|
|
788
|
+
|
|
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
|
+
```
|
|
797
|
+
|
|
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.
|
|
799
|
+
|
|
752
800
|
**Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
|
|
753
801
|
|
|
754
802
|
**`fetchVaultDocumentDownloadUrl(documentId)`** — non-React function that shares the hook's client-side URL cache. Use from PDF exporters, image preloaders, or other non-component code paths.
|
|
@@ -789,8 +837,122 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
|
|
|
789
837
|
- 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.
|
|
790
838
|
- `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.
|
|
791
839
|
- 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.
|
|
840
|
+
- **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.
|
|
792
841
|
- **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.
|
|
793
842
|
|
|
843
|
+
**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.
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
// Ask for EDIT access up front…
|
|
847
|
+
const picked = await userDocs.pick({ maxDocuments: 1, access: 'write' });
|
|
848
|
+
// …or later, for a document you can already read (e.g. one that arrived by
|
|
849
|
+
// handoff). The host skips the picker and asks about just that file.
|
|
850
|
+
const asked = await userDocs.requestEditAccess(documentId);
|
|
851
|
+
// CHECK it: a resolved promise is not "yes". If you held no grant on that
|
|
852
|
+
// document the host shows the ordinary picker instead, so what comes back may
|
|
853
|
+
// be a different file, or view-only.
|
|
854
|
+
const granted =
|
|
855
|
+
'documents' in asked &&
|
|
856
|
+
asked.documents[0]?.documentId === documentId &&
|
|
857
|
+
asked.documents[0]?.access === 'write';
|
|
858
|
+
|
|
859
|
+
// Save in place: same documentId. A string (text types) or bytes, up to 15 MB.
|
|
860
|
+
const { document } = await userDocs.get(documentId);
|
|
861
|
+
// `revision` is optional in the type — a host that predates edit access does
|
|
862
|
+
// not send it, and without it there is nothing to save against.
|
|
863
|
+
if (!document.revision) throw new Error('This host cannot save to the user’s files.');
|
|
864
|
+
const saved = await userDocs.saveContent({
|
|
865
|
+
documentId,
|
|
866
|
+
content: bytes, // string | Uint8Array | ArrayBuffer | Blob
|
|
867
|
+
expectedRevision: document.revision, // REQUIRED — it is the user's file
|
|
868
|
+
});
|
|
869
|
+
// keep saved.revision for the next save
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
- `document.access` is `'read'` or `'write'`. Open a `'read'` document read-only; a save to one rejects with `EDIT_ACCESS_REQUIRED`. The user can lower access to view-only or remove it at any time (Vault → App Access) — handle `EDIT_ACCESS_REQUIRED` and `DOCUMENT_NOT_FOUND` on any save, and stop autosaving when you see one.
|
|
873
|
+
- `expectedRevision` is required (`EXPECTED_REVISION_REQUIRED`). `VERSION_CONFLICT` means the file changed elsewhere — re-read, merge, retry; never loop blindly.
|
|
874
|
+
- The document's type cannot change (`MIME_CHANGE_NOT_ALLOWED`), and a `string` can only be saved onto a text document (or one with no recorded type, which stays untyped). Rename, tags and delete stay the user's.
|
|
875
|
+
- The platform keeps the user's earlier versions (at most one per 10 minutes per document). They count against the user's storage; `STORAGE_QUOTA_EXCEEDED` fails the save — tell the user, do not retry.
|
|
876
|
+
- Saves are rate-limited to 12 a minute per user, text and bytes together. Debounce autosave to at most one save every 10 seconds (plus a final save on blur or close) — never per keystroke. A refusal rejects with `err.code === 'RATE_LIMIT'` whether the host or the server made it; surface it rather than retrying in a loop.
|
|
877
|
+
- Content past 15 MB rejects locally with `OWN_DOCUMENT_SAVE_TOO_LARGE`, before anything is sent.
|
|
878
|
+
- A save does not re-index the file for search — the user does that from the file's page.
|
|
879
|
+
- Dev harness: **mock mode** implements all of this against the fixture documents (a `pick()` without `access: 'write'` leaves them read-only, so the refusal is testable). Live mode refuses — only the real user can grant edit access.
|
|
880
|
+
|
|
881
|
+
### Sending a document for signature — Document Sign handoff
|
|
882
|
+
|
|
883
|
+
**Permission:** `navigation:open_arche`.
|
|
884
|
+
|
|
885
|
+
Hand one of the user's Vault PDFs to Document Sign with recipients pre-filled.
|
|
886
|
+
They place the signature fields and press Send there — a handoff never sends
|
|
887
|
+
anything, and there is no way for your app to send on their behalf.
|
|
888
|
+
|
|
889
|
+
```tsx
|
|
890
|
+
import { useFiasNavigation } from '@fias/arche-sdk';
|
|
891
|
+
|
|
892
|
+
const { openArche } = useFiasNavigation();
|
|
893
|
+
|
|
894
|
+
openArche('arche_document_sign', {
|
|
895
|
+
payload: {
|
|
896
|
+
kind: 'sign_document',
|
|
897
|
+
// One uuid per user intent, NOT per call. The platform keys idempotency
|
|
898
|
+
// on it: re-presenting one resolves to the same draft instead of
|
|
899
|
+
// creating a duplicate, and reusing a stale one reopens the old draft.
|
|
900
|
+
requestId: crypto.randomUUID(),
|
|
901
|
+
documentId, // a user-vault PDF
|
|
902
|
+
title: 'MSA — Acme Corp',
|
|
903
|
+
recipients: [{ email: 'alice@acme.com', name: 'Alice', role: 'signer' }],
|
|
904
|
+
correlationId: approvalRecordId, // opaque; echoed back, never interpreted
|
|
905
|
+
// (fixed at creation — see below)
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
`correlationId` is fixed when the envelope is first created. Re-presenting a
|
|
911
|
+
`requestId` returns the SAME draft, and the result echoes the correlationId
|
|
912
|
+
from that FIRST call — a different one sent on the repeat is ignored, not
|
|
913
|
+
refused, for the same reason recipients are not re-seeded: a repeat must not
|
|
914
|
+
overwrite what the first call recorded. `documentId` is part of the request's
|
|
915
|
+
identity and IS refused when it differs (409); `correlationId` is metadata
|
|
916
|
+
riding along. If you want fresh metadata, mint a fresh `requestId`.
|
|
917
|
+
|
|
918
|
+
When the user leaves Document Sign they can come back to your app, and you
|
|
919
|
+
receive a `sign_document_result`: `{ requestId, outcome, envelopeId? }`, where
|
|
920
|
+
`outcome` is `sent`, `saved_draft` or `cancelled`. An `envelopeId` alone does
|
|
921
|
+
NOT mean anything was sent — an envelope id exists from the moment a draft is
|
|
922
|
+
created, which is why the outcome is there.
|
|
923
|
+
|
|
924
|
+
**Treat the result as a pointer, never as a record.** It tells you which
|
|
925
|
+
envelope to look at; it is not evidence of what happened to it. Match it to an
|
|
926
|
+
outstanding request by `requestId`, consume it once, and never re-point an
|
|
927
|
+
existing association because a `correlationId` matched.
|
|
928
|
+
|
|
929
|
+
**A handoff ENDS YOUR APP'S PROCESS. Anything you will need afterwards has to
|
|
930
|
+
be written down before you call `openArche`.** The host navigates away and
|
|
931
|
+
tears your iframe down, so React state, refs, module variables — everything
|
|
932
|
+
in memory — are gone when the user comes back. Your app remounts from
|
|
933
|
+
nothing.
|
|
934
|
+
|
|
935
|
+
That is one rule, and it is easy to under-apply. It is not only the
|
|
936
|
+
outstanding `requestId`: it is the document the user selected, the record
|
|
937
|
+
they were working on, the form they half-filled, the step they were on.
|
|
938
|
+
Anything you would be annoyed to lose. Write it to `useFiasStorage()` or your
|
|
939
|
+
Data Store on the way out and restore it on the way in. Browser storage is
|
|
940
|
+
not an option — a plugin iframe is opaque-origin, so `localStorage` and
|
|
941
|
+
`sessionStorage` throw.
|
|
942
|
+
|
|
943
|
+
The `requestId` is the one that fails loudest: keep it in memory and every
|
|
944
|
+
reply comes back unmatchable, so you cannot tell a genuine result from a
|
|
945
|
+
forged one — which is the whole reason to match. The rest fail quietly, as a
|
|
946
|
+
user who returns to an app that has forgotten what they were doing.
|
|
947
|
+
|
|
948
|
+
Note the ordering this implies, and do not treat it as an edge case: the
|
|
949
|
+
result **normally arrives before your restore read resolves** — the handoff
|
|
950
|
+
is delivered as soon as your SDK reports ready, which is before your first
|
|
951
|
+
storage round trip comes back. Measured at about a second's head start in
|
|
952
|
+
local dev. Judging on arrival therefore rejects every genuine reply, not an
|
|
953
|
+
occasional one. Hold the result until you know what was outstanding, then
|
|
954
|
+
evaluate it.
|
|
955
|
+
|
|
794
956
|
### `useArcheHandoff()` — Receive a document the user asked to open in your app
|
|
795
957
|
|
|
796
958
|
**Permission:** none to receive. Reading the document is a separate act (`vault:user-documents:read`, via `useVaultUserDocuments().getBytes`).
|
|
@@ -863,7 +1025,7 @@ Rules that matter:
|
|
|
863
1025
|
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
864
1026
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
865
1027
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
866
|
-
-
|
|
1028
|
+
- **Test it in the dev harness:** the **✦ Fias AI** toolbar button opens the real assistant over your plugin (Live mode; turns are real and use your credits). It describes your plugin exactly as the platform would: manifest actions always, runtime (`useFiasAIActions`) actions and `state` only for a trusted arche, and the window names any runtime action it is withholding. In the harness it can use your plugin's actions plus read-only Fias tools, never writes or sends. The builder preview has no assistant.
|
|
867
1029
|
- **`openAssistant()`** (the hook's return) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
|
|
868
1030
|
|
|
869
1031
|
### `useArcheAssets()` — Contributor-published asset library
|
|
@@ -1317,7 +1479,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1317
1479
|
|
|
1318
1480
|
### Size and File Limits
|
|
1319
1481
|
|
|
1320
|
-
- **Bundle size:** Max 5 MB
|
|
1482
|
+
- **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)
|
|
1321
1483
|
- **Dependencies:** Max 20, exact semver versions only (e.g., `"4.4.7"`, not `"^4.4.7"`)
|
|
1322
1484
|
- Platform packages (`react`, `react-dom`, `@fias/arche-sdk`) are provided — do not include in `dependencies`
|
|
1323
1485
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.19.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -545,11 +545,34 @@ const { entities, isLoading } = useImageEntities({
|
|
|
545
545
|
supportsReferenceImage: true,
|
|
546
546
|
});
|
|
547
547
|
// entities[i]: { entityId, displayName, provider, modes, supportedSizes,
|
|
548
|
-
// supportsReferenceImage, isPlatformRecommended,
|
|
548
|
+
// supportsReferenceImage, isPlatformRecommended,
|
|
549
|
+
// creditsPerImage, estimatedDurationMs, company, strengths,
|
|
550
|
+
// providerParams, ... }
|
|
549
551
|
```
|
|
550
552
|
|
|
551
553
|
Each entry is scrubbed — only fields a UI legitimately needs (no provider model strings, no endpoints, no execution config).
|
|
552
554
|
|
|
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.
|
|
556
|
+
|
|
557
|
+
**Tunable engine parameters.** `providerParams` describes what a model exposes for tuning — enough to render a settings panel without hardcoding a thing:
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
// param: { key, label, description, controlType, defaultValue,
|
|
561
|
+
// min?, max?, step?, options?, category? }
|
|
562
|
+
// controlType: 'slider' | 'number' | 'select' | 'checkbox' | 'textarea'
|
|
563
|
+
const seed = entity.providerParams?.find((p) => p.key === 'seed');
|
|
564
|
+
await generate({ entityId, prompt, providerParams: { seed: 42 } });
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**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
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
generate({ entityId, prompt, quality: 'high' }); // honoured
|
|
571
|
+
generate({ entityId, prompt, providerParams: { gpt_quality: 'high' } }); // SILENTLY IGNORED
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Its `options` are still how you build the control — only the destination differs.
|
|
575
|
+
|
|
553
576
|
### Image-editing capability hooks (auto-generated)
|
|
554
577
|
|
|
555
578
|
**Permission:** `entities:image_edit`
|
|
@@ -683,7 +706,7 @@ const { url, expiresAt, contentType } = await vault.getDownloadUrl(documentId);
|
|
|
683
706
|
// List documents this arche owns
|
|
684
707
|
const { documents, nextCursor } = await vault.list({ search: 'invoice', limit: 50 });
|
|
685
708
|
|
|
686
|
-
// Write a small text/JSON document (UTF-8, ≤
|
|
709
|
+
// Write a small text/JSON document (UTF-8, ≤ 200 KB — use upload() for anything larger)
|
|
687
710
|
const { documentId } = await vault.write({
|
|
688
711
|
name: 'settings.json',
|
|
689
712
|
content: JSON.stringify(settings),
|
|
@@ -712,11 +735,23 @@ const { bytes: saved } = await vault.readBytes(documentId);
|
|
|
712
735
|
// Overwrite a document THIS arche created IN PLACE — same documentId, bytes
|
|
713
736
|
// replaced, no version history. The autosave path (a spreadsheet saving as
|
|
714
737
|
// the user works); to publish a distinct new version use write({
|
|
715
|
-
// replacesDocumentId }) instead.
|
|
716
|
-
// names the cap
|
|
717
|
-
//
|
|
738
|
+
// replacesDocumentId }) instead. ONE method for any content up to 15 MB
|
|
739
|
+
// (OWN_DOCUMENT_SAVE_TOO_LARGE names the cap — the same one readBytes serves,
|
|
740
|
+
// so anything you save you can reopen): a string is saved as UTF-8 text (the
|
|
741
|
+
// document must be a text type); a Uint8Array / ArrayBuffer / Blob is saved
|
|
742
|
+
// as-is, for any type. Transport is automatic — never split, chunk or re-upload
|
|
743
|
+
// to work around size. Debounce: a few seconds of idle plus blur/close — never
|
|
744
|
+
// per keystroke — and surface RATE_LIMIT verbatim rather than retrying. A save
|
|
718
745
|
// drops the document's search index and does not re-run extraction.
|
|
719
746
|
await vault.saveContent({ documentId, content: JSON.stringify(sheet) });
|
|
747
|
+
await vault.saveContent({ documentId, content: pdfBytes }); // a PDF editor saving its PDF
|
|
748
|
+
|
|
749
|
+
// Don't let two tabs clobber each other: pass the `revision` you last saw
|
|
750
|
+
// (list() items, read(), readBytes().document and every saveContent result
|
|
751
|
+
// carry one — treat it as opaque). A stale one rejects with VERSION_CONFLICT
|
|
752
|
+
// and the document is untouched: re-read, merge or ask, retry with the new one.
|
|
753
|
+
let { revision } = (await vault.readBytes(documentId)).document;
|
|
754
|
+
({ revision } = await vault.saveContent({ documentId, content, expectedRevision: revision }));
|
|
720
755
|
|
|
721
756
|
// Semantic search across documents this arche owns
|
|
722
757
|
// (burns user credits per the AI Markup Invariant — call sparingly)
|
|
@@ -749,6 +784,19 @@ const saved = await vault.upload(bytes, {
|
|
|
749
784
|
|
|
750
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.
|
|
751
786
|
|
|
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`.
|
|
788
|
+
|
|
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
|
+
```
|
|
797
|
+
|
|
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.
|
|
799
|
+
|
|
752
800
|
**Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
|
|
753
801
|
|
|
754
802
|
**`fetchVaultDocumentDownloadUrl(documentId)`** — non-React function that shares the hook's client-side URL cache. Use from PDF exporters, image preloaders, or other non-component code paths.
|
|
@@ -789,8 +837,122 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
|
|
|
789
837
|
- 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.
|
|
790
838
|
- `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.
|
|
791
839
|
- 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.
|
|
840
|
+
- **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.
|
|
792
841
|
- **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.
|
|
793
842
|
|
|
843
|
+
**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.
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
// Ask for EDIT access up front…
|
|
847
|
+
const picked = await userDocs.pick({ maxDocuments: 1, access: 'write' });
|
|
848
|
+
// …or later, for a document you can already read (e.g. one that arrived by
|
|
849
|
+
// handoff). The host skips the picker and asks about just that file.
|
|
850
|
+
const asked = await userDocs.requestEditAccess(documentId);
|
|
851
|
+
// CHECK it: a resolved promise is not "yes". If you held no grant on that
|
|
852
|
+
// document the host shows the ordinary picker instead, so what comes back may
|
|
853
|
+
// be a different file, or view-only.
|
|
854
|
+
const granted =
|
|
855
|
+
'documents' in asked &&
|
|
856
|
+
asked.documents[0]?.documentId === documentId &&
|
|
857
|
+
asked.documents[0]?.access === 'write';
|
|
858
|
+
|
|
859
|
+
// Save in place: same documentId. A string (text types) or bytes, up to 15 MB.
|
|
860
|
+
const { document } = await userDocs.get(documentId);
|
|
861
|
+
// `revision` is optional in the type — a host that predates edit access does
|
|
862
|
+
// not send it, and without it there is nothing to save against.
|
|
863
|
+
if (!document.revision) throw new Error('This host cannot save to the user’s files.');
|
|
864
|
+
const saved = await userDocs.saveContent({
|
|
865
|
+
documentId,
|
|
866
|
+
content: bytes, // string | Uint8Array | ArrayBuffer | Blob
|
|
867
|
+
expectedRevision: document.revision, // REQUIRED — it is the user's file
|
|
868
|
+
});
|
|
869
|
+
// keep saved.revision for the next save
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
- `document.access` is `'read'` or `'write'`. Open a `'read'` document read-only; a save to one rejects with `EDIT_ACCESS_REQUIRED`. The user can lower access to view-only or remove it at any time (Vault → App Access) — handle `EDIT_ACCESS_REQUIRED` and `DOCUMENT_NOT_FOUND` on any save, and stop autosaving when you see one.
|
|
873
|
+
- `expectedRevision` is required (`EXPECTED_REVISION_REQUIRED`). `VERSION_CONFLICT` means the file changed elsewhere — re-read, merge, retry; never loop blindly.
|
|
874
|
+
- The document's type cannot change (`MIME_CHANGE_NOT_ALLOWED`), and a `string` can only be saved onto a text document (or one with no recorded type, which stays untyped). Rename, tags and delete stay the user's.
|
|
875
|
+
- The platform keeps the user's earlier versions (at most one per 10 minutes per document). They count against the user's storage; `STORAGE_QUOTA_EXCEEDED` fails the save — tell the user, do not retry.
|
|
876
|
+
- Saves are rate-limited to 12 a minute per user, text and bytes together. Debounce autosave to at most one save every 10 seconds (plus a final save on blur or close) — never per keystroke. A refusal rejects with `err.code === 'RATE_LIMIT'` whether the host or the server made it; surface it rather than retrying in a loop.
|
|
877
|
+
- Content past 15 MB rejects locally with `OWN_DOCUMENT_SAVE_TOO_LARGE`, before anything is sent.
|
|
878
|
+
- A save does not re-index the file for search — the user does that from the file's page.
|
|
879
|
+
- Dev harness: **mock mode** implements all of this against the fixture documents (a `pick()` without `access: 'write'` leaves them read-only, so the refusal is testable). Live mode refuses — only the real user can grant edit access.
|
|
880
|
+
|
|
881
|
+
### Sending a document for signature — Document Sign handoff
|
|
882
|
+
|
|
883
|
+
**Permission:** `navigation:open_arche`.
|
|
884
|
+
|
|
885
|
+
Hand one of the user's Vault PDFs to Document Sign with recipients pre-filled.
|
|
886
|
+
They place the signature fields and press Send there — a handoff never sends
|
|
887
|
+
anything, and there is no way for your app to send on their behalf.
|
|
888
|
+
|
|
889
|
+
```tsx
|
|
890
|
+
import { useFiasNavigation } from '@fias/arche-sdk';
|
|
891
|
+
|
|
892
|
+
const { openArche } = useFiasNavigation();
|
|
893
|
+
|
|
894
|
+
openArche('arche_document_sign', {
|
|
895
|
+
payload: {
|
|
896
|
+
kind: 'sign_document',
|
|
897
|
+
// One uuid per user intent, NOT per call. The platform keys idempotency
|
|
898
|
+
// on it: re-presenting one resolves to the same draft instead of
|
|
899
|
+
// creating a duplicate, and reusing a stale one reopens the old draft.
|
|
900
|
+
requestId: crypto.randomUUID(),
|
|
901
|
+
documentId, // a user-vault PDF
|
|
902
|
+
title: 'MSA — Acme Corp',
|
|
903
|
+
recipients: [{ email: 'alice@acme.com', name: 'Alice', role: 'signer' }],
|
|
904
|
+
correlationId: approvalRecordId, // opaque; echoed back, never interpreted
|
|
905
|
+
// (fixed at creation — see below)
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
`correlationId` is fixed when the envelope is first created. Re-presenting a
|
|
911
|
+
`requestId` returns the SAME draft, and the result echoes the correlationId
|
|
912
|
+
from that FIRST call — a different one sent on the repeat is ignored, not
|
|
913
|
+
refused, for the same reason recipients are not re-seeded: a repeat must not
|
|
914
|
+
overwrite what the first call recorded. `documentId` is part of the request's
|
|
915
|
+
identity and IS refused when it differs (409); `correlationId` is metadata
|
|
916
|
+
riding along. If you want fresh metadata, mint a fresh `requestId`.
|
|
917
|
+
|
|
918
|
+
When the user leaves Document Sign they can come back to your app, and you
|
|
919
|
+
receive a `sign_document_result`: `{ requestId, outcome, envelopeId? }`, where
|
|
920
|
+
`outcome` is `sent`, `saved_draft` or `cancelled`. An `envelopeId` alone does
|
|
921
|
+
NOT mean anything was sent — an envelope id exists from the moment a draft is
|
|
922
|
+
created, which is why the outcome is there.
|
|
923
|
+
|
|
924
|
+
**Treat the result as a pointer, never as a record.** It tells you which
|
|
925
|
+
envelope to look at; it is not evidence of what happened to it. Match it to an
|
|
926
|
+
outstanding request by `requestId`, consume it once, and never re-point an
|
|
927
|
+
existing association because a `correlationId` matched.
|
|
928
|
+
|
|
929
|
+
**A handoff ENDS YOUR APP'S PROCESS. Anything you will need afterwards has to
|
|
930
|
+
be written down before you call `openArche`.** The host navigates away and
|
|
931
|
+
tears your iframe down, so React state, refs, module variables — everything
|
|
932
|
+
in memory — are gone when the user comes back. Your app remounts from
|
|
933
|
+
nothing.
|
|
934
|
+
|
|
935
|
+
That is one rule, and it is easy to under-apply. It is not only the
|
|
936
|
+
outstanding `requestId`: it is the document the user selected, the record
|
|
937
|
+
they were working on, the form they half-filled, the step they were on.
|
|
938
|
+
Anything you would be annoyed to lose. Write it to `useFiasStorage()` or your
|
|
939
|
+
Data Store on the way out and restore it on the way in. Browser storage is
|
|
940
|
+
not an option — a plugin iframe is opaque-origin, so `localStorage` and
|
|
941
|
+
`sessionStorage` throw.
|
|
942
|
+
|
|
943
|
+
The `requestId` is the one that fails loudest: keep it in memory and every
|
|
944
|
+
reply comes back unmatchable, so you cannot tell a genuine result from a
|
|
945
|
+
forged one — which is the whole reason to match. The rest fail quietly, as a
|
|
946
|
+
user who returns to an app that has forgotten what they were doing.
|
|
947
|
+
|
|
948
|
+
Note the ordering this implies, and do not treat it as an edge case: the
|
|
949
|
+
result **normally arrives before your restore read resolves** — the handoff
|
|
950
|
+
is delivered as soon as your SDK reports ready, which is before your first
|
|
951
|
+
storage round trip comes back. Measured at about a second's head start in
|
|
952
|
+
local dev. Judging on arrival therefore rejects every genuine reply, not an
|
|
953
|
+
occasional one. Hold the result until you know what was outstanding, then
|
|
954
|
+
evaluate it.
|
|
955
|
+
|
|
794
956
|
### `useArcheHandoff()` — Receive a document the user asked to open in your app
|
|
795
957
|
|
|
796
958
|
**Permission:** none to receive. Reading the document is a separate act (`vault:user-documents:read`, via `useVaultUserDocuments().getBytes`).
|
|
@@ -863,7 +1025,7 @@ Rules that matter:
|
|
|
863
1025
|
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
864
1026
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
865
1027
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
866
|
-
-
|
|
1028
|
+
- **Test it in the dev harness:** the **✦ Fias AI** toolbar button opens the real assistant over your plugin (Live mode; turns are real and use your credits). It describes your plugin exactly as the platform would: manifest actions always, runtime (`useFiasAIActions`) actions and `state` only for a trusted arche, and the window names any runtime action it is withholding. In the harness it can use your plugin's actions plus read-only Fias tools, never writes or sends. The builder preview has no assistant.
|
|
867
1029
|
- **`openAssistant()`** (the hook's return) asks the HOST to open the Fias AI panel — use it for a contextual affordance (an empty-state "Ask Fias AI to get started" button). The host already shows its own "Works with Fias AI" chip, debounces your requests, and ignores them for a while after the user closes the panel — never call it in a loop or on mount. There is deliberately no way to pass text along: the assistant's input belongs to the user.
|
|
868
1030
|
|
|
869
1031
|
### `useArcheAssets()` — Contributor-published asset library
|
|
@@ -1317,7 +1479,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1317
1479
|
|
|
1318
1480
|
### Size and File Limits
|
|
1319
1481
|
|
|
1320
|
-
- **Bundle size:** Max 5 MB
|
|
1482
|
+
- **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)
|
|
1321
1483
|
- **Dependencies:** Max 20, exact semver versions only (e.g., `"4.4.7"`, not `"^4.4.7"`)
|
|
1322
1484
|
- Platform packages (`react`, `react-dom`, `@fias/arche-sdk`) are provided — do not include in `dependencies`
|
|
1323
1485
|
|