@fias/arche-sdk 1.12.0 → 1.13.1

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.
@@ -2,8 +2,6 @@
2
2
 
3
3
  This project is a FIAS platform plugin — a React application that runs in a sandboxed iframe within the FIAS marketplace. This file provides the context AI coding assistants need to build, test, and submit plugins effectively.
4
4
 
5
- For other AI tool instruction files, see `AGENTS.md` (identical content).
6
-
7
5
  ## Project Structure
8
6
 
9
7
  ```
@@ -183,11 +181,11 @@ function AISummarizer() {
183
181
  });
184
182
  }
185
183
 
186
- // streamingText holds the accumulated tokens during the call AND continues to
187
- // hold the full text after the call completes — it is NOT cleared. result.output
188
- // also contains the full text once invoke() resolves. Render ONE slot only —
189
- // streamingText while loading, result.output afterwards. Showing both renders
190
- // the response twice.
184
+ // `streamingText` accumulates tokens during the call AND continues to hold
185
+ // the full text after the call resolves — it is NOT cleared. `result.output`
186
+ // also contains the full text once `invoke()` resolves. Render ONE slot only
187
+ // — `streamingText` while loading, `result.output` afterwards — or the
188
+ // response appears twice.
191
189
  return (
192
190
  <div>
193
191
  <button onClick={() => summarize('...')} disabled={isLoading}>
@@ -207,7 +205,43 @@ The `entityId` references a published model entity. Browse available models with
207
205
  **Permission:** `entities:image_generate`
208
206
  **Returns:** `ImageGenerationApi`
209
207
 
210
- Generate images using platform image models. Images are saved to the user's Fias file system and a display URL is returned. Use `npx fias-dev entities --detail <entityId>` to check supported sizes, qualities, and styles for each model.
208
+ Generate images using platform image models. Images are saved to the user's Fias file system and a display URL is returned.
209
+
210
+ #### Choosing an image model
211
+
212
+ The `entityId` parameter accepts three forms. **Prefer the selector forms** (`platformRecommended` / `providerRecommended`) over hardcoded entity IDs: the platform's recommendations evolve, models get retired, and a selector lets your plugin pick up the current best automatically without a code edit. Hardcoded IDs are still supported for back-compat and for cases where you want a specific model — but if the platform retires that exact entity, your plugin breaks unless the platform has registered a successor.
213
+
214
+ ```tsx
215
+ // 1. Platform's blessed pick for a workflow + optional style (RECOMMENDED).
216
+ // Asking for `image-to-image` will never return a model that can't do it.
217
+ generate({
218
+ entityId: { platformRecommended: { mode: 'text-to-image', style: 'illustration' } },
219
+ prompt: "A child's storybook page of a fox in the forest",
220
+ });
221
+
222
+ // 2. A specific provider's blessed pick. Use when your plugin's value prop
223
+ // depends on a particular provider's quirks.
224
+ generate({
225
+ entityId: { providerRecommended: { provider: 'openai', mode: 'image-to-image' } },
226
+ prompt: 'Make it look like an oil painting',
227
+ referenceImage: { fileId: previousImage.fileId },
228
+ });
229
+
230
+ // 3. Explicit entityId — back-compat path. Use only when you want a specific
231
+ // model. If the entity has been deprecated, the response includes
232
+ // `deprecation: { requestedEntityId, resolvedEntityId, deprecatedAt }` and
233
+ // you get the successor.
234
+ generate({
235
+ entityId: 'ent_modeldef_gpt_image_1_5',
236
+ prompt: 'A serene mountain landscape at sunset',
237
+ });
238
+ ```
239
+
240
+ Mode values: `'text-to-image'` (baseline), `'image-to-image'` (requires `referenceImage`), `'vector'` (SVG output). Style values: `'illustration'` (stylized / digital-art) or `'photoreal'`.
241
+
242
+ Use `npx fias-dev entities --detail <entityId>` to check supported sizes, qualities, and styles for each model.
243
+
244
+ #### Full example
211
245
 
212
246
  ```tsx
213
247
  import { useImageGeneration } from '@fias/arche-sdk';
@@ -220,9 +254,9 @@ function ImageMaker() {
220
254
  <button
221
255
  onClick={() =>
222
256
  generate({
223
- entityId: 'ent_modeldef_gpt_image_1_5',
257
+ entityId: { platformRecommended: { mode: 'text-to-image' } },
224
258
  prompt: 'A serene mountain landscape at sunset',
225
- size: '1024x1024',
259
+ size: '16:9',
226
260
  quality: 'high',
227
261
  })
228
262
  }
@@ -230,63 +264,227 @@ function ImageMaker() {
230
264
  >
231
265
  {isLoading ? 'Generating...' : 'Generate Image'}
232
266
  </button>
233
- {result && <img src={result.imageUrl} alt="Generated" />}
267
+ {result && (
268
+ <>
269
+ <img src={result.imageUrl} alt="Generated" />
270
+ {result.deprecation && (
271
+ <p style={{ color: 'orange' }}>
272
+ Note: requested entity "{result.deprecation.requestedEntityId}" is deprecated;
273
+ auto-substituted "{result.deprecation.resolvedEntityId}". Update your code.
274
+ </p>
275
+ )}
276
+ </>
277
+ )}
234
278
  {error && <p>Error: {error.message}</p>}
235
279
  </div>
236
280
  );
237
281
  }
