@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.
- package/dist/entity-ops.d.ts +7 -1
- package/dist/entity-ops.d.ts.map +1 -1
- package/dist/entity-ops.js +7 -1
- package/dist/entity-ops.js.map +1 -1
- package/dist/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +17 -13
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +156 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +192 -24
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +202 -20
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +184 -31
- package/dist/types.d.ts +193 -72
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +177 -28
- package/templates/default/CLAUDE.md +177 -28
package/dist/hooks.test.js
CHANGED
|
@@ -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 () => {
|