@fias/arche-sdk 2.27.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hooks.js CHANGED
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FiasContentError = exports.PLUGIN_CONTENT_ENTITY_ID = exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
3
+ exports.FiasContentError = exports.FiasLocalStoreError = exports.PLUGIN_LOCAL_STORE_ENTITY_ID = exports.PLUGIN_CONTENT_ENTITY_ID = exports.ArcheAssetError = exports.ARCHE_ASSET_BYTES_ENTITY_ID = exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
4
4
  exports.useFiasUser = useFiasUser;
5
5
  exports.useFiasTheme = useFiasTheme;
6
6
  exports.useFiasVendoredAssets = useFiasVendoredAssets;
7
7
  exports.useFiasStorage = useFiasStorage;
8
8
  exports.useEntityInvocation = useEntityInvocation;
9
9
  exports.useTextModels = useTextModels;
10
+ exports.useFiasHolder = useFiasHolder;
10
11
  exports.useImageGeneration = useImageGeneration;
11
12
  exports.useImageEntities = useImageEntities;
12
13
  exports.useClientEntity = useClientEntity;
@@ -21,10 +22,16 @@ exports.useFiasStore = useFiasStore;
21
22
  exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
22
23
  exports.useVaultDocuments = useVaultDocuments;
23
24
  exports.useArcheAssets = useArcheAssets;
25
+ exports.useArcheAssetBytes = useArcheAssetBytes;
26
+ exports.useLocalStore = useLocalStore;
24
27
  exports.useFiasContent = useFiasContent;
25
28
  exports.useFiasPreviewState = useFiasPreviewState;
26
29
  exports.useDataSubscription = useDataSubscription;
27
30
  exports.useVaultUserDocuments = useVaultUserDocuments;
31
+ exports.useFiasConnections = useFiasConnections;
32
+ exports.useFiasX = useFiasX;
33
+ exports.useFiasSchedules = useFiasSchedules;
34
+ exports.useFiasLinkedIn = useFiasLinkedIn;
28
35
  exports.useCompanyEmail = useCompanyEmail;
29
36
  exports.useCommunityAssets = useCommunityAssets;
30
37
  exports.useArcheHandoff = useArcheHandoff;
@@ -290,6 +297,26 @@ function useTextModels() {
290
297
  }, [bridge]);
291
298
  return models;
292
299
  }