238
282
  ```
239
283
 
240
- ### `useFiasFiles()` — User-files filesystem (SDK ≥ 1.9.0)
284
+ ### `useImageEntities()` — Discover image-generation entities
285
+
286
+ **Permission:** `entities:image_generate`
287
+ **Returns:** `{ entities, isLoading, error, refetch }`
241
288
 
242
- **Permissions:** `files:read` for `list`/`read`/`getDownloadUrl`; add `files:write` for `write`/`update`/`delete`
243
- **Returns:** `FiasFilesApi`
289
+ Use to render a "let the user pick a model" picker. For the common case of "just give me something good," skip discovery and pass a selector directly to `useImageGeneration`:
244
290
 
245
- Access the user's Fias filesystem, scoped to files this arche owns. Files written through this hook (and images created via `useImageGeneration()`) appear in MyData under `/My-Fias/{ArcheName}/`. Listing is by ownership — moving a file in MyData doesn't hide it from the arche.
291
+ ```tsx
292
+ await generate({
293
+ entityId: { platformRecommended: { mode: 'text-to-image' } },
294
+ prompt: '...',
295
+ });
296
+ ```
246
297
 
247
- The most common use is `getDownloadUrl(fileId)` to refresh the presigned URL on a generated image after the original (~1h) expires:
298
+ Discovery hook usage:
248
299
 
249
300
  ```tsx
250
- import { useFiasFiles, useImageGeneration } from '@fias/arche-sdk';
301
+ import { useImageEntities } from '@fias/arche-sdk';
251
302
 
