@fias/create-fias-plugin 1.11.1 → 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 +145 -6
- package/templates/default/CLAUDE.md +145 -6
package/package.json
CHANGED
|
@@ -706,7 +706,7 @@ const { url, expiresAt, contentType } = await vault.getDownloadUrl(documentId);
|
|
|
706
706
|
// List documents this arche owns
|
|
707
707
|
const { documents, nextCursor } = await vault.list({ search: 'invoice', limit: 50 });
|
|
708
708
|
|
|
709
|
-
// Write a small text/JSON document (UTF-8, ≤
|
|
709
|
+
// Write a small text/JSON document (UTF-8, ≤ 200 KB — use upload() for anything larger)
|
|
710
710
|
const { documentId } = await vault.write({
|
|
711
711
|
name: 'settings.json',
|
|
712
712
|
content: JSON.stringify(settings),
|
|
@@ -735,11 +735,23 @@ const { bytes: saved } = await vault.readBytes(documentId);
|
|
|
735
735
|
// Overwrite a document THIS arche created IN PLACE — same documentId, bytes
|
|
736
736
|
// replaced, no version history. The autosave path (a spreadsheet saving as
|
|
737
737
|
// the user works); to publish a distinct new version use write({
|
|
738
|
-
// replacesDocumentId }) instead.
|
|
739
|
-
// names the cap
|
|
740
|
-
//
|
|
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
|
|
741
745
|
// drops the document's search index and does not re-run extraction.
|
|
742
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 }));
|
|
743
755
|
|
|
744
756
|
// Semantic search across documents this arche owns
|
|
745
757
|
// (burns user credits per the AI Markup Invariant — call sparingly)
|
|
@@ -772,6 +784,19 @@ const saved = await vault.upload(bytes, {
|
|
|
772
784
|
|
|
773
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.
|
|
774
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
|
+
|
|
775
800
|
**Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
|
|
776
801
|
|
|
777
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.
|
|
@@ -812,8 +837,122 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
|
|
|
812
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.
|
|
813
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.
|
|
814
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.
|
|
815
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.
|
|
816
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
|
+
|
|
817
956
|
### `useArcheHandoff()` — Receive a document the user asked to open in your app
|
|
818
957
|
|
|
819
958
|
**Permission:** none to receive. Reading the document is a separate act (`vault:user-documents:read`, via `useVaultUserDocuments().getBytes`).
|
|
@@ -886,7 +1025,7 @@ Rules that matter:
|
|
|
886
1025
|
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
887
1026
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
888
1027
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
889
|
-
-
|
|
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.
|
|
890
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.
|
|
891
1030
|
|
|
892
1031
|
### `useArcheAssets()` — Contributor-published asset library
|
|
@@ -1340,7 +1479,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1340
1479
|
|
|
1341
1480
|
### Size and File Limits
|
|
1342
1481
|
|
|
1343
|
-
- **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)
|
|
1344
1483
|
- **Dependencies:** Max 20, exact semver versions only (e.g., `"4.4.7"`, not `"^4.4.7"`)
|
|
1345
1484
|
- Platform packages (`react`, `react-dom`, `@fias/arche-sdk`) are provided — do not include in `dependencies`
|
|
1346
1485
|
|
|
@@ -706,7 +706,7 @@ const { url, expiresAt, contentType } = await vault.getDownloadUrl(documentId);
|
|
|
706
706
|
// List documents this arche owns
|
|
707
707
|
const { documents, nextCursor } = await vault.list({ search: 'invoice', limit: 50 });
|
|
708
708
|
|
|
709
|
-
// Write a small text/JSON document (UTF-8, ≤
|
|
709
|
+
// Write a small text/JSON document (UTF-8, ≤ 200 KB — use upload() for anything larger)
|
|
710
710
|
const { documentId } = await vault.write({
|
|
711
711
|
name: 'settings.json',
|
|
712
712
|
content: JSON.stringify(settings),
|
|
@@ -735,11 +735,23 @@ const { bytes: saved } = await vault.readBytes(documentId);
|
|
|
735
735
|
// Overwrite a document THIS arche created IN PLACE — same documentId, bytes
|
|
736
736
|
// replaced, no version history. The autosave path (a spreadsheet saving as
|
|
737
737
|
// the user works); to publish a distinct new version use write({
|
|
738
|
-
// replacesDocumentId }) instead.
|
|
739
|
-
// names the cap
|
|
740
|
-
//
|
|
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
|
|
741
745
|
// drops the document's search index and does not re-run extraction.
|
|
742
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 }));
|
|
743
755
|
|
|
744
756
|
// Semantic search across documents this arche owns
|
|
745
757
|
// (burns user credits per the AI Markup Invariant — call sparingly)
|
|
@@ -772,6 +784,19 @@ const saved = await vault.upload(bytes, {
|
|
|
772
784
|
|
|
773
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.
|
|
774
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
|
+
|
|
775
800
|
**Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
|
|
776
801
|
|
|
777
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.
|
|
@@ -812,8 +837,122 @@ const { bytes } = await userDocs.getBytes(id); // raw bytes for PROCESSING binar
|
|
|
812
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.
|
|
813
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.
|
|
814
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.
|
|
815
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.
|
|
816
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
|
+
|
|
817
956
|
### `useArcheHandoff()` — Receive a document the user asked to open in your app
|
|
818
957
|
|
|
819
958
|
**Permission:** none to receive. Reading the document is a separate act (`vault:user-documents:read`, via `useVaultUserDocuments().getBytes`).
|
|
@@ -886,7 +1025,7 @@ Rules that matter:
|
|
|
886
1025
|
- **UI state only.** Actions manipulate what the user could do in your interface; anything touching server state stays in your own code paths.
|
|
887
1026
|
- `requiresConfirmation: true` makes the assistant ask the user before dispatching. It escalates only — nothing can skip a confirmation the platform requires.
|
|
888
1027
|
- Manifest actions win id collisions with runtime ones; the merged list is capped at 50.
|
|
889
|
-
-
|
|
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.
|
|
890
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.
|
|
891
1030
|
|
|
892
1031
|
### `useArcheAssets()` — Contributor-published asset library
|
|
@@ -1340,7 +1479,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
|
|
|
1340
1479
|
|
|
1341
1480
|
### Size and File Limits
|
|
1342
1481
|
|
|
1343
|
-
- **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)
|
|
1344
1483
|
- **Dependencies:** Max 20, exact semver versions only (e.g., `"4.4.7"`, not `"^4.4.7"`)
|
|
1345
1484
|
- Platform packages (`react`, `react-dom`, `@fias/arche-sdk`) are provided — do not include in `dependencies`
|
|
1346
1485
|
|