@defdone/annon 0.0.0-stage → 0.1.0-alpha.1
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 +63 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +591 -2
- package/dist/api/annon-types.d.ts +108 -0
- package/dist/api/annon-types.js +2 -0
- package/dist/api/annon.d.ts +117 -0
- package/dist/api/annon.js +1152 -0
- package/dist/api/hosted-record.d.ts +13 -0
- package/dist/api/hosted-record.js +149 -0
- package/dist/api/sharing-types.d.ts +109 -0
- package/dist/api/sharing-types.js +1 -0
- package/dist/api/sharing.d.ts +36 -0
- package/dist/api/sharing.js +879 -0
- package/dist/browser/download.d.ts +19 -0
- package/dist/browser/download.js +52 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/login-bundle.js +151 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +216 -0
- package/dist/browser/purchase-bundle.js +84 -0
- package/dist/browser/purchases.d.ts +98 -0
- package/dist/browser/purchases.js +735 -0
- package/dist/browser/record.d.ts +55 -0
- package/dist/browser/record.js +16 -0
- package/dist/browser/selling.d.ts +146 -0
- package/dist/browser/selling.js +418 -0
- package/dist/browser/wallets.d.ts +53 -0
- package/dist/browser/wallets.js +286 -0
- package/dist/bytes.d.ts +4 -0
- package/dist/bytes.js +16 -0
- package/dist/download/download.d.ts +59 -0
- package/dist/download/download.js +384 -0
- package/dist/download/host.d.ts +56 -0
- package/dist/download/host.js +161 -0
- package/dist/download/http.d.ts +23 -0
- package/dist/download/http.js +87 -0
- package/dist/download/proof.d.ts +20 -0
- package/dist/download/proof.js +79 -0
- package/dist/download/upload.d.ts +130 -0
- package/dist/download/upload.js +408 -0
- package/dist/envelope/evm.d.ts +4 -0
- package/dist/envelope/evm.js +32 -0
- package/dist/envelope/pay2.d.ts +176 -0
- package/dist/envelope/pay2.js +541 -0
- package/dist/envelope/replay.d.ts +45 -0
- package/dist/envelope/replay.js +186 -0
- package/dist/envelope/stream.d.ts +78 -0
- package/dist/envelope/stream.js +372 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +300 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/keys/near-ckd.d.ts +47 -0
- package/dist/keys/near-ckd.js +96 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.js +4 -0
- package/dist/near/nep413.d.ts +22 -0
- package/dist/near/nep413.js +39 -0
- package/dist/near/rpc.d.ts +170 -0
- package/dist/near/rpc.js +423 -0
- package/dist/near/texts.d.ts +39 -0
- package/dist/near/texts.js +60 -0
- package/dist/net/ip.d.ts +7 -0
- package/dist/net/ip.js +62 -0
- package/dist/net/retry.d.ts +3 -0
- package/dist/net/retry.js +31 -0
- package/dist/node/adapter-checks.d.ts +6 -0
- package/dist/node/adapter-checks.js +13 -0
- package/dist/node/adapter.d.ts +8 -0
- package/dist/node/adapter.js +98 -0
- package/dist/node/agent.d.ts +49 -0
- package/dist/node/agent.js +266 -0
- package/dist/node/amounts.d.ts +2 -0
- package/dist/node/amounts.js +20 -0
- package/dist/node/browser-login.d.ts +25 -0
- package/dist/node/browser-login.js +219 -0
- package/dist/node/budget.d.ts +24 -0
- package/dist/node/budget.js +59 -0
- package/dist/node/buy-commands.d.ts +24 -0
- package/dist/node/buy-commands.js +301 -0
- package/dist/node/cli.d.ts +60 -0
- package/dist/node/cli.js +201 -0
- package/dist/node/command-context.d.ts +82 -0
- package/dist/node/command-context.js +77 -0
- package/dist/node/config.d.ts +37 -0
- package/dist/node/config.js +189 -0
- package/dist/node/core-adapter.d.ts +43 -0
- package/dist/node/core-adapter.js +122 -0
- package/dist/node/errors.d.ts +8 -0
- package/dist/node/errors.js +12 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +5 -0
- package/dist/node/io.d.ts +13 -0
- package/dist/node/io.js +112 -0
- package/dist/node/keychain.d.ts +10 -0
- package/dist/node/keychain.js +67 -0
- package/dist/node/main.d.ts +2 -0
- package/dist/node/main.js +21 -0
- package/dist/node/offers.d.ts +16 -0
- package/dist/node/offers.js +91 -0
- package/dist/node/origin.d.ts +11 -0
- package/dist/node/origin.js +80 -0
- package/dist/node/paths.d.ts +3 -0
- package/dist/node/paths.js +71 -0
- package/dist/node/payment-terms.d.ts +26 -0
- package/dist/node/payment-terms.js +127 -0
- package/dist/node/purchase-commands.d.ts +16 -0
- package/dist/node/purchase-commands.js +237 -0
- package/dist/node/purchase-guard.d.ts +19 -0
- package/dist/node/purchase-guard.js +53 -0
- package/dist/node/relay-transport.d.ts +6 -0
- package/dist/node/relay-transport.js +173 -0
- package/dist/node/resources.d.ts +2 -0
- package/dist/node/resources.js +26 -0
- package/dist/node/retry-gate.d.ts +15 -0
- package/dist/node/retry-gate.js +53 -0
- package/dist/node/seller-commands.d.ts +12 -0
- package/dist/node/seller-commands.js +207 -0
- package/dist/node/sharing.d.ts +3 -0
- package/dist/node/sharing.js +28 -0
- package/dist/node/shops.d.ts +17 -0
- package/dist/node/shops.js +74 -0
- package/dist/node/signer-mock.d.ts +11 -0
- package/dist/node/signer-mock.js +79 -0
- package/dist/node/signer-wallet.d.ts +8 -0
- package/dist/node/signer-wallet.js +149 -0
- package/dist/node/signing.d.ts +29 -0
- package/dist/node/signing.js +200 -0
- package/dist/node/storage-commands.d.ts +11 -0
- package/dist/node/storage-commands.js +50 -0
- package/dist/node/sync-dir.d.ts +1 -0
- package/dist/node/sync-dir.js +11 -0
- package/dist/node/wallets.d.ts +13 -0
- package/dist/node/wallets.js +146 -0
- package/dist/oneclick/oneclick.d.ts +132 -0
- package/dist/oneclick/oneclick.js +342 -0
- package/dist/pay/backup.d.ts +70 -0
- package/dist/pay/backup.js +402 -0
- package/dist/pay/base.d.ts +135 -0
- package/dist/pay/base.js +386 -0
- package/dist/pay/eip3009.d.ts +59 -0
- package/dist/pay/eip3009.js +72 -0
- package/dist/pay/pay.d.ts +86 -0
- package/dist/pay/pay.js +297 -0
- package/dist/pay/purchase.d.ts +103 -0
- package/dist/pay/purchase.js +20 -0
- package/dist/pay/recover.d.ts +31 -0
- package/dist/pay/recover.js +34 -0
- package/dist/pay/seller.d.ts +130 -0
- package/dist/pay/seller.js +444 -0
- package/dist/pay/settle.d.ts +96 -0
- package/dist/pay/settle.js +308 -0
- package/dist/pay/split.d.ts +115 -0
- package/dist/pay/split.js +256 -0
- package/dist/pay/store-node.d.ts +8 -0
- package/dist/pay/store-node.js +224 -0
- package/dist/pay/store.d.ts +54 -0
- package/dist/pay/store.js +135 -0
- package/dist/pay/x402.d.ts +95 -0
- package/dist/pay/x402.js +136 -0
- package/dist/relay/admission.d.ts +8 -0
- package/dist/relay/admission.js +58 -0
- package/dist/relay/capabilities.d.ts +36 -0
- package/dist/relay/capabilities.js +232 -0
- package/dist/relay/fees.d.ts +17 -0
- package/dist/relay/fees.js +105 -0
- package/dist/relay/index.d.ts +10 -0
- package/dist/relay/index.js +8 -0
- package/dist/relay/keeper-balance.d.ts +17 -0
- package/dist/relay/keeper-balance.js +46 -0
- package/dist/relay/keeper-runner.d.ts +114 -0
- package/dist/relay/keeper-runner.js +703 -0
- package/dist/relay/keeper.d.ts +38 -0
- package/dist/relay/keeper.js +73 -0
- package/dist/relay/ledger.d.ts +155 -0
- package/dist/relay/ledger.js +392 -0
- package/dist/relay/relay-checks.d.ts +3 -0
- package/dist/relay/relay-checks.js +244 -0
- package/dist/relay/relay-reconciliation.d.ts +15 -0
- package/dist/relay/relay-reconciliation.js +293 -0
- package/dist/relay/relay-request.d.ts +6 -0
- package/dist/relay/relay-request.js +139 -0
- package/dist/relay/relay-transactions.d.ts +23 -0
- package/dist/relay/relay-transactions.js +182 -0
- package/dist/relay/relay-types.d.ts +128 -0
- package/dist/relay/relay-types.js +17 -0
- package/dist/relay/relay.d.ts +102 -0
- package/dist/relay/relay.js +426 -0
- package/dist/relay/reservation.d.ts +29 -0
- package/dist/relay/reservation.js +132 -0
- package/dist/relay/sponsorship.d.ts +42 -0
- package/dist/relay/sponsorship.js +184 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +1 -0
- package/dist/settle-near/codec.d.ts +17 -0
- package/dist/settle-near/codec.js +86 -0
- package/dist/settle-near/identity.d.ts +27 -0
- package/dist/settle-near/identity.js +80 -0
- package/dist/settle-near/purchase.d.ts +40 -0
- package/dist/settle-near/purchase.js +153 -0
- package/dist/settle-near/terms.d.ts +37 -0
- package/dist/settle-near/terms.js +89 -0
- package/dist/shops/discovery.d.ts +95 -0
- package/dist/shops/discovery.js +384 -0
- package/dist/wallet/account-only.d.ts +11 -0
- package/dist/wallet/account-only.js +6 -0
- package/dist/wallet/uncertain.d.ts +8 -0
- package/dist/wallet/uncertain.js +13 -0
- package/dist/wallet/wallet.d.ts +100 -0
- package/dist/wallet/wallet.js +204 -0
- package/package.json +136 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { AdapterError } from './errors.js';
|
|
4
|
+
/** Canonical paths are passed to I/O; tool-relative paths start at the operator directory. */
|
|
5
|
+
export async function confinePaths(input, workDir) {
|
|
6
|
+
if (!['sell', 'share', 'open', 'verify'].includes(input.command))
|
|
7
|
+
return input;
|
|
8
|
+
if (input.command === 'verify' && /^https?:/.test(input.target ?? ''))
|
|
9
|
+
return input;
|
|
10
|
+
try {
|
|
11
|
+
const root = await realpath(workDir);
|
|
12
|
+
const inside = (path) => {
|
|
13
|
+
const tail = relative(root, path);
|
|
14
|
+
if (tail === '..' ||
|
|
15
|
+
tail.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
|
|
16
|
+
isAbsolute(tail))
|
|
17
|
+
throw new Error();
|
|
18
|
+
return path;
|
|
19
|
+
};
|
|
20
|
+
const confined = async (value, output = false) => {
|
|
21
|
+
if (!value || value.includes('\0') || value.split(/[\\/]/).includes('..'))
|
|
22
|
+
throw new Error();
|
|
23
|
+
const path = resolve(root, value);
|
|
24
|
+
try {
|
|
25
|
+
return inside(await realpath(path));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (!output || error.code !== 'ENOENT')
|
|
29
|
+
throw error;
|
|
30
|
+
// A dangling symlink is an existing entry, never a new output path.
|
|
31
|
+
const entry = await lstat(path).catch((error) => {
|
|
32
|
+
if (error.code !== 'ENOENT')
|
|
33
|
+
throw error;
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
if (entry)
|
|
37
|
+
throw new Error();
|
|
38
|
+
return inside(resolve(await realpath(dirname(path)), basename(path)));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const next = { ...input };
|
|
42
|
+
if (input.command === 'share') {
|
|
43
|
+
if (!input.target)
|
|
44
|
+
throw new Error();
|
|
45
|
+
next.target = await confined(input.target);
|
|
46
|
+
}
|
|
47
|
+
else if (input.command === 'sell') {
|
|
48
|
+
if (!input.target)
|
|
49
|
+
throw new Error();
|
|
50
|
+
next.target = await confined(input.target);
|
|
51
|
+
next.output = await confined(input.output ?? `${input.target}.pay2`, true);
|
|
52
|
+
}
|
|
53
|
+
else if (input.command === 'open') {
|
|
54
|
+
const isId = (value) => /^0x[0-9a-f]{64}$/.test(value);
|
|
55
|
+
if (input.target !== undefined && !isId(input.target))
|
|
56
|
+
next.target = await confined(input.target);
|
|
57
|
+
if (input.purchase !== undefined && !isId(input.purchase))
|
|
58
|
+
next.purchase = await confined(input.purchase);
|
|
59
|
+
if (input.output !== undefined)
|
|
60
|
+
next.output = await confined(input.output, true);
|
|
61
|
+
if (input.file !== undefined)
|
|
62
|
+
next.file = await confined(input.file);
|
|
63
|
+
}
|
|
64
|
+
else if (input.target !== undefined)
|
|
65
|
+
next.target = await confined(input.target);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new AdapterError('path_not_allowed', 'File paths must stay inside the configured workDir without parent traversal or symlink escape.', 2);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Config } from '../near/rpc.js';
|
|
2
|
+
import type { OneClickQuote } from '../oneclick/oneclick.js';
|
|
3
|
+
import type { PaymentSummary, PurchaseRecord, SourceAsset, SourcePayment } from '../pay/purchase.js';
|
|
4
|
+
import { type NearTerms } from '../settle-near/terms.js';
|
|
5
|
+
type PaymentConfig = Pick<Config, 'network' | 'nearUsdc' | 'usdc'>;
|
|
6
|
+
export declare function validatePaymentToken(t: {
|
|
7
|
+
network: string;
|
|
8
|
+
route: string;
|
|
9
|
+
token: string | undefined;
|
|
10
|
+
}, config?: PaymentConfig): void;
|
|
11
|
+
export type { SourceAsset, SourcePayment } from '../pay/purchase.js';
|
|
12
|
+
/** Project only public facts from verified, encrypted purchase state, from required payment summaries. */
|
|
13
|
+
export declare function savedPayment(record: PurchaseRecord, source?: SourcePayment | undefined): PaymentSummary;
|
|
14
|
+
export declare function quotedSource(quote: OneClickQuote, asset: SourceAsset): SourcePayment;
|
|
15
|
+
export declare function humanPaymentTerms(t: {
|
|
16
|
+
file: string;
|
|
17
|
+
resource: string;
|
|
18
|
+
network: string;
|
|
19
|
+
route: string;
|
|
20
|
+
token: string | undefined;
|
|
21
|
+
price: string;
|
|
22
|
+
recipients: NearTerms['recipients'];
|
|
23
|
+
remainder: number;
|
|
24
|
+
baseRecipient?: string | undefined;
|
|
25
|
+
source?: SourcePayment;
|
|
26
|
+
}, config?: PaymentConfig): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { usdc } from '../settle-near/codec.js';
|
|
2
|
+
import { BASE_USDC, NEAR_USDC } from '../settle-near/terms.js';
|
|
3
|
+
import { AdapterError } from './errors.js';
|
|
4
|
+
export function validatePaymentToken(t, config) {
|
|
5
|
+
const deployment = config?.network ?? (t.route === 'base' ? 'near:mainnet' : t.network);
|
|
6
|
+
const expected = t.route === 'base' && t.network === 'eip155:8453'
|
|
7
|
+
? (config?.usdc ?? (deployment === 'near:mainnet' ? BASE_USDC : undefined))
|
|
8
|
+
: (t.route === 'near' || t.route === '1click') && t.network === deployment
|
|
9
|
+
? (config?.nearUsdc ?? (deployment === 'near:mainnet' ? NEAR_USDC : undefined))
|
|
10
|
+
: undefined;
|
|
11
|
+
const canonical = deployment === 'near:mainnet' ? (t.route === 'base' ? BASE_USDC : NEAR_USDC) : expected;
|
|
12
|
+
if (!['near:mainnet', 'near:testnet', 'near:sandbox'].includes(deployment) ||
|
|
13
|
+
!expected ||
|
|
14
|
+
t.token !== expected ||
|
|
15
|
+
t.token !== canonical)
|
|
16
|
+
throw new AdapterError('network_mismatch', 'Payment requires the canonical USDC token for this network.', 2);
|
|
17
|
+
}
|
|
18
|
+
/** Saved summaries are optional metadata. Discard a malformed source rather than fail a purchase. */
|
|
19
|
+
function validSource(value) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const s = value;
|
|
23
|
+
const text = (value, max) => typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
24
|
+
const identifier = (value, max = 128) => text(value, max) && /^[A-Za-z0-9._:-]+$/.test(value);
|
|
25
|
+
return (identifier(s.chain) &&
|
|
26
|
+
identifier(s.asset) &&
|
|
27
|
+
(s.symbol === undefined || identifier(s.symbol, 32)) &&
|
|
28
|
+
(s.contractAddress === undefined || identifier(s.contractAddress)) &&
|
|
29
|
+
text(s.amount, 39) &&
|
|
30
|
+
/^[1-9][0-9]*$/.test(s.amount) &&
|
|
31
|
+
BigInt(s.amount) < 2n ** 128n &&
|
|
32
|
+
(s.decimals === undefined ||
|
|
33
|
+
(typeof s.decimals === 'number' &&
|
|
34
|
+
Number.isInteger(s.decimals) &&
|
|
35
|
+
s.decimals >= 0 &&
|
|
36
|
+
s.decimals <= 255)) &&
|
|
37
|
+
(s.amountFormatted === undefined ||
|
|
38
|
+
(text(s.amountFormatted, 300) && /^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(s.amountFormatted))) &&
|
|
39
|
+
text(s.depositBefore, 64) &&
|
|
40
|
+
Number.isFinite(Date.parse(s.depositBefore)));
|
|
41
|
+
}
|
|
42
|
+
/** Project only public facts from verified, encrypted purchase state, from required payment summaries. */
|
|
43
|
+
export function savedPayment(record, source = record.payment.source) {
|
|
44
|
+
const route = record.payment.route;
|
|
45
|
+
return {
|
|
46
|
+
amountUsdc: usdc(BigInt(record.terms.price)),
|
|
47
|
+
amountBaseUnits: record.terms.price,
|
|
48
|
+
asset: { symbol: 'USDC', id: record.terms.usdc },
|
|
49
|
+
network: record.terms.network,
|
|
50
|
+
route,
|
|
51
|
+
...(route === '1click' && validSource(source)
|
|
52
|
+
? {
|
|
53
|
+
source: {
|
|
54
|
+
chain: source.chain,
|
|
55
|
+
asset: source.asset,
|
|
56
|
+
amount: source.amount,
|
|
57
|
+
depositBefore: source.depositBefore,
|
|
58
|
+
...(source.symbol !== undefined ? { symbol: source.symbol } : {}),
|
|
59
|
+
...(source.decimals !== undefined ? { decimals: source.decimals } : {}),
|
|
60
|
+
...(source.contractAddress !== undefined
|
|
61
|
+
? { contractAddress: source.contractAddress }
|
|
62
|
+
: {}),
|
|
63
|
+
...(source.amountFormatted !== undefined
|
|
64
|
+
? { amountFormatted: source.amountFormatted }
|
|
65
|
+
: {}),
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function quotedSource(quote, asset) {
|
|
72
|
+
const amount = quote.deposit.amount;
|
|
73
|
+
const decimals = asset.decimals;
|
|
74
|
+
const q = quote.response.quote;
|
|
75
|
+
let formatted;
|
|
76
|
+
if (decimals !== undefined) {
|
|
77
|
+
const digits = BigInt(amount)
|
|
78
|
+
.toString()
|
|
79
|
+
.padStart(decimals + 1, '0');
|
|
80
|
+
const fraction = decimals === 0 ? '' : digits.slice(-decimals).replace(/0+$/, '');
|
|
81
|
+
formatted =
|
|
82
|
+
(decimals === 0 ? digits : digits.slice(0, -decimals)) + (fraction ? `.${fraction}` : '');
|
|
83
|
+
}
|
|
84
|
+
else if (typeof q?.amountInFormatted === 'string' &&
|
|
85
|
+
/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(q.amountInFormatted)) {
|
|
86
|
+
formatted = q.amountInFormatted;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...asset,
|
|
90
|
+
amount,
|
|
91
|
+
...(formatted === undefined ? {} : { amountFormatted: formatted }),
|
|
92
|
+
depositBefore: quote.deposit.depositBefore,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function humanPaymentTerms(t, config) {
|
|
96
|
+
validatePaymentToken(t, config);
|
|
97
|
+
const price = BigInt(t.price);
|
|
98
|
+
const amounts = t.recipients.map((r) => (price * BigInt(r.bps)) / 10000n);
|
|
99
|
+
const rest = price - amounts.reduce((sum, n) => sum + n, 0n);
|
|
100
|
+
amounts[t.remainder] = (amounts[t.remainder] ?? 0n) + rest;
|
|
101
|
+
const recipient = (i) => {
|
|
102
|
+
const r = t.recipients[i];
|
|
103
|
+
return r ? `${r.identity.kind}:${r.identity.id}` : '';
|
|
104
|
+
};
|
|
105
|
+
return [
|
|
106
|
+
`File: ${t.file}`,
|
|
107
|
+
`Resource: ${t.resource}`,
|
|
108
|
+
`Network: ${t.network}`,
|
|
109
|
+
`Route: ${t.route}`,
|
|
110
|
+
`Token: USDC on ${t.route === 'base' ? 'Base' : 'NEAR'} (${t.token})`,
|
|
111
|
+
`Price: ${usdc(price)} USDC`,
|
|
112
|
+
...(t.source
|
|
113
|
+
? [
|
|
114
|
+
`Source chain: ${t.source.chain === 'sol' ? 'Solana (sol)' : t.source.chain === 'eth' ? 'Ethereum (eth)' : t.source.chain}`,
|
|
115
|
+
`Source asset: ${t.source.symbol ?? t.source.asset}${t.source.contractAddress ? ` (${t.source.contractAddress})` : ''}`,
|
|
116
|
+
`Source asset ID: ${t.source.asset}`,
|
|
117
|
+
`Source amount: ${t.source.amountFormatted === undefined ? 'human units unavailable' : `${t.source.amountFormatted} ${t.source.symbol ?? 'tokens'}`} (${t.source.amount} base units)`,
|
|
118
|
+
`Deposit before: ${t.source.depositBefore}`,
|
|
119
|
+
]
|
|
120
|
+
: []),
|
|
121
|
+
...t.recipients.map((r, i) => `Recipient: ${recipient(i)} | ${r.bps / 100}% | ${usdc(amounts[i] ?? 0n)} USDC`),
|
|
122
|
+
`Rounding remainder: ${usdc(rest)} USDC to ${recipient(t.remainder)}`,
|
|
123
|
+
...(t.route === 'base' && t.baseRecipient
|
|
124
|
+
? [`Base payment recipient: ${t.baseRecipient}`]
|
|
125
|
+
: []),
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CommandSession, RequestContext } from './command-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* `status`, first step: a purchase already accepted on chain, or the pending answer when no
|
|
4
|
+
* backup secret is configured. Returns nothing when the saved record has to be checked.
|
|
5
|
+
*/
|
|
6
|
+
declare function statusOnChain(ctx: RequestContext): Promise<Record<string, unknown> | undefined>;
|
|
7
|
+
/** `status` from the saved purchase record: accepted, or still pending with a retry delay. */
|
|
8
|
+
declare function status(ctx: CommandSession): Promise<Record<string, unknown>>;
|
|
9
|
+
/** `recover`: purchases found for the recovery wallet, with per-purchase public errors. */
|
|
10
|
+
declare function recover(ctx: CommandSession): Promise<Record<string, unknown>>;
|
|
11
|
+
/**
|
|
12
|
+
* `open`: loads the purchase or recovery record, waits for acceptance, then claims the key and
|
|
13
|
+
* decrypts a local file or a download into the output. Unknown commands end here with usage.
|
|
14
|
+
*/
|
|
15
|
+
declare function open(ctx: CommandSession): Promise<Record<string, unknown>>;
|
|
16
|
+
export { open, recover, status, statusOnChain };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** Buyer commands on an existing purchase: `status`, `recover` and `open`. */
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { download } from '../download/download.js';
|
|
5
|
+
import { inspectHeader } from '../envelope/pay2.js';
|
|
6
|
+
import { getPurchase } from '../near/rpc.js';
|
|
7
|
+
import { encryptedPurchaseStore, encryptedRecoveryStore, importPurchase, importRecovery, } from '../pay/backup.js';
|
|
8
|
+
import { purchaseKey } from '../pay/purchase.js';
|
|
9
|
+
import { fileStore } from '../pay/store-node.js';
|
|
10
|
+
import { bytes, hex } from '../settle-near/codec.js';
|
|
11
|
+
import { backupPath, fail, isId } from './adapter-checks.js';
|
|
12
|
+
import { AdapterError, publicError, usage } from './errors.js';
|
|
13
|
+
import { fileSource, newOutput, withOutput, withSpool } from './io.js';
|
|
14
|
+
import { resourceHeader } from './offers.js';
|
|
15
|
+
import { resourceAllowed } from './resources.js';
|
|
16
|
+
import { retryGate } from './retry-gate.js';
|
|
17
|
+
import { readBackupSecret } from './wallets.js';
|
|
18
|
+
/**
|
|
19
|
+
* `status`, first step: a purchase already accepted on chain, or the pending answer when no
|
|
20
|
+
* backup secret is configured. Returns nothing when the saved record has to be checked.
|
|
21
|
+
*/
|
|
22
|
+
async function statusOnChain(ctx) {
|
|
23
|
+
const { c, d, env, o, paymentDetails, pendingSetup } = ctx;
|
|
24
|
+
if (!o.target || !isId(o.target))
|
|
25
|
+
throw usage();
|
|
26
|
+
if (d.agent)
|
|
27
|
+
await retryGate({ ...c, purchaseId: o.target }).check();
|
|
28
|
+
const p = await (d.purchase ?? getPurchase)(c.nearRpc, c.contractId, o.target);
|
|
29
|
+
if (p)
|
|
30
|
+
return {
|
|
31
|
+
purchaseId: o.target,
|
|
32
|
+
state: 'accepted',
|
|
33
|
+
fileId: p.file_id,
|
|
34
|
+
grants: p.grants,
|
|
35
|
+
next: 'open',
|
|
36
|
+
...(await paymentDetails(async () => encryptedPurchaseStore(fileStore(join(c.stateDir, 'records')), await readBackupSecret(o, env)).load(purchaseKey(o.target)))),
|
|
37
|
+
};
|
|
38
|
+
if (!o.backupSecretFile && !env.ANNON_BACKUP_SECRET_FILE && !env.ANNON_BACKUP_SECRET)
|
|
39
|
+
return pendingSetup(o.target);
|
|
40
|
+
}
|
|
41
|
+
/** `status` from the saved purchase record: accepted, or still pending with a retry delay. */
|
|
42
|
+
async function status(ctx) {
|
|
43
|
+
const { c, d, env, o, paymentDetails, pendingSetup, sdk, wallet } = ctx;
|
|
44
|
+
if (!o.target)
|
|
45
|
+
throw usage();
|
|
46
|
+
let record;
|
|
47
|
+
try {
|
|
48
|
+
record = await sdk.store.load(purchaseKey(o.target));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return pendingSetup(o.target);
|
|
52
|
+
}
|
|
53
|
+
if (!record)
|
|
54
|
+
return pendingSetup(o.target);
|
|
55
|
+
if (o.wallet || env.ANNON_WALLET)
|
|
56
|
+
await wallet();
|
|
57
|
+
let accepted;
|
|
58
|
+
try {
|
|
59
|
+
accepted = await sdk.accept(record ?? undefined, o.target);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const safe = publicError(error);
|
|
63
|
+
if (d.agent && safe.retryAfterSeconds !== undefined)
|
|
64
|
+
await retryGate({ ...c, purchaseId: o.target }).defer(safe.code, safe.retryAfterSeconds);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (d.agent && !accepted)
|
|
68
|
+
await retryGate({ ...c, purchaseId: o.target }).defer('settlement_pending', 30);
|
|
69
|
+
return {
|
|
70
|
+
purchaseId: o.target,
|
|
71
|
+
state: accepted ? 'accepted' : 'pending',
|
|
72
|
+
...(await paymentDetails(async () => record)),
|
|
73
|
+
...(accepted ? {} : { retryAfterSeconds: 30 }),
|
|
74
|
+
next: accepted
|
|
75
|
+
? 'open'
|
|
76
|
+
: 'Check the payment and retry status. 1Click SUCCESS alone is not acceptance.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** `recover`: purchases found for the recovery wallet, with per-purchase public errors. */
|
|
80
|
+
async function recover(ctx) {
|
|
81
|
+
const { c, o, sdk, wallet } = ctx;
|
|
82
|
+
if (!o.recovery || (o.purchase && !isId(o.purchase)))
|
|
83
|
+
throw usage();
|
|
84
|
+
const errors = [];
|
|
85
|
+
const recovered = await sdk.recover({
|
|
86
|
+
onError: (purchaseId, error) => {
|
|
87
|
+
const { code, message } = publicError(error);
|
|
88
|
+
errors.push({ purchaseId, code, message });
|
|
89
|
+
},
|
|
90
|
+
wallet: await wallet(),
|
|
91
|
+
...(o.purchase ? { purchaseId: o.purchase } : {}),
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
errors,
|
|
95
|
+
recovered: recovered.map((r) => ({
|
|
96
|
+
purchaseId: r.purchase_id,
|
|
97
|
+
fileId: r.file_id,
|
|
98
|
+
grant: r.grant,
|
|
99
|
+
backup: backupPath(c, r.id),
|
|
100
|
+
})),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `open`: loads the purchase or recovery record, waits for acceptance, then claims the key and
|
|
105
|
+
* decrypts a local file or a download into the output. Unknown commands end here with usage.
|
|
106
|
+
*/
|
|
107
|
+
async function open(ctx) {
|
|
108
|
+
const { c, d, env, fetcher, o, paymentDetails, rawStore, readListing, resources, sdk, secret, wallet, } = ctx;
|
|
109
|
+
if (o.command !== 'open' || !o.target || !o.output)
|
|
110
|
+
throw usage();
|
|
111
|
+
if (d.agent && o.url && !resourceAllowed(c.allowedResources, o.url))
|
|
112
|
+
return fail('resource_not_allowed', 'The resource URL is outside the configured policy.', 2);
|
|
113
|
+
await newOutput(o.output);
|
|
114
|
+
const ref = o.purchase ?? o.target;
|
|
115
|
+
let record;
|
|
116
|
+
let recovery;
|
|
117
|
+
if (isId(ref)) {
|
|
118
|
+
recovery =
|
|
119
|
+
(await encryptedRecoveryStore(rawStore, secret).load(`annon/recovery/${ref}`)) ?? undefined;
|
|
120
|
+
if (!recovery || recovery.grant === null)
|
|
121
|
+
record = (await sdk.store.load(purchaseKey(ref))) ?? undefined;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const encrypted = await readFile(ref, 'utf8');
|
|
125
|
+
try {
|
|
126
|
+
record = await importPurchase(encrypted, secret);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
recovery = await importRecovery(encrypted, secret);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!record && !recovery)
|
|
133
|
+
return fail('purchase_required', 'Provide a saved purchase ID or encrypted purchase/recovery backup.', 2);
|
|
134
|
+
if (record &&
|
|
135
|
+
(record.purchase.contract !== c.contractId || record.terms.contractId !== c.contractId))
|
|
136
|
+
throw new Error('backup contract mismatch');
|
|
137
|
+
const fileId = recovery?.file_id ?? record?.purchase.file_id;
|
|
138
|
+
if (!fileId)
|
|
139
|
+
throw usage();
|
|
140
|
+
if (o.wallet || env.ANNON_WALLET)
|
|
141
|
+
await wallet();
|
|
142
|
+
const purchaseId = recovery?.purchase_id ?? record?.id;
|
|
143
|
+
if (!purchaseId)
|
|
144
|
+
throw usage();
|
|
145
|
+
const resource = await resources.load(fileId);
|
|
146
|
+
const local = o.file ?? (o.purchase ? o.target : undefined);
|
|
147
|
+
const remoteUrl = o.url ?? resource?.url;
|
|
148
|
+
if (d.agent && !local && remoteUrl && !resourceAllowed(c.allowedResources, remoteUrl))
|
|
149
|
+
return fail('resource_not_allowed', 'The resource URL is outside the configured policy.', 2);
|
|
150
|
+
const settlementGate = retryGate({ ...c, purchaseId });
|
|
151
|
+
const downloadGate = !local && remoteUrl
|
|
152
|
+
? retryGate({ ...c, purchaseId, fileId, host: new URL(remoteUrl).origin })
|
|
153
|
+
: undefined;
|
|
154
|
+
if (d.agent) {
|
|
155
|
+
await settlementGate.check();
|
|
156
|
+
await downloadGate?.check();
|
|
157
|
+
}
|
|
158
|
+
let accepted;
|
|
159
|
+
try {
|
|
160
|
+
accepted = await sdk.accept(record, purchaseId);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const safe = publicError(error);
|
|
164
|
+
if (d.agent && safe.retryAfterSeconds !== undefined)
|
|
165
|
+
await settlementGate.defer(safe.code, safe.retryAfterSeconds);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (!accepted) {
|
|
169
|
+
if (d.agent)
|
|
170
|
+
await settlementGate.defer('settlement_pending', 30);
|
|
171
|
+
throw new AdapterError('settlement_pending', publicError({ code: 'settlement_pending' }).message, 4);
|
|
172
|
+
}
|
|
173
|
+
let readFileSource;
|
|
174
|
+
if (local) {
|
|
175
|
+
const source = await fileSource(local);
|
|
176
|
+
readFileSource = () => source;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const url = o.url ?? resource?.url;
|
|
180
|
+
if (!url)
|
|
181
|
+
return fail('file_required', 'Provide --file or --url to open this saved purchase.', 2);
|
|
182
|
+
if (d.agent && !resourceAllowed(c.allowedResources, url))
|
|
183
|
+
return fail('resource_not_allowed', 'The resource URL is outside the configured policy.', 2);
|
|
184
|
+
const header = resource && resource.contractId === c.contractId
|
|
185
|
+
? bytes(resource.header, (resource.header.length - 2) / 2)
|
|
186
|
+
: await resourceHeader(url, c, fetcher);
|
|
187
|
+
if (record &&
|
|
188
|
+
hex(await (await import('../envelope/pay2.js')).sha256(header)) !== record.checkedHeaderHash)
|
|
189
|
+
throw new Error('header mismatch');
|
|
190
|
+
const { listing } = await readListing(fileId);
|
|
191
|
+
await inspectHeader(header, listing, c.contractId);
|
|
192
|
+
const purchaseId = recovery?.purchase_id ?? record?.id;
|
|
193
|
+
const buyerSecret = recovery?.buyerSecret ?? record?.buyerSecret;
|
|
194
|
+
if (!purchaseId || !buyerSecret || recovery?.grant === null)
|
|
195
|
+
throw new Error('grant not final');
|
|
196
|
+
const payment = record?.purchase.route === 'base-usdc'
|
|
197
|
+
? [...record.attempts].reverse().find((attempt) => attempt.payload)?.payload
|
|
198
|
+
: undefined;
|
|
199
|
+
readFileSource = () => download({
|
|
200
|
+
url,
|
|
201
|
+
contractId: c.contractId,
|
|
202
|
+
fileId,
|
|
203
|
+
header,
|
|
204
|
+
proof: {
|
|
205
|
+
secret: bytes(buyerSecret, 32),
|
|
206
|
+
purchase_id: purchaseId,
|
|
207
|
+
grant: recovery?.grant ?? 0,
|
|
208
|
+
},
|
|
209
|
+
fetch: fetcher,
|
|
210
|
+
...(payment ? { payment: { header: payment } } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// claim_key is permissionless; a configured NEAR wallet can pay gas instead of a relayer.
|
|
214
|
+
if (o.wallet || env.ANNON_WALLET)
|
|
215
|
+
await wallet();
|
|
216
|
+
const opened = await withOutput(o.output, (sink) => sdk.openTo({
|
|
217
|
+
file: readFileSource(),
|
|
218
|
+
sink,
|
|
219
|
+
spool: (stream, use) => withSpool(o.output, stream, use),
|
|
220
|
+
...(recovery?.grant != null ? { recovery } : record ? { record } : {}),
|
|
221
|
+
})).catch(async (error) => {
|
|
222
|
+
const safe = publicError(error);
|
|
223
|
+
if (d.agent &&
|
|
224
|
+
safe.retryAfterSeconds !== undefined &&
|
|
225
|
+
['download_limited', 'download_in_progress', 'rate_limited', 'unavailable'].includes(safe.code))
|
|
226
|
+
await downloadGate?.defer(safe.code, safe.retryAfterSeconds);
|
|
227
|
+
throw error;
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
fileId,
|
|
231
|
+
purchaseId: recovery?.purchase_id ?? record?.id,
|
|
232
|
+
output: o.output,
|
|
233
|
+
bytes: opened.meta.size,
|
|
234
|
+
...(await paymentDetails(async () => record ?? (recovery ? sdk.store.load(purchaseKey(recovery.purchase_id)) : null))),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export { open, recover, status, statusOnChain };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Store } from '../pay/store.js';
|
|
2
|
+
export interface PurchaseGuard {
|
|
3
|
+
id: string;
|
|
4
|
+
revision: number;
|
|
5
|
+
owner: string | null;
|
|
6
|
+
until: number;
|
|
7
|
+
purchases: string[];
|
|
8
|
+
}
|
|
9
|
+
/** Reserve before confirmation and signing; checkpoints renew the in-progress claim. */
|
|
10
|
+
export declare function claimPurchase(store: Store<PurchaseGuard>, key: string, o: {
|
|
11
|
+
known?: readonly string[];
|
|
12
|
+
force?: boolean;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
live: (purchaseId: string, claimUntil: number) => Promise<boolean>;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
renew: () => Promise<void>;
|
|
17
|
+
saved: (purchaseId: string) => Promise<void>;
|
|
18
|
+
release: () => Promise<void>;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AdapterError } from './errors.js';
|
|
3
|
+
const refused = () => new AdapterError('purchase_open', 'A saved purchase already exists for this file. Resume it, or explicitly use --force for another purchase.', 2);
|
|
4
|
+
/** Reserve before confirmation and signing; checkpoints renew the in-progress claim. */
|
|
5
|
+
export async function claimPurchase(store, key, o) {
|
|
6
|
+
const now = o.now ?? (() => Math.floor(Date.now() / 1000));
|
|
7
|
+
const owner = randomUUID();
|
|
8
|
+
let held;
|
|
9
|
+
for (;;) {
|
|
10
|
+
const before = await store.load(key);
|
|
11
|
+
if (before?.owner && before.until > now())
|
|
12
|
+
throw refused();
|
|
13
|
+
const purchases = Array.from(new Set([...(before?.purchases ?? []), ...(o.known ?? [])]));
|
|
14
|
+
if (!o.force)
|
|
15
|
+
for (const id of purchases)
|
|
16
|
+
if (await o.live(id, before?.until ?? 0))
|
|
17
|
+
throw refused();
|
|
18
|
+
const next = {
|
|
19
|
+
id: key,
|
|
20
|
+
revision: (before?.revision ?? -1) + 1,
|
|
21
|
+
owner,
|
|
22
|
+
until: now() + 120,
|
|
23
|
+
purchases,
|
|
24
|
+
};
|
|
25
|
+
if (await store.compareAndSet(key, before, next)) {
|
|
26
|
+
held = next;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let queue = Promise.resolve();
|
|
31
|
+
const update = (patch) => {
|
|
32
|
+
const pending = queue.then(async () => {
|
|
33
|
+
const next = {
|
|
34
|
+
...held,
|
|
35
|
+
revision: held.revision + 1,
|
|
36
|
+
...patch(),
|
|
37
|
+
};
|
|
38
|
+
if (!(await store.compareAndSet(key, held, next)))
|
|
39
|
+
throw new AdapterError('store_conflict', 'The purchase reservation changed. Resume saved state.', 4);
|
|
40
|
+
held = next;
|
|
41
|
+
});
|
|
42
|
+
queue = pending.catch(() => { });
|
|
43
|
+
return pending;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
renew: () => update(() => ({ until: Math.max(held.until, now() + 120) })),
|
|
47
|
+
saved: (purchaseId) => update(() => ({
|
|
48
|
+
until: Math.max(held.until, now() + 120),
|
|
49
|
+
purchases: Array.from(new Set([...held.purchases, purchaseId])),
|
|
50
|
+
})),
|
|
51
|
+
release: () => update(() => ({ owner: null })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EncryptedRecord } from '../pay/backup.js';
|
|
2
|
+
import type { Store } from '../pay/store.js';
|
|
3
|
+
import type { AdapterSdk, RequestContext } from './command-context.js';
|
|
4
|
+
/** The SDK for this run, wired to the relay transport and the relayer's sponsorship policy. */
|
|
5
|
+
declare function createSdk(ctx: RequestContext, rawStore: Store<EncryptedRecord>, secret: Uint8Array): AdapterSdk;
|
|
6
|
+
export { createSdk };
|