@fias/create-fias-plugin 1.24.0 → 1.25.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/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.33.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -92,6 +92,7 @@ function MyComponent() {
|
|
|
92
92
|
- `spacing`: `{ xs, sm, md, lg, xl }` (CSS values like `'8px'`)
|
|
93
93
|
- `fonts`: `{ body, heading, mono }` (font-family strings)
|
|
94
94
|
- `components`: `{ borderRadius, buttonRadius, cardRadius, inputRadius, shadowSm, shadowMd, shadowLg, borderWidth }`
|
|
95
|
+
- `brand?`: `{ displayName, logoUrl, primaryColor, accentColor, headerTextColor }` — the Company's brand, present only when your app runs on that Company's portal and the Company has set one. Each field may be `null` (colours are `#RRGGBB`; `logoUrl` is for an `<img src>`). Absent on fias.io: keep your own palette as the fallback, and re-read it on every theme update (the brand can arrive after your first render). In the dev harness, set `"mockBrand": { … }` in `fias-dev.config.json` to see your app on a portal.
|
|
95
96
|
|
|
96
97
|
### `useFiasVendoredAssets(importSpecifier)` — Self-hosted vendored-library asset URLs
|
|
97
98
|
|
|
@@ -346,6 +347,30 @@ A collection can carry rules the platform checks on **every** write, so a buggy
|
|
|
346
347
|
|
|
347
348
|
Rules that relate two documents (`references`, `ledgerSums`) are checked at the **end** of the write, so write the ledger row and the counter, or the target and the document pointing at it, together in one `batch` in any order. A violation rejects the whole write with `CONSTRAINT_VIOLATION`. Rules are **tighten-only**: you can add rules (at most 10 per kind), never remove or change one.
|
|
348
349
|
|
|
350
|
+
#### Attested logs (`attested`)
|
|
351
|
+
|
|
352
|
+
**Permission:** `data:attested` (requires `data:store`; human-reviewed)
|
|
353
|
+
|
|
354
|
+
An audit log whose "who did it" your code can't fake. Declare it in `fias-plugin.json` on a **new** collection, with `appendOnly` beside it:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
"permissions": ["data:store", "data:workspace", "data:attested"],
|
|
358
|
+
"collections": [
|
|
359
|
+
{ "name": "audit_log", "scope": "workspace", "constraints": { "appendOnly": true, "attested": true } }
|
|
360
|
+
]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- Fias stamps every row you add with `attestation`: `{ personRef, agentRef?, channel, recordedAt, appVersion?, buildHash? }` (`recordedAt` is when Fias stored the row; `appVersion` and `buildHash` name the release live when it was stored, on every row of your published app and never on a test row). It is never part of `data`, and nothing you send can set it. Read it from `getDocument()`, `query()` and `search()`; `put()` and each `batch()` write return the one they stamped.
|
|
364
|
+
- `personRef` is the app user ref of the person accountable for the row; name it with `resolvePeople`. `agentRef` is set only when an agent key wrote it (`channel: 'agent'`), and never resolves to a name: show "an agent acting for <person>".
|
|
365
|
+
- `channel` is `session`, `portal` (a Company portal), `assistant` (Fias AI), `agent` or `non_production`. Every row written in the dev harness or a Forge test is `non_production`, even after test data is carried into your published app — never present those as real.
|
|
366
|
+
- Rows can't be overwritten, or deleted one by one, by you, your users, agents or your contributor CLI (which can't write to an attested collection at all, and reads attestations without `personRef` / `agentRef`; agents and Fias AI read only the channel, time and release).
|
|
367
|
+
- In your published app only the publish creates it: `createCollection()` with the same constraints just finds it, and creating one there that the manifest didn't is refused (`ATTESTED_MANIFEST_ONLY`). In the dev harness and a Forge test your code creates it, as for any collection (the harness still checks your manifest declares it); a builder preview can't have one. Test data carried into your app at its first publish may hold only logs that release declares.
|
|
368
|
+
- It is refused on `shared` scope and on a collection that already exists.
|
|
369
|
+
- Fias turns attested logs on platform by platform, either for every app or for a short pilot list. Until it has for your app, a publish or a Forge test that creates a new log is refused (`ATTESTED_LOGS_NOT_AVAILABLE`); leave the log out of your manifest until then, never declare it without `attested` (that name could then never become a log). The dev harness's live mode is shared by all your apps, so it creates a new log only once logs are on for every app — a pilot app tests its log in Forge. A log your app already has keeps working whatever the switch says, and its Forge test channel can always recreate it.
|
|
370
|
+
- **Keep it declared.** Every later release must declare it, by name, with `attested`, and keep `data:attested`; a publish that drops either is refused (`ATTESTED_LOG_UNDECLARED`) — unless the log holds no real rows (empty, or only test rows carried in from a Forge test channel) and you leave it out of `collections` entirely; naming it without `attested` is always refused. Your app can write only to the logs its live release declares. A published log is never deleted, even empty (`COLLECTION_ATTESTED`), so its name stays a log. It lasts as long as your app: no workspace that holds a log — your app's own, a guild's or a Fias org's — can be archived (`WORKSPACE_ATTESTED`), so don't offer to archive one.
|
|
371
|
+
- In a Fias org workspace, adding a row also needs the workspace's approval of your app to cover `data:attested`; otherwise it is refused (`ARCHE_NOT_APPROVED`) and the workspace's admins see the app waiting to be approved again.
|
|
372
|
+
- Call it **platform-attested**. It proves who wrote the row, when and through which release, not that `data` is true; never call it "tamper-evident".
|
|
373
|
+
|
|
349
374
|
### Letting agents use your data (`agentAccess`)
|
|
350
375
|
|
|
351
376
|
**Permission:** `data:agents` (requires `data:store`)
|
|
@@ -642,7 +667,7 @@ const [assignee] = await workspaces.resolvePeople([task.assigneeRef], shared.wor
|
|
|
642
667
|
const label = assignee?.displayName ?? 'Former member';
|
|
643
668
|
```
|
|
644
669
|
|
|
645
|
-
**Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives), `listMembers`, `addPeople`, `updateMember`, `removeMember`, `resolvePeople`. **Add people with `addPeople(workspaceId, { role? })`**: Fias draws a sheet in which the person picks co-members of their Fias workspace or types an exact username, and may change the role (you suggest `'admin'`, `'member'` or `'viewer'`); it resolves with `{ added: [{ ref, displayName, role }] }` — empty if they added no one. Your code never names who to add. (`addMember(workspaceId, username, …)` is deprecated: it works only for apps from before app user refs, and in the dev harness and Forge test channel; elsewhere it rejects with `ADD_MEMBER_USE_SHEET`.) Members come back named by your app's `ref` for them and their `displayName` — never a username or id — and `updateMember` / `removeMember` take that `{ ref }`. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers`/`resolvePeople` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `ADD_PEOPLE_UNAVAILABLE` (no sheet here: the Forge test channel, Company portals), `MEMBER_TARGET_USE_REF` (name members by `{ ref }`), `SHEET_BUSY`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
|
|
670
|
+
**Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives. A workspace holding an attested log can't be archived), `listMembers`, `addPeople`, `updateMember`, `removeMember`, `resolvePeople`. **Add people with `addPeople(workspaceId, { role? })`**: Fias draws a sheet in which the person picks co-members of their Fias workspace or types an exact username, and may change the role (you suggest `'admin'`, `'member'` or `'viewer'`); it resolves with `{ added: [{ ref, displayName, role }] }` — empty if they added no one. Your code never names who to add. (`addMember(workspaceId, username, …)` is deprecated: it works only for apps from before app user refs, and in the dev harness and Forge test channel; elsewhere it rejects with `ADD_MEMBER_USE_SHEET`.) Members come back named by your app's `ref` for them and their `displayName` — never a username or id — and `updateMember` / `removeMember` take that `{ ref }`. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers`/`resolvePeople` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `ADD_PEOPLE_UNAVAILABLE` (no sheet here: the Forge test channel, Company portals), `MEMBER_TARGET_USE_REF` (name members by `{ ref }`), `SHEET_BUSY`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`, `WORKSPACE_ATTESTED`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
|
|
646
671
|
|
|
647
672
|
### `useFiasAccess()` — The current user's own access in a workspace
|
|
648
673
|
|
|
@@ -1828,15 +1853,16 @@ const result = await web.extract({
|
|
|
1828
1853
|
},
|
|
1829
1854
|
},
|
|
1830
1855
|
maxAgeSeconds: 24 * 3600,
|
|
1856
|
+
reads: 2, // money: read twice, keep only what both readings agree on
|
|
1831
1857
|
});
|
|
1832
|
-
// result.data.rates, result.site.name, result.fetchedAt, result.fromCache, result.discarded
|
|
1858
|
+
// result.data.rates, result.site.name, result.fetchedAt, result.fromCache, result.discarded, result.disagreed
|
|
1833
1859
|
```
|
|
1834
1860
|
|
|
1835
1861
|
Your app reads pages only on **websites Fias approved** (regulated institutions such as credit unions and banks, and government sites) — any page on an approved site, so a redesigned or moved page keeps working. To have a site approved, ask Fias. `listSites()` lists them (free).
|
|
1836
1862
|
|
|
1837
|
-
`extract()` never returns the page's text — not even a short label. You send a **form** — up to 30 fields of kinds `number` and `integer` (optional `min`/`max`), `boolean`, `date` (`YYYY-MM-DD`), `enum` (your own `values`) and `list` (up to 200 rows of those) — and Fias reads the page into it. There is no free-text kind: describe what you need as numbers, dates, yes/no and choices you list (a product type, not a product name). Every value may be `null` (the page did not say); a value that broke its field's rules comes back `null` and is named in `discarded`. URLs must be `https://`; `redirected: true` means the site moved the page within itself (the new address is not returned). The reader fills the form from whatever layout the page has, but it never runs JavaScript: a page that builds its figures with scripts comes back with `scriptHeavy: true` and mostly nulls — read the site's PDF rate sheet instead.
|
|
1863
|
+
`extract()` never returns the page's text — not even a short label. You send a **form** — up to 30 fields of kinds `number` and `integer` (optional `min`/`max`), `boolean`, `date` (`YYYY-MM-DD`), `enum` (your own `values`) and `list` (up to 200 rows of those) — and Fias reads the page into it. There is no free-text kind: describe what you need as numbers, dates, yes/no and choices you list (a product type, not a product name). Every value may be `null` (the page did not say); a value that broke its field's rules comes back `null` and is named in `discarded`. A model reading a page is not deterministic, so **where a wrong number matters (money), pass `reads: 2`**: the page is read twice, independently, and only a value both readings agree on is returned — a field they disagree on is `null` (a list keeps only the rows both produced) and is named in `disagreed`, for a person to confirm. A list named in `disagreed` may be missing rows the page prints: never conclude "absent on the page" (not offered) from a field in `disagreed` — treat it as unknown. Agreement says a value was stable across two readings, not that it is right; always show `site.name` and `fetchedAt` beside a figure so a person can check it. With two readings `discarded` names a list whole (`rates`), not a row. Two readings are two reader charges; the platform may raise the floor for every app, so check `disagreed` whatever you asked for. The reader's model, and so a reading's price, is chosen by the platform. URLs must be `https://`; `redirected: true` means the site moved the page within itself (the new address is not returned). The reader fills the form from whatever layout the page has, but it never runs JavaScript: a page that builds its figures with scripts comes back with `scriptHeavy: true` and mostly nulls — read the site's PDF rate sheet instead.
|
|
1838
1864
|
|
|
1839
|
-
One copy of each page serves every app, and a page is fetched from its site **at most once an hour**. `maxAgeSeconds` (default one day, 3600–30 days) is the oldest copy you accept. A fresh fetch (about a tenth of a cent) and
|
|
1865
|
+
One copy of each page serves every app, and a page is fetched from its site **at most once an hour**. `maxAgeSeconds` (default one day, 3600–30 days) is the oldest copy you accept. A fresh fetch (about a tenth of a cent) and each reading (a fast model's tokens — well under a cent for a typical page; two with `reads: 2`) are charged to the user; a cached result costs nothing (`fromCache: true`). `stale: true` means the site could not be asked again just now and you got the last copy. Always show `site.name` and `fetchedAt` beside the figures. Not available in the builder preview or the test channel. **Error codes:** `WEB_SOURCES_DISABLED`, `INVALID_URL`, `SITE_NOT_APPROVED`, `PAGE_EXCLUDED` (a part of the site where the public posts), `ROBOTS_DISALLOWED`, `FETCH_FAILED`, `RESPONSE_TOO_LARGE`, `CONTENT_TYPE_NOT_ALLOWED`, `EXTRACTION_FAILED`, `SOURCE_BUSY`, `RATE_LIMIT` (surface it, never retry in a loop), `INSUFFICIENT_CREDITS`, `PERMISSION_DENIED`.
|
|
1840
1866
|
|
|
1841
1867
|
### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
|
|
1842
1868
|
|
|
@@ -2342,7 +2368,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2342
2368
|
| `expandedDescription` | No | Long-form marketplace copy |
|
|
2343
2369
|
| `main` | Yes | Entry point source file |
|
|
2344
2370
|
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
2345
|
-
| `categorySlug` | No |
|
|
2371
|
+
| `categorySlug` | No | Deprecated and ignored — Arche Store categories are `store.categories` |
|
|
2346
2372
|
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on every publish (local, staging and prod) and rehosted as your marketplace/store icon. PNG, JPEG, WebP or SVG, ≤5 MB; raster images must also be ≤1024×1024 px (SVG has no pixel limit). An invalid icon rejects the submission: a first-publish listing fee is refunded, but the AI review that already ran is still billed. To change it, replace the file (or the path) and republish; removing the field keeps the current icon. |
|
|
2347
2373
|
| `tags` | No | Discovery tags |
|
|
2348
2374
|
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
@@ -2355,6 +2381,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2355
2381
|
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
2356
2382
|
| `isGame` | No | Default `false`. `true` lists your arche on the Games page (`/a/games`) and badges it as a game in the Arche Store. Requires `game`. Omitting it on a republish takes the arche out of Games |
|
|
2357
2383
|
| `game` | With `isGame` | `{ "genres": [...] }` — 1-4 (more only where a Fias admin allowed your arche) of `action`, `rpg`, `board`, `cards`, `casual`, `crafting`, `flying`, `match`, `platform`, `puzzle`, `strategy`, `word`, `racing`, `sports`, `simulation`, `trivia`, `music`, `educational`, `cozy`, `idle`, `tower-defense` (a kind of `strategy`: its games are listed there too). Refused without `isGame: true` |
|
|
2384
|
+
| `store` | No | How your arche presents itself in the Arche Store: `{ "subtitle", "categories", "releaseNotes", "languages", "screenshots" }`, every field optional (see "Your Arche Store page" below). Omitting a field on a republish clears it |
|
|
2358
2385
|
| `sdk` | Yes | SDK version range |
|
|
2359
2386
|
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
2360
2387
|
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
@@ -2372,7 +2399,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2372
2399
|
|
|
2373
2400
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
2374
2401
|
|
|
2375
|
-
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `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`, `vault:creative:read`, `vault:creative:write`, `opener:send`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`, `connections:use`, `schedule:invoke`, `web:sources:read`
|
|
2402
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `data:attested`, `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`, `vault:creative:read`, `vault:creative:write`, `opener:send`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`, `connections:use`, `schedule:invoke`, `web:sources:read`
|
|
2376
2403
|
|
|
2377
2404
|
**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`.
|
|
2378
2405
|
|
|
@@ -2563,6 +2590,33 @@ If the AI review rejects your submission, the listing-fee portion is
|
|
|
2563
2590
|
refunded automatically (the per-review portion is not, since review work
|
|
2564
2591
|
happened either way).
|
|
2565
2592
|
|
|
2593
|
+
### Your Arche Store page
|
|
2594
|
+
|
|
2595
|
+
Every listed arche gets a public page in the Arche Store. The manifest's `store` block decides how it reads; every field is optional, and a republish that omits one clears it.
|
|
2596
|
+
|
|
2597
|
+
```json
|
|
2598
|
+
{
|
|
2599
|
+
"store": {
|
|
2600
|
+
"subtitle": "Budgets that balance",
|
|
2601
|
+
"categories": ["finance", "productivity"],
|
|
2602
|
+
"releaseNotes": "New: monthly reports.\nFixed: rounding in CSV exports.",
|
|
2603
|
+
"languages": ["en", "pt-BR"],
|
|
2604
|
+
"screenshots": [
|
|
2605
|
+
{ "src": "store/dashboard.png", "alt": "This month's spending by category" },
|
|
2606
|
+
{ "src": "store/phone.png" }
|
|
2607
|
+
]
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
```
|
|
2611
|
+
|
|
2612
|
+
- `subtitle` — the line under your app's name, up to 40 characters, one line.
|
|
2613
|
+
- `categories` — one or two of `ai-assistants`, `productivity`, `business`, `finance`, `communication`, `social`, `graphics-design`, `photo-video`, `music-audio`, `education`, `developer-tools`, `utilities`, `entertainment`, `lifestyle`, `health-fitness`, `travel`, `food-drink`, `shopping`, `news`, `reference`, `sports`. The first is your primary category: it shows under your app's name and is the one you are ranked in. **Games don't take categories** — a game is listed under its `game.genres`.
|
|
2614
|
+
- `releaseNotes` — this release's "What's New", up to 4,000 characters of plain text.
|
|
2615
|
+
- `languages` — the languages your app works in, as language tags (`"en"`, `"pt-BR"`), up to 20.
|
|
2616
|
+
- `screenshots` — up to 10, shown in order on your page. Each `src` is a file directly in a `store/` folder at your plugin's root (not `public/`, which is served as part of your app). PNG, JPEG or WebP, at most 2 MB, the shorter side at least 320 pixels and the longer at most 4096, between 0.4:1 (a tall phone screen) and 2.5:1 (a wide desktop one). `alt` (up to 120 characters) says what it shows. Only the files you name are sent. Each one is checked by an image safety screen when you publish, billed with your review; one the screen is unsure of is shown once a person at Fias approves it, and the rest of your release goes live meanwhile. A wide screenshot becomes your page's link preview.
|
|
2617
|
+
|
|
2618
|
+
Your app answers searches for its categories' words too — a `finance` app is found by "budget apps" even if its description never says "budget". `npx fias-dev validate` checks the block offline.
|
|
2619
|
+
|
|
2566
2620
|
### Managing the asset library (for `useArcheAssets`)
|
|
2567
2621
|
|
|
2568
2622
|
`useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <prod|staging|local>` (default `prod`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
|
|
@@ -2611,11 +2665,22 @@ Manage them on your arche's settings page ("Store listings"), or from a `store-l
|
|
|
2611
2665
|
"isGame": true,
|
|
2612
2666
|
"genres": ["word"],
|
|
2613
2667
|
"icon": "store-art/wordtrace.png"
|
|
2668
|
+
},
|
|
2669
|
+
{
|
|
2670
|
+
"path": "/tools/budget",
|
|
2671
|
+
"title": "Budget Planner",
|
|
2672
|
+
"description": "Plan a month of spending in five minutes.",
|
|
2673
|
+
"isGame": false,
|
|
2674
|
+
"genres": [],
|
|
2675
|
+
"categories": ["finance"],
|
|
2676
|
+
"icon": "store-art/budget.png"
|
|
2614
2677
|
}
|
|
2615
2678
|
]
|
|
2616
2679
|
}
|
|
2617
2680
|
```
|
|
2618
2681
|
|
|
2682
|
+
A listing that isn't a game names up to two Arche Store `categories` instead of genres (the same ids as the manifest's `store.categories`, the first its primary one); a game's `categories` stay empty.
|
|
2683
|
+
|
|
2619
2684
|
```bash
|
|
2620
2685
|
npx fias-dev listings list --env staging # Slots, live and retired listings
|
|
2621
2686
|
npx fias-dev listings push --env staging # Show the plan and the charge, then ask
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- fias-sdk-guide-version: 2.
|
|
1
|
+
<!-- fias-sdk-guide-version: 2.33.0 -->
|
|
2
2
|
|
|
3
3
|
# FIAS Plugin Development Guide
|
|
4
4
|
|
|
@@ -92,6 +92,7 @@ function MyComponent() {
|
|
|
92
92
|
- `spacing`: `{ xs, sm, md, lg, xl }` (CSS values like `'8px'`)
|
|
93
93
|
- `fonts`: `{ body, heading, mono }` (font-family strings)
|
|
94
94
|
- `components`: `{ borderRadius, buttonRadius, cardRadius, inputRadius, shadowSm, shadowMd, shadowLg, borderWidth }`
|
|
95
|
+
- `brand?`: `{ displayName, logoUrl, primaryColor, accentColor, headerTextColor }` — the Company's brand, present only when your app runs on that Company's portal and the Company has set one. Each field may be `null` (colours are `#RRGGBB`; `logoUrl` is for an `<img src>`). Absent on fias.io: keep your own palette as the fallback, and re-read it on every theme update (the brand can arrive after your first render). In the dev harness, set `"mockBrand": { … }` in `fias-dev.config.json` to see your app on a portal.
|
|
95
96
|
|
|
96
97
|
### `useFiasVendoredAssets(importSpecifier)` — Self-hosted vendored-library asset URLs
|
|
97
98
|
|
|
@@ -346,6 +347,30 @@ A collection can carry rules the platform checks on **every** write, so a buggy
|
|
|
346
347
|
|
|
347
348
|
Rules that relate two documents (`references`, `ledgerSums`) are checked at the **end** of the write, so write the ledger row and the counter, or the target and the document pointing at it, together in one `batch` in any order. A violation rejects the whole write with `CONSTRAINT_VIOLATION`. Rules are **tighten-only**: you can add rules (at most 10 per kind), never remove or change one.
|
|
348
349
|
|
|
350
|
+
#### Attested logs (`attested`)
|
|
351
|
+
|
|
352
|
+
**Permission:** `data:attested` (requires `data:store`; human-reviewed)
|
|
353
|
+
|
|
354
|
+
An audit log whose "who did it" your code can't fake. Declare it in `fias-plugin.json` on a **new** collection, with `appendOnly` beside it:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
"permissions": ["data:store", "data:workspace", "data:attested"],
|
|
358
|
+
"collections": [
|
|
359
|
+
{ "name": "audit_log", "scope": "workspace", "constraints": { "appendOnly": true, "attested": true } }
|
|
360
|
+
]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- Fias stamps every row you add with `attestation`: `{ personRef, agentRef?, channel, recordedAt, appVersion?, buildHash? }` (`recordedAt` is when Fias stored the row; `appVersion` and `buildHash` name the release live when it was stored, on every row of your published app and never on a test row). It is never part of `data`, and nothing you send can set it. Read it from `getDocument()`, `query()` and `search()`; `put()` and each `batch()` write return the one they stamped.
|
|
364
|
+
- `personRef` is the app user ref of the person accountable for the row; name it with `resolvePeople`. `agentRef` is set only when an agent key wrote it (`channel: 'agent'`), and never resolves to a name: show "an agent acting for <person>".
|
|
365
|
+
- `channel` is `session`, `portal` (a Company portal), `assistant` (Fias AI), `agent` or `non_production`. Every row written in the dev harness or a Forge test is `non_production`, even after test data is carried into your published app — never present those as real.
|
|
366
|
+
- Rows can't be overwritten, or deleted one by one, by you, your users, agents or your contributor CLI (which can't write to an attested collection at all, and reads attestations without `personRef` / `agentRef`; agents and Fias AI read only the channel, time and release).
|
|
367
|
+
- In your published app only the publish creates it: `createCollection()` with the same constraints just finds it, and creating one there that the manifest didn't is refused (`ATTESTED_MANIFEST_ONLY`). In the dev harness and a Forge test your code creates it, as for any collection (the harness still checks your manifest declares it); a builder preview can't have one. Test data carried into your app at its first publish may hold only logs that release declares.
|
|
368
|
+
- It is refused on `shared` scope and on a collection that already exists.
|
|
369
|
+
- Fias turns attested logs on platform by platform, either for every app or for a short pilot list. Until it has for your app, a publish or a Forge test that creates a new log is refused (`ATTESTED_LOGS_NOT_AVAILABLE`); leave the log out of your manifest until then, never declare it without `attested` (that name could then never become a log). The dev harness's live mode is shared by all your apps, so it creates a new log only once logs are on for every app — a pilot app tests its log in Forge. A log your app already has keeps working whatever the switch says, and its Forge test channel can always recreate it.
|
|
370
|
+
- **Keep it declared.** Every later release must declare it, by name, with `attested`, and keep `data:attested`; a publish that drops either is refused (`ATTESTED_LOG_UNDECLARED`) — unless the log holds no real rows (empty, or only test rows carried in from a Forge test channel) and you leave it out of `collections` entirely; naming it without `attested` is always refused. Your app can write only to the logs its live release declares. A published log is never deleted, even empty (`COLLECTION_ATTESTED`), so its name stays a log. It lasts as long as your app: no workspace that holds a log — your app's own, a guild's or a Fias org's — can be archived (`WORKSPACE_ATTESTED`), so don't offer to archive one.
|
|
371
|
+
- In a Fias org workspace, adding a row also needs the workspace's approval of your app to cover `data:attested`; otherwise it is refused (`ARCHE_NOT_APPROVED`) and the workspace's admins see the app waiting to be approved again.
|
|
372
|
+
- Call it **platform-attested**. It proves who wrote the row, when and through which release, not that `data` is true; never call it "tamper-evident".
|
|
373
|
+
|
|
349
374
|
### Letting agents use your data (`agentAccess`)
|
|
350
375
|
|
|
351
376
|
**Permission:** `data:agents` (requires `data:store`)
|
|
@@ -642,7 +667,7 @@ const [assignee] = await workspaces.resolvePeople([task.assigneeRef], shared.wor
|
|
|
642
667
|
const label = assignee?.displayName ?? 'Former member';
|
|
643
668
|
```
|
|
644
669
|
|
|
645
|
-
**Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives), `listMembers`, `addPeople`, `updateMember`, `removeMember`, `resolvePeople`. **Add people with `addPeople(workspaceId, { role? })`**: Fias draws a sheet in which the person picks co-members of their Fias workspace or types an exact username, and may change the role (you suggest `'admin'`, `'member'` or `'viewer'`); it resolves with `{ added: [{ ref, displayName, role }] }` — empty if they added no one. Your code never names who to add. (`addMember(workspaceId, username, …)` is deprecated: it works only for apps from before app user refs, and in the dev harness and Forge test channel; elsewhere it rejects with `ADD_MEMBER_USE_SHEET`.) Members come back named by your app's `ref` for them and their `displayName` — never a username or id — and `updateMember` / `removeMember` take that `{ ref }`. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers`/`resolvePeople` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `ADD_PEOPLE_UNAVAILABLE` (no sheet here: the Forge test channel, Company portals), `MEMBER_TARGET_USE_REF` (name members by `{ ref }`), `SHEET_BUSY`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
|
|
670
|
+
**Methods:** `create`, `openForWorkspace`, `list`, `get`, `archive` (owner only — final: an archived workspace cannot be reopened, and it is deleted with its documents 30 days later; say so before your user archives. A workspace holding an attested log can't be archived), `listMembers`, `addPeople`, `updateMember`, `removeMember`, `resolvePeople`. **Add people with `addPeople(workspaceId, { role? })`**: Fias draws a sheet in which the person picks co-members of their Fias workspace or types an exact username, and may change the role (you suggest `'admin'`, `'member'` or `'viewer'`); it resolves with `{ added: [{ ref, displayName, role }] }` — empty if they added no one. Your code never names who to add. (`addMember(workspaceId, username, …)` is deprecated: it works only for apps from before app user refs, and in the dev harness and Forge test channel; elsewhere it rejects with `ADD_MEMBER_USE_SHEET`.) Members come back named by your app's `ref` for them and their `displayName` — never a username or id — and `updateMember` / `removeMember` take that `{ ref }`. **Rate limits (per minute):** `create`/`archive` 20, member ops 30, `list`/`get`/`listMembers`/`resolvePeople` 120. **Error codes:** `NOT_A_MEMBER`, `FORBIDDEN`, `ROLE_CEILING`, `ADD_PEOPLE_UNAVAILABLE` (no sheet here: the Forge test channel, Company portals), `MEMBER_TARGET_USE_REF` (name members by `{ ref }`), `SHEET_BUSY`, `MUST_KEEP_ONE_OWNER`, `WORKSPACE_NOT_FOUND`, `WORKSPACE_ATTESTED`, `HOLDER_BACKED_WORKSPACE`, `HOLDER_NOT_ELIGIBLE`. Read workspace documents with `useFiasDataStore().get`/`query`/`search` passing `{ workspaceId }` (every active member can read; make the collection `searchable` to use `search`). **Per-collection role floors:** raise a workspace collection's minimum role at creation — `createCollection('board_minutes', { userScope: 'workspace', writeMinRole: 'admin', readMinRole: 'member' })` makes that collection admin-write / member-read, above the default (write ≥ member, read ≥ viewer).
|
|
646
671
|
|
|
647
672
|
### `useFiasAccess()` — The current user's own access in a workspace
|
|
648
673
|
|
|
@@ -1828,15 +1853,16 @@ const result = await web.extract({
|
|
|
1828
1853
|
},
|
|
1829
1854
|
},
|
|
1830
1855
|
maxAgeSeconds: 24 * 3600,
|
|
1856
|
+
reads: 2, // money: read twice, keep only what both readings agree on
|
|
1831
1857
|
});
|
|
1832
|
-
// result.data.rates, result.site.name, result.fetchedAt, result.fromCache, result.discarded
|
|
1858
|
+
// result.data.rates, result.site.name, result.fetchedAt, result.fromCache, result.discarded, result.disagreed
|
|
1833
1859
|
```
|
|
1834
1860
|
|
|
1835
1861
|
Your app reads pages only on **websites Fias approved** (regulated institutions such as credit unions and banks, and government sites) — any page on an approved site, so a redesigned or moved page keeps working. To have a site approved, ask Fias. `listSites()` lists them (free).
|
|
1836
1862
|
|
|
1837
|
-
`extract()` never returns the page's text — not even a short label. You send a **form** — up to 30 fields of kinds `number` and `integer` (optional `min`/`max`), `boolean`, `date` (`YYYY-MM-DD`), `enum` (your own `values`) and `list` (up to 200 rows of those) — and Fias reads the page into it. There is no free-text kind: describe what you need as numbers, dates, yes/no and choices you list (a product type, not a product name). Every value may be `null` (the page did not say); a value that broke its field's rules comes back `null` and is named in `discarded`. URLs must be `https://`; `redirected: true` means the site moved the page within itself (the new address is not returned). The reader fills the form from whatever layout the page has, but it never runs JavaScript: a page that builds its figures with scripts comes back with `scriptHeavy: true` and mostly nulls — read the site's PDF rate sheet instead.
|
|
1863
|
+
`extract()` never returns the page's text — not even a short label. You send a **form** — up to 30 fields of kinds `number` and `integer` (optional `min`/`max`), `boolean`, `date` (`YYYY-MM-DD`), `enum` (your own `values`) and `list` (up to 200 rows of those) — and Fias reads the page into it. There is no free-text kind: describe what you need as numbers, dates, yes/no and choices you list (a product type, not a product name). Every value may be `null` (the page did not say); a value that broke its field's rules comes back `null` and is named in `discarded`. A model reading a page is not deterministic, so **where a wrong number matters (money), pass `reads: 2`**: the page is read twice, independently, and only a value both readings agree on is returned — a field they disagree on is `null` (a list keeps only the rows both produced) and is named in `disagreed`, for a person to confirm. A list named in `disagreed` may be missing rows the page prints: never conclude "absent on the page" (not offered) from a field in `disagreed` — treat it as unknown. Agreement says a value was stable across two readings, not that it is right; always show `site.name` and `fetchedAt` beside a figure so a person can check it. With two readings `discarded` names a list whole (`rates`), not a row. Two readings are two reader charges; the platform may raise the floor for every app, so check `disagreed` whatever you asked for. The reader's model, and so a reading's price, is chosen by the platform. URLs must be `https://`; `redirected: true` means the site moved the page within itself (the new address is not returned). The reader fills the form from whatever layout the page has, but it never runs JavaScript: a page that builds its figures with scripts comes back with `scriptHeavy: true` and mostly nulls — read the site's PDF rate sheet instead.
|
|
1838
1864
|
|
|
1839
|
-
One copy of each page serves every app, and a page is fetched from its site **at most once an hour**. `maxAgeSeconds` (default one day, 3600–30 days) is the oldest copy you accept. A fresh fetch (about a tenth of a cent) and
|
|
1865
|
+
One copy of each page serves every app, and a page is fetched from its site **at most once an hour**. `maxAgeSeconds` (default one day, 3600–30 days) is the oldest copy you accept. A fresh fetch (about a tenth of a cent) and each reading (a fast model's tokens — well under a cent for a typical page; two with `reads: 2`) are charged to the user; a cached result costs nothing (`fromCache: true`). `stale: true` means the site could not be asked again just now and you got the last copy. Always show `site.name` and `fetchedAt` beside the figures. Not available in the builder preview or the test channel. **Error codes:** `WEB_SOURCES_DISABLED`, `INVALID_URL`, `SITE_NOT_APPROVED`, `PAGE_EXCLUDED` (a part of the site where the public posts), `ROBOTS_DISALLOWED`, `FETCH_FAILED`, `RESPONSE_TOO_LARGE`, `CONTENT_TYPE_NOT_ALLOWED`, `EXTRACTION_FAILED`, `SOURCE_BUSY`, `RATE_LIMIT` (surface it, never retry in a loop), `INSUFFICIENT_CREDITS`, `PERMISSION_DENIED`.
|
|
1840
1866
|
|
|
1841
1867
|
### `useFiasX()` / `useFiasLinkedIn()` — Act as a connected account
|
|
1842
1868
|
|
|
@@ -2342,7 +2368,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2342
2368
|
| `expandedDescription` | No | Long-form marketplace copy |
|
|
2343
2369
|
| `main` | Yes | Entry point source file |
|
|
2344
2370
|
| `archeType` | Yes | `"tool"` or `"site"` |
|
|
2345
|
-
| `categorySlug` | No |
|
|
2371
|
+
| `categorySlug` | No | Deprecated and ignored — Arche Store categories are `store.categories` |
|
|
2346
2372
|
| `icon` | No | Path to an icon image **under `public/`** (e.g. `public/icon.png`) — only `src/` and `public/` are packaged. Applied on every publish (local, staging and prod) and rehosted as your marketplace/store icon. PNG, JPEG, WebP or SVG, ≤5 MB; raster images must also be ≤1024×1024 px (SVG has no pixel limit). An invalid icon rejects the submission: a first-publish listing fee is refunded, but the AI review that already ran is still billed. To change it, replace the file (or the path) and republish; removing the field keeps the current icon. |
|
|
2347
2373
|
| `tags` | No | Discovery tags |
|
|
2348
2374
|
| `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
|
|
@@ -2355,6 +2381,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2355
2381
|
| `isListed` | No | Default `true`. Visible in marketplace store |
|
|
2356
2382
|
| `isGame` | No | Default `false`. `true` lists your arche on the Games page (`/a/games`) and badges it as a game in the Arche Store. Requires `game`. Omitting it on a republish takes the arche out of Games |
|
|
2357
2383
|
| `game` | With `isGame` | `{ "genres": [...] }` — 1-4 (more only where a Fias admin allowed your arche) of `action`, `rpg`, `board`, `cards`, `casual`, `crafting`, `flying`, `match`, `platform`, `puzzle`, `strategy`, `word`, `racing`, `sports`, `simulation`, `trivia`, `music`, `educational`, `cozy`, `idle`, `tower-defense` (a kind of `strategy`: its games are listed there too). Refused without `isGame: true` |
|
|
2384
|
+
| `store` | No | How your arche presents itself in the Arche Store: `{ "subtitle", "categories", "releaseNotes", "languages", "screenshots" }`, every field optional (see "Your Arche Store page" below). Omitting a field on a republish clears it |
|
|
2358
2385
|
| `sdk` | Yes | SDK version range |
|
|
2359
2386
|
| `dependencies` | No | npm packages with **exact** versions (max 20) |
|
|
2360
2387
|
| `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
|
|
@@ -2372,7 +2399,7 @@ The SDK also exports the following for advanced use cases. Most plugins don't ne
|
|
|
2372
2399
|
|
|
2373
2400
|
Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
|
|
2374
2401
|
|
|
2375
|
-
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `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`, `vault:creative:read`, `vault:creative:write`, `opener:send`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`, `connections:use`, `schedule:invoke`, `web:sources:read`
|
|
2402
|
+
**Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `storage:local`, `data:store`, `data:search`, `data:workspace`, `data:attested`, `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`, `vault:creative:read`, `vault:creative:write`, `opener:send`, `assets:read`, `assets:community:read`, `assets:community:publish`, `litcraft:hub`, `navigation:open_arche`, `sandbox:vendored-libraries`, `ai:actions`, `connections:use`, `schedule:invoke`, `web:sources:read`
|
|
2376
2403
|
|
|
2377
2404
|
**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`.
|
|
2378
2405
|
|
|
@@ -2563,6 +2590,33 @@ If the AI review rejects your submission, the listing-fee portion is
|
|
|
2563
2590
|
refunded automatically (the per-review portion is not, since review work
|
|
2564
2591
|
happened either way).
|
|
2565
2592
|
|
|
2593
|
+
### Your Arche Store page
|
|
2594
|
+
|
|
2595
|
+
Every listed arche gets a public page in the Arche Store. The manifest's `store` block decides how it reads; every field is optional, and a republish that omits one clears it.
|
|
2596
|
+
|
|
2597
|
+
```json
|
|
2598
|
+
{
|
|
2599
|
+
"store": {
|
|
2600
|
+
"subtitle": "Budgets that balance",
|
|
2601
|
+
"categories": ["finance", "productivity"],
|
|
2602
|
+
"releaseNotes": "New: monthly reports.\nFixed: rounding in CSV exports.",
|
|
2603
|
+
"languages": ["en", "pt-BR"],
|
|
2604
|
+
"screenshots": [
|
|
2605
|
+
{ "src": "store/dashboard.png", "alt": "This month's spending by category" },
|
|
2606
|
+
{ "src": "store/phone.png" }
|
|
2607
|
+
]
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
```
|
|
2611
|
+
|
|
2612
|
+
- `subtitle` — the line under your app's name, up to 40 characters, one line.
|
|
2613
|
+
- `categories` — one or two of `ai-assistants`, `productivity`, `business`, `finance`, `communication`, `social`, `graphics-design`, `photo-video`, `music-audio`, `education`, `developer-tools`, `utilities`, `entertainment`, `lifestyle`, `health-fitness`, `travel`, `food-drink`, `shopping`, `news`, `reference`, `sports`. The first is your primary category: it shows under your app's name and is the one you are ranked in. **Games don't take categories** — a game is listed under its `game.genres`.
|
|
2614
|
+
- `releaseNotes` — this release's "What's New", up to 4,000 characters of plain text.
|
|
2615
|
+
- `languages` — the languages your app works in, as language tags (`"en"`, `"pt-BR"`), up to 20.
|
|
2616
|
+
- `screenshots` — up to 10, shown in order on your page. Each `src` is a file directly in a `store/` folder at your plugin's root (not `public/`, which is served as part of your app). PNG, JPEG or WebP, at most 2 MB, the shorter side at least 320 pixels and the longer at most 4096, between 0.4:1 (a tall phone screen) and 2.5:1 (a wide desktop one). `alt` (up to 120 characters) says what it shows. Only the files you name are sent. Each one is checked by an image safety screen when you publish, billed with your review; one the screen is unsure of is shown once a person at Fias approves it, and the rest of your release goes live meanwhile. A wide screenshot becomes your page's link preview.
|
|
2617
|
+
|
|
2618
|
+
Your app answers searches for its categories' words too — a `finance` app is found by "budget apps" even if its description never says "budget". `npx fias-dev validate` checks the block offline.
|
|
2619
|
+
|
|
2566
2620
|
### Managing the asset library (for `useArcheAssets`)
|
|
2567
2621
|
|
|
2568
2622
|
`useArcheAssets()` reads a contributor-curated set of images pinned to the arche's published version. Populate that library with the `assets` subcommands. Each command accepts `--env <prod|staging|local>` (default `prod`) and `--arche-id <id>` (auto-resolved from the manifest when omitted).
|
|
@@ -2611,11 +2665,22 @@ Manage them on your arche's settings page ("Store listings"), or from a `store-l
|
|
|
2611
2665
|
"isGame": true,
|
|
2612
2666
|
"genres": ["word"],
|
|
2613
2667
|
"icon": "store-art/wordtrace.png"
|
|
2668
|
+
},
|
|
2669
|
+
{
|
|
2670
|
+
"path": "/tools/budget",
|
|
2671
|
+
"title": "Budget Planner",
|
|
2672
|
+
"description": "Plan a month of spending in five minutes.",
|
|
2673
|
+
"isGame": false,
|
|
2674
|
+
"genres": [],
|
|
2675
|
+
"categories": ["finance"],
|
|
2676
|
+
"icon": "store-art/budget.png"
|
|
2614
2677
|
}
|
|
2615
2678
|
]
|
|
2616
2679
|
}
|
|
2617
2680
|
```
|
|
2618
2681
|
|
|
2682
|
+
A listing that isn't a game names up to two Arche Store `categories` instead of genres (the same ids as the manifest's `store.categories`, the first its primary one); a game's `categories` stay empty.
|
|
2683
|
+
|
|
2619
2684
|
```bash
|
|
2620
2685
|
npx fias-dev listings list --env staging # Slots, live and retired listings
|
|
2621
2686
|
npx fias-dev listings push --env staging # Show the plan and the charge, then ask
|