zklicensing 0.1.0-beta.2 → 0.1.0-beta.21
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/README.md +417 -201
- package/build/agent.d.ts +371 -0
- package/build/agent.d.ts.map +1 -0
- package/build/agent.js +553 -0
- package/build/agent.js.map +1 -0
- package/build/agentActions.d.ts +841 -0
- package/build/agentActions.d.ts.map +1 -0
- package/build/agentActions.js +581 -0
- package/build/agentActions.js.map +1 -0
- package/build/agentRecord.d.ts +83 -0
- package/build/agentRecord.d.ts.map +1 -0
- package/build/agentRecord.js +69 -0
- package/build/agentRecord.js.map +1 -0
- package/build/agentVendor.d.ts +220 -0
- package/build/agentVendor.d.ts.map +1 -0
- package/build/agentVendor.js +306 -0
- package/build/agentVendor.js.map +1 -0
- package/build/appsLookup.d.ts +14 -0
- package/build/appsLookup.d.ts.map +1 -0
- package/build/appsLookup.js +12 -0
- package/build/appsLookup.js.map +1 -0
- package/build/archiveBootstrap.d.ts +31 -5
- package/build/archiveBootstrap.d.ts.map +1 -0
- package/build/archiveBootstrap.js +140 -47
- package/build/archiveBootstrap.js.map +1 -0
- package/build/atomicJson.d.ts +5 -0
- package/build/atomicJson.d.ts.map +1 -0
- package/build/atomicJson.js +55 -0
- package/build/atomicJson.js.map +1 -0
- package/build/bindClient.d.ts +85 -0
- package/build/bindClient.d.ts.map +1 -0
- package/build/bindClient.js +187 -0
- package/build/bindClient.js.map +1 -0
- package/build/buyClient.d.ts +27 -4
- package/build/buyClient.d.ts.map +1 -0
- package/build/buyClient.js +72 -26
- package/build/buyClient.js.map +1 -0
- package/build/chainHead.d.ts +61 -0
- package/build/chainHead.d.ts.map +1 -0
- package/build/chainHead.js +88 -0
- package/build/chainHead.js.map +1 -0
- package/build/compactMerkleMap.d.ts +72 -0
- package/build/compactMerkleMap.d.ts.map +1 -0
- package/build/compactMerkleMap.js +224 -0
- package/build/compactMerkleMap.js.map +1 -0
- package/build/contractInterface.d.ts +191 -51
- package/build/contractInterface.d.ts.map +1 -0
- package/build/contractInterface.js +234 -43
- package/build/contractInterface.js.map +1 -0
- package/build/contractTypes.d.ts +9 -0
- package/build/contractTypes.d.ts.map +1 -0
- package/build/contractTypes.js +28 -0
- package/build/contractTypes.js.map +1 -0
- package/build/countryStats.d.ts +12 -3
- package/build/countryStats.d.ts.map +1 -0
- package/build/countryStats.js +87 -23
- package/build/countryStats.js.map +1 -0
- package/build/deployClient.d.ts +138 -24
- package/build/deployClient.d.ts.map +1 -0
- package/build/deployClient.js +91 -31
- package/build/deployClient.js.map +1 -0
- package/build/deviceIdentity.d.ts +68 -0
- package/build/deviceIdentity.d.ts.map +1 -0
- package/build/deviceIdentity.js +209 -0
- package/build/deviceIdentity.js.map +1 -0
- package/build/freezeClient.d.ts +26 -6
- package/build/freezeClient.d.ts.map +1 -0
- package/build/freezeClient.js +30 -6
- package/build/freezeClient.js.map +1 -0
- package/build/generationClient.d.ts +6 -3
- package/build/generationClient.d.ts.map +1 -0
- package/build/generationClient.js +25 -38
- package/build/generationClient.js.map +1 -0
- package/build/holderSecret.d.ts +70 -0
- package/build/holderSecret.d.ts.map +1 -0
- package/build/holderSecret.js +76 -0
- package/build/holderSecret.js.map +1 -0
- package/build/index.d.ts +38 -38
- package/build/index.d.ts.map +1 -0
- package/build/index.js +97 -121
- package/build/index.js.map +1 -0
- package/build/keysClient.d.ts +142 -0
- package/build/keysClient.d.ts.map +1 -0
- package/build/keysClient.js +109 -0
- package/build/keysClient.js.map +1 -0
- package/build/licenseLeaf.d.ts +76 -0
- package/build/licenseLeaf.d.ts.map +1 -0
- package/build/licenseLeaf.js +189 -0
- package/build/licenseLeaf.js.map +1 -0
- package/build/licenseRecord.d.ts +68 -5
- package/build/licenseRecord.d.ts.map +1 -0
- package/build/licenseRecord.js +26 -1
- package/build/licenseRecord.js.map +1 -0
- package/build/licenseStore.d.ts +81 -4
- package/build/licenseStore.d.ts.map +1 -0
- package/build/licenseStore.js +400 -30
- package/build/licenseStore.js.map +1 -0
- package/build/licenseTrustless.d.ts +43 -0
- package/build/licenseTrustless.d.ts.map +1 -0
- package/build/licenseTrustless.js +180 -0
- package/build/licenseTrustless.js.map +1 -0
- package/build/manifestVerify.d.ts +29 -1
- package/build/manifestVerify.d.ts.map +1 -0
- package/build/manifestVerify.js +46 -4
- package/build/manifestVerify.js.map +1 -0
- package/build/minaPriceHistory.d.ts +9 -0
- package/build/minaPriceHistory.d.ts.map +1 -0
- package/build/minaPriceHistory.js +152 -0
- package/build/minaPriceHistory.js.map +1 -0
- package/build/onChainRoot.d.ts +60 -0
- package/build/onChainRoot.d.ts.map +1 -0
- package/build/onChainRoot.js +74 -0
- package/build/onChainRoot.js.map +1 -0
- package/build/ownershipLicenseHash.d.ts +25 -0
- package/build/ownershipLicenseHash.d.ts.map +1 -0
- package/build/ownershipLicenseHash.js +29 -0
- package/build/ownershipLicenseHash.js.map +1 -0
- package/build/ownershipSignature.d.ts +153 -0
- package/build/ownershipSignature.d.ts.map +1 -0
- package/build/ownershipSignature.js +297 -0
- package/build/ownershipSignature.js.map +1 -0
- package/build/permitCheckFlow.d.ts +155 -0
- package/build/permitCheckFlow.d.ts.map +1 -0
- package/build/permitCheckFlow.js +196 -0
- package/build/permitCheckFlow.js.map +1 -0
- package/build/permitDescribe.d.ts +124 -0
- package/build/permitDescribe.d.ts.map +1 -0
- package/build/permitDescribe.js +324 -0
- package/build/permitDescribe.js.map +1 -0
- package/build/permitInterface.d.ts +85 -0
- package/build/permitInterface.d.ts.map +1 -0
- package/build/permitInterface.js +121 -0
- package/build/permitInterface.js.map +1 -0
- package/build/permitMessages.d.ts +313 -0
- package/build/permitMessages.d.ts.map +1 -0
- package/build/permitMessages.js +462 -0
- package/build/permitMessages.js.map +1 -0
- package/build/permitProof.d.ts +108 -0
- package/build/permitProof.d.ts.map +1 -0
- package/build/permitProof.js +202 -0
- package/build/permitProof.js.map +1 -0
- package/build/permitRegistryRebuild.d.ts +166 -0
- package/build/permitRegistryRebuild.d.ts.map +1 -0
- package/build/permitRegistryRebuild.js +347 -0
- package/build/permitRegistryRebuild.js.map +1 -0
- package/build/permitTree.d.ts +88 -0
- package/build/permitTree.d.ts.map +1 -0
- package/build/permitTree.js +230 -0
- package/build/permitTree.js.map +1 -0
- package/build/permitsClient.d.ts +680 -0
- package/build/permitsClient.d.ts.map +1 -0
- package/build/permitsClient.js +597 -0
- package/build/permitsClient.js.map +1 -0
- package/build/proveErrors.d.ts +50 -0
- package/build/proveErrors.d.ts.map +1 -0
- package/build/proveErrors.js +76 -0
- package/build/proveErrors.js.map +1 -0
- package/build/receipt.d.ts +95 -0
- package/build/receipt.d.ts.map +1 -0
- package/build/receipt.js +84 -0
- package/build/receipt.js.map +1 -0
- package/build/refundClient.d.ts +7 -12
- package/build/refundClient.d.ts.map +1 -0
- package/build/refundClient.js +52 -21
- package/build/refundClient.js.map +1 -0
- package/build/registryTrustVerify.d.ts +78 -0
- package/build/registryTrustVerify.d.ts.map +1 -0
- package/build/registryTrustVerify.js +129 -0
- package/build/registryTrustVerify.js.map +1 -0
- package/build/releaseClient.d.ts +26 -0
- package/build/releaseClient.d.ts.map +1 -0
- package/build/releaseClient.js +65 -0
- package/build/releaseClient.js.map +1 -0
- package/build/renewClient.d.ts +28 -2
- package/build/renewClient.d.ts.map +1 -0
- package/build/renewClient.js +31 -27
- package/build/renewClient.js.map +1 -0
- package/build/seatName.d.ts +8 -0
- package/build/seatName.d.ts.map +1 -0
- package/build/seatName.js +136 -0
- package/build/seatName.js.map +1 -0
- package/build/slotConstants.d.ts +15 -0
- package/build/slotConstants.d.ts.map +1 -0
- package/build/slotConstants.js +17 -0
- package/build/slotConstants.js.map +1 -0
- package/build/statsClient.d.ts +61 -7
- package/build/statsClient.d.ts.map +1 -0
- package/build/statsClient.js +44 -5
- package/build/statsClient.js.map +1 -0
- package/build/successorChain.d.ts +35 -0
- package/build/successorChain.d.ts.map +1 -0
- package/build/successorChain.js +69 -0
- package/build/successorChain.js.map +1 -0
- package/build/taxId.d.ts +44 -0
- package/build/taxId.d.ts.map +1 -0
- package/build/taxId.js +212 -0
- package/build/taxId.js.map +1 -0
- package/build/trustAnchorVerify.d.ts +49 -0
- package/build/trustAnchorVerify.d.ts.map +1 -0
- package/build/trustAnchorVerify.js +432 -0
- package/build/trustAnchorVerify.js.map +1 -0
- package/build/trustAnchorVerifyCli.d.ts +45 -0
- package/build/trustAnchorVerifyCli.d.ts.map +1 -0
- package/build/trustAnchorVerifyCli.js +235 -0
- package/build/trustAnchorVerifyCli.js.map +1 -0
- package/build/txBroadcastClient.d.ts +26 -0
- package/build/txBroadcastClient.d.ts.map +1 -0
- package/build/txBroadcastClient.js +20 -0
- package/build/txBroadcastClient.js.map +1 -0
- package/build/unbindClient.d.ts +77 -0
- package/build/unbindClient.d.ts.map +1 -0
- package/build/unbindClient.js +169 -0
- package/build/unbindClient.js.map +1 -0
- package/build/units.d.ts +28 -0
- package/build/units.d.ts.map +1 -0
- package/build/units.js +65 -0
- package/build/units.js.map +1 -0
- package/build/vatRates.d.ts +54 -0
- package/build/vatRates.d.ts.map +1 -0
- package/build/vatRates.js +82 -0
- package/build/vatRates.js.map +1 -0
- package/build/vatRatesData.d.ts +5 -0
- package/build/vatRatesData.d.ts.map +1 -0
- package/build/vatRatesData.js +42 -0
- package/build/vatRatesData.js.map +1 -0
- package/build/vendorAccountClient.d.ts +66 -0
- package/build/vendorAccountClient.d.ts.map +1 -0
- package/build/vendorAccountClient.js +47 -0
- package/build/vendorAccountClient.js.map +1 -0
- package/build/verifyCore.d.ts +4 -1
- package/build/verifyCore.d.ts.map +1 -0
- package/build/verifyCore.js +21 -10
- package/build/verifyCore.js.map +1 -0
- package/build/verifyOffline.d.ts +85 -0
- package/build/verifyOffline.d.ts.map +1 -0
- package/build/verifyOffline.js +139 -0
- package/build/verifyOffline.js.map +1 -0
- package/build/verifyService.d.ts.map +1 -0
- package/build/verifyService.js +571 -501
- package/build/verifyService.js.map +1 -0
- package/build/verifyWithAncestors.d.ts +19 -0
- package/build/verifyWithAncestors.d.ts.map +1 -0
- package/build/verifyWithAncestors.js +127 -0
- package/build/verifyWithAncestors.js.map +1 -0
- package/build/zipToState.d.ts +2 -0
- package/build/zipToState.d.ts.map +1 -0
- package/build/zipToState.js +94 -0
- package/build/zipToState.js.map +1 -0
- package/package.json +141 -16
- package/src/agent.ts +682 -0
- package/src/agentActions.ts +637 -0
- package/src/agentRecord.ts +121 -0
- package/src/agentVendor.ts +356 -0
- package/src/appsLookup.ts +48 -0
- package/src/archiveBootstrap.ts +386 -0
- package/src/atomicJson.ts +53 -0
- package/src/bindClient.ts +297 -0
- package/src/buyClient.ts +344 -0
- package/src/chainHead.ts +106 -0
- package/src/compactMerkleMap.ts +222 -0
- package/src/contractInterface.ts +465 -0
- package/src/contractTypes.ts +31 -0
- package/src/countryStats.ts +200 -0
- package/src/deployClient.ts +517 -0
- package/src/deviceIdentity.ts +249 -0
- package/src/freezeClient.ts +162 -0
- package/src/generationClient.ts +145 -0
- package/src/holderSecret.ts +91 -0
- package/src/index.ts +362 -0
- package/src/keysClient.ts +135 -0
- package/src/licenseLeaf.ts +221 -0
- package/src/licenseRecord.ts +190 -0
- package/src/licenseStore.ts +767 -0
- package/src/licenseTrustless.ts +207 -0
- package/src/manifestVerify.ts +164 -0
- package/src/minaPriceHistory.ts +166 -0
- package/src/onChainRoot.ts +90 -0
- package/src/ownershipLicenseHash.ts +31 -0
- package/src/ownershipSignature.ts +421 -0
- package/src/permitCheckFlow.ts +227 -0
- package/src/permitDescribe.ts +362 -0
- package/src/permitInterface.ts +146 -0
- package/src/permitMessages.ts +572 -0
- package/src/permitProof.ts +230 -0
- package/src/permitRegistryRebuild.ts +423 -0
- package/src/permitTree.ts +232 -0
- package/src/permitsClient.ts +746 -0
- package/src/proveErrors.ts +98 -0
- package/src/receipt.ts +156 -0
- package/src/refundClient.ts +177 -0
- package/src/registryTrustVerify.ts +192 -0
- package/src/releaseClient.ts +101 -0
- package/src/renewClient.ts +234 -0
- package/src/seatName.ts +165 -0
- package/src/slotConstants.ts +18 -0
- package/src/statsClient.ts +249 -0
- package/src/successorChain.ts +87 -0
- package/src/taxId.ts +227 -0
- package/src/trustAnchorVerify.ts +550 -0
- package/src/trustAnchorVerifyCli.ts +234 -0
- package/src/txBroadcastClient.ts +48 -0
- package/src/unbindClient.ts +233 -0
- package/src/units.ts +74 -0
- package/src/vatRates.ts +100 -0
- package/src/vatRatesData.ts +43 -0
- package/src/vendorAccountClient.ts +120 -0
- package/src/verifyCore.ts +100 -0
- package/src/verifyOffline.ts +212 -0
- package/src/verifyService.ts +982 -0
- package/src/verifyWithAncestors.ts +163 -0
- package/src/zipToState.ts +94 -0
- package/build/licenseProof.d.ts +0 -26
- package/build/licenseProof.js +0 -41
- package/build/migrateClient.d.ts +0 -43
- package/build/migrateClient.js +0 -47
- package/build/ownership.d.ts +0 -34
- package/build/ownership.js +0 -147
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* agentRecord.ts — part of `zklicensing/agent/actions`
|
|
6
|
+
*
|
|
7
|
+
* A signed record of one action an AI agent took: what it did (action,
|
|
8
|
+
* target, amounts, transaction, receipt number), under which boundary (a
|
|
9
|
+
* hash of the owner's spending policy, the standing consent it relied on)
|
|
10
|
+
* and when, signed by the agent's own key. A portable claim about the
|
|
11
|
+
* agent's activity that anyone can check with `verifyActionRecord`, kept
|
|
12
|
+
* apart from the license or permit file.
|
|
13
|
+
*
|
|
14
|
+
* What it establishes, and what it doesn't (`limits`, repeated in every
|
|
15
|
+
* record): the agent key signed this account of the action, and the
|
|
16
|
+
* transaction is on chain for anyone to check. It does not prove the policy
|
|
17
|
+
* was enforced: that ran in software on the owner's machine, not in an
|
|
18
|
+
* attested environment, so a compromised host could have bypassed it. Nor
|
|
19
|
+
* does it cover anything the agent did by another path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Network, PreparedAction } from './agent.js';
|
|
23
|
+
|
|
24
|
+
export const ACTION_RECORD_LIMITS =
|
|
25
|
+
'Signed by the agent key: an account of the action and the boundary it was taken under. The transaction is on chain. ' +
|
|
26
|
+
'The owner\'s policy was enforced in software on the owner\'s machine, not in an attested environment, and this record does not cover actions taken by other paths.';
|
|
27
|
+
|
|
28
|
+
export type AgentActionRecord = {
|
|
29
|
+
type: 'zklicensing-agent-action';
|
|
30
|
+
version: 1;
|
|
31
|
+
network: Network;
|
|
32
|
+
action: PreparedAction['action'] | 'register-app' | 'set-prices';
|
|
33
|
+
/** The agent wallet that acted and signed. */
|
|
34
|
+
agent: string;
|
|
35
|
+
/** The zkApp or registry acted on. */
|
|
36
|
+
target: string;
|
|
37
|
+
/** License hash or permit subject. */
|
|
38
|
+
reference: string;
|
|
39
|
+
summary: string;
|
|
40
|
+
/** Nanomina, as prepared: total leaves the payer. */
|
|
41
|
+
amounts: PreparedAction['amounts'];
|
|
42
|
+
txHash: string;
|
|
43
|
+
/** The receipt or credit note number the keeper issued, if any. */
|
|
44
|
+
receipt: string | null;
|
|
45
|
+
/** SHA-256 of the owner's policy (canonical JSON), or null when none was set. */
|
|
46
|
+
policySha256: string | null;
|
|
47
|
+
/** The standing consent relied on (its statement's SHA-256), or none. */
|
|
48
|
+
consent: { givenBy: 'none' } | { givenBy: 'agent'; statementSha256: string; kinds: string[] };
|
|
49
|
+
at: string;
|
|
50
|
+
limits: string;
|
|
51
|
+
/** The text the agent key signed (actionRecordStatement). */
|
|
52
|
+
statement: string;
|
|
53
|
+
signature: { field: string; scalar: string };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type RecordFields = Omit<AgentActionRecord, 'type' | 'version' | 'limits' | 'statement' | 'signature'>;
|
|
57
|
+
|
|
58
|
+
async function sha256Hex(text: string): Promise<string> {
|
|
59
|
+
const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
60
|
+
return [...new Uint8Array(d)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** JSON with object keys sorted, so the same policy always hashes the same. */
|
|
64
|
+
export function canonicalJson(v: unknown): string {
|
|
65
|
+
if (Array.isArray(v)) return `[${v.map(canonicalJson).join(',')}]`;
|
|
66
|
+
if (v && typeof v === 'object') {
|
|
67
|
+
return `{${Object.keys(v as object).sort().filter(k => (v as any)[k] !== undefined).map(k => `${JSON.stringify(k)}:${canonicalJson((v as any)[k])}`).join(',')}}`;
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify(v);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** SHA-256 of a policy's canonical JSON; null for no policy. */
|
|
73
|
+
export async function policyHash(policy: unknown): Promise<string | null> {
|
|
74
|
+
return policy ? sha256Hex(canonicalJson(policy)) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The exact text the agent key signs. */
|
|
78
|
+
export function actionRecordStatement(r: RecordFields): string {
|
|
79
|
+
const a = r.amounts;
|
|
80
|
+
return [
|
|
81
|
+
'zkLicensing agent action record',
|
|
82
|
+
`network: ${r.network}`,
|
|
83
|
+
`action: ${r.action}`,
|
|
84
|
+
`agent: ${r.agent}`,
|
|
85
|
+
`target: ${r.target}`,
|
|
86
|
+
`reference: ${r.reference}`,
|
|
87
|
+
`summary: ${r.summary}`,
|
|
88
|
+
`amount: ${a.total} nanomina (to ${a.recipient}: ${a.toRecipient}; platform: ${a.toPlatform})`,
|
|
89
|
+
`transaction: ${r.txHash}`,
|
|
90
|
+
`receipt: ${r.receipt ?? 'none'}`,
|
|
91
|
+
`policy sha256: ${r.policySha256 ?? 'none'}`,
|
|
92
|
+
`consent: ${r.consent.givenBy === 'agent' ? `standing consent ${r.consent.statementSha256} (${r.consent.kinds.join(', ')})` : 'none'}`,
|
|
93
|
+
`at: ${r.at}`,
|
|
94
|
+
`limits: ${ACTION_RECORD_LIMITS}`,
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Signs a record with the agent wallet (a text signature, as for sign-in). */
|
|
99
|
+
export async function signActionRecord(wallet: { address: string; signMessage(m: string): Promise<{ field: string; scalar: string }> }, fields: Omit<RecordFields, 'agent'>): Promise<AgentActionRecord> {
|
|
100
|
+
const r: RecordFields = { ...fields, agent: wallet.address };
|
|
101
|
+
const statement = actionRecordStatement(r);
|
|
102
|
+
return { type: 'zklicensing-agent-action', version: 1, ...r, limits: ACTION_RECORD_LIMITS, statement, signature: await wallet.signMessage(statement) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Checks a record: its text matches its fields and the agent key signed it.
|
|
107
|
+
* Pass `policy` to check that it was taken under that policy, and the
|
|
108
|
+
* transaction can be looked up on chain by its hash.
|
|
109
|
+
*/
|
|
110
|
+
export async function verifyActionRecord(r: AgentActionRecord, opts: { policy?: unknown } = {}): Promise<{ ok: boolean; reason: string | null }> {
|
|
111
|
+
if (r?.type !== 'zklicensing-agent-action' || r.version !== 1) return { ok: false, reason: 'not an agent action record' };
|
|
112
|
+
const { type: _t, version: _v, limits, statement, signature, ...fields } = r;
|
|
113
|
+
if (limits !== ACTION_RECORD_LIMITS) return { ok: false, reason: 'the limits text was changed' };
|
|
114
|
+
if (statement !== actionRecordStatement(fields)) return { ok: false, reason: 'the statement does not match the record' };
|
|
115
|
+
if (opts.policy !== undefined && (await policyHash(opts.policy)) !== r.policySha256) return { ok: false, reason: 'taken under another policy' };
|
|
116
|
+
const { default: Client } = await import('mina-signer');
|
|
117
|
+
const signer = new Client({ network: r.network === 'mainnet' ? 'mainnet' : 'testnet' });
|
|
118
|
+
return signer.verifyMessage({ data: statement, publicKey: r.agent, signature })
|
|
119
|
+
? { ok: true, reason: null }
|
|
120
|
+
: { ok: false, reason: 'the signature does not verify for the agent key' };
|
|
121
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* agentVendor.ts — part of `zklicensing/agent/actions`
|
|
6
|
+
*
|
|
7
|
+
* A vendor run by an AI agent: the agent's key is the vendor wallet (it
|
|
8
|
+
* receives the payments and signs for the app), acting for its owner.
|
|
9
|
+
*
|
|
10
|
+
* acceptVendorTerms the Vendor Terms and legal details, under a standing
|
|
11
|
+
* consent covering vendor-terms (recorded as given by
|
|
12
|
+
* the agent)
|
|
13
|
+
* registerApp deploys the app's contract (paying the registration
|
|
14
|
+
* fee; the consent covers its withdrawal waiver) and
|
|
15
|
+
* lists it
|
|
16
|
+
* setPrices changes the tier prices on chain
|
|
17
|
+
* updateListing edits the listing (re-reviewed on mainnet)
|
|
18
|
+
* updateDetails changes the legal details on every app
|
|
19
|
+
* status, feeStatement, trustManifest
|
|
20
|
+
*
|
|
21
|
+
* Every step checks the owner's policy (allowed actions, networks, targets,
|
|
22
|
+
* per-action and daily caps); paying steps return a signed action record.
|
|
23
|
+
* What an agent cannot do: host files on the vendor's own website (the
|
|
24
|
+
* trust manifest, and on mainnet the vendor's buy, renew and refund pages).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AgentError, type Network } from './agent.js';
|
|
28
|
+
import {
|
|
29
|
+
checkActionPolicy, consentFor,
|
|
30
|
+
type AgentWallet, type AgentPolicy, type SpendLedger, type StandingConsent,
|
|
31
|
+
} from './agentActions.js';
|
|
32
|
+
import { signActionRecord, policyHash } from './agentRecord.js';
|
|
33
|
+
|
|
34
|
+
/** Registration fee per network (MINA), as the keeper charges it; plus 1 MINA account creation and the transaction fee. */
|
|
35
|
+
export const REGISTRATION_FEE_MINA: Record<Network, number> = { devnet: 10, mainnet: 1200 };
|
|
36
|
+
const VENDOR_TC_ORIGIN = 'https://zklicensing.com';
|
|
37
|
+
|
|
38
|
+
export type VendorTerms = { title: string; version: string; documentHash: string; archiveUrl: string };
|
|
39
|
+
|
|
40
|
+
export type VendorLegalInput = {
|
|
41
|
+
legalName: string;
|
|
42
|
+
invoiceEmail: string;
|
|
43
|
+
/** ISO 3166-1 alpha-2. */
|
|
44
|
+
country: string;
|
|
45
|
+
billingAddress: { line1: string; line2?: string; city: string; postalCode: string };
|
|
46
|
+
/** VAT or tax ID (required; EU numbers are checked in VIES). */
|
|
47
|
+
vatId: string;
|
|
48
|
+
companyRegistrationRegistry: string;
|
|
49
|
+
companyRegistrationNumber: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type VendorListing = {
|
|
53
|
+
appId: string;
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
longDescription?: string;
|
|
57
|
+
category: string;
|
|
58
|
+
/** https:// — written into the contract at deployment and fixed after. */
|
|
59
|
+
website: string;
|
|
60
|
+
supportEmail: string;
|
|
61
|
+
contactEmail?: string;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
showInMarketplace?: boolean;
|
|
64
|
+
/** MINA per tier; 0 = not offered (at least one above 0). */
|
|
65
|
+
prices: { monthly: number; yearly: number; fiveYear: number; perpetual: number };
|
|
66
|
+
/** Devices a license can bind, 0..5 (fixed at deployment). */
|
|
67
|
+
deviceCap?: number;
|
|
68
|
+
/** Your own buy / renew / refund pages (required on mainnet) and policy pages. */
|
|
69
|
+
buyUrl?: string; renewUrl?: string; refundUrl?: string;
|
|
70
|
+
termsOfServiceUrl?: string; privacyPolicyUrl?: string; refundPolicyUrl?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The vendor acceptance text the wallet signs (the keeper derives the same). */
|
|
74
|
+
export function vendorAcceptanceMessage(f: {
|
|
75
|
+
vendorAddress: string; terms: VendorTerms; legalName: string; country: string; vatId: string; billingHash: string; acceptedAt: string;
|
|
76
|
+
}): string {
|
|
77
|
+
return [
|
|
78
|
+
'I accept the zkLicensing Vendor Terms of Service.',
|
|
79
|
+
'',
|
|
80
|
+
`Document: ${f.terms.title}`,
|
|
81
|
+
`Version: ${f.terms.version}`,
|
|
82
|
+
`Published: ${VENDOR_TC_ORIGIN}${f.terms.archiveUrl}`,
|
|
83
|
+
`Fingerprint: ${f.terms.documentHash}`,
|
|
84
|
+
'',
|
|
85
|
+
`Legal entity: ${f.legalName}`,
|
|
86
|
+
`Country: ${f.country}`,
|
|
87
|
+
f.vatId.length > 0 ? `VAT: ${f.vatId}` : 'VAT: (not registered)',
|
|
88
|
+
`Billing address: ${f.billingHash}`,
|
|
89
|
+
'',
|
|
90
|
+
`Signed by wallet: ${f.vendorAddress}`,
|
|
91
|
+
`Accepted at (UTC): ${f.acceptedAt}`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** "sha256:<hex>" of the billing address in the keeper's fixed key order. */
|
|
96
|
+
export async function billingHash(a: VendorLegalInput['billingAddress']): Promise<string> {
|
|
97
|
+
const canonical = JSON.stringify({ line1: a.line1, line2: a.line2 ?? '', city: a.city, postalCode: a.postalCode });
|
|
98
|
+
const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical));
|
|
99
|
+
return 'sha256:' + [...new Uint8Array(d)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createVendorAgent(opts: {
|
|
103
|
+
keeperUrl: string;
|
|
104
|
+
network: Network;
|
|
105
|
+
wallet: AgentWallet;
|
|
106
|
+
policy?: AgentPolicy;
|
|
107
|
+
ledger?: SpendLedger;
|
|
108
|
+
consent?: StandingConsent;
|
|
109
|
+
/** The site for approval links (default by network). */
|
|
110
|
+
siteUrl?: string;
|
|
111
|
+
fetcher?: typeof fetch;
|
|
112
|
+
}) {
|
|
113
|
+
const base = opts.keeperUrl.replace(/\/+$/, '');
|
|
114
|
+
const f = opts.fetcher ?? fetch;
|
|
115
|
+
const net = opts.network;
|
|
116
|
+
const w = opts.wallet;
|
|
117
|
+
const site = opts.siteUrl ?? (net === 'mainnet' ? 'https://zklicensing.com' : 'https://dev.zklicensing.com');
|
|
118
|
+
let permits: Promise<{ signIn(): Promise<string> }> | null = null;
|
|
119
|
+
// The wallet sign-in (VendorSession) the vendor endpoints need: the same one the permits client makes.
|
|
120
|
+
const session = async () => (await (permits ??= import('./permitsClient.js').then(m =>
|
|
121
|
+
m.createPermitsClient({ keeperUrl: opts.keeperUrl, network: net, wallet: w, fetcher: opts.fetcher })))).signIn();
|
|
122
|
+
|
|
123
|
+
async function call<T>(method: string, path: string, body?: unknown, auth = false): Promise<T> {
|
|
124
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
125
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
126
|
+
if (auth) headers.Authorization = `VendorSession ${await session()}`;
|
|
127
|
+
const r = await f(base + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
128
|
+
const text = await r.text();
|
|
129
|
+
let j: any = null;
|
|
130
|
+
try { j = text ? JSON.parse(text) : null; } catch { j = text; }
|
|
131
|
+
if (!r.ok) throw new AgentError(j?.error ?? `HTTP ${r.status}`, j?.code ?? (r.status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR'), r.status, r.status >= 500);
|
|
132
|
+
return j as T;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The owner's standing consent covering vendor-terms (fails closed, as for buyers). */
|
|
136
|
+
const vendorConsent = () => consentFor({
|
|
137
|
+
requiresConsent: ['vendor-terms'], agentMayConsent: true, approvalUrl: `${site}/register.html`, network: net,
|
|
138
|
+
} as any, w.address, opts.consent, { keeperUrl: opts.keeperUrl, fetcher: opts.fetcher });
|
|
139
|
+
|
|
140
|
+
const toMinaNano = (m: number) => BigInt(Math.round(m * 1e9));
|
|
141
|
+
|
|
142
|
+
async function record(action: 'register-app' | 'set-prices', a: { target: string; reference: string; summary: string; totalNano: bigint; txHash: string; consent: Awaited<ReturnType<typeof vendorConsent>> | { givenBy: 'none' } }) {
|
|
143
|
+
try {
|
|
144
|
+
const actionRecord = await signActionRecord(w, {
|
|
145
|
+
network: net, action, target: a.target, reference: a.reference, summary: a.summary,
|
|
146
|
+
amounts: { total: a.totalNano.toString(), toRecipient: a.totalNano.toString(), toPlatform: a.totalNano.toString(), recipient: 'zkLicensing platform' },
|
|
147
|
+
txHash: a.txHash, receipt: null, policySha256: await policyHash(opts.policy),
|
|
148
|
+
consent: a.consent.givenBy === 'agent' ? { givenBy: 'agent', statementSha256: a.consent.statementSha256, kinds: [...a.consent.kinds] } : { givenBy: 'none' },
|
|
149
|
+
at: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
return { actionRecord, warnings: [] as string[] };
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
return { warnings: [`The action record could not be signed (${e?.message ?? e}).`] };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Refuses an App ID this wallet can't register on this network (taken, or its prefix reserved by another vendor). */
|
|
158
|
+
async function appIdFree(appId: string): Promise<void> {
|
|
159
|
+
const r = await (opts.fetcher ?? fetch)(`${opts.keeperUrl.replace(/\/+$/, '')}/apps/${encodeURIComponent(appId)}?network=${net}&vendor=${encodeURIComponent(w.address)}`);
|
|
160
|
+
const b = await r.json().catch(() => null) as { error?: string; ownedBy?: string } | null;
|
|
161
|
+
if (r.status === 409) throw new AgentError(b?.error ?? `App ID prefix of "${appId}" is reserved by another vendor`, 'APP_ID_PREFIX_RESERVED', 409, false);
|
|
162
|
+
if (r.ok) throw new AgentError(`App ID "${appId}" is already registered on ${net}`, 'APP_ID_TAKEN', 409, false);
|
|
163
|
+
if (r.status === 404 && b?.ownedBy && b.ownedBy !== w.address) throw new AgentError(`App ID "${appId}" belongs to another vendor`, 'APP_ID_TAKEN', 409, false);
|
|
164
|
+
if (r.status !== 404) throw new AgentError(b?.error ?? `Could not check App ID "${appId}" (${r.status})`, 'UPSTREAM_UNAVAILABLE', r.status, true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
address: w.address,
|
|
169
|
+
|
|
170
|
+
/** The Vendor Terms and legal details, signed by the agent key for its owner. */
|
|
171
|
+
async acceptVendorTerms(legal: VendorLegalInput, terms: VendorTerms): Promise<{ status: string; review: unknown; taxIdCheck: unknown; network: Network }> {
|
|
172
|
+
checkActionPolicy(opts.policy, { action: 'accept-vendor-terms', network: net });
|
|
173
|
+
await vendorConsent();
|
|
174
|
+
const acceptedAt = new Date().toISOString();
|
|
175
|
+
const signedMessage = vendorAcceptanceMessage({
|
|
176
|
+
vendorAddress: w.address, terms, legalName: legal.legalName, country: legal.country, vatId: legal.vatId,
|
|
177
|
+
billingHash: await billingHash(legal.billingAddress), acceptedAt,
|
|
178
|
+
});
|
|
179
|
+
const signature = await w.signMessage(signedMessage);
|
|
180
|
+
return call('POST', '/vendors/accept', {
|
|
181
|
+
vendorAddress: w.address, network: net, tcVersion: terms.version, tcDocumentHash: terms.documentHash, acceptedAt,
|
|
182
|
+
signature, signedMessage, legal, agentConsent: opts.consent,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/** Review status on this network, the terms accepted and the tax ID check. */
|
|
187
|
+
status: () => call<any>('GET', `/vendors/${w.address}/tos?network=${net}`, undefined, true),
|
|
188
|
+
|
|
189
|
+
/** Changes the legal details on every app the vendor owns. */
|
|
190
|
+
async updateDetails(legal: Partial<VendorLegalInput>) {
|
|
191
|
+
checkActionPolicy(opts.policy, { action: 'update-vendor-details', network: net });
|
|
192
|
+
return call<{ ok: true; appsUpdated: number }>('PATCH', `/vendors/${w.address}/legal`, { payload: legal }, true);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Deploys the app's contract (the agent wallet pays the registration fee:
|
|
197
|
+
* the owner's policy caps it, the standing consent covers its withdrawal
|
|
198
|
+
* waiver) and lists it. The legal details are the ones signed in
|
|
199
|
+
* acceptVendorTerms (changed with updateDetails), never given per app.
|
|
200
|
+
*/
|
|
201
|
+
async registerApp(listing: VendorListing) {
|
|
202
|
+
const feeNano = toMinaNano(REGISTRATION_FEE_MINA[net] + 1);
|
|
203
|
+
checkActionPolicy(opts.policy, { action: 'register-app', network: net, amountNano: feeNano }, opts.ledger);
|
|
204
|
+
const consent = await vendorConsent();
|
|
205
|
+
const p = listing.prices;
|
|
206
|
+
if (![p.monthly, p.yearly, p.fiveYear, p.perpetual].some(x => x > 0)) throw new AgentError('At least one tier needs a price above 0', 'INVALID_REQUEST', 0, false);
|
|
207
|
+
if (!/^https:\/\//.test(listing.website)) throw new AgentError('website must be an https:// URL (it is written into the contract)', 'INVALID_REQUEST', 0, false);
|
|
208
|
+
await appIdFree(listing.appId); // before anything is signed or paid: the listing can't then be refused for it
|
|
209
|
+
const appSig = await w.signMessage(listing.appId);
|
|
210
|
+
await session(); // sign in before paying: the listing step needs it
|
|
211
|
+
const { proveDeploy } = await import('./deployClient.js');
|
|
212
|
+
const proved = await proveDeploy({
|
|
213
|
+
keeperUrl: opts.keeperUrl, senderPublicKey: w.address, vendorAddress: w.address,
|
|
214
|
+
priceMonthlyMina: p.monthly, priceYearlyMina: p.yearly, priceFiveYearMina: p.fiveYear, pricePerpetualMina: p.perpetual,
|
|
215
|
+
deviceCap: listing.deviceCap ?? 0, website: listing.website, network: net, fetcher: opts.fetcher,
|
|
216
|
+
});
|
|
217
|
+
const waiverAt = new Date().toISOString();
|
|
218
|
+
const txHash = await w.signAndSendTransaction(proved.provenTxJson);
|
|
219
|
+
opts.ledger?.record(feeNano);
|
|
220
|
+
const warnings: string[] = [];
|
|
221
|
+
await call('POST', '/apps', {
|
|
222
|
+
appId: listing.appId, name: listing.name, description: listing.description, longDescription: listing.longDescription ?? '',
|
|
223
|
+
category: listing.category, website: listing.website, zkAppAddress: proved.zkAppAddress, vendorAddress: w.address,
|
|
224
|
+
priceMonthlyMina: p.monthly, priceYearlyMina: p.yearly, priceFiveYearMina: p.fiveYear, pricePerpetualMina: p.perpetual,
|
|
225
|
+
deviceCap: listing.deviceCap ?? 0, txHash, verificationKeyHash: proved.verificationKeyHash,
|
|
226
|
+
tags: listing.tags ?? [], contactEmail: listing.contactEmail ?? '', supportEmail: listing.supportEmail,
|
|
227
|
+
showInMarketplace: listing.showInMarketplace !== false,
|
|
228
|
+
...(listing.buyUrl ? { buyUrl: listing.buyUrl } : {}), ...(listing.renewUrl ? { renewUrl: listing.renewUrl } : {}),
|
|
229
|
+
...(listing.refundUrl ? { refundUrl: listing.refundUrl } : {}),
|
|
230
|
+
...(listing.termsOfServiceUrl ? { termsOfServiceUrl: listing.termsOfServiceUrl } : {}),
|
|
231
|
+
...(listing.privacyPolicyUrl ? { privacyPolicyUrl: listing.privacyPolicyUrl } : {}),
|
|
232
|
+
...(listing.refundPolicyUrl ? { refundPolicyUrl: listing.refundPolicyUrl } : {}),
|
|
233
|
+
deployFeeWaiverAcceptedAt: waiverAt, network: net, signature: appSig,
|
|
234
|
+
// The waiver is the agent's, under the owner's consent: the keeper records it as such.
|
|
235
|
+
...(consent.givenBy === 'agent' ? { agentConsent: opts.consent } : {}),
|
|
236
|
+
}, true).catch(e => {
|
|
237
|
+
// The contract is deployed and paid for: never throw past this point; say how to finish.
|
|
238
|
+
warnings.push(`The contract is deployed (${proved.zkAppAddress}) but the listing was not recorded (${(e as Error).message}). Finish it from the dashboard with this address.`);
|
|
239
|
+
});
|
|
240
|
+
const rec = await record('register-app', { target: proved.zkAppAddress, reference: listing.appId, summary: `Register ${listing.name} (${listing.appId}) on ${net}`, totalNano: feeNano, txHash, consent });
|
|
241
|
+
return {
|
|
242
|
+
appId: listing.appId, zkAppAddress: proved.zkAppAddress, verificationKeyHash: proved.verificationKeyHash, txHash,
|
|
243
|
+
warnings: [...warnings, ...rec.warnings], ...(rec.actionRecord ? { actionRecord: rec.actionRecord } : {}),
|
|
244
|
+
next: [
|
|
245
|
+
`Host the trust manifest: trustManifest('${listing.appId}') gives the file for ${listing.website.replace(/\/+$/, '')}/.well-known/zklicensing/${proved.zkAppAddress}.json`,
|
|
246
|
+
net === 'mainnet' ? 'Mainnet listings are reviewed before they appear; buy, renew and refund pages on your own site are required.' : 'Devnet listings appear once the deployment is seen on chain.',
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/** Changes the tier prices on chain (and the listing's copy). */
|
|
252
|
+
async setPrices(appId: string, prices: VendorListing['prices']) {
|
|
253
|
+
const r = await call<{ app?: { deployments?: Record<string, { zkAppAddress?: string }>; vendorAddress?: string } }>('GET', `/apps/${encodeURIComponent(appId)}?network=${net}`);
|
|
254
|
+
const zkAppAddress = r.app?.deployments?.[net]?.zkAppAddress;
|
|
255
|
+
if (r.app?.vendorAddress && r.app.vendorAddress !== w.address) throw new AgentError(`${appId} belongs to another vendor wallet`, 'FORBIDDEN', 403, false);
|
|
256
|
+
if (!zkAppAddress) throw new AgentError(`${appId} has no ${net} deployment`, 'NOT_FOUND', 404, false);
|
|
257
|
+
checkActionPolicy(opts.policy, { action: 'set-prices', network: net, target: zkAppAddress, amountNano: 0n });
|
|
258
|
+
const proved = await call<{ provenTxJson: string }>('POST', '/prove/setPrice', {
|
|
259
|
+
vendorAddress: w.address, zkAppAddress, network: net,
|
|
260
|
+
priceMonthlyMina: prices.monthly, priceYearlyMina: prices.yearly, priceFiveYearMina: prices.fiveYear, pricePerpetualMina: prices.perpetual,
|
|
261
|
+
});
|
|
262
|
+
const txHash = await w.signAndSendTransaction(proved.provenTxJson);
|
|
263
|
+
const warnings: string[] = [];
|
|
264
|
+
// The listing's cached prices (the chain is the source of truth; the keeper reconciles).
|
|
265
|
+
await call('PUT', `/apps/${encodeURIComponent(appId)}`, {
|
|
266
|
+
vendorAddress: w.address, signature: await w.signMessage(appId),
|
|
267
|
+
payload: { priceMonthlyMina: prices.monthly, priceYearlyMina: prices.yearly, priceFiveYearMina: prices.fiveYear, pricePerpetualMina: prices.perpetual },
|
|
268
|
+
}, true).catch(e => warnings.push(`Prices are set on chain; the listing will catch up (${(e as Error).message}).`));
|
|
269
|
+
const rec = await record('set-prices', { target: zkAppAddress, reference: appId, summary: `Set prices of ${appId} on ${net}`, totalNano: 0n, txHash, consent: { givenBy: 'none' } });
|
|
270
|
+
return { appId, zkAppAddress, txHash, warnings: [...warnings, ...rec.warnings], ...(rec.actionRecord ? { actionRecord: rec.actionRecord } : {}) };
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Takes the app's next redeploy step: it moves to a new contract account
|
|
275
|
+
* with every license carried over. The first call pauses its sales and
|
|
276
|
+
* answers `draining` (call again after readyAt); the agent key then signs
|
|
277
|
+
* the retirement, and the agent wallet sends and pays for each step:
|
|
278
|
+
* before a hard fork the retirement alone (`retired`, the new account
|
|
279
|
+
* follows after the fork); otherwise the new account's deployment (1 MINA
|
|
280
|
+
* account creation + fee).
|
|
281
|
+
*/
|
|
282
|
+
async redeployApp(appId: string) {
|
|
283
|
+
const r = await call<{ app?: { deployments?: Record<string, { zkAppAddress?: string }>; vendorAddress?: string } }>('GET', `/apps/${encodeURIComponent(appId)}?network=${net}`);
|
|
284
|
+
const zkAppAddress = r.app?.deployments?.[net]?.zkAppAddress;
|
|
285
|
+
if (r.app?.vendorAddress && r.app.vendorAddress !== w.address) throw new AgentError(`${appId} belongs to another vendor wallet`, 'FORBIDDEN', 403, false);
|
|
286
|
+
if (!zkAppAddress) throw new AgentError(`${appId} has no ${net} deployment`, 'NOT_FOUND', 404, false);
|
|
287
|
+
checkActionPolicy(opts.policy, { action: 'redeploy-app', network: net, target: zkAppAddress, amountNano: toMinaNano(1.1) }, opts.ledger);
|
|
288
|
+
const d = await import('./deployClient.js');
|
|
289
|
+
let retireSignature: string | undefined;
|
|
290
|
+
for (let round = 0; round < 3; round++) {
|
|
291
|
+
const { nonce } = await d.getVendorRedeployChallenge({ keeperUrl: opts.keeperUrl, appId, network: net, fetcher: opts.fetcher });
|
|
292
|
+
const signature = await w.signMessage(nonce);
|
|
293
|
+
const res = await d.proveVendorRedeploy({ keeperUrl: opts.keeperUrl, appId, nonce, signature, senderPublicKey: w.address, retireSignature, fetcher: opts.fetcher });
|
|
294
|
+
if ('status' in res && res.status === 'draining') return { appId, status: 'draining' as const, readyAt: res.readyAt, expiresAt: res.expiresAt, next: 'Sales are paused while in-flight proofs land: call redeploy_app again after readyAt.' };
|
|
295
|
+
if ('status' in res && res.status === 'sign-retire') { retireSignature = await w.signFields(res.retireMessage); continue; }
|
|
296
|
+
if (!('provenTxJson' in res)) break;
|
|
297
|
+
const txHash = await w.signAndSendTransaction(res.provenTxJson);
|
|
298
|
+
await d.registerVendorRedeploy({ keeperUrl: opts.keeperUrl, appId, redeployId: res.redeployId, txHash, fetcher: opts.fetcher });
|
|
299
|
+
if (res.step === 'retire') {
|
|
300
|
+
opts.ledger?.record(toMinaNano(0.1));
|
|
301
|
+
return { appId, status: 'retired' as const, txHash, next: 'Retired ahead of the hard fork: call redeploy_app again after the fork to deploy the new account.' };
|
|
302
|
+
}
|
|
303
|
+
opts.ledger?.record(toMinaNano(1.1));
|
|
304
|
+
return { appId, status: 'deployed' as const, zkAppAddress: res.zkAppAddress, txHash, next: 'The listing moves to the new address once it is seen on chain; host the trust manifest for it (trustManifest).' };
|
|
305
|
+
}
|
|
306
|
+
throw new AgentError('The keeper did not accept the retire signature', 'HTTP_ERROR', 0, false);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/** Calls off this vendor's redeploy before the app is retired on chain (sales resume). */
|
|
310
|
+
async cancelRedeploy(appId: string) {
|
|
311
|
+
const d = await import('./deployClient.js');
|
|
312
|
+
const { nonce } = await d.getVendorRedeployChallenge({ keeperUrl: opts.keeperUrl, appId, network: net, fetcher: opts.fetcher });
|
|
313
|
+
return d.cancelVendorRedeploy({ keeperUrl: opts.keeperUrl, appId, nonce, signature: await w.signMessage(nonce), fetcher: opts.fetcher });
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/** Edits the listing (name, descriptions, category, tags, emails, page links); mainnet edits are re-reviewed. */
|
|
317
|
+
async updateListing(appId: string, payload: Partial<Omit<VendorListing, 'appId' | 'website' | 'prices' | 'deviceCap'>>, visibility?: 'active' | 'inactive') {
|
|
318
|
+
checkActionPolicy(opts.policy, { action: 'update-listing', network: net });
|
|
319
|
+
return call<any>('PUT', `/apps/${encodeURIComponent(appId)}`, {
|
|
320
|
+
vendorAddress: w.address, signature: await w.signMessage(appId), payload,
|
|
321
|
+
...(visibility ? { network: net, status: visibility } : {}),
|
|
322
|
+
}, true);
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* What forced retirements left in escrow on this vendor's retired
|
|
327
|
+
* contracts (Vendor Terms §8.7): per contract, the purchases whose funds
|
|
328
|
+
* stay there, their buyers and the total.
|
|
329
|
+
*/
|
|
330
|
+
stranded: () => call<{ contracts: Array<{ appId: string | null; zkAppAddress: string; currentZkAppAddress: string | null; retireTxHash: string; strandedAt: string; count: number; totalNano: string; purchases: Array<{ licenseHash: string; buyerAddress: string | null; purchaseAmount: string; purchaseSlot: number; txHash: string }> }>; count: number; totalNano: string }>(
|
|
331
|
+
'GET', `/vendors/${w.address}/stranded?network=${net}`, undefined, true),
|
|
332
|
+
|
|
333
|
+
/** The platform's monthly fee statement for this vendor (month YYYY-MM). */
|
|
334
|
+
feeStatement: (month: string) => call<{ statement: unknown }>('GET', `/vendors/${w.address}/fee-statements?network=${net}&month=${encodeURIComponent(month)}`, undefined, true).then(r => r.statement),
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The platform-signed trust manifest the vendor hosts on its website, at
|
|
338
|
+
* /.well-known/zklicensing/<address>.json for every generation address.
|
|
339
|
+
* An agent can't upload it: give the files to the owner.
|
|
340
|
+
*/
|
|
341
|
+
async trustManifest(appId: string): Promise<{ content: string; files: Array<{ path: string; url: string | null }> }> {
|
|
342
|
+
const r = await f(`${base}/apps/${encodeURIComponent(appId)}?network=${net}`, { headers: { Accept: 'application/json' } });
|
|
343
|
+
if (!r.ok) throw new AgentError(`No manifest for ${appId} on ${net} (HTTP ${r.status})`, r.status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR', r.status, false);
|
|
344
|
+
// The same bytes the dashboard's "Download manifest" saves: the signed generation list (and the public listing).
|
|
345
|
+
const content = await r.text();
|
|
346
|
+
const j = JSON.parse(content) as { generations?: Array<{ zkAppAddress: string }>; app?: { website?: string } };
|
|
347
|
+
const gens = j.generations ?? [];
|
|
348
|
+
const website = j.app?.website ?? null;
|
|
349
|
+
return {
|
|
350
|
+
content,
|
|
351
|
+
files: gens.map(g => ({ path: `/.well-known/zklicensing/${g.zkAppAddress}.json`, url: website ? `${website.replace(/\/+$/, '')}/.well-known/zklicensing/${g.zkAppAddress}.json` : null })),
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
export type VendorAgent = ReturnType<typeof createVendorAgent>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
// Pure lookup helper used by verifyService.ts + keeperService.ts to resolve
|
|
5
|
+
// a client-pinned zkAppAddress back to its owning AppRecord, matching either
|
|
6
|
+
// the current live address or any entry in deploymentHistory. Under the
|
|
7
|
+
// backend-routing design a buyer whose app was built against the original
|
|
8
|
+
// gen-1 address stays valid forever even after N generation bumps — this
|
|
9
|
+
// helper is the anchor of that routing.
|
|
10
|
+
//
|
|
11
|
+
// Kept as a standalone pure function (not a method on any registry class)
|
|
12
|
+
// so it can be unit-tested without booting the Express service, and so the
|
|
13
|
+
// backend package's zklicensing/src/appsLookup.ts can share the exact same
|
|
14
|
+
// semantics. See that file for the mirror.
|
|
15
|
+
|
|
16
|
+
// Minimum shape of a per-network deployment slice for lookup: the current
|
|
17
|
+
// live address plus the retired-generation history (empty when the app has
|
|
18
|
+
// never been redeployed on that network).
|
|
19
|
+
export type LookupableDeployment = {
|
|
20
|
+
zkAppAddress: string;
|
|
21
|
+
deploymentHistory?: Array<{ zkAppAddress: string }>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Minimum shape of an AppRecord we can resolve by address. Each network has
|
|
25
|
+
// its own deployment (or none) — mainnet and devnet redeploys don't share a
|
|
26
|
+
// zkAppAddress or history, so the lookup walks both slots independently.
|
|
27
|
+
export type LookupableApp = {
|
|
28
|
+
deployments: {
|
|
29
|
+
devnet?: LookupableDeployment;
|
|
30
|
+
mainnet?: LookupableDeployment;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Return the first app whose devnet OR mainnet deployment matches (current
|
|
35
|
+
// live address or any retired generation in deploymentHistory). Under the
|
|
36
|
+
// backend-routing design a buyer who pinned an original devnet address stays
|
|
37
|
+
// resolvable across N mainnet promotions and any number of retired gens.
|
|
38
|
+
export function findAppByAnyAddress<T extends LookupableApp>(
|
|
39
|
+
apps: readonly T[],
|
|
40
|
+
zkAppAddress: string,
|
|
41
|
+
): T | undefined {
|
|
42
|
+
const matches = (d?: LookupableDeployment): boolean =>
|
|
43
|
+
!!d && (
|
|
44
|
+
d.zkAppAddress === zkAppAddress ||
|
|
45
|
+
(d.deploymentHistory ?? []).some(h => h.zkAppAddress === zkAppAddress)
|
|
46
|
+
);
|
|
47
|
+
return apps.find(a => matches(a.deployments.devnet) || matches(a.deployments.mainnet));
|
|
48
|
+
}
|