@fias/arche-sdk 2.30.0 → 2.32.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
@@ -16,6 +16,7 @@ exports.useSurface = useSurface;
16
16
  exports.useFiasNavigation = useFiasNavigation;
17
17
  exports.usePersistentState = usePersistentState;
18
18
  exports.useFiasChrome = useFiasChrome;
19
+ exports.useOpener = useOpener;
19
20
  exports.useStepNavigation = useStepNavigation;
20
21
  exports.useFiasDataStore = useFiasDataStore;
21
22
  exports.useFiasWorkspaces = useFiasWorkspaces;
@@ -34,6 +35,7 @@ exports.useDataSubscription = useDataSubscription;
34
35
  exports.useVaultUserDocuments = useVaultUserDocuments;
35
36
  exports.useCreativeLibrary = useCreativeLibrary;
36
37
  exports.useFiasConnections = useFiasConnections;
38
+ exports.useFiasWebSources = useFiasWebSources;
37
39
  exports.useFiasX = useFiasX;
38
40
  exports.useFiasSchedules = useFiasSchedules;
39
41
  exports.useFiasLinkedIn = useFiasLinkedIn;
@@ -107,6 +109,8 @@ function createDebouncedWriter(bridge) {
107
109
  }
108
110
  return { schedule, flush };
109
111
  }
112
+ /** The platform's `resolve_people` limit per call; `resolvePeople` pages above it. */
113
+ const RESOLVE_PEOPLE_PAGE = 100;
110
114
  /**
111
115
  * Access the current user's profile information.
112
116
  * Requires the `user:profile:read` permission in fias-plugin.json.
@@ -438,6 +442,20 @@ function useImageEntities(filter) {
438
442
  * run, so we use the same 2-minute ceiling as the image/audio surfaces.
439
443
  */
440
444
  const CLIENT_ENTITY_INVOKE_TIMEOUT_MS = 120000;
