@profullstack/coinpay 0.21.0 → 0.22.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/CHANGELOG.md +12 -0
- package/bin/coinpay.js +56 -5
- package/package.json +1 -1
- package/src/documents-publish.d.ts +29 -0
- package/src/documents-publish.js +110 -0
- package/src/documents.d.ts +6 -2
- package/src/documents.js +42 -9
- package/src/index.d.ts +3 -0
- package/src/index.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.22.0] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `coinpay document publish <dir> --business-id <id>`: encrypt and publish a whole directory, keeping
|
|
12
|
+
its structure (under a folder named like the directory, or `--folder dest`, `--folder /` for the top
|
|
13
|
+
level). Publishing again syncs: new files upload, changed files become a new version, unchanged files
|
|
14
|
+
are skipped, signed documents are left alone, nothing remote is deleted. `--dry-run` shows the plan;
|
|
15
|
+
hidden files and `node_modules` are skipped unless `--include-hidden`.
|
|
16
|
+
- `coinpay document move <id> <folder>`; `upload --folder a/b`; `list` shows folders.
|
|
17
|
+
- Any file type can be uploaded (unknown types are opaque downloads; the server only holds ciphertext).
|
|
18
|
+
- SDK: `publishDirectory`, `planPublish`, `moveDocument`; `uploadDocument({ folder })`.
|
|
19
|
+
|
|
8
20
|
## [0.21.0] - 2026-10-19
|
|
9
21
|
|
|
10
22
|
### Added
|
package/bin/coinpay.js
CHANGED
|
@@ -80,7 +80,9 @@ import {
|
|
|
80
80
|
downloadDocument,
|
|
81
81
|
listDocumentVersions,
|
|
82
82
|
uploadDocumentVersion,
|
|
83
|
+
moveDocument,
|
|
83
84
|
} from '../src/documents.js';
|
|
85
|
+
import { publishDirectory } from '../src/documents-publish.js';
|
|
84
86
|
import { readFileSync, writeFileSync, existsSync, unlinkSync, chmodSync } from 'fs';
|
|
85
87
|
import { execSync, spawn } from 'child_process';
|
|
86
88
|
import { createInterface } from 'readline';
|
|
@@ -552,7 +554,11 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
552
554
|
${colors.bright}document${colors.reset} (end-to-end encrypted: CoinPay never sees the contents)
|
|
553
555
|
vault-setup Create the documents passphrase (--business-id, admins)
|
|
554
556
|
list Documents (--business-id) with signing progress
|
|
555
|
-
upload <file> Encrypt and upload: --business-id [--title] [--message]
|
|
557
|
+
upload <file> Encrypt and upload: --business-id [--folder a/b] [--title] [--message]
|
|
558
|
+
publish <dir> Sync a whole directory, keeping its structure: --business-id
|
|
559
|
+
[--folder dest | --folder / for top level] [--dry-run] [--include-hidden]
|
|
560
|
+
new files upload, changed ones become versions, signed ones are left alone
|
|
561
|
+
move <id> <folder> File a document elsewhere (/ = top level)
|
|
556
562
|
share <id> --signers "Ada <ada@x.com>, bob@y.com" [--editors "..."] [--viewers "..."]
|
|
557
563
|
[--message] [--no-email] [--email-key puts the key in the email]
|
|
558
564
|
Prints each full link (with #k= key): send it privately
|
|
@@ -1328,7 +1334,7 @@ async function documentsVault(client, businessId) {
|
|
|
1328
1334
|
|
|
1329
1335
|
async function handleDocument(subcommand, args, flags) {
|
|
1330
1336
|
const client = createClient();
|
|
1331
|
-
requireFlagValues(flags, ['business-id', 'title', 'message', 'signers', 'viewers', 'editors', 'reason', 'out', 'note', 'revision']);
|
|
1337
|
+
requireFlagValues(flags, ['business-id', 'title', 'message', 'signers', 'viewers', 'editors', 'reason', 'out', 'note', 'revision', 'folder']);
|
|
1332
1338
|
switch (subcommand) {
|
|
1333
1339
|
case 'vault-setup': {
|
|
1334
1340
|
if (!flags['business-id']) usageExit('coinpay document vault-setup --business-id <id>');
|
|
@@ -1344,12 +1350,56 @@ async function handleDocument(subcommand, args, flags) {
|
|
|
1344
1350
|
if (!rows.length) return print.info('No documents');
|
|
1345
1351
|
for (const d of rows) {
|
|
1346
1352
|
const progress = d.signers ? ' ' + d.signed + '/' + d.signers + ' signed' : '';
|
|
1347
|
-
print.info(d.id + ' ' + d.title + ' ' + d.status + progress);
|
|
1353
|
+
print.info(d.id + ' ' + (d.folder ? d.folder + '/' : '') + d.title + ' ' + d.status + progress);
|
|
1348
1354
|
}
|
|
1349
1355
|
return;
|
|
1350
1356
|
}
|
|
1357
|
+
case 'publish': {
|
|
1358
|
+
if (args.length !== 1 || !flags['business-id']) usageExit('coinpay document publish <dir> --business-id <id> [--folder path | --folder /] [--dry-run] [--include-hidden]');
|
|
1359
|
+
if (!existsSync(args[0])) usageExit('No such directory: ' + args[0]);
|
|
1360
|
+
const businessId = flags['business-id'];
|
|
1361
|
+
const dryRun = !!flags['dry-run'];
|
|
1362
|
+
const vault = dryRun ? undefined : await documentsVault(client, businessId);
|
|
1363
|
+
const labels = { upload: 'new', version: 'new version', unchanged: 'unchanged', locked: 'signed, left alone', too_large: 'too large (9 MB max)', empty: 'empty, skipped' };
|
|
1364
|
+
const result = await publishDirectory(client, {
|
|
1365
|
+
businessId,
|
|
1366
|
+
vault,
|
|
1367
|
+
dir: args[0],
|
|
1368
|
+
folder: flags.folder,
|
|
1369
|
+
includeHidden: !!flags['include-hidden'],
|
|
1370
|
+
dryRun,
|
|
1371
|
+
onFile: (p) => {
|
|
1372
|
+
if (flags.json) return;
|
|
1373
|
+
const where = (p.folder ? p.folder + '/' : '') + p.filename;
|
|
1374
|
+
if (p.result === 'ok') print.success((p.action === 'upload' ? 'uploaded ' : 'v' + p.version + ' ') + where);
|
|
1375
|
+
else print.error('failed ' + where + ': ' + p.error);
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
if (flags.json) return print.json(result);
|
|
1379
|
+
const count = (a) => result.plan.filter((p) => p.action === a).length;
|
|
1380
|
+
if (dryRun) {
|
|
1381
|
+
for (const p of result.plan) print.info(labels[p.action].padEnd(20) + ' ' + (p.folder ? p.folder + '/' : '') + p.filename);
|
|
1382
|
+
print.info('Dry run: nothing was uploaded.');
|
|
1383
|
+
}
|
|
1384
|
+
const failed = result.plan.filter((p) => p.result === 'failed').length;
|
|
1385
|
+
print.info(
|
|
1386
|
+
'Into ' + (result.folder ? result.folder + '/' : 'the top level') + ': ' +
|
|
1387
|
+
count('upload') + ' new, ' + count('version') + ' new versions, ' + count('unchanged') + ' unchanged' +
|
|
1388
|
+
(count('locked') ? ', ' + count('locked') + ' signed (left alone)' : '') +
|
|
1389
|
+
(count('too_large') ? ', ' + count('too_large') + ' too large' : '') +
|
|
1390
|
+
(result.skippedHidden ? ', ' + result.skippedHidden + ' hidden skipped' : '') +
|
|
1391
|
+
(failed ? ', ' + failed + ' FAILED' : '')
|
|
1392
|
+
);
|
|
1393
|
+
if (failed) process.exitCode = 1;
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
case 'move': {
|
|
1397
|
+
if (args.length !== 2) usageExit('coinpay document move <id> <folder> (use / for the top level)');
|
|
1398
|
+
const result = await moveDocument(client, args[0], args[1]);
|
|
1399
|
+
return flags.json ? print.json(result) : print.success('Moved to ' + (result.folder || 'the top level'));
|
|
1400
|
+
}
|
|
1351
1401
|
case 'upload': {
|
|
1352
|
-
if (args.length !== 1 || !flags['business-id']) usageExit('coinpay document upload <file> --business-id <id> [--title "..."] [--message "..."]');
|
|
1402
|
+
if (args.length !== 1 || !flags['business-id']) usageExit('coinpay document upload <file> --business-id <id> [--folder path] [--title "..."] [--message "..."]');
|
|
1353
1403
|
const vault = await documentsVault(client, flags['business-id']);
|
|
1354
1404
|
const doc = await uploadDocument(client, {
|
|
1355
1405
|
businessId: flags['business-id'],
|
|
@@ -1358,6 +1408,7 @@ async function handleDocument(subcommand, args, flags) {
|
|
|
1358
1408
|
filename: args[0],
|
|
1359
1409
|
title: flags.title,
|
|
1360
1410
|
message: flags.message,
|
|
1411
|
+
folder: flags.folder,
|
|
1361
1412
|
});
|
|
1362
1413
|
if (flags.json) return print.json(doc);
|
|
1363
1414
|
print.success('Uploaded (encrypted): ' + doc.id);
|
|
@@ -1445,7 +1496,7 @@ async function handleDocument(subcommand, args, flags) {
|
|
|
1445
1496
|
return print.success('Deleted');
|
|
1446
1497
|
}
|
|
1447
1498
|
default:
|
|
1448
|
-
usageExit('coinpay document <vault-setup|list|upload|share|get|versions|new-version|link|revoke|download|void|delete>');
|
|
1499
|
+
usageExit('coinpay document <vault-setup|list|upload|publish|move|share|get|versions|new-version|link|revoke|download|void|delete>');
|
|
1449
1500
|
}
|
|
1450
1501
|
}
|
|
1451
1502
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/coinpay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CoinPayClient } from './client.js';
|
|
2
|
+
import type { CoinPayDocument, UnlockedVault } from './documents.js';
|
|
3
|
+
|
|
4
|
+
export interface PublishEntry {
|
|
5
|
+
abs: string;
|
|
6
|
+
rel: string;
|
|
7
|
+
size: number;
|
|
8
|
+
filename: string;
|
|
9
|
+
folder: string | null;
|
|
10
|
+
sha256?: string;
|
|
11
|
+
action: 'upload' | 'version' | 'unchanged' | 'locked' | 'too_large' | 'empty';
|
|
12
|
+
documentId?: string;
|
|
13
|
+
version?: number;
|
|
14
|
+
result?: 'ok' | 'failed';
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PublishResult {
|
|
19
|
+
plan: PublishEntry[];
|
|
20
|
+
skippedHidden: number;
|
|
21
|
+
folder: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function walkDirectory(dir: string, opts?: { includeHidden?: boolean }): Promise<{ files: { abs: string; rel: string; size: number }[]; skipped: number }>;
|
|
25
|
+
export function planPublish(local: Omit<PublishEntry, 'action'>[], remote: Pick<CoinPayDocument, 'id' | 'folder' | 'filename' | 'sha256' | 'status' | 'signed'>[]): PublishEntry[];
|
|
26
|
+
export function publishDirectory(
|
|
27
|
+
client: CoinPayClient,
|
|
28
|
+
opts: { businessId: string; vault?: UnlockedVault; dir: string; folder?: string; includeHidden?: boolean; dryRun?: boolean; concurrency?: number; onFile?: (entry: PublishEntry) => void }
|
|
29
|
+
): Promise<PublishResult>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish a local directory to a business's documents: every file is
|
|
3
|
+
* encrypted on this machine (see documents.js) and filed under the same
|
|
4
|
+
* folder structure. Publishing again syncs: new files are uploaded, changed
|
|
5
|
+
* files become a new version, unchanged ones are skipped, and documents that
|
|
6
|
+
* are already signed are never touched. Nothing remote is ever deleted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
10
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
isSkippedPath,
|
|
13
|
+
listDocuments,
|
|
14
|
+
MAX_DOCUMENT_BYTES,
|
|
15
|
+
normalizeFolder,
|
|
16
|
+
sha256Hex,
|
|
17
|
+
uploadDocument,
|
|
18
|
+
uploadDocumentVersion,
|
|
19
|
+
} from './documents.js';
|
|
20
|
+
|
|
21
|
+
/** Every file under `dir`, with its path relative to `dir` ('/'-separated). Symlinks are not followed. */
|
|
22
|
+
export async function walkDirectory(dir, { includeHidden = false } = {}) {
|
|
23
|
+
const root = resolve(dir);
|
|
24
|
+
const out = [];
|
|
25
|
+
let skipped = 0;
|
|
26
|
+
const visit = async (abs) => {
|
|
27
|
+
for (const entry of await readdir(abs, { withFileTypes: true })) {
|
|
28
|
+
const child = join(abs, entry.name);
|
|
29
|
+
const rel = relative(root, child).split(sep).join('/');
|
|
30
|
+
if (!includeHidden && isSkippedPath(rel)) {
|
|
31
|
+
skipped++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (entry.isDirectory()) await visit(child);
|
|
35
|
+
else if (entry.isFile()) out.push({ abs: child, rel, size: (await stat(child)).size });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
await visit(root);
|
|
39
|
+
out.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
40
|
+
return { files: out, skipped };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const key = (folder, filename) => `${folder ?? ''}\u0000${filename}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide what to do with each local file. `remote` is listDocuments() output
|
|
47
|
+
* (newest first, so the first match for a path wins). Pure: no I/O.
|
|
48
|
+
*
|
|
49
|
+
* Actions: upload | version | unchanged | locked (signed, completed, declined
|
|
50
|
+
* or voided remotely) | too_large | empty.
|
|
51
|
+
*/
|
|
52
|
+
export function planPublish(local, remote) {
|
|
53
|
+
const byPath = new Map();
|
|
54
|
+
for (const d of remote) if (!byPath.has(key(d.folder, d.filename))) byPath.set(key(d.folder, d.filename), d);
|
|
55
|
+
return local.map((f) => {
|
|
56
|
+
if (f.size === 0) return { ...f, action: 'empty' };
|
|
57
|
+
if (f.size > MAX_DOCUMENT_BYTES) return { ...f, action: 'too_large' };
|
|
58
|
+
const existing = byPath.get(key(f.folder, f.filename));
|
|
59
|
+
if (!existing) return { ...f, action: 'upload' };
|
|
60
|
+
if (existing.sha256 === f.sha256) return { ...f, action: 'unchanged', documentId: existing.id };
|
|
61
|
+
const locked = ['completed', 'declined', 'voided'].includes(existing.status) || (existing.signed ?? 0) > 0;
|
|
62
|
+
return { ...f, action: locked ? 'locked' : 'version', documentId: existing.id, status: existing.status };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Publish `dir`. `folder` is where its contents go: by default a folder named
|
|
68
|
+
* like the directory (as a desktop copy would); pass '' or '/' for the top level.
|
|
69
|
+
*/
|
|
70
|
+
export async function publishDirectory(client, { businessId, vault, dir, folder, includeHidden = false, dryRun = false, concurrency = 3, onFile } = {}) {
|
|
71
|
+
if (!businessId) throw new Error('businessId is required');
|
|
72
|
+
const prefix = normalizeFolder(folder === undefined ? basename(resolve(dir)) : folder);
|
|
73
|
+
const { files, skipped } = await walkDirectory(dir, { includeHidden });
|
|
74
|
+
|
|
75
|
+
const local = [];
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
const parts = f.rel.split('/');
|
|
78
|
+
const filename = parts.pop();
|
|
79
|
+
const entry = { ...f, filename, folder: normalizeFolder([prefix ?? '', ...parts].join('/')) };
|
|
80
|
+
if (f.size > 0 && f.size <= MAX_DOCUMENT_BYTES) entry.sha256 = await sha256Hex(await readFile(f.abs));
|
|
81
|
+
local.push(entry);
|
|
82
|
+
}
|
|
83
|
+
const plan = planPublish(local, await listDocuments(client, { businessId }));
|
|
84
|
+
if (dryRun) return { plan, skippedHidden: skipped, folder: prefix };
|
|
85
|
+
|
|
86
|
+
let next = 0;
|
|
87
|
+
const work = plan.filter((p) => p.action === 'upload' || p.action === 'version');
|
|
88
|
+
const worker = async () => {
|
|
89
|
+
while (next < work.length) {
|
|
90
|
+
const p = work[next++];
|
|
91
|
+
try {
|
|
92
|
+
const bytes = await readFile(p.abs);
|
|
93
|
+
if (p.action === 'upload') {
|
|
94
|
+
const doc = await uploadDocument(client, { businessId, vault, bytes, filename: p.filename, folder: p.folder });
|
|
95
|
+
p.documentId = doc.id;
|
|
96
|
+
} else {
|
|
97
|
+
const result = await uploadDocumentVersion(client, p.documentId, { vault, bytes, filename: p.filename, note: 'published from ' + p.rel });
|
|
98
|
+
p.version = result.version;
|
|
99
|
+
}
|
|
100
|
+
p.result = 'ok';
|
|
101
|
+
} catch (err) {
|
|
102
|
+
p.result = 'failed';
|
|
103
|
+
p.error = err.message;
|
|
104
|
+
}
|
|
105
|
+
onFile?.(p);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, work.length) }, worker));
|
|
109
|
+
return { plan, skippedHidden: skipped, folder: prefix };
|
|
110
|
+
}
|
package/src/documents.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface CoinPayDocument {
|
|
|
5
5
|
business_id: string;
|
|
6
6
|
title: string;
|
|
7
7
|
message: string | null;
|
|
8
|
+
folder: string | null;
|
|
8
9
|
filename: string;
|
|
9
10
|
content_type: string;
|
|
10
11
|
bytes: number;
|
|
@@ -62,12 +63,15 @@ export interface UnlockedVault {
|
|
|
62
63
|
version: number;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export function listDocuments(client: CoinPayClient, opts?: { businessId?: string; status?: string }): Promise<CoinPayDocument[]>;
|
|
66
|
+
export function listDocuments(client: CoinPayClient, opts?: { businessId?: string; status?: string; folder?: string }): Promise<CoinPayDocument[]>;
|
|
67
|
+
export function moveDocument(client: CoinPayClient, documentId: string, folder: string | null): Promise<{ folder: string | null }>;
|
|
68
|
+
export function normalizeFolder(input: string | null | undefined): string | null;
|
|
69
|
+
export function contentTypeFor(filename: string): string;
|
|
66
70
|
export function getDocument(client: CoinPayClient, documentId: string): Promise<{ document: CoinPayDocument & { signed_available: boolean }; recipients: DocumentRecipient[]; events: unknown[]; versions: DocumentVersion[]; certificate: unknown }>;
|
|
67
71
|
export function listDocumentVaults(client: CoinPayClient, opts?: { businessId?: string }): Promise<DocumentVault[]>;
|
|
68
72
|
export function setupDocumentsVault(client: CoinPayClient, opts: { businessId: string; passphrase: string }): Promise<UnlockedVault>;
|
|
69
73
|
export function unlockDocumentsVault(client: CoinPayClient, opts: { businessId: string; passphrase: string }): Promise<UnlockedVault>;
|
|
70
|
-
export function uploadDocument(client: CoinPayClient, opts: { businessId: string; vault: UnlockedVault; bytes: Uint8Array; filename: string; title?: string; message?: string }): Promise<CoinPayDocument>;
|
|
74
|
+
export function uploadDocument(client: CoinPayClient, opts: { businessId: string; vault: UnlockedVault; bytes: Uint8Array; filename: string; title?: string; message?: string; folder?: string | null }): Promise<CoinPayDocument>;
|
|
71
75
|
export function shareDocument(
|
|
72
76
|
client: CoinPayClient,
|
|
73
77
|
documentId: string,
|
package/src/documents.js
CHANGED
|
@@ -38,6 +38,37 @@ export const DOCUMENT_TYPES = {
|
|
|
38
38
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
export const GENERIC_CONTENT_TYPE = 'application/octet-stream';
|
|
42
|
+
export const MAX_DOCUMENT_BYTES = 9 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
/** Known types preview in the browser; anything else is an opaque download (it is ciphertext to the server). */
|
|
45
|
+
export function contentTypeFor(filename) {
|
|
46
|
+
const name = String(filename);
|
|
47
|
+
const ext = name.includes('.') ? name.toLowerCase().split('.').pop() : '';
|
|
48
|
+
return DOCUMENT_TYPES[ext] ?? GENERIC_CONTENT_TYPE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Same rules as the web app (src/lib/documents/types.ts): '/'-joined, no '.', '..' refused, null = top level. */
|
|
52
|
+
export function normalizeFolder(input) {
|
|
53
|
+
if (input == null) return null;
|
|
54
|
+
const segments = String(input)
|
|
55
|
+
.split(/[\\/]+/)
|
|
56
|
+
// eslint-disable-next-line no-control-regex
|
|
57
|
+
.map((s) => s.replace(/[\u0000-\u001f\u007f]/g, '').trim())
|
|
58
|
+
.filter((s) => s && s !== '.');
|
|
59
|
+
if (segments.some((s) => s === '..')) throw new Error('Folder paths cannot contain ".."');
|
|
60
|
+
if (segments.length > 20) throw new Error('Folders can be at most 20 levels deep');
|
|
61
|
+
const joined = segments.map((s) => s.slice(0, 100)).join('/');
|
|
62
|
+
if (joined.length > 1000) throw new Error('Folder path is too long');
|
|
63
|
+
return joined || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isSkippedPath(relativePath) {
|
|
67
|
+
return String(relativePath)
|
|
68
|
+
.split(/[\\/]+/)
|
|
69
|
+
.some((s) => (s.startsWith('.') && s !== '.' && s !== '..') || s === 'node_modules' || s === 'Thumbs.db' || s === 'desktop.ini');
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
export const AAD = {
|
|
42
73
|
original: (documentId) => `coinpay-doc:original:${documentId}`,
|
|
43
74
|
version: (documentId, n) => (n === 1 ? `coinpay-doc:original:${documentId}` : `coinpay-doc:original:${documentId}:v${n}`),
|
|
@@ -122,8 +153,8 @@ export function linkWithKey(url, documentKey) {
|
|
|
122
153
|
// API
|
|
123
154
|
// ---------------------------------------------------------------------------
|
|
124
155
|
|
|
125
|
-
export async function listDocuments(client, { businessId, status } = {}) {
|
|
126
|
-
const data = await client.request('/documents' + q({ business_id: businessId, status }));
|
|
156
|
+
export async function listDocuments(client, { businessId, status, folder } = {}) {
|
|
157
|
+
const data = await client.request('/documents' + q({ business_id: businessId, status, folder }));
|
|
127
158
|
return data.documents ?? [];
|
|
128
159
|
}
|
|
129
160
|
|
|
@@ -168,10 +199,8 @@ async function documentKey(client, doc, vault) {
|
|
|
168
199
|
}
|
|
169
200
|
|
|
170
201
|
/** Encrypt and upload. `bytes` is the file content; `filename` decides its type. */
|
|
171
|
-
export async function uploadDocument(client, { businessId, vault, bytes, filename, title, message }) {
|
|
172
|
-
const
|
|
173
|
-
const contentType = DOCUMENT_TYPES[ext];
|
|
174
|
-
if (!contentType) throw new Error('Unsupported file type: use ' + Object.keys(DOCUMENT_TYPES).join(', '));
|
|
202
|
+
export async function uploadDocument(client, { businessId, vault, bytes, filename, title, message, folder }) {
|
|
203
|
+
const contentType = contentTypeFor(filename);
|
|
175
204
|
const plain = new Uint8Array(bytes);
|
|
176
205
|
const docId = globalThis.crypto.randomUUID();
|
|
177
206
|
const dk = newKey();
|
|
@@ -180,6 +209,7 @@ export async function uploadDocument(client, { businessId, vault, bytes, filenam
|
|
|
180
209
|
business_id: businessId,
|
|
181
210
|
title: title || String(filename).replace(/\.[A-Za-z0-9]{1,5}$/, ''),
|
|
182
211
|
message: message || null,
|
|
212
|
+
folder: normalizeFolder(folder),
|
|
183
213
|
filename: String(filename).split(/[\\/]/).pop(),
|
|
184
214
|
content_type: contentType,
|
|
185
215
|
bytes: plain.length,
|
|
@@ -230,6 +260,11 @@ export async function voidDocument(client, documentId, { reason } = {}) {
|
|
|
230
260
|
return client.request('/documents/' + id(documentId), json('PATCH', { action: 'void', ...(reason ? { reason } : {}) }));
|
|
231
261
|
}
|
|
232
262
|
|
|
263
|
+
/** File a document under another folder (null = top level). */
|
|
264
|
+
export async function moveDocument(client, documentId, folder) {
|
|
265
|
+
return client.request('/documents/' + id(documentId), json('PATCH', { action: 'move', folder: normalizeFolder(folder) }));
|
|
266
|
+
}
|
|
267
|
+
|
|
233
268
|
export async function deleteDocument(client, documentId) {
|
|
234
269
|
return client.request('/documents/' + id(documentId), { method: 'DELETE' });
|
|
235
270
|
}
|
|
@@ -246,9 +281,7 @@ export async function listDocumentVersions(client, documentId) {
|
|
|
246
281
|
*/
|
|
247
282
|
export async function uploadDocumentVersion(client, documentId, { vault, bytes, filename, note }) {
|
|
248
283
|
const doc = (await getDocument(client, documentId)).document;
|
|
249
|
-
const
|
|
250
|
-
const contentType = DOCUMENT_TYPES[ext];
|
|
251
|
-
if (!contentType) throw new Error('Unsupported file type: use ' + Object.keys(DOCUMENT_TYPES).join(', '));
|
|
284
|
+
const contentType = contentTypeFor(filename);
|
|
252
285
|
const dk = await documentKey(client, doc, vault);
|
|
253
286
|
const plain = new Uint8Array(bytes);
|
|
254
287
|
const next = doc.current_version + 1;
|
package/src/index.d.ts
CHANGED
|
@@ -129,7 +129,10 @@ export {
|
|
|
129
129
|
downloadDocument,
|
|
130
130
|
listDocumentVersions,
|
|
131
131
|
uploadDocumentVersion,
|
|
132
|
+
moveDocument,
|
|
132
133
|
} from './documents.js';
|
|
134
|
+
export { publishDirectory, planPublish } from './documents-publish.js';
|
|
135
|
+
export type { PublishEntry, PublishResult } from './documents-publish.js';
|
|
133
136
|
export type { CoinPayDocument, DocumentRecipient, DocumentVault, DocumentVersion, UnlockedVault } from './documents.js';
|
|
134
137
|
|
|
135
138
|
// Webhook exports
|
package/src/index.js
CHANGED
|
@@ -143,7 +143,9 @@ import {
|
|
|
143
143
|
downloadDocument,
|
|
144
144
|
listDocumentVersions,
|
|
145
145
|
uploadDocumentVersion,
|
|
146
|
+
moveDocument,
|
|
146
147
|
} from './documents.js';
|
|
148
|
+
import { publishDirectory, planPublish } from './documents-publish.js';
|
|
147
149
|
|
|
148
150
|
// Wallet exports
|
|
149
151
|
import {
|
|
@@ -343,6 +345,9 @@ export {
|
|
|
343
345
|
downloadDocument,
|
|
344
346
|
listDocumentVersions,
|
|
345
347
|
uploadDocumentVersion,
|
|
348
|
+
moveDocument,
|
|
349
|
+
publishDirectory,
|
|
350
|
+
planPublish,
|
|
346
351
|
|
|
347
352
|
// Wallet
|
|
348
353
|
WalletClient,
|