@fias/arche-sdk 2.19.2 → 2.21.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.
@@ -283,6 +283,65 @@ describe('SDK hooks', () => {
283
283
  expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'list', { prefix: undefined }));
284
284
  });
285
285
  });
286
+ describe('useDocumentSign (reading back a handed-over envelope)', () => {
287
+ const ENVELOPE_ID = 'dse_0123456789abcdef0123456789abcdef';
288
+ it('unwraps the envelope from the entity_invoke output envelope', async () => {
289
+ const { useDocumentSign } = require('./hooks');
290
+ const sign = useDocumentSign();
291
+ mockBridge.request.mockResolvedValue(output({
292
+ envelope: {
293
+ envelopeId: ENVELOPE_ID,
294
+ title: 'MSA — Acme Corp',
295
+ status: 'completed',
296
+ signingOrder: 'parallel',
297
+ recipients: [{ recipientId: 'dsr_a', name: 'Alice', role: 'signer', status: 'signed' }],
298
+ },
299
+ }));
300
+ const envelope = await sign.getEnvelope(ENVELOPE_ID);
301
+ expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.DOCUMENT_SIGN_ENTITY_ID, 'get_consented_envelope', {
302
+ envelopeId: ENVELOPE_ID,
303
+ }));
304
+ // The hook returns the envelope itself, not `{ envelope }` — a plugin
305
+ // should not have to know the transport shape.
306
+ expect(envelope.status).toBe('completed');
307
+ expect(envelope.recipients[0].name).toBe('Alice');
308
+ });
309
+ it('returns the events page as-is, including an empty timeline', async () => {
310
+ // An authorized envelope with no activity is a real zero and must not
311
+ // look like a refusal; the transport already distinguishes them (a
312
+ // refusal rejects), so the hook must not re-interpret an empty list.
313
+ const { useDocumentSign } = require('./hooks');
314
+ const sign = useDocumentSign();
315
+ mockBridge.request.mockResolvedValue(output({ events: [], total: 0, limit: 100, offset: 0 }));
316
+ const page = await sign.listEvents(ENVELOPE_ID);
317
+ expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.DOCUMENT_SIGN_ENTITY_ID, 'list_consented_envelope_events', {
318
+ envelopeId: ENVELOPE_ID,
319
+ limit: undefined,
320
+ offset: undefined,
321
+ }));
322
+ expect(page).toEqual({ events: [], total: 0, limit: 100, offset: 0 });
323
+ });
324
+ it('passes limit and offset through', async () => {
325
+ const { useDocumentSign } = require('./hooks');
326
+ const sign = useDocumentSign();
327
+ mockBridge.request.mockResolvedValue(output({ events: [], total: 0, limit: 10, offset: 20 }));
328
+ await sign.listEvents(ENVELOPE_ID, { limit: 10, offset: 20 });
329
+ expect(mockBridge.request).toHaveBeenCalledWith(...entityOp(entity_ops_1.DOCUMENT_SIGN_ENTITY_ID, 'list_consented_envelope_events', {
330
+ envelopeId: ENVELOPE_ID,
331
+ limit: 10,
332
+ offset: 20,
333
+ }));
334
+ });
335
+ it('surfaces a revoked-or-unknown refusal to the caller', async () => {
336
+ // One error for every refusal, and the plugin has to handle it — there
337
+ // is no retry that helps, and swallowing it would leave an approval
338
+ // record stuck on "in progress" forever with nothing said.
339
+ const { useDocumentSign } = require('./hooks');
340
+ const sign = useDocumentSign();
341
+ mockBridge.request.mockRejectedValue(new Error('ENVELOPE_NOT_FOUND_OR_NOT_GRANTED'));
342
+ await expect(sign.getEnvelope(ENVELOPE_ID)).rejects.toThrow('ENVELOPE_NOT_FOUND_OR_NOT_GRANTED');
343
+ });
344
+ });
286
345
  describe('useVaultUserDocuments.getBytes (consented binary reads)', () => {
287
346
  it('invokes get_consented_document_bytes and decodes the base64 payload', async () => {
288
347
  const { useVaultUserDocuments } = require('./hooks');
@@ -327,6 +386,16 @@ describe('SDK hooks', () => {
327
386
  access: undefined,
328
387
  });
329
388
  });
389
+ it('pick({ accept }) forwards the accepted types as a plain array', async () => {
390
+ const { useVaultUserDocuments } = require('./hooks');
391
+ mockBridge.request.mockResolvedValue({ canceled: true });
392
+ await useVaultUserDocuments().pick({ accept: ['image/*', '.pdf'] });
393
+ expect(mockBridge.request).toHaveBeenCalledWith('vault_documents_pick', {
394
+ maxDocuments: undefined,
395
+ access: undefined,
396
+ accept: ['image/*', '.pdf'],
397
+ });
398
+ });
330
399
  it('requestEditAccess names ONE document and asks for write — through the same host-mediated op', async () => {
331
400
  const { useVaultUserDocuments } = require('./hooks');
332
401
  mockBridge.request.mockResolvedValue({ canceled: true });
@@ -598,26 +667,6 @@ describe('SDK hooks', () => {
598
667
  expect(mockBridge.request.mock.calls[0][0]).toBe('entity_invoke');
599
668
  });
600
669
  });
601
- describe('useVaultDocuments.promoteToUserFiles', () => {
602
- it('asks the HOST to promote by sending ONLY the document id and the flag on the update op', async () => {
603
- const { useVaultDocuments } = require('./hooks');
604
- const DOC = 'vdoc_0123456789abcdef0123456789abcdef';
605
- mockBridge.request.mockResolvedValue({ documentId: DOC, visibility: 'user-vault' });
606
- const res = await useVaultDocuments().promoteToUserFiles(DOC);
607
- // No name, no tags, no destination: a promote is its own decision, and
608
- // the host refuses the flag bundled with anything else.
609
- expect(mockBridge.request).toHaveBeenCalledWith('vault_documents_update', {
610
- documentId: DOC,
611
- userVisible: true,
612
- });
613
- expect(res).toEqual({ documentId: DOC, visibility: 'user-vault' });
614
- });
615
- it('propagates a decline', async () => {
616
- const { useVaultDocuments } = require('./hooks');
617
- mockBridge.request.mockRejectedValue(Object.assign(new Error('declined'), { code: 'SAVE_DECLINED' }));
618
- await expect(useVaultDocuments().promoteToUserFiles('vdoc_0123456789abcdef0123456789abcdef')).rejects.toMatchObject({ code: 'SAVE_DECLINED' });
619
- });
620
- });
621
670
  describe('useEntityInvocation', () => {
622
671
  it('returns invoke function and initial state', () => {
623
672
  const { useEntityInvocation } = require('./hooks');
@@ -1004,6 +1053,39 @@ describe('SDK hooks', () => {
1004
1053
  documentId: 'vdoc_2',
1005
1054
  });
1006
1055
  });
1056
+ it("upload({ destination: 'documents' }) asks the host for its Save dialog", async () => {
1057
+ const { useVaultDocuments } = freshHooks();
1058
+ const api = useVaultDocuments();
1059
+ const saved = {
1060
+ documentId: 'vdoc_s',
1061
+ status: 'uploaded',
1062
+ name: 'Report (2).pdf',
1063
+ access: 'read',
1064
+ };
1065
+ mockBridge.request.mockResolvedValueOnce(saved);
1066
+ const res = await api.upload(new Uint8Array([1, 2]), {
1067
+ name: 'Report.pdf',
1068
+ mimeType: 'application/pdf',
1069
+ destination: 'documents',
1070
+ });
1071
+ expect(res).toEqual(saved);
1072
+ expect(mockBridge.request).toHaveBeenCalledWith('vault_documents_upload', expect.objectContaining({ name: 'Report.pdf', destination: 'documents' }));
1073
+ });
1074
+ it('saveCopy rides the update op with only the saveCopy options', async () => {
1075
+ const { useVaultDocuments } = freshHooks();
1076
+ const api = useVaultDocuments();
1077
+ mockBridge.request.mockResolvedValue({ documentId: 'vdoc_c', status: 'uploaded' });
1078
+ await api.saveCopy('vdoc_own', { suggestedName: 'Final.png' });
1079
+ expect(mockBridge.request).toHaveBeenLastCalledWith('vault_documents_update', {
1080
+ documentId: 'vdoc_own',
1081
+ saveCopy: { suggestedName: 'Final.png' },
1082
+ });
1083
+ await api.saveCopy('vdoc_own');
1084
+ expect(mockBridge.request).toHaveBeenLastCalledWith('vault_documents_update', {
1085
+ documentId: 'vdoc_own',
1086
+ saveCopy: {},
1087
+ });
1088
+ });
1007
1089
  it('getDownloadUrl caches a still-valid presigned URL', async () => {
1008
1090
  const { useVaultDocuments } = freshHooks();
1009
1091
  const api = useVaultDocuments();
@@ -1694,6 +1776,106 @@ describe('SDK hooks', () => {
1694
1776
  expect(asset.assetId).toBe('aset_1');
1695
1777
  });
1696
1778
  });
1779
+ describe('useFiasContent (content packs, ADR-021)', () => {
1780
+ const ENTITY = 'ent_intg_plugin_content_00000000';
1781
+ const load = () => {
1782
+ const { useFiasContent, FiasContentError } = require('./hooks');
1783
+ return { content: useFiasContent(), FiasContentError };
1784
+ };
1785
+ it('lists through the generic client_entity_invoke op, verb in the input', async () => {
1786
+ const { content } = load();
1787
+ const files = [{ path: 'lessons/01.md', sizeBytes: 5, mimeType: 'text/markdown' }];
1788
+ mockBridge.request.mockResolvedValueOnce({ files });
1789
+ expect(await content.list('lessons/')).toEqual(files);
1790
+ expect(mockBridge.request).toHaveBeenCalledWith('client_entity_invoke', { entityId: ENTITY, input: { verb: 'list', prefix: 'lessons/' } }, 120000);
1791
+ });
1792
+ it('reads text and parses JSON', async () => {
1793
+ const { content } = load();
1794
+ mockBridge.request.mockResolvedValueOnce({
1795
+ results: { 'a.md': { text: '# A', mimeType: 'text/markdown' } },
1796
+ });
1797
+ expect(await content.getText('a.md')).toBe('# A');
1798
+ mockBridge.request.mockResolvedValueOnce({
1799
+ results: { 'c.json': { text: '{"n":1}', mimeType: 'application/json' } },
1800
+ });
1801
+ expect(await content.getJson('c.json')).toEqual({ n: 1 });
1802
+ });
1803
+ it('throws a coded FiasContentError for a per-file and a whole-call failure', async () => {
1804
+ const { content, FiasContentError } = load();
1805
+ mockBridge.request.mockResolvedValueOnce({
1806
+ results: { 'x.md': { error: { code: 'CONTENT_NOT_FOUND', message: 'not in pack' } } },
1807
+ });
1808
+ const perFile = await content.getText('x.md').catch((e) => e);
1809
+ expect(perFile).toBeInstanceOf(FiasContentError);
1810
+ expect(perFile).toMatchObject({ code: 'CONTENT_NOT_FOUND', path: 'x.md' });
1811
+ mockBridge.request.mockResolvedValueOnce({
1812
+ error: { code: 'CONTENT_UNAVAILABLE_IN_PREVIEW', message: 'preview' },
1813
+ });
1814
+ await expect(content.list()).rejects.toMatchObject({
1815
+ code: 'CONTENT_UNAVAILABLE_IN_PREVIEW',
1816
+ });
1817
+ });
1818
+ it.each([
1819
+ ['Permission denied: entities:client_invoke', undefined, 'PERMISSION_DENIED'],
1820
+ ['Rate limit exceeded for client_entity_invoke', undefined, 'RATE_LIMIT'],
1821
+ ['Bridge request timed out', 'TIMEOUT', 'CONTENT_UNAVAILABLE'],
1822
+ ])('maps a bridge failure (%s, code %s) to %s', async (message, code, expected) => {
1823
+ const { content } = load();
1824
+ mockBridge.request.mockRejectedValueOnce(Object.assign(new Error(message), { code }));
1825
+ await expect(content.list()).rejects.toMatchObject({ code: expected });
1826
+ });
1827
+ it('reads bytes, and batches with per-file errors', async () => {
1828
+ const { content, FiasContentError } = load();
1829
+ const bytes = new Uint8Array([1, 2]).buffer;
1830
+ mockBridge.request.mockResolvedValueOnce({
1831
+ results: { 'a.bin': { bytes, mimeType: 'application/octet-stream' } },
1832
+ });
1833
+ expect(await content.getBytes('a.bin')).toBe(bytes);
1834
+ mockBridge.request.mockResolvedValueOnce({
1835
+ results: {
1836
+ 'a.md': { text: '# A', mimeType: 'text/markdown' },
1837
+ 'b.png': { error: { code: 'CONTENT_NOT_TEXT', message: 'image/png, not text' } },
1838
+ },
1839
+ });
1840
+ const many = await content.getMany(['a.md', 'b.png', 'gone.md'], 'text');
1841
+ expect(many['a.md']).toBe('# A');
1842
+ expect(many['b.png']).toMatchObject({ code: 'CONTENT_NOT_TEXT', path: 'b.png' });
1843
+ expect(many['gone.md']).toBeInstanceOf(FiasContentError);
1844
+ });
1845
+ it('builds one typed blob: URL per path from host-cached bytes', async () => {
1846
+ const { content } = load();
1847
+ const created = [];
1848
+ const original = URL.createObjectURL;
1849
+ URL.createObjectURL = jest.fn((blob) => {
1850
+ created.push(blob);
1851
+ return 'blob:plugin/1';
1852
+ });
1853
+ try {
1854
+ mockBridge.request.mockResolvedValueOnce({
1855
+ results: {
1856
+ 'img/cover-unique.webp': { bytes: new ArrayBuffer(3), mimeType: 'image/webp' },
1857
+ },
1858
+ });
1859
+ expect(await content.getObjectUrl('img/cover-unique.webp')).toBe('blob:plugin/1');
1860
+ expect(await content.getObjectUrl('img/cover-unique.webp')).toBe('blob:plugin/1');
1861
+ expect(created).toHaveLength(1);
1862
+ expect(created[0].type).toBe('image/webp');
1863
+ }
1864
+ finally {
1865
+ URL.createObjectURL = original;
1866
+ }
1867
+ });
1868
+ it('returns a signed streaming URL with its expiry', async () => {
1869
+ const { content } = load();
1870
+ mockBridge.request.mockResolvedValueOnce({
1871
+ results: { 'a.ogg': { url: 'https://cdn/x?sig', expiresAt: '2026-09-24T00:00:00Z' } },
1872
+ });
1873
+ expect(await content.getUrl('a.ogg')).toEqual({
1874
+ url: 'https://cdn/x?sig',
1875
+ expiresAt: '2026-09-24T00:00:00Z',
1876
+ });
1877
+ });
1878
+ });
1697
1879
  });
1698
1880
  describe('useDataSubscription (ADR-012 Phase M4)', () => {
1699
1881
  const flush = async () => {