300
+ /**
301
+ * The Fias workspace the plugin was opened in: the user's own (`personal`) or
302
+ * an organization's (`org`), and the user's role there.
303
+ *
304
+ * Requires `user:profile:read` or `data:workspace`. Returns `null` until the
305
+ * host has delivered it (usually immediately), and when the host could not
306
+ * resolve it. The value never changes while the plugin is open — switching
307
+ * workspace reopens the plugin. It carries no id and no name, and the
308
+ * platform applies the workspace to every call itself.
309
+ */
310
+ function useFiasHolder() {
311
+ const bridge = useBridge();
312
+ const [holder, setHolder] = (0, react_1.useState)(bridge.getHolder());
313
+ (0, react_1.useEffect)(() => {
314
+ // Init may have arrived between the initial render and this effect.
315
+ setHolder(bridge.getHolder());
316
+ return bridge.onHolderReady(setHolder);
317
+ }, [bridge]);
318
+ return holder;
319
+ }
293
320
  /**
294
321
  * How long `generate` waits for an image. High-quality renders at large sizes
295
322
  * run past two minutes, and the platform keeps working on them for up to
@@ -947,6 +974,8 @@ function useFiasDataStore() {
947
974
  * const workspaces = useFiasWorkspaces();
948
975
  * const ws = await workspaces.create('Acme Corp');
949
976
  * await workspaces.addMember(ws.workspaceId, otherUserId, 'member');
977
+ * // or, inside a Fias org workspace, its members' shared space:
978
+ * const shared = await workspaces.openForWorkspace();
950
979
  * // then, with useFiasDataStore():
951
980
  * await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
952
981
  *
@@ -958,6 +987,12 @@ function useFiasWorkspaces() {
958
987
  const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { displayName });
959
988
  return res.workspace;
960
989
  }, [bridge]);
990
+ const openForWorkspace = (0, react_1.useCallback)(async () => {
991
+ // The workspace is the host's acting one, stamped on the call by the host;
992
+ // the plugin never names it.
993
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { forActingHolder: true });
994
+ return res.workspace;
995
+ }, [bridge]);
961
996
  const list = (0, react_1.useCallback)(async () => {
962
997
  const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list');
963
998
  return res.workspaces;
@@ -992,7 +1027,27 @@ function useFiasWorkspaces() {
992
1027
  username,
993
1028
  });
994
1029
  }, [bridge]);
995
- return (0, react_1.useMemo)(() => ({ create, list, get, archive, listMembers, addMember, updateMember, removeMember }), [create, list, get, archive, listMembers, addMember, updateMember, removeMember]);
1030
+ return (0, react_1.useMemo)(() => ({
1031
+ create,
1032
+ openForWorkspace,
1033
+ list,
1034
+ get,
1035
+ archive,
1036
+ listMembers,
1037
+ addMember,
1038
+ updateMember,
1039
+ removeMember,
1040
+ }), [
1041
+ create,
1042
+ openForWorkspace,
1043
+ list,
1044
+ get,
1045
+ archive,
1046
+ listMembers,
1047
+ addMember,
1048
+ updateMember,
1049
+ removeMember,
1050
+ ]);
996
1051
  }
997
1052
  /**
998
1053
  * Access the in-arche purchase (IAP) store.
@@ -1457,6 +1512,16 @@ function bytesToBase64(bytes) {
1457
1512
  * state, and refresh `signedUrl` via `getUrl(assetId)` when it's about
1458
1513
  * to expire.
1459
1514
  *
1515
+ * The library holds images and short sound effects (MP3 / WAV). `list` and
1516
+ * `index` return images unless you pass `kinds` (e.g. `{ kinds: ['audio'] }`),
1517
+ * and with `kinds` the entries are typed `ArcheLibraryAsset` — narrow on
1518
+ * `contentType` before using an image's size. Keeping sounds here is
1519
+ * optional; music is not accepted. Sound effects that must play on cue need
1520
+ * decoded Web Audio buffers: load library sounds with
1521
+ * {@link useArcheAssetBytes}`().getMany` once per session, or bundle them in
1522
+ * the plugin's JS (base64) — not in `public/`, which only an `<audio>`
1523
+ * element can play.
1524
+ *
1460
1525
  * Permission: `assets:read`.
1461
1526
  *
1462
1527
  * @example
@@ -1471,12 +1536,14 @@ function useArcheAssets() {
1471
1536
  cursor: options?.cursor,
1472
1537
  limit: options?.limit,
1473
1538
  tags: options?.tags,
1539
+ kinds: options?.kinds,
1474
1540
  });
1475
1541
  }, [bridge]);
1476
1542
  const index = (0, react_1.useCallback)(async (options) => {
1477
1543
  return bridge.request('arche_assets_index', {
1478
1544
  tags: options?.tags,
1479
1545
  groupBy: options?.groupBy,
1546
+ kinds: options?.kinds,
1480
1547
  });
1481
1548
  }, [bridge]);
1482
1549
  const getUrl = (0, react_1.useCallback)(async (assetId) => {
@@ -1484,6 +1551,269 @@ function useArcheAssets() {
1484
1551
  }, [bridge]);
1485
1552
  return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
1486
1553
  }
1554
+ /**
1555
+ * Read the bytes of assets in this arche's asset library — for Web Audio
1556
+ * (`AudioContext.decodeAudioData`), canvas pixel work, or anything else a
1557
+ * signed URL cannot do (the plugin sandbox has no network). Read each sound
1558
+ * once per session and keep the decoded buffer.
1559
+ *
1560
+ * To load a set of sounds, pass the listed entries to `getMany`: up to 100
1561
+ * are read per call, so a whole sound set takes one or two calls. One call
1562
+ * per sound would trip the bridge's runaway-loop guard (more than 50 calls in
1563
+ * 5 seconds), which blocks every read for 10 seconds — `getBytes` calls made
1564
+ * together are batched for the same reason. The host page fetches each asset,
1565
+ * so each read costs one asset load like an `<img>` does. Unavailable in the
1566
+ * builder preview.
1567
+ *
1568
+ * Sounds are ready to trigger only once their buffers are decoded: wait for
1569
+ * the load before the first cue, and resume the `AudioContext` on the first
1570
+ * user gesture.
1571
+ *
1572
+ * Permissions: `entities:client_invoke` AND `assets:read`.
1573
+ *
1574
+ * @example
1575
+ * const { list } = useArcheAssets();
1576
+ * const { getMany } = useArcheAssetBytes();
1577
+ * const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
1578
+ * const ctx = new AudioContext();
1579
+ * const loaded = await getMany(entries);
1580
+ * const buffers = new Map<string, AudioBuffer>();
1581
+ * for (const entry of entries) {
1582
+ * const result = loaded[entry.assetId];
1583
+ * if (result instanceof Error) continue; // result.code says why
1584
+ * buffers.set(entry.name ?? entry.assetId, await ctx.decodeAudioData(result.bytes));
1585
+ * }
1586
+ */
1587
+ function useArcheAssetBytes() {
1588
+ const bridge = useBridge();
1589
+ const getBytes = (0, react_1.useCallback)(async (asset) => {
1590
+ let ref;
1591
+ if (typeof asset === 'string') {
1592
+ try {
1593
+ ref = await bridge.request('arche_assets_get_url', { assetId: asset });
1594
+ }
1595
+ catch (err) {
1596
+ throw toArcheAssetError(err);
1597
+ }
1598
+ }
1599
+ else {
1600
+ ref = asset;
1601
+ }
1602
+ return enqueueAssetByteRead(bridge, ref);
1603
+ }, [bridge]);
1604
+ const getMany = (0, react_1.useCallback)(async (assets) => {
1605
+ const read = await readAssetBytesBatch(bridge, assets);
1606
+ const out = {};
1607
+ for (const [assetId, result] of read)
1608
+ out[assetId] = result;
1609
+ return out;
1610
+ }, [bridge]);
1611
+ return (0, react_1.useMemo)(() => ({ getBytes, getMany }), [getBytes, getMany]);
1612
+ }
1613
+ /**
1614
+ * Entity ID of the arche asset bytes client-side entity — the host-side
1615
+ * reader behind {@link useArcheAssetBytes}. Literal here so the SDK
1616
+ * stays bundle-clean (no `@fias/entities` runtime dep).
1617
+ *
1618
+ * SYNC NOTE: must match `ARCHE_ASSET_BYTES_ENTITY_ID` in
1619
+ * `packages/entities/src/integrations/arche-asset-bytes/config.ts`. The
1620
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this.
1621
+ */
1622
+ exports.ARCHE_ASSET_BYTES_ENTITY_ID = 'ent_intg_arche_asset_bytes_00000';
1623
+ /** A failed asset byte read (`useArcheAssetBytes`): `code` says why. */
1624
+ class ArcheAssetError extends Error {
1625
+ constructor(message, code) {
1626
+ super(message);
1627
+ this.code = code;
1628
+ this.name = 'ArcheAssetError';
1629
+ }
1630
+ }
1631
+ exports.ArcheAssetError = ArcheAssetError;
1632
+ /** A listed URL with less than this many seconds left is replaced, not reused. */
1633
+ const ASSET_URL_MIN_REMAINING_SECONDS = 30;
1634
+ /** Most URLs the host reads in one call (`ARCHE_ASSET_BYTES_MAX_URLS` in the executor). */
1635
+ const ASSET_BYTES_MAX_URLS_PER_CALL = 100;
1636
+ /**
1637
+ * Most lapsed URLs one batch replaces with `arche_assets_get_url`, one call
1638
+ * each. Covers URLs that lapse in flight while staying far below the bridge's
1639
+ * 50-calls-in-5-seconds guard; a batch of older entries is answered
1640
+ * `ASSET_URL_EXPIRED` past this, so the plugin lists again (one call).
1641
+ */
1642
+ const ASSET_URL_MAX_REFRESHES_PER_BATCH = 10;
1643
+ /** One `read` call: the outcome of each URL, in order. Throws when the call itself is refused. */
1644
+ async function readSignedUrls(bridge, signedUrls) {
1645
+ let response;
1646
+ try {
1647
+ response = await bridge.request('client_entity_invoke', { entityId: exports.ARCHE_ASSET_BYTES_ENTITY_ID, input: { verb: 'read', signedUrls } }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
1648
+ }
1649
+ catch (err) {
1650
+ throw toArcheAssetError(err);
1651
+ }
1652
+ if ('error' in response) {
1653
+ throw new ArcheAssetError(response.error.message, response.error.code);
1654
+ }
1655
+ return response.results;
1656
+ }
1657
+ function fileError(result) {
1658
+ return result && 'error' in result
1659
+ ? new ArcheAssetError(result.error.message, result.error.code)
1660
+ : new ArcheAssetError('the asset could not be read', 'ASSET_UNAVAILABLE');
1661
+ }
1662
+ /**
1663
+ * Read `items` in calls of up to 100 URLs, recording each outcome in `out`.
1664
+ * Assets that did not fit a call's byte budget beside the others go again
1665
+ * together until a pass reads nothing more (then each is too large on its
1666
+ * own). Returns the reused URLs that lapsed in flight, for a fresh URL.
1667
+ */
1668
+ async function readInBatches(bridge, items, out) {
1669
+ const lapsed = [];
1670
+ let queue = items;
1671
+ while (queue.length > 0) {
1672
+ const overBudget = [];
1673
+ let readCount = 0;
1674
+ for (let at = 0; at < queue.length; at += ASSET_BYTES_MAX_URLS_PER_CALL) {
1675
+ const chunk = queue.slice(at, at + ASSET_BYTES_MAX_URLS_PER_CALL);
1676
+ const results = await readSignedUrls(bridge, chunk.map((item) => item.signedUrl));
1677
+ chunk.forEach((item, i) => {
1678
+ const result = results[i];
1679
+ if (result && !('error' in result)) {
1680
+ out.set(item.assetId, result);
1681
+ readCount += 1;
1682
+ }
1683
+ else if (result?.error.code === 'ASSET_URL_EXPIRED' && !item.refreshed) {
1684
+ // A reused URL can lapse before the host's fetch lands (or the
1685
+ // device clock runs fast); a fresh one recovers it.
1686
+ lapsed.push(item);
1687
+ }
1688
+ else if (result?.error.code === 'ASSET_TOO_LARGE' && chunk.length > 1) {
1689
+ overBudget.push(item);
1690
+ }
1691
+ else {
1692
+ out.set(item.assetId, fileError(result));
1693
+ }
1694
+ });
1695
+ }
1696
+ if (readCount === 0) {
1697
+ for (const item of overBudget) {
1698
+ out.set(item.assetId, new ArcheAssetError('the asset is too large to read', 'ASSET_TOO_LARGE'));
1699
+ }
1700
+ break;
1701
+ }
1702
+ queue = overBudget;
1703
+ }
1704
+ return lapsed;
1705
+ }
1706
+ /**
1707
+ * Read `refs` in as few host calls as possible: reuse each listed URL while it
1708
+ * has time left, replace a bounded number of lapsed ones, and send up to 100
1709
+ * URLs per call. Throws only when a call is refused as a whole.
1710
+ */
1711
+ async function readAssetBytesBatch(bridge, refs) {
1712
+ const out = new Map();
1713
+ let refreshesLeft = ASSET_URL_MAX_REFRESHES_PER_BATCH;
1714
+ // A fresh URL for each lapsed item, within the batch's refresh allowance.
1715
+ const refresh = async (assetIds) => {
1716
+ const fresh = [];
1717
+ for (const assetId of assetIds) {
1718
+ if (refreshesLeft <= 0) {
1719
+ out.set(assetId, new ArcheAssetError('the asset URL has expired; list the assets again for fresh URLs', 'ASSET_URL_EXPIRED'));
1720
+ continue;
1721
+ }
1722
+ refreshesLeft -= 1;
1723
+ try {
1724
+ const asset = await bridge.request('arche_assets_get_url', { assetId });
1725
+ fresh.push({ assetId, signedUrl: asset.signedUrl, refreshed: true });
1726
+ }
1727
+ catch (err) {
1728
+ out.set(assetId, toArcheAssetError(err));
1729
+ }
1730
+ }
1731
+ return fresh;
1732
+ };
1733
+ // One read per asset, whatever the caller passed twice.
1734
+ const unique = new Map();
1735
+ for (const ref of refs)
1736
+ if (!unique.has(ref.assetId))
1737
+ unique.set(ref.assetId, ref);
1738
+ const nowSeconds = Date.now() / 1000;
1739
+ const reusable = [];
1740
+ const stale = [];
1741
+ for (const ref of unique.values()) {
1742
+ if (ref.expiresAt - nowSeconds > ASSET_URL_MIN_REMAINING_SECONDS) {
1743
+ reusable.push({ assetId: ref.assetId, signedUrl: ref.signedUrl, refreshed: false });
1744
+ }
1745
+ else {
1746
+ stale.push(ref.assetId);
1747
+ }
1748
+ }
1749
+ const pending = [...reusable, ...(await refresh(stale))];
1750
+ const lapsed = await readInBatches(bridge, pending, out);
1751
+ if (lapsed.length > 0) {
1752
+ await readInBatches(bridge, await refresh(lapsed.map((item) => item.assetId)), out);
1753
+ }
1754
+ return out;
1755
+ }
1756
+ const pendingAssetByteReads = new WeakMap();
1757
+ function enqueueAssetByteRead(bridge, ref) {
1758
+ let batch = pendingAssetByteReads.get(bridge);
1759
+ if (!batch) {
1760
+ const created = { refs: [], waiters: [] };
1761
+ pendingAssetByteReads.set(bridge, created);
1762
+ batch = created;
1763
+ void Promise.resolve().then(() => flushAssetByteReads(bridge, created));
1764
+ }
1765
+ batch.refs.push(ref);
1766
+ const queued = batch;
1767
+ return new Promise((resolve, reject) => {
1768
+ queued.waiters.push({ assetId: ref.assetId, resolve, reject });
1769
+ });
1770
+ }
1771
+ async function flushAssetByteReads(bridge, batch) {
1772
+ pendingAssetByteReads.delete(bridge);
1773
+ let results;
1774
+ try {
1775
+ results = await readAssetBytesBatch(bridge, batch.refs);
1776
+ }
1777
+ catch (err) {
1778
+ const failure = toArcheAssetError(err);
1779
+ for (const waiter of batch.waiters)
1780
+ waiter.reject(failure);
1781
+ return;
1782
+ }
1783
+ const handedOut = new Set();
1784
+ for (const waiter of batch.waiters) {
1785
+ const result = results.get(waiter.assetId) ??
1786
+ new ArcheAssetError('the asset could not be read', 'ASSET_UNAVAILABLE');
1787
+ if (result instanceof ArcheAssetError) {
1788
+ waiter.reject(result);
1789
+ }
1790
+ else if (handedOut.has(waiter.assetId)) {
1791
+ // Two callers asked for one asset: each gets its own buffer, because
1792
+ // `decodeAudioData` detaches the one it is given.
1793
+ waiter.resolve({ ...result, bytes: result.bytes.slice(0) });
1794
+ }
1795
+ else {
1796
+ handedOut.add(waiter.assetId);
1797
+ waiter.resolve(result);
1798
+ }
1799
+ }
1800
+ }
1801
+ function toArcheAssetError(err) {
1802
+ if (err instanceof ArcheAssetError)
1803
+ return err;
1804
+ const message = err instanceof Error ? err.message : String(err);
1805
+ const code = err?.code;
1806
+ if (/permission denied/i.test(message)) {
1807
+ return new ArcheAssetError(`${message} — useArcheAssetBytes needs both "assets:read" and "entities:client_invoke" in fias-plugin.json`, 'PERMISSION_DENIED');
1808
+ }
1809
+ if (code === 'RATE_LIMITED' || code === 'RATE_LIMIT' || /rate limit/i.test(message)) {
1810
+ return new ArcheAssetError(message, 'RATE_LIMIT');
1811
+ }
1812
+ if (/rapid-fire loop|runaway loop/i.test(message)) {
1813
+ return new ArcheAssetError(`${message} Load many assets with useArcheAssetBytes().getMany(entries) — one call per 100.`, 'RATE_LIMIT');
1814
+ }
1815
+ return new ArcheAssetError(message, 'ASSET_UNAVAILABLE');
1816
+ }
1487
1817
  /**
1488
1818
  * Entity ID of the plugin content pack client-side entity (ADR-021) — the
1489
1819
  * host-side reader behind {@link useFiasContent}. Literal here so the SDK
@@ -1494,6 +1824,143 @@ function useArcheAssets() {
1494
1824
  * architecture test `sdk-entity-id-constants-match.test.ts` pins this.
1495
1825
  */
1496
1826
  exports.PLUGIN_CONTENT_ENTITY_ID = 'ent_intg_plugin_content_00000000';
1827
+ /**
1828
+ * Entity ID of the plugin local store client-side entity (ADR-031) — the
1829
+ * host-side store behind {@link useLocalStore}. Literal here so the SDK stays
1830
+ * bundle-clean (no `@fias/entities` runtime dep).
1831
+ *
1832
+ * SYNC NOTE: must match `PLUGIN_LOCAL_STORE_ENTITY_ID` in
1833
+ * `packages/entities/src/integrations/plugin-local-store/config.ts`. The
1834
+ * architecture test `sdk-entity-id-constants-match.test.ts` pins this.
1835
+ */
1836
+ exports.PLUGIN_LOCAL_STORE_ENTITY_ID = 'ent_intg_plugin_local_store_0000';
1837
+ /** A failed local-store call: `code` says why. */
1838
+ class FiasLocalStoreError extends Error {
1839
+ constructor(message, code) {
1840
+ super(message);
1841
+ this.code = code;
1842
+ this.name = 'FiasLocalStoreError';
1843
+ }
1844
+ }
1845
+ exports.FiasLocalStoreError = FiasLocalStoreError;
1846
+ function toLocalStoreError(err) {
1847
+ if (err instanceof FiasLocalStoreError)
1848
+ return err;
1849
+ const message = err instanceof Error ? err.message : String(err);
1850
+ // The bridge's centralized gates carry a code; the registry's own refusals
1851
+ // (per-entity permission, per-entity rate bucket) are plain Errors, so the
1852
+ // message is the fallback, never the first look.
1853
+ const raw = err?.code;
1854
+ const code = raw === 'PERMISSION_DENIED' || /permission denied/i.test(message)
1855
+ ? 'PERMISSION_DENIED'
1856
+ : raw === 'RATE_LIMIT' || raw === 'RATE_LIMITED' || /rate limit/i.test(message)
1857
+ ? 'RATE_LIMIT'
1858
+ : 'LOCAL_STORE_UNAVAILABLE';
1859
+ return new FiasLocalStoreError(message, code);
1860
+ }
1861
+ /**
1862
+ * Keep working data on the user's device — a recovery journal, drafts, a
1863
+ * cache — so a refresh, a crash or a closed tab loses nothing. Writes take
1864
+ * milliseconds and never touch the network: the host page keeps the data in
1865
+ * its own browser storage, in a space only this plugin can read, for the
1866
+ * signed-in user in the workspace they are working in (Personal and each org
1867
+ * workspace keep separate data; switching workspace switches the store).
1868
+ * Signing out clears it.
1869
+ *
1870
+ * This is NOT where a user's work is kept. It is best-effort device storage —
1871
+ * a browser may clear site data, and nothing here reaches another device —
1872
+ * so write the journal here and save the user's files through the Vault
1873
+ * (`useVaultUserDocuments().saveContent`, `upload`, or the Save dialog).
1874
+ *
1875
+ * Values: a string, bytes (`ArrayBuffer` / `Uint8Array`), or any JSON value;
1876
+ * `get` returns the same kind. One value ≤ 64 MB; the whole store for this
1877
+ * user ≤ 256 MB. Over budget, `put` throws `LOCAL_STORE_QUOTA_EXCEEDED`
1878
+ * rather than evicting — decide what to keep with `estimate()` and `list()`.
1879
+ * For a large snapshot (an image, a big document), put BYTES: a JSON value is
1880
+ * validated and serialized on the host page, which costs time proportional
1881
+ * to its size on every write, while bytes are only copied. Restore a journal
1882
+ * with `getMany(keys)` — one call for up to 100 entries, instead of one call
1883
+ * per key.
1884
+ *
1885
+ * Permission: `storage:local`.
1886
+ *
1887
+ * @example
1888
+ * const store = useLocalStore();
1889
+ * await store.put(`session/${docId}`, snapshotJson); // on every change
1890
+ * const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load
1891
+ * await store.delete(`session/${docId}`); // on close
1892
+ */
1893
+ function useLocalStore() {
1894
+ const bridge = useBridge();
1895
+ const call = (0, react_1.useCallback)(async (input) => {
1896
+ let response;
1897
+ try {
1898
+ response = await bridge.request('client_entity_invoke', { entityId: exports.PLUGIN_LOCAL_STORE_ENTITY_ID, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
1899
+ }
1900
+ catch (err) {
1901
+ throw toLocalStoreError(err);
1902
+ }
1903
+ if (response?.error) {
1904
+ throw new FiasLocalStoreError(response.error.message, response.error.code);
1905
+ }
1906
+ return response ?? {};
1907
+ }, [bridge]);
1908
+ const get = (0, react_1.useCallback)(async (key) => (await call({ verb: 'get', key })).entry ?? null, [call]);
1909
+ const getText = (0, react_1.useCallback)(async (key) => {
1910
+ const entry = await get(key);
1911
+ if (!entry)
1912
+ return null;
1913
+ if (typeof entry.value !== 'string') {
1914
+ throw new FiasLocalStoreError(`"${key}" does not hold text`, 'LOCAL_STORE_WRONG_KIND');
1915
+ }
1916
+ return entry.value;
1917
+ }, [get]);
1918
+ const getBytes = (0, react_1.useCallback)(async (key) => {
1919
+ const entry = await get(key);
1920
+ if (!entry)
1921
+ return null;
1922
+ if (!(entry.value instanceof ArrayBuffer)) {
1923
+ throw new FiasLocalStoreError(`"${key}" does not hold bytes`, 'LOCAL_STORE_WRONG_KIND');
1924
+ }
1925
+ return entry.value;
1926
+ }, [get]);
1927
+ const getJson = (0, react_1.useCallback)(async (key) => {
1928
+ const entry = await get(key);
1929
+ if (!entry)
1930
+ return null;
1931
+ if (entry.value instanceof ArrayBuffer) {
1932
+ throw new FiasLocalStoreError(`"${key}" holds bytes, not JSON`, 'LOCAL_STORE_WRONG_KIND');
1933
+ }
1934
+ // A top-level string is always stored as text, so `put(key,
1935
+ // JSON.stringify(x))` reads back here the same as `put(key, x)`.
1936
+ if (typeof entry.value === 'string') {
1937
+ try {
1938
+ return JSON.parse(entry.value);
1939
+ }
1940
+ catch {
1941
+ throw new FiasLocalStoreError(`"${key}" holds text that is not JSON`, 'LOCAL_STORE_WRONG_KIND');
1942
+ }
1943
+ }
1944
+ return entry.value;
1945
+ }, [get]);
1946
+ const getMany = (0, react_1.useCallback)(async (keys) => (await call({ verb: 'get_many', keys }))
1947
+ .entries ?? [], [call]);
1948
+ const put = (0, react_1.useCallback)(async (key, value) => ({
1949
+ sizeBytes: (await call({ verb: 'put', key, value })).sizeBytes ?? 0,
1950
+ }), [call]);
1951
+ const del = (0, react_1.useCallback)(async (key) => {
1952
+ await call({ verb: 'delete', key });
1953
+ }, [call]);
1954
+ const list = (0, react_1.useCallback)(async (prefix) => (await call({ verb: 'list', ...(prefix !== undefined ? { prefix } : {}) })).entries ?? [], [call]);
1955
+ const clear = (0, react_1.useCallback)(async () => ({
1956
+ deleted: (await call({ verb: 'clear' })).deleted ?? 0,
1957
+ }), [call]);
1958
+ const estimate = (0, react_1.useCallback)(async () => {
1959
+ const res = await call({ verb: 'estimate' });
1960
+ return { usedBytes: res.usedBytes ?? 0, budgetBytes: res.budgetBytes ?? 0 };
1961
+ }, [call]);
1962
+ return (0, react_1.useMemo)(() => ({ get, getMany, getText, getBytes, getJson, put, delete: del, list, clear, estimate }), [get, getMany, getText, getBytes, getJson, put, del, list, clear, estimate]);
1963
+ }
1497
1964
  /** A failed content read: `code` says why, `path` names the file when there is one. */
1498
1965
  class FiasContentError extends Error {
1499
1966
  constructor(message, code, path) {
@@ -1975,6 +2442,133 @@ async function fetchCommunityAssetUrls(bridge, assetIds) {
1975
2442
  }
1976
2443
  return hits;
1977
2444
  }
2445
+ /** Connecting waits on a person (and maybe a provider sign-in in a popup). */
2446
+ const CONNECT_TIMEOUT_MS = 10 * 60 * 1000;
2447
+ /**
2448
+ * Outside accounts (X, LinkedIn, …) the workspace the app is open in lets it
2449
+ * use (ADR-029 §2). Requires `connections:use`.
2450
+ *
2451
+ * The app never sees a credential. `connect(provider)` asks the host to show
2452
+ * its connect screen, where the person connects an account or lets this app
2453
+ * use one already connected; the host makes every grant. Acting as an account
2454
+ * (posting, reading its audience) is a verb on the provider's own entity.
2455
+ *
2456
+ * @example
2457
+ * const connections = useFiasConnections();
2458
+ * const accounts = await connections.list();
2459
+ * const x = accounts.find((a) => a.provider === 'x') ?? (await connections.connect('x'));
2460
+ */
2461
+ function useFiasConnections() {
2462
+ const bridge = useBridge();
2463
+ const list = (0, react_1.useCallback)(async () => {
2464
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.CONNECTIONS_ENTITY_ID, 'list');
2465
+ return res.connections;
2466
+ }, [bridge]);
2467
+ const connect = (0, react_1.useCallback)(async (provider) => {
2468
+ const result = await bridge.request('client_entity_invoke', { entityId: entity_ops_1.CONNECTIONS_CONNECT_ENTITY_ID, input: { provider } }, CONNECT_TIMEOUT_MS);
2469
+ if (result && 'error' in result) {
2470
+ throw new bridge_1.FiasBridgeError(result.error.message, result.error.code);
2471
+ }
2472
+ return result;
2473
+ }, [bridge]);
2474
+ const release = (0, react_1.useCallback)(async (connectionId) => {
2475
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.CONNECTIONS_ENTITY_ID, 'release', { connectionId });
2476
+ }, [bridge]);
2477
+ return (0, react_1.useMemo)(() => ({ list, connect, release }), [list, connect, release]);
2478
+ }
2479
+ /**
2480
+ * X, as an account the workspace this app is open in let it use (ADR-029 §3).
2481
+ *
2482
+ * Requires `connections:use` in `fias-plugin.json`. Get a `connectionId` from
2483
+ * `useFiasConnections()` (`list`, or `connect('x')`). Each call is charged to
2484
+ * the user as X prices it: a read by what it returns, a post per post — and a
2485
+ * post with a link (any URL or web address) costs more than a plain one. `publishPost` puts a confirm sheet in front of the
2486
+ * person — the platform's own words: the account, the workspace, the post —
2487
+ * and resolves only after they approve. A decline rejects with
2488
+ * `FiasBridgeError` code `USER_DECLINED`. Never available in the dev harness:
2489
+ * a post needs a person in the Fias app.
2490
+ *
2491
+ * @example
2492
+ * ```tsx
2493
+ * const x = useFiasX();
2494
+ * const { posts } = await x.publishPost({ connectionId, posts: [{ text: 'Hello' }] });
2495
+ * ```
2496
+ */
2497
+ function useFiasX() {
2498
+ const bridge = useBridge();
2499
+ return (0, react_1.useMemo)(() => ({
2500
+ publishPost: (params) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.X_ENTITY_ID, 'publish_post', { ...params }),
2501
+ searchUsers: async (connectionId, query, options) => (await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.X_ENTITY_ID, 'search_users', {
2502
+ connectionId,
2503
+ query,
2504
+ ...options,
2505
+ })).users,
2506
+ getUserPosts: async (connectionId, options) => (await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.X_ENTITY_ID, 'get_user_posts', {
2507
+ connectionId,
2508
+ ...options,
2509
+ })).posts,
2510
+ getPostMetrics: async (connectionId, postIds) => (await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.X_ENTITY_ID, 'get_post_metrics', { connectionId, postIds })).metrics,
2511
+ getAccountStats: (connectionId) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.X_ENTITY_ID, 'get_account_stats', {
2512
+ connectionId,
2513
+ }),
2514
+ }), [bridge]);
2515
+ }
2516
+ /**
2517
+ * Schedule one call for later (ADR-029 §5) — e.g. a post at 9 AM tomorrow.
2518
+ *
2519
+ * Requires `schedule:invoke` in `fias-plugin.json`, plus whatever the call
2520
+ * itself needs (`connections:use` to post). For an action outside Fias the
2521
+ * person approves the exact call AND its time in a confirm sheet first; a
2522
+ * decline rejects with `USER_DECLINED`. When it is due every check runs again
2523
+ * — if the account was disconnected or the app lost access, nothing is sent
2524
+ * and `list()` shows it as `refused`. There is no edit: cancel and schedule
2525
+ * again (a new approval).
2526
+ *
2527
+ * @example
2528
+ * ```tsx
2529
+ * const schedules = useFiasSchedules();
2530
+ * await schedules.schedule({
2531
+ * entityId: X_ENTITY_ID,
2532
+ * verb: 'publish_post',
2533
+ * payload: { connectionId, posts: [{ text: 'Launch day' }] },
2534
+ * fireAt: tomorrowAtNine,
2535
+ * });
2536
+ * ```
2537
+ */
2538
+ function useFiasSchedules() {
2539
+ const bridge = useBridge();
2540
+ return (0, react_1.useMemo)(() => ({
2541
+ schedule: (params) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.SCHEDULED_INVOCATIONS_ENTITY_ID, 'schedule', {
2542
+ entityId: params.entityId,
2543
+ verb: params.verb,
2544
+ payload: params.payload,
2545
+ fireAt: params.fireAt instanceof Date ? params.fireAt.toISOString() : params.fireAt,
2546
+ }),
2547
+ list: async (options) => (await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.SCHEDULED_INVOCATIONS_ENTITY_ID, 'list', {
2548
+ ...(options?.limit !== undefined ? { limit: options.limit } : {}),
2549
+ ...(options?.scheduleIds ? { scheduleIds: options.scheduleIds } : {}),
2550
+ })).schedules,
2551
+ cancel: async (scheduleId) => {
2552
+ await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.SCHEDULED_INVOCATIONS_ENTITY_ID, 'cancel', { scheduleId });
2553
+ },
2554
+ }), [bridge]);
2555
+ }
2556
+ /**
2557
+ * LinkedIn, as a member account the workspace this app is open in let it use
2558
+ * (ADR-029 §3). Requires `connections:use`; get a `connectionId` from
2559
+ * `useFiasConnections()`. Each call is charged to the user. No post-metrics or
2560
+ * audience calls yet: LinkedIn serves them only with analytics scopes the
2561
+ * connection does not ask for. `publishPost` puts
2562
+ * a confirm sheet in front of the person and resolves only after they approve;
2563
+ * a decline rejects with code `USER_DECLINED`.
2564
+ */
2565
+ function useFiasLinkedIn() {
2566
+ const bridge = useBridge();
2567
+ return (0, react_1.useMemo)(() => ({
2568
+ publishPost: (params) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.LINKEDIN_ENTITY_ID, 'publish_post', { ...params }),
2569
+ searchOrganizations: async (connectionId, vanityName) => (await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.LINKEDIN_ENTITY_ID, 'search_organizations', { connectionId, query: vanityName })).organizations,
2570
+ }), [bridge]);
2571
+ }
1978
2572
  /**
1979
2573
  * Send email AS the Company this app is being used in (ADR-023), from the
1980
2574
  * Company's own verified domain — not from Fias.
@@ -2130,9 +2724,11 @@ function useCommunityAssets() {
2130
2724
  * mutate the source. And you are the authoritative type gate — if you cannot
2131
2725
  * open this file, tell the user plainly rather than opening something broken.
2132
2726
  *
2133
- * To be offered as a destination at all, your arche must be registered for
2134
- * document intake on the platform side; declaring this hook alone does not
2135
- * enrol you.
2727
+ * To be offered as a destination at all, declare what you open in
2728
+ * `fias-plugin.json` (`"fileIntake": { "kinds": ["document"], "types": [".docx"] }`;
2729
+ * `types` narrows a kind to the files you really open) and have Fias approve
2730
+ * it; declaring this hook alone does not enrol you. A kind or type added later
2731
+ * stays off until it is approved too.
2136
2732
  */
2137
2733
  function useArcheHandoff(onHandoff) {
2138
2734
  const bridge = useBridge();