445
+ /**
446
+ * Whether a `client_entity_invoke` failed because the host has no executor
447
+ * for the entity (an older Fias, or the dev harness): the capability is simply
448
+ * not available there. Hosts answer `CLIENT_ENTITY_UNAVAILABLE`; older ones
449
+ * only said so in the message ("no client-side executor …"), which is still
450
+ * recognised. Any other bridge error is the app's to fix.
451
+ */
452
+ function isClientEntityUnavailable(err) {
453
+ if (!(err instanceof bridge_1.FiasBridgeError))
454
+ return false;
455
+ if (err.code === 'CLIENT_ENTITY_UNAVAILABLE')
456
+ return true;
457
+ return err.code === 'BRIDGE_ERROR' && err.message.includes('no client-side executor');
458
+ }
441
459
  /**
442
460
  * Invoke a client-side ("on-device") entity through the bridge's generic
443
461
  * `client_entity_invoke` op. Client-side entities execute entirely in the
@@ -757,6 +775,7 @@ const INERT_CHROME = {
757
775
  sidebarCollapsed: false,
758
776
  hostExpandedBar: true,
759
777
  hostExpandButton: true,
778
+ opener: null,
760
779
  };
761
780
  /**
762
781
  * The host chrome around your arche — Fias's standard expand/contract (the
@@ -819,6 +838,7 @@ function useFiasChrome() {
819
838
  const openAssistant = (0, react_1.useCallback)(() => bridge.openAssistant(), [bridge]);
820
839
  return (0, react_1.useMemo)(() => ({
821
840
  ...(chrome ?? INERT_CHROME),
841
+ opener: chrome?.opener ?? null,
822
842
  supported: supported && chrome !== null,
823
843
  setFocusMode,
824
844
  setSidebarCollapsed,
@@ -837,6 +857,72 @@ function useFiasChrome() {
837
857
  openAssistant,
838
858
  ]);
839
859
  }
860
+ /** The person answers the send sheet; it waits on them, like the picker. */
861
+ const SEND_TO_OPENER_TIMEOUT_MS = 30 * 60 * 1000;
862
+ /**
863
+ * Where the person came from, when a Fias tool opened your arche — the forge
864
+ * page's "Open FiasWorks Studio", the Arche Builder's (ADR-037 D8) — and the
865
+ * two things you can do about it:
866
+ *
867
+ * - `returnTo()` takes the person back there.
868
+ * - `send(file)` offers a Vault file YOUR arche created ("Use in {name}"):
869
+ * Fias shows the person a sheet naming the file and the destination, and
870
+ * their Send moves it — into a forge's `public/`, or an Arche Builder
871
+ * arche's asset library. Requires `opener:send`.
872
+ *
873
+ * `opener` is null when nothing opened your arche; show neither then. Reading
874
+ * it does not take the host chrome over (unlike `useFiasChrome`).
875
+ *
876
+ * @example
877
+ * const { opener, send, returnTo } = useOpener();
878
+ * {opener && <button onClick={returnTo}>Return to {opener.name}</button>}
879
+ * {opener && <button onClick={() => send({ fileId, name: 'pumpkin.png' })}>
880
+ * Use in {opener.name}</button>}
881
+ */
882
+ function useOpener() {
883
+ const bridge = useBridge();
884
+ const [opener, setOpener] = (0, react_1.useState)(bridge.getChrome()?.opener ?? null);
885
+ (0, react_1.useEffect)(() => {
886
+ let cancelled = false;
887
+ bridge.waitForInit().then(() => {
888
+ if (!cancelled)
889
+ setOpener(bridge.getChrome()?.opener ?? null);
890
+ });
891
+ const unsubscribe = bridge.onChromeUpdate((next) => setOpener(next?.opener ?? null));
892
+ return () => {
893
+ cancelled = true;
894
+ unsubscribe();
895
+ };
896
+ }, [bridge]);
897
+ const returnTo = (0, react_1.useCallback)(() => {
898
+ if (bridge.getChrome()?.opener)
899
+ bridge.navigateHost('opener');
900
+ }, [bridge]);
901
+ const send = (0, react_1.useCallback)(async (file) => {
902
+ if (!bridge.getChrome()?.opener) {
903
+ throw new bridge_1.FiasBridgeError('Nothing opened this arche, so there is nowhere to send', 'NO_OPENER');
904
+ }
905
+ let answer;
906
+ try {
907
+ answer = await bridge.request('client_entity_invoke', {
908
+ entityId: entity_ops_1.SEND_TO_OPENER_ENTITY_ID,
909
+ input: { fileId: file.fileId, ...(file.name ? { name: file.name } : {}) },
910
+ }, SEND_TO_OPENER_TIMEOUT_MS);
911
+ }
912
+ catch (err) {
913
+ // A host without it (an older Fias, the dev harness).
914
+ if (isClientEntityUnavailable(err)) {
915
+ throw new bridge_1.FiasBridgeError('Sending to another arche is not available here', 'SEND_UNAVAILABLE');
916
+ }
917
+ throw err;
918
+ }
919
+ if (answer && 'error' in answer) {
920
+ throw new bridge_1.FiasBridgeError(answer.error.message, answer.error.code);
921
+ }
922
+ return answer;
923
+ }, [bridge]);
924
+ return (0, react_1.useMemo)(() => ({ opener, returnTo, send }), [opener, returnTo, send]);
925
+ }
840
926
  /**
841
927
  * Manage step navigation for multi-node plugins.
842
928
  * Listens for step_navigate messages from the host to support
@@ -1074,6 +1160,17 @@ function useFiasDataStore() {
1074
1160
  batch,
1075
1161
  ]);
1076
1162
  }
1163
+ /** A member target on the wire: a typed username, or the app's ref (ADR-040 §4). */
1164
+ function memberTargetPayload(member) {
1165
+ if (typeof member === 'string')
1166
+ return { username: member };
1167
+ if (member && typeof member === 'object' && typeof member.ref === 'string') {
1168
+ return { ref: member.ref };
1169
+ }
1170
+ // Most often `m.username` from a roster: apps created after app user refs
1171
+ // get `ref` there instead (ADR-040 §4).
1172
+ throw new Error('useFiasWorkspaces: name the member by { ref } from listMembers() (or a username a person typed)');
1173
+ }
1077
1174
  /**
1078
1175
  * Access workspace tenancy — team/org workspaces with role-gated membership
1079
1176
  * (owner/admin/member/viewer) over `workspace`-scoped Data Store collections
@@ -1087,7 +1184,8 @@ function useFiasDataStore() {
1087
1184
  * @example
1088
1185
  * const workspaces = useFiasWorkspaces();
1089
1186
  * const ws = await workspaces.create('Acme Corp');
1090
- * await workspaces.addMember(ws.workspaceId, otherUserId, 'member');
1187
+ * // a person picks who to add, in a sheet Fias draws (ADR-040):
1188
+ * const { added } = await workspaces.addPeople(ws.workspaceId);
1091
1189
  * // or, inside a Fias org workspace, its members' shared space:
1092
1190
  * const shared = await workspaces.openForWorkspace();
1093
1191
  * // then, with useFiasDataStore():
@@ -1118,10 +1216,41 @@ function useFiasWorkspaces() {
1118
1216
  const archive = (0, react_1.useCallback)(async (workspaceId) => {
1119
1217
  await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'archive', { workspaceId });
1120
1218
  }, [bridge]);
1219
+ const resolvePeople = (0, react_1.useCallback)(async (refs, workspaceId) => {
1220
+ // Any length, in order: nothing to ask for an empty list, and the
1221
+ // platform's 100-per-call limit is the SDK's to page, not the app's.
1222
+ const people = [];
1223
+ for (let i = 0; i < refs.length; i += RESOLVE_PEOPLE_PAGE) {
1224
+ const page = refs.slice(i, i + RESOLVE_PEOPLE_PAGE);
1225
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'resolve_people', workspaceId === undefined ? { refs: page } : { refs: page, workspaceId });
1226
+ people.push(...res.people);
1227
+ }
1228
+ return people;
1229
+ }, [bridge]);
1121
1230
  const listMembers = (0, react_1.useCallback)(async (workspaceId) => {
1122
1231
  const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list_members', { workspaceId });
1123
1232
  return res.members;
1124
1233
  }, [bridge]);
1234
+ const addPeople = (0, react_1.useCallback)(async (workspaceId, options) => {
1235
+ let answer;
1236
+ try {
1237
+ answer = await bridge.request('client_entity_invoke', {
1238
+ entityId: entity_ops_1.WORKSPACE_ADD_PEOPLE_ENTITY_ID,
1239
+ input: options?.role ? { workspaceId, role: options.role } : { workspaceId },
1240
+ }, ADD_PEOPLE_SHEET_TIMEOUT_MS);
1241
+ }
1242
+ catch (err) {
1243
+ // A host without the sheet (an older Fias or dev harness) has no
1244
+ // executor for it: the sheet is simply not available there.
1245
+ if (isClientEntityUnavailable(err)) {
1246
+ throw new bridge_1.FiasBridgeError('Adding people is not available here', 'ADD_PEOPLE_UNAVAILABLE');
1247
+ }
1248
+ throw err;
1249
+ }
1250
+ if ('error' in answer)
1251
+ throw new bridge_1.FiasBridgeError(answer.error.message, answer.error.code);
1252
+ return { added: answer.added };
1253
+ }, [bridge]);
1125
1254
  const addMember = (0, react_1.useCallback)(async (workspaceId, username, role, expiresAt) => {
1126
1255
  const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'add_member', {
1127
1256
  workspaceId,
@@ -1131,14 +1260,14 @@ function useFiasWorkspaces() {
1131
1260
  });
1132
1261
  return res.member;
1133
1262
  }, [bridge]);
1134
- const updateMember = (0, react_1.useCallback)(async (workspaceId, username, role) => {
1135
- const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', { workspaceId, username, role });
1263
+ const updateMember = (0, react_1.useCallback)(async (workspaceId, member, role) => {
1264
+ const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', { workspaceId, ...memberTargetPayload(member), role });
1136
1265
  return res.member;
1137
1266
  }, [bridge]);
1138
- const removeMember = (0, react_1.useCallback)(async (workspaceId, username) => {
1267
+ const removeMember = (0, react_1.useCallback)(async (workspaceId, member) => {
1139
1268
  await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'remove_member', {
1140
1269
  workspaceId,
1141
- username,
1270
+ ...memberTargetPayload(member),
1142
1271
  });
1143
1272
  }, [bridge]);
1144
1273
  return (0, react_1.useMemo)(() => ({
@@ -1148,9 +1277,11 @@ function useFiasWorkspaces() {
1148
1277
  get,
1149
1278
  archive,
1150
1279
  listMembers,
1280
+ addPeople,
1151
1281
  addMember,
1152
1282
  updateMember,
1153
1283
  removeMember,
1284
+ resolvePeople,
1154
1285
  }), [
1155
1286
  create,
1156
1287
  openForWorkspace,
@@ -1158,9 +1289,11 @@ function useFiasWorkspaces() {
1158
1289
  get,
1159
1290
  archive,
1160
1291
  listMembers,
1292
+ addPeople,
1161
1293
  addMember,
1162
1294
  updateMember,
1163
1295
  removeMember,
1296
+ resolvePeople,
1164
1297
  ]);
1165
1298
  }
1166
1299
  /** The refusals that mean "no access here" — the workspaces `get` verb's two answers. */