252
- function PortraitGallery({ savedFileId }: { savedFileId: string }) {
253
- const { getDownloadUrl } = useFiasFiles();
254
- const [url, setUrl] = useState<string | null>(null);
303
+ const { entities, isLoading } = useImageEntities({
304
+ modes: ['image-to-image'],
305
+ supportsReferenceImage: true,
306
+ });
307
+ // entities[i]: { entityId, displayName, provider, modes, supportedSizes,
308
+ // supportsReferenceImage, isPlatformRecommended, ... }
309
+ ```
255
310
 
256
- useEffect(() => {
257
- let cancelled = false;
258
- getDownloadUrl(savedFileId).then((r) => {
259
- if (!cancelled) setUrl(r.url);
260
- });
261
- return () => {
262
- cancelled = true;
263
- };
264
- }, [savedFileId, getDownloadUrl]);
311
+ Each entry is scrubbed — only fields a UI legitimately needs (no provider model strings, no endpoints, no execution config).
265
312
 
266
- return url ? <img src={url} alt="" /> : null;
267
- }
313
+ ### `useBackgroundRemoval()` — Client-side background removal
314
+
315
+ **Permission:** `entities:image_remove_background`
316
+ **Returns:** `BackgroundRemovalApi`
317
+
318
+ Runs the imgly WASM model inside the host page — bytes never leave the user's device. Returns a transparent PNG.
319
+
320
+ ```tsx
321
+ import { useBackgroundRemoval } from '@fias/arche-sdk';
322
+
323
+ const { removeBackground, isLoading } = useBackgroundRemoval();
324
+ const transparentPng = await removeBackground(jpegBlob);
325
+ ```
326
+
327
+ **Limits:** input ≤ 25 MiB, PNG/JPEG only, rate-limited to 10/min per arche per user. Throws on size violation rather than returning a result.
328
+
329
+ ### Audio capability hooks (auto-generated)
330
+
331
+ **Permission:** `entities:audio_generate`
332
+ **Returns (per hook):** `{ invoke, isLoading, result, error }`
333
+
334
+ Specialty capabilities expose **typed convenience hooks** generated from the platform's surface registry. Each hook bakes in a `surfaceKey` and infers params/result types from the surface's schemas.
335
+
336
+ Currently shipped audio hooks: `useLyriaMusic`, `useElevenLabsTts`, `useElevenLabsMusic`, `useElevenLabsSfx`, `useElevenLabsStt`, `useElevenLabsAudioIsolation`, `useElevenLabsForcedAlignment`, `useElevenLabsVoiceChanger`, `useElevenLabsVoiceDesign`. Browse the full surface list with `npx fias-dev entities`.
337
+
338
+ A legacy `useAudioGeneration()` hook still ships for back-compat with code written before the typed-surface migration. New plugins should use the typed hooks above — the legacy hook will be removed in a future major version.
339
+
340
+ ```tsx
341
+ import { useElevenLabsTts } from '@fias/arche-sdk';
342
+
343
+ const { invoke, isLoading, result } = useElevenLabsTts();
344
+ await invoke({ text: 'Hello world', voiceId: '...' });
345
+ // result: { audioUrl, audioRef, durationMinutes, costCredits, ... }
346
+ ```
347
+
348
+ For surfaces without a typed wrapper yet, the generic substrate:
349
+
350
+ ```tsx
351
+ import { useSurface } from '@fias/arche-sdk';
352
+
353
+ const { invoke } = useSurface<MyParams, MyResult>('my.surface-key');
268
354
  ```
269
355
 
270
- **Store the `fileId`, not the `imageUrl`.** Image-generation results include both, but `imageUrl` is a short-lived presigned link. Persist `fileId` in your data store and resolve to a fresh URL on render via `getDownloadUrl`.
356
+ Default per-request timeout is 180 s (covers TTS for long text and Lyria multi-clip generation).
357
+
358
+ ### `useVaultDocuments()` — User-owned Vault documents
271
359
 
272
- **Same-name writes overwrite.** Calling `write({ name: 'save.json', content })` twice with the same name reuses the existing `fileId` and replaces the content — useful for periodic snapshots.
360
+ **Permissions:** `vault:documents:read` (list / read / getDownloadUrl / search), `vault:documents:write` (write / update / delete / attach / detach / upload)
361
+ **Returns:** `VaultDocumentsApi`
273
362
 
274
- **`read()` is text-only.** It rejects binary content types with HTTP 415. Use `getDownloadUrl(fileId)` and pass the URL to `<img>`, `fetch`, etc. for binaries.
363
+ The Vault is the platform's persistent document store for the signed-in user. Distinct from `useFiasStorage` (per-plugin sandbox): Vault docs live in the user's My Data, survive plugin uninstall, and can be cross-referenced from other arches. Documents this arche creates are scoped to it (`source_arche_id`) and surfaced to the user under `/My-Fias/Arches/<archeId>/`.
364
+
365
+ **Most common use:** images returned from `useImageGeneration()` are auto-saved here. The result includes a `documentId` — pass it to `getDownloadUrl()` later to refresh the presigned URL after the original ~1h TTL expires.
275
366
 
276
367
  ```tsx
277
- const files = useFiasFiles();
368
+ import { useVaultDocuments } from '@fias/arche-sdk';
369
+
370
+ const vault = useVaultDocuments();
371
+
372
+ // Refresh a presigned URL (cached client-side, auto-refreshed before expiry)
373
+ const { url, expiresAt, contentType } = await vault.getDownloadUrl(documentId);
278
374
 
279
- await files.write({
280
- name: 'save.json',
281
- content: JSON.stringify(state),
375
+ // List documents this arche owns
376
+ const { documents, nextCursor } = await vault.list({ search: 'invoice', limit: 50 });
377
+
378
+ // Write a small text/JSON document (UTF-8, ≤ 1 MB)
379
+ const { documentId } = await vault.write({
380
+ name: 'settings.json',
381
+ content: JSON.stringify(settings),
282
382
  contentType: 'application/json',
383
+ tags: ['config'],
384
+ });
385
+
386
+ // Upload a binary or larger document — wraps the two-step presigned-PUT flow
387
+ const result = await vault.upload(bytes, {
388
+ name: 'photo.jpg',
389
+ mimeType: 'image/jpeg',
283
390
  });
284
- const { content } = await files.read(fileId);
285
- const { files: page, nextCursor } = await files.list({ limit: 20 });
286
- await files.update(fileId, { tags: ['draft'] });
287
- await files.delete(fileId);
391
+ // result.status transitions 'uploaded' → 'available' once indexed for search
392
+
393
+ // Semantic search across documents this arche owns
394
+ // (burns user credits per the AI Markup Invariant — call sparingly)
395
+ const { matches } = await vault.search('quarterly revenue', { topK: 5 });
396
+
397
+ // Update / delete
398
+ await vault.update(documentId, { name: 'new-name.json', tags: ['archived'] });
399
+ await vault.delete(documentId); // soft-delete, recoverable from My Data Trash
288
400
  ```
289
401
 
402
+ **Cross-arche attachment.** A document can be referenced from another arche's record (Business Contact, Company, Fias 360 Renewal in V-1). The reference points at the document but does NOT expose its contents to the target arche.
403
+
404
+ ```tsx
405
+ const { referenceId } = await vault.attach(documentId, {
406
+ regardingType: 'vault_business_contact',
407
+ regardingId: contactId,
408
+ });
409
+ await vault.detach(documentId, referenceId);
410
+ ```
411
+
412
+ **Sensitivity tiers** (`'standard'` | `'confidential'` | `'proprietary'`) gate downstream read access per the Vault's standard matrix. Defaults to `'standard'`.
413
+
414
+ **`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.
415
+
416
+ ### `useArcheAssets()` — Contributor-published asset library
417
+
418
+ **Permission:** `assets:read`
419
+ **Returns:** `ArcheAssetsApi`
420
+
421
+ A frozen set of images the arche's contributor uploaded and pinned to the published version — logos, brand images, hole maps, reference photos. Each entry has an immutable `assetId` and a short-lived `signedUrl` (~5 min) suitable for `<img src>`.
422
+
423
+ ```tsx
424
+ import { useArcheAssets } from '@fias/arche-sdk';
425
+
426
+ const { list, getUrl } = useArcheAssets();
427
+
428
+ const { entries, nextCursor } = await list({ limit: 50 });
429
+ // entries[i]: { assetId, signedUrl, expiresAt, contentType, width, height,
430
+ // sizeBytes, name, altText, tags }
431
+
432
+ // Refresh a signed URL when it's about to expire
433
+ const fresh = await getUrl(assetId);
434
+ ```
435
+
436
+ Cache the `assetId`; refresh `signedUrl` via `getUrl()` rather than persisting URLs across sessions.
437
+
438
+ ### `useFiasStore()` — In-app purchases (IAP)
439
+
440
+ **Permission:** `store:purchase`
441
+ **Returns:** `FiasStoreApi`
442
+
443
+ How a plugin charges the signed-in user for anything inside the arche — one-time unlocks, consumables, subscriptions, paid sign-ups, donations. **All in-arche payments go through this hook.** The arche never handles payment directly: the platform shows a confirmation overlay, debits the user's Fias credit balance, and returns a result. Do not import Stripe or any external payment SDK — they are sandboxed out and not needed.
444
+
445
+ Products are declared in `fias-plugin.json` under a `products` array (see Manifest Reference below). Each product has a stable `identifier` your code passes to `purchase()`. Prices are denominated in **Fias credits**, not USD.
446
+
447
+ ```tsx
448
+ import { useFiasStore } from '@fias/arche-sdk';
449
+
450
+ function UnlockButton() {
451
+ const { products, purchase, hasEntitlement, isLoading } = useFiasStore();
452
+ const product = products.find((p) => p.productIdentifier === 'pro_unlock');
453
+
454
+ if (isLoading || !product) return null;
455
+ if (hasEntitlement('pro_unlock')) return <p>Pro features unlocked.</p>;
456
+
457
+ async function buy() {
458
+ const result = await purchase('pro_unlock');
459
+ if (!result.success) {
460
+ // Common codes: INSUFFICIENT_CREDITS, USER_CANCELLED, PRODUCT_NOT_FOUND
461
+ console.error(result.error);
462
+ }
463
+ // On success, `entitlements` updates automatically.
464
+ }
465
+
466
+ return <button onClick={buy}>Unlock for {product.priceCredits} credits</button>;
467
+ }
468
+ ```
469
+
470
+ **Product types:**
471
+
472
+ - `consumable` — buyable repeatedly (tokens, energy, single donations, sign-up entries). Does NOT create an entitlement; track usage in `useFiasDataStore`.
473
+ - `non_consumable` — one-time purchase, permanent entitlement.
474
+ - `auto_renewable` — subscription that renews automatically. Requires `subscriptionPeriod`: `weekly` | `monthly` | `quarterly` | `yearly`.
475
+ - `non_renewing` — time-limited access that does NOT renew (season pass).
476
+
477
+ **API surface:**
478
+
479
+ - `products: StoreProduct[]` — loaded on mount.
480
+ - `purchase(productIdentifier, { quantity? }) → StorePurchaseResult` — triggers the platform confirmation overlay. `quantity` only applies to consumables.
481
+ - `entitlements: StoreEntitlement[]` — current non-consumable + subscription entitlements; updated automatically after a successful purchase.
482
+ - `hasEntitlement(productIdentifier) → boolean` — convenience check.
483
+ - `getPurchaseHistory() → Promise<StorePurchase[]>` — full log including consumables.
484
+ - `restorePurchases() → Promise<StoreEntitlement[]>` — re-sync entitlements from the platform.
485
+
486
+ **Error handling:** `purchase()` returns `{ success: false, error, code }` rather than throwing — always branch on `result.success`. The user must be signed in to a Fias account; IAP is not available to anonymous visitors on public arches.
487
+
290
488
  ### `useFiasNavigation()` — In-plugin routing
291
489
 
292
490
  **Permission:** None required
@@ -303,21 +501,18 @@ navigateTo('/settings');
303
501
 
304
502
  **Returns:** `StepNavigationApi`
305
503
 
306
- Call **once** in the top-level App component. Share `currentStep` and `setCurrentStep` with step components via React context — do not call `useStepNavigation` from step components (each call creates an independent state).
307
-
308
504
  ```tsx
309
505
  import { useStepNavigation } from '@fias/arche-sdk';
310
506
 
311
- // Without persistence — currentStep resets on preview rebuild
312
- const { currentStep, setCurrentStep } = useStepNavigation('step-1');
313
-
314
- // With persistence — currentStep survives preview rebuilds (SDK ≥ 1.7.0)
507
+ // Pass `persistKey` to survive preview rebuilds (do NOT additionally wrap
508
+ // currentStep in usePersistentState — that creates two state sources and
509
+ // a bidirectional sync loop).
315
510
  const { currentStep, setCurrentStep } = useStepNavigation('step-1', {
316
- persistKey: 'currentStep',
511
+ persistKey: 'workflow-step',
317
512
  });
318
513
  ```
319
514
 
320
- **Do NOT wrap `currentStep` in `usePersistentState`.** Two independent step-state sources create a bidirectional `useEffect` sync loop that spams `storage_write` and flashes the UI between steps. `persistKey` is the only correct way to persist the current step.
515
+ The host can also drive step changes from outside the iframe — for example, when the user clicks a node in an external graph view. The hook listens for those `step_navigate` messages and updates `currentStep` automatically, so unexpected step transitions aren't a bug to chase.
321
516
 
322
517
  ### `usePersistentState()` — Auto-saving state
323
518
 
@@ -332,21 +527,40 @@ const [count, setCount] = usePersistentState<number>('counter', 0);
332
527
  // Automatically persists to storage on change
333
528
  ```
334
529
 
335
- Use for domain data (form inputs, selections, AI results). **Do not use for `currentStep`** — use `useStepNavigation({ persistKey })` instead.
336
-
337
- **Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously on every setter call, but the underlying `storage_write` is coalesced with a 250 ms trailing-edge debounce and a 1 s max-wait, and flushed on unmount. This makes the hook safe to call from `requestAnimationFrame`, `mousemove`, and other high-frequency handlers — bursts no longer trip the `storage_write` rate limit.
338
-
339
- Trade-off: reading the same key via `useFiasStorage().readFile('__state/foo')` immediately after a `setValue` can see a stale value for up to ~250 ms. Read through the hook instead of bypassing it.
530
+ **Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously, but the underlying `storage_write` is coalesced (250 ms trailing-edge debounce, 1 s max-wait, flushed on unmount). Safe to call from `requestAnimationFrame` and other high-frequency handlers. For state that updates every frame (game positions, drag coordinates), still prefer plain `useState` — persisting transient state is wasteful and not useful on reload.
340
531
 
341
532
  ### `fias` — Imperative utilities
342
533
 
534
+ Use the `fias` namespace when you need bridge operations from outside a React component (event handlers attached imperatively, utility modules, non-React entry points).
535
+
343
536
  ```tsx
344
537
  import { fias } from '@fias/arche-sdk';
345
538
 
539
+ fias.ready(); // Manual ready signal (FiasProvider does this automatically)
346
540
  fias.resize(800); // Resize iframe height
347
541
  fias.showToast('Saved!', 'success'); // Toast: 'info' | 'success' | 'warning' | 'error'
542
+
543
+ // fias.dataStore mirrors useFiasDataStore() — same operations, same limits.
544
+ // Requires the `data:store` permission.
545
+ await fias.dataStore.createCollection('scores', { userScope: 'user' });
546
+ await fias.dataStore.put('scores', 'doc-key', { score: 100 });
547
+ const doc = await fias.dataStore.get('scores', 'doc-key');
548
+ await fias.dataStore.query('scores', {
549
+ filters: [{ field: 'score', op: 'gte', value: 50 }],
550
+ });
551
+ await fias.dataStore.delete('scores', 'doc-key');
552
+ await fias.dataStore.listCollections();
553
+ await fias.dataStore.deleteCollection('scores');
348
554
  ```
349
555
 
556
+ ### Advanced exports (rarely needed)
557
+
558
+ The SDK also exports the following for advanced use cases. Most plugins don't need them.
559
+
560
+ - `VALID_PLUGIN_PERMISSIONS`, `isValidPluginPermission(perm)` — runtime permission validation.
561
+ - Theme catalog: `DARK_THEME`, `LIGHT_THEME`, `getDefaultTheme()`, `THEME_CATALOG`, `getThemeById()`, `FONT_PAIRINGS`, `getFontPairingById()` — useful when the plugin lets the user pick from preset themes/fonts.
562
+ - Bridge introspection: `FiasBridge`, `getBridge()`, `resetBridge()`, `SDK_BRIDGE_PROTOCOL_VERSION`, `SDK_REQUIRES_HOST_PROTOCOL_VERSION` — testing and advanced cases that need direct bridge control.
563
+
350
564
  ## Manifest Reference (`fias-plugin.json`)
351
565
 
352
566
  ```json
@@ -366,23 +580,78 @@ fias.showToast('Saved!', 'success'); // Toast: 'info' | 'success' | 'warning' |
366
580
 
367
581
  **Fields:**
368
582
 
369
- | Field | Required | Description |
370
- | -------------- | -------- | --------------------------------------------------------- |
371
- | `name` | Yes | Plugin identifier (lowercase, hyphens) |
372
- | `version` | Yes | Semver (e.g., `"1.0.0"`) |
373
- | `description` | Yes | Short marketplace description |
374
- | `main` | Yes | Entry point source file |
375
- | `archeType` | Yes | `"tool"` or `"site"` |
376
- | `tags` | No | Discovery tags |
377
- | `pricing` | Yes | `{ model: "free" }` or `"fixed"`, `"per_use"`, `"tiered"` |
378
- | `permissions` | Yes | Array of permission scopes |
379
- | `sdk` | Yes | SDK version range |
380
- | `dependencies` | No | npm packages with **exact** versions (max 20) |
381
-
382
- **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `entities:invoke`, `entities:image_generate`
583
+ | Field | Req | Description |
584
+ | --------------------- | ---- | ------------------------------------------------------------------------------------------------------- |
585
+ | `name` | Yes | Plugin identifier (lowercase, hyphens) |
586
+ | `version` | Yes | Semver (e.g., `"1.0.0"`) |
587
+ | `description` | Yes | Short marketplace description |
588
+ | `expandedDescription` | No | Long-form marketplace copy |
589
+ | `main` | Yes | Entry point source file |
590
+ | `archeType` | Yes | `"tool"` or `"site"` |
591
+ | `categorySlug` | No | Marketplace category for discovery |
592
+ | `icon` | No | Relative path to icon asset (shown in marketplace + arche header) |
593
+ | `tags` | No | Discovery tags |
594
+ | `pricing` | Yes | Marketplace **listing** pricing (see "Pricing detail" below). NOT for in-arche charges — use `products` |
595
+ | `permissions` | Yes | Array of permission scopes (see below) |
596
+ | `products` | No | IAP product catalog for `useFiasStore()` (see "Defining IAP products" below) |
597
+ | `isPublic` | No | Default `false`. Anonymous visitors can view (see "Public arches" below) |
598
+ | `isFullScreen` | No | Default `false`. Renders without platform chrome — implies `archeType: "site"` |
599
+ | `isListed` | No | Default `true`. Visible in marketplace store |
600
+ | `sdk` | Yes | SDK version range |
601
+ | `dependencies` | No | npm packages with **exact** versions (max 20) |
602
+ | `archeIds` | Auto | Per-environment IDs written by the CLI after first publish — don't hand-edit |
603
+
604
+ **Pricing detail:**
605
+
606
+ `pricing` is the marketplace **listing** model — controls what the platform charges users to access the arche itself. It does NOT cover charges the plugin makes from inside (entry fees, unlocks, donations) — for those, use IAP via `products`.
607
+
608
+ | `pricing.model` | Required sub-fields |
609
+ | --------------- | ------------------------------- |
610
+ | `"free"` | none |
611
+ | `"fixed"` | `priceCents` |
612
+ | `"per_use"` | `baseFeeCents` |
613
+ | `"tiered"` | `tiers: [{ upTo, priceCents }]` |
614
+
615
+ Other optional sub-fields: `currency: "usd"` (only USD supported today), `platformFeePercent` (override the default platform cut — non-standard).
616
+
617
+ **Permissions:** `theme:read`, `user:profile:read`, `storage:sandbox`, `data:store`, `entities:invoke`, `entities:image_generate`, `entities:image_remove_background`, `entities:audio_generate`, `store:purchase`, `vault:documents:read`, `vault:documents:write`, `assets:read`
383
618
 
384
619
  **Using AI:** Add `"entities:invoke"` to permissions, then use `useEntityInvocation()` with a model entity ID and your own `systemPrompt`. Browse available models with `npx fias-dev entities`.
385
620
 
621
+ **Defining IAP products (for `useFiasStore`):**
622
+
623
+ Add a `products` array to declare what users can buy inside the arche. Each entry needs a stable `identifier` your code passes to `purchase()`. Prices are in Fias credits.
624
+
625
+ ```json
626
+ {
627
+ "products": [
628
+ {
629
+ "identifier": "pro_unlock",
630
+ "type": "non_consumable",
631
+ "name": "Pro features",
632
+ "description": "Permanent unlock for advanced features.",
633
+ "priceCredits": 500
634
+ },
635
+ {
636
+ "identifier": "monthly_sub",
637
+ "type": "auto_renewable",
638
+ "name": "Monthly subscription",
639
+ "description": "Renewing access.",
640
+ "priceCredits": 200,
641
+ "subscriptionPeriod": "monthly",
642
+ "subscriptionGroup": "pro"
643
+ }
644
+ ],
645
+ "permissions": ["store:purchase"]
646
+ }
647
+ ```
648
+
649
+ `store:purchase` must be in `permissions` whenever `products` is non-empty. Use `consumable` for things bought repeatedly (single donations, sign-up entries), `non_consumable` for one-time unlocks, `auto_renewable` / `non_renewing` for time-bounded access.
650
+
651
+ **Public arches (`isPublic: true`):**
652
+
653
+ Setting `isPublic: true` lets anonymous visitors (no Fias account) view the arche at `/a/<archeId>`. The UI renders without the platform sidebar. **Bridge operations still require auth** — `useFiasDataStore` writes, `useFiasStore.purchase()`, `useFiasStorage` reads, vault operations, etc. all require the visitor to be signed in. Public mode is for read-only landing pages, marketing pages, and shared-content viewers — not for collecting registrations or payments from anonymous users.
654
+
386
655
  ## Plugin Constraints
387
656
 
388
657
  These are hard limits enforced by the platform. Code that violates these will fail review or be blocked at runtime.
@@ -414,7 +683,7 @@ When you hit a constraint, say so AND tell the user: file an Entity request on t
414
683
  - `storage_read`: 300/minute
415
684
  - `storage_list`, `storage_delete`: 60/minute
416
685
 
417
- A separate **runaway-loop detector** also fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target for diagnostics, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) called 51 times in 5s.` — if you see this, look at the identified path and debounce the updates at the source.
686
+ A separate **runaway-loop detector** fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) …` — the path tells you where to debounce at the source.
418
687
 
419
688
  ### Security Rules (enforced during review)
420
689
 
@@ -435,9 +704,16 @@ A separate **runaway-loop detector** also fires if any method is called >50 time
435
704
 
436
705
  ### Check for Updates (DO THIS FIRST)
437
706
 
438
- **At the start of every new session**, check if the FIAS packages and tooling are up to date:
707
+ **At the start of every new session**, install dependencies if they aren't already, then check if the FIAS packages and tooling are up to date:
439
708
 
440
709
  ```bash
710
+ # 1. Install deps first if node_modules/ is missing.
711
+ # fias-dev is a binary inside @fias/plugin-dev-harness, not a standalone
712
+ # npm package — `npx fias-dev …` before `npm install` falls through to
713
+ # the registry and prints a misleading "404 Not Found" for fias-dev.
714
+ [ -d node_modules ] || npm install
715
+
716
+ # 2. Then check for updates.
441
717
  npm outdated @fias/arche-sdk @fias/plugin-dev-harness
442
718
  npx fias-dev sync --dry-run
443
719
  ```
@@ -446,18 +722,6 @@ If newer package versions are available, tell the user and ask if they want to u
446
722
 
447
723
  If `sync --dry-run` shows pending changes, tell the user and offer to run `npx fias-dev sync` to update AI instruction files and config from the latest SDK templates. This never touches source code or project-specific files (`package.json`, `fias-plugin.json`, `src/`).
448
724
 
449
- #### UI cache after a harness upgrade
450
-
451
- The harness pulls its host-page UI (toolbar, mode toggle, sign-in modal) from a CDN and caches it in `~/.fias/harness-cache/`. The cached UI takes priority over the bundled copy that ships in the npm package.
452
-
453
- When you upgrade `@fias/plugin-dev-harness` but the CDN hasn't been redeployed yet, the cached UI can be older than the CLI. fias-dev now detects this case automatically and falls back to the bundled UI — look for this line in the startup log:
454
-
455
- ```
456
- ℹ Cached harness UI v1.14.3 is older than CLI v1.18.0 — using bundled UI.
457
- ```
458
-
459
- That message means everything is fine; the bundled UI exactly matches your CLI version. If you ever see stale behavior persist across an upgrade (and the line above didn't appear), delete `~/.fias/harness-cache/` and restart.
460
-
461
725
  ### Starting Development
462
726
 
463
727
  ```bash
@@ -494,39 +758,6 @@ npx fias-dev entities --type "model-definition" # Filter by type
494
758
  npx fias-dev entities --detail ent_modeldef_gpt_image_1_5 # Full entity details (capabilities, sizes, pricing)
495
759
  ```
496
760
 
497
- ### Debugging — Read the Browser's View Directly
498
-
499
- The harness mirrors every browser-side event to a per-port log file at `/tmp/fias-dev-<port>.log` (so usually `/tmp/fias-dev-3200.log`). The file path is also printed at startup:
500
-
501
- ```
502
- Browser dev-log mirror: /tmp/fias-dev-3200.log
503
- ```
504
-
505
- What's captured:
506
-
507
- - Every Dev Console entry (`send` / `recv` / `info` / `error` / `toast` / `cost` / `nav`)
508
- - Every uncaught JS error (`window.error`)
509
- - Every unhandled promise rejection (`unhandledrejection`)
510
- - Bridge requests + responses with payload previews
511
-
512
- Format is one line per event:
513
-
514
- ```
515
- 2026-05-24T07:14:33.123Z [ERROR] window.error {"message":"...","file":"...","line":42}
516
- 2026-05-24T07:14:35.456Z [SEND] image_generate {"entityId":"ent_modeldef_..."}
517
- 2026-05-24T07:14:36.789Z [RECV] response {...}
518
- ```
519
-
520
- **For AI agents pair-debugging with a developer:** tail this file instead of asking the developer to open DevTools and copy-paste output. Reproduce the failing action in the browser, then read the file — you'll see exactly what the iframe and harness exchanged.
521
-
522
- ```bash
523
- tail -f /tmp/fias-dev-3200.log
524
- ```
525
-
526
- The file is truncated on every `fias-dev` start, so it always reflects the current session.
527
-
528
- The mirror uses `navigator.sendBeacon` (with a `fetch` fallback) so there's a sub-second delay before the most recent events land in the file. If you're chasing a race, give it a beat before reading.
529
-
530
761
  ### Validating the Manifest
531
762
 
532
763
  ```bash
@@ -549,84 +780,6 @@ If the AI review rejects your submission, the listing-fee portion is
549
780
  refunded automatically (the per-review portion is not, since review work
550
781
  happened either way).
551
782
 
552
- ## Common Pitfalls
553
-
554
- These are real bugs that have shipped from AI-generated plugins. Avoid them.
555
-
556
- ### Don't navigate from a `useEffect` that watches derived state
557
-
558
- If a step component derives a value from context state and uses `useEffect` to redirect when that derived value is missing, the redirect can fire mid-update and yank the user away from a working screen. Common shape:
559
-
560
- ```tsx
561
- // ❌ BUG: redirects whenever conversations changes between renders
562
- const currentConversation = conversations.find((c) => c.id === currentConversationId);
563
-
564
- useEffect(() => {
565
- if (!currentConversation) {
566
- setCurrentStep('list');
567
- }
568
- }, [currentConversation, setCurrentStep]); // fires on every conversations change
569
- ```
570
-
571
- `currentConversation` is a _derived_ value — its reference changes every time `conversations` changes. The effect re-runs and may redirect during a state transition (e.g., right after the user sends a message and the component is mid-update). The user reports "I sent a message and got bounced back" or "the response never showed up".
572
-
573
- ```tsx
574
- // ✅ FIX: gate on the *id* (which is durable) and render a loading state when
575
- // the conversation can't be found yet — let the user navigate back manually
576
- // instead of forcing it.
577
- useEffect(() => {
578
- if (!currentConversationId) setCurrentStep('list');
579
- }, [currentConversationId, setCurrentStep]);
580
-
581
- if (!currentConversation) return <div>Loading conversation…</div>;
582
- ```
583
-
584
- ### Use functional updaters when an `await` sits between reads and writes
585
-
586
- When you read state, `await` something, then write back, the closure captures the _stale_ state. Use the functional form so the setter receives the latest value at the time of the update:
587
-
588
- ```tsx
589
- // ❌ BUG: `messages` here is the snapshot from before await
590
- const messages = currentConversation.messages;
591
- const response = await invoke({ entityId, input });
592
- setConversations(
593
- conversations.map((c) => (c.id === id ? { ...c, messages: [...messages, response] } : c)),
594
- );
595
-
596
- // ✅ FIX: functional updater reads fresh state at update time
597
- await invoke({ entityId, input });
598
- setConversations((prev) =>
599
- prev.map((c) => (c.id === id ? { ...c, messages: [...c.messages, response] } : c)),
600
- );
601
- ```
602
-
603
- This matters most for `usePersistentState` on lists: the user can fire many updates quickly, and lost-update bugs are common with snapshot-style updates.
604
-
605
- ### Render `streamingText` OR `result.output` — never both
606
-
607
- Already covered in the `useEntityInvocation` section above, but worth repeating: `streamingText` is _not_ cleared after the call completes — it keeps holding the full final text. Showing `{streamingText && ...}` AND `{result && ...}` in separate JSX nodes prints the response twice as soon as `result` arrives. Use one slot: `{isLoading ? streamingText : result?.output}`.
608
-
609
- ### Verify visually when the user reports a UI bug
610
-
611
- If a user says "I don't see the response" or "the layout's broken", don't rely solely on `console.log` and `useFiasStorage` reads. Inspect the actual rendered output. The platform exposes `get_preview_screenshot` to AI builders for exactly this reason.
612
-
613
- ### Don't persist per-frame state
614
-
615
- For state that updates every frame (game position, drag coordinates, animated values), use plain `useState` — not `usePersistentState`. Persisting every ball-position tick writes unbounded data to durable storage and doesn't survive reloads in any useful way anyway (the ball will be somewhere different when the player resumes).
616
-
617
- Hook-level debouncing (SDK ≥ 1.8.0) makes `usePersistentState` safe under bursts, but you still shouldn't persist what you'll discard on reload. Keep ephemeral state in `useState` and persist only meaningful checkpoints (final score, selected difficulty, high-score list).
618
-
619
- ```tsx
620
- // ❌ Persisting ball position every frame — meaningless on reload
621
- const [gameState, setGameState] = usePersistentState<GameState>('gameState', initial);
622
- requestAnimationFrame(() => setGameState(advance(gameState)));
623
-
624
- // ✅ Ephemeral for live gameplay, persisted for results
625
- const [gameState, setGameState] = useState<GameState>(initial);
626
- const [highScores, setHighScores] = usePersistentState<Score[]>('highScores', []);
627
- // persist highScores only when a run completes
628
- ```
629
-
630
783
  ## Common Patterns
631
784
 
632
785
  ### Theme-Aware Card Component