@@ -1245,13 +1378,10 @@ function useFiasAccess(workspaceId) {
1245
1378
  }
1246
1379
  catch (err) {
1247
1380
  // A host without the sheet (an older Fias, or an older dev harness) has
1248
- // no executor for it and answers a generic bridge error naming that —
1249
- // "no client-side executor registered" / "no client-side executor for …
1250
- // in the dev harness": the sheet is simply not available. Any other bridge error (a missing permission, a call loop)
1251
- // is the app's to fix, so it passes through unchanged.
1252
- if (err instanceof bridge_1.FiasBridgeError &&
1253
- err.code === 'BRIDGE_ERROR' &&
1254
- err.message.includes('no client-side executor')) {
1381
+ // no executor for it: the sheet is simply not available. Any other
1382
+ // bridge error (a missing permission, a call loop) is the app's to fix,
1383
+ // so it passes through unchanged.
1384
+ if (isClientEntityUnavailable(err)) {
1255
1385
  throw new bridge_1.FiasBridgeError('Managing access is not available here', 'ACCESS_UNAVAILABLE');
1256
1386
  }
1257
1387
  throw err;
@@ -1295,6 +1425,8 @@ const lastAccessAnswers = new Map();
1295
1425
  const FOCUS_REFETCH_MIN_MS = 30000;
1296
1426
  /** How long a person may keep the access sheet open before the call gives up. */
1297
1427
  const ACCESS_SHEET_TIMEOUT_MS = 30 * 60 * 1000;
1428
+ /** The Add people sheet waits on a person, like the access sheet. */
1429
+ const ADD_PEOPLE_SHEET_TIMEOUT_MS = ACCESS_SHEET_TIMEOUT_MS;
1298
1430
  const sharedAccessByBridge = new WeakMap();
1299
1431
  function peekSharedAccess(bridge, workspaceId) {
1300
1432
  return sharedAccessByBridge.get(bridge)?.get(workspaceId);
@@ -2958,7 +3090,7 @@ async function fetchCommunityAssetUrls(bridge, assetIds) {
2958
3090
  return hits;
2959
3091
  }
2960
3092
  /**
2961
- * The user's Creative Library (ADR-035): their styles and characters, made in
3093
+ * The user's Creative Library (ADR-037): their styles and characters, made in
2962
3094
  * FiasWorks Studio or any arche they allow, owned by them — or, in a team
2963
3095
  * workspace that approved this arche, the workspace's. Requires
2964
3096
  * `vault:creative:read`; `create` / `update` / `remove` / `restore` also need
@@ -2998,9 +3130,7 @@ function useCreativeLibrary() {
2998
3130
  catch (err) {
2999
3131
  // A host without the picker (an older Fias, the dev harness) has no
3000
3132
  // executor for it: the picker is simply not available there.
3001
- if (err instanceof bridge_1.FiasBridgeError &&
3002
- err.code === 'BRIDGE_ERROR' &&
3003
- err.message.includes('no client-side executor')) {
3133
+ if (isClientEntityUnavailable(err)) {
3004
3134
  throw new bridge_1.FiasBridgeError('The Creative Library picker is not available here', 'PICKER_UNAVAILABLE');
3005
3135
  }
3006
3136
  throw err;
@@ -3047,6 +3177,54 @@ function useFiasConnections() {
3047
3177
  }, [bridge]);
3048
3178
  return (0, react_1.useMemo)(() => ({ list, connect, release }), [list, connect, release]);
3049
3179
  }
3180
+ /**
3181
+ * Pages on websites Fias approved, read as DATA (ADR-038). Requires
3182
+ * `web:sources:read`.
3183
+ *
3184
+ * The app names a page on an approved site and a form — the fields it wants —
3185
+ * and gets back values of those kinds (numbers, dates, yes/no, the app's own
3186
+ * choices, lists of them), each `null` when the page does not say. It never
3187
+ * receives the page's text, not even a short label. One copy of each page serves every app, fetched at most
3188
+ * once an hour; a fresh fetch and its reading are charged to the user (well
3189
+ * under a cent for a typical page), and a cached result is free.
3190
+ *
3191
+ * @example
3192
+ * const web = useFiasWebSources();
3193
+ * const result = await web.extract<{
3194
+ * rates: Array<{ termMonths: number | null; ira: boolean | null; apy: number | null }>;
3195
+ * }>({
3196
+ * url: 'https://www.becu.org/everyday-banking/certificates',
3197
+ * form: {
3198
+ * description: 'Share certificate rates',
3199
+ * fields: {
3200
+ * rates: {
3201
+ * kind: 'list',
3202
+ * item: {
3203
+ * termMonths: { kind: 'integer', min: 1, max: 120, description: 'Term in months' },
3204
+ * ira: { kind: 'boolean', description: 'An IRA certificate' },
3205
+ * apy: { kind: 'number', min: 0, max: 25, description: 'APY in percent' },
3206
+ * },
3207
+ * },
3208
+ * },
3209
+ * },
3210
+ * });
3211
+ * // Show result.site.name and result.fetchedAt beside the figures.
3212
+ */
3213
+ function useFiasWebSources() {
3214
+ const bridge = useBridge();
3215
+ const listSites = (0, react_1.useCallback)(async (options) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.WEB_SOURCES_ENTITY_ID, 'list_sites', {
3216
+ ...(options?.category !== undefined ? { category: options.category } : {}),
3217
+ ...(options?.search !== undefined ? { search: options.search } : {}),
3218
+ ...(options?.limit !== undefined ? { limit: options.limit } : {}),
3219
+ ...(options?.offset !== undefined ? { offset: options.offset } : {}),
3220
+ }), [bridge]);
3221
+ const extract = (0, react_1.useCallback)(async (request) => (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.WEB_SOURCES_ENTITY_ID, 'extract', {
3222
+ url: request.url,
3223
+ form: request.form,
3224
+ ...(request.maxAgeSeconds !== undefined ? { maxAgeSeconds: request.maxAgeSeconds } : {}),
3225
+ }), [bridge]);
3226
+ return (0, react_1.useMemo)(() => ({ listSites, extract }), [listSites, extract]);
3227
+ }
3050
3228
  /**
3051
3229
  * X, as an account the workspace this app is open in let it use (ADR-029 §3).
3052
3230
  *
@@ -3151,7 +3329,8 @@ function useFiasLinkedIn() {
3151
3329
  * The From address is always the Company's — you only choose recipients and
3152
3330
  * content.
3153
3331
  *
3154
- * Name members by `userId`; you never see their addresses. Every email carries
3332
+ * Name members by their ref (`useFiasUser().ref`, or refs you stored); you
3333
+ * never see their addresses. Every email carries
3155
3334
  * a one-click unsubscribe, and people who used it are skipped (`muted`).
3156
3335
  *
3157
3336
  * @example
@@ -3159,7 +3338,7 @@ function useFiasLinkedIn() {
3159
3338
  * const email = useCompanyEmail();
3160
3339
  * const result = await email.send({
3161
3340
  * requestKey: `digest-${weekId}`,
3162
- * to: members.map((m) => ({ userId: m.userId })),
3341
+ * to: assigneeRefs.map((ref) => ({ ref })),
3163
3342
  * subject: 'Your weekly digest',
3164
3343
  * text: digestText,
3165
3344
  * html: digestHtml,