@vallum/manifest 0.0.0-prerelease
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 +8 -0
- package/dist/ap2Mapping.d.ts +69 -0
- package/dist/ap2Mapping.js +130 -0
- package/dist/fixtures.d.ts +2 -0
- package/dist/fixtures.js +58 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/schema.d.ts +55 -0
- package/dist/schema.js +1 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +98 -0
- package/dist/x402Mapping.d.ts +49 -0
- package/dist/x402Mapping.js +109 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @vallum/manifest
|
|
2
|
+
|
|
3
|
+
Versioned Agent Transaction Manifest schema and validator for Vallum.
|
|
4
|
+
|
|
5
|
+
This package represents an agent action before policy or chain execution. It
|
|
6
|
+
validates structure, required fields, expiry, budget, idempotency, simulation,
|
|
7
|
+
human mandate, and receipt requirements. It does not make policy decisions,
|
|
8
|
+
execute transactions, or carry secrets/private keys.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AgentTransactionManifest, type ManifestParty } from "./schema.js";
|
|
2
|
+
export declare const AP2_CHECKOUT_MANDATE_VCT: "mandate.checkout.1";
|
|
3
|
+
export declare const AP2_PAYMENT_MANDATE_VCT: "mandate.payment.1";
|
|
4
|
+
export interface AP2Merchant {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly website?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AP2Amount {
|
|
10
|
+
readonly amount: number;
|
|
11
|
+
readonly currency: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AP2PaymentInstrument {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly type: string;
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AP2CheckoutMandate {
|
|
19
|
+
readonly vct: string;
|
|
20
|
+
readonly checkout_jwt: string;
|
|
21
|
+
readonly checkout_hash: string;
|
|
22
|
+
readonly iat?: number;
|
|
23
|
+
readonly exp?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface AP2PaymentMandate {
|
|
26
|
+
readonly vct: string;
|
|
27
|
+
readonly transaction_id: string;
|
|
28
|
+
readonly payment_mandate_id?: string;
|
|
29
|
+
readonly payee: AP2Merchant;
|
|
30
|
+
readonly payment_amount: AP2Amount;
|
|
31
|
+
readonly payment_instrument: AP2PaymentInstrument;
|
|
32
|
+
readonly execution_date?: string;
|
|
33
|
+
readonly risk_data?: Record<string, unknown>;
|
|
34
|
+
readonly iat?: number;
|
|
35
|
+
readonly exp?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface AP2TrustedSurface {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly nonAgentic: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface AP2MandateBundle {
|
|
42
|
+
readonly mode: "direct" | "autonomous";
|
|
43
|
+
readonly checkoutMandate: AP2CheckoutMandate;
|
|
44
|
+
readonly paymentMandate: AP2PaymentMandate;
|
|
45
|
+
readonly trustedSurface: AP2TrustedSurface;
|
|
46
|
+
readonly disputeEvidenceReference?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface AP2ManifestMappingContext {
|
|
49
|
+
readonly agent: ManifestParty;
|
|
50
|
+
readonly owner: ManifestParty;
|
|
51
|
+
readonly wallet?: AgentTransactionManifest["wallet"];
|
|
52
|
+
readonly packageId: string;
|
|
53
|
+
readonly module?: string;
|
|
54
|
+
readonly functionName: string;
|
|
55
|
+
readonly templateId?: string;
|
|
56
|
+
readonly templateVersion?: string;
|
|
57
|
+
readonly displayName?: string;
|
|
58
|
+
readonly maxGasBudget: number;
|
|
59
|
+
readonly idempotencyKey: string;
|
|
60
|
+
readonly now?: Date;
|
|
61
|
+
readonly simulationHash?: string;
|
|
62
|
+
}
|
|
63
|
+
export type AP2MappingErrorCode = "UNSUPPORTED_AP2_MANDATE_VERSION" | "TRUSTED_SURFACE_MUST_BE_NON_AGENTIC" | "MANDATE_REFERENCE_MISMATCH" | "AP2_MANDATE_EXPIRED" | "INVALID_AP2_MANDATE";
|
|
64
|
+
export declare class AP2MappingError extends Error {
|
|
65
|
+
readonly code: AP2MappingErrorCode;
|
|
66
|
+
constructor(code: AP2MappingErrorCode, message: string);
|
|
67
|
+
}
|
|
68
|
+
export declare function mapAP2MandatesToManifest(bundle: AP2MandateBundle, context: AP2ManifestMappingContext): AgentTransactionManifest;
|
|
69
|
+
export declare function ap2PaymentMandateId(mandate: AP2PaymentMandate): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AGENT_TRANSACTION_MANIFEST_VERSION, } from "./schema.js";
|
|
2
|
+
export const AP2_CHECKOUT_MANDATE_VCT = "mandate.checkout.1";
|
|
3
|
+
export const AP2_PAYMENT_MANDATE_VCT = "mandate.payment.1";
|
|
4
|
+
export class AP2MappingError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "AP2MappingError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function mapAP2MandatesToManifest(bundle, context) {
|
|
13
|
+
validateBundle(bundle, context.now ?? new Date());
|
|
14
|
+
const action = {
|
|
15
|
+
packageId: context.packageId,
|
|
16
|
+
...(context.module ? { module: context.module } : {}),
|
|
17
|
+
functionName: context.functionName,
|
|
18
|
+
...(context.templateId ? { templateId: context.templateId } : {}),
|
|
19
|
+
...(context.templateVersion ? { templateVersion: context.templateVersion } : {}),
|
|
20
|
+
...(context.displayName ? { displayName: context.displayName } : {}),
|
|
21
|
+
};
|
|
22
|
+
const paymentId = ap2PaymentMandateId(bundle.paymentMandate);
|
|
23
|
+
return {
|
|
24
|
+
version: AGENT_TRANSACTION_MANIFEST_VERSION,
|
|
25
|
+
agent: context.agent,
|
|
26
|
+
owner: context.owner,
|
|
27
|
+
...(context.wallet ? { wallet: context.wallet } : {}),
|
|
28
|
+
intent: `Execute AP2 payment mandate ${paymentId} for checkout ${bundle.checkoutMandate.checkout_hash}`,
|
|
29
|
+
spend: {
|
|
30
|
+
maxGasBudget: context.maxGasBudget,
|
|
31
|
+
maxPayment: {
|
|
32
|
+
amount: String(bundle.paymentMandate.payment_amount.amount),
|
|
33
|
+
asset: bundle.paymentMandate.payment_amount.currency,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
action,
|
|
37
|
+
counterparty: {
|
|
38
|
+
id: `ap2:${bundle.paymentMandate.payee.id}`,
|
|
39
|
+
},
|
|
40
|
+
scope: [
|
|
41
|
+
"standard:ap2",
|
|
42
|
+
`ap2:mode:${bundle.mode}`,
|
|
43
|
+
`ap2:checkout:${bundle.checkoutMandate.checkout_hash}`,
|
|
44
|
+
`ap2:payment:${paymentId}`,
|
|
45
|
+
"ap2:trusted-surface:non-agentic",
|
|
46
|
+
],
|
|
47
|
+
expiresAt: expiryFor(bundle).toISOString(),
|
|
48
|
+
idempotencyKey: context.idempotencyKey,
|
|
49
|
+
simulation: {
|
|
50
|
+
required: true,
|
|
51
|
+
status: "passed",
|
|
52
|
+
hash: context.simulationHash ?? "sha256:ap2-mock-simulation",
|
|
53
|
+
},
|
|
54
|
+
receipt: {
|
|
55
|
+
required: true,
|
|
56
|
+
templateId: "receipt:ap2:v1",
|
|
57
|
+
},
|
|
58
|
+
humanMandate: {
|
|
59
|
+
required: true,
|
|
60
|
+
mandateId: paymentId,
|
|
61
|
+
},
|
|
62
|
+
refundPolicy: {
|
|
63
|
+
type: "refund_to_owner",
|
|
64
|
+
},
|
|
65
|
+
metadata: {
|
|
66
|
+
ap2CheckoutMandateVct: bundle.checkoutMandate.vct,
|
|
67
|
+
ap2PaymentMandateVct: bundle.paymentMandate.vct,
|
|
68
|
+
ap2Mode: bundle.mode,
|
|
69
|
+
ap2CheckoutHash: bundle.checkoutMandate.checkout_hash,
|
|
70
|
+
ap2PaymentMandateId: paymentId,
|
|
71
|
+
ap2PaymentTransactionId: bundle.paymentMandate.transaction_id,
|
|
72
|
+
ap2PayeeId: bundle.paymentMandate.payee.id,
|
|
73
|
+
ap2TrustedSurfaceId: bundle.trustedSurface.id,
|
|
74
|
+
ap2TrustedSurfaceNonAgentic: String(bundle.trustedSurface.nonAgentic),
|
|
75
|
+
...(bundle.disputeEvidenceReference ? { ap2DisputeEvidenceReference: bundle.disputeEvidenceReference } : {}),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function ap2PaymentMandateId(mandate) {
|
|
80
|
+
return mandate.payment_mandate_id ?? mandate.transaction_id;
|
|
81
|
+
}
|
|
82
|
+
function validateBundle(bundle, now) {
|
|
83
|
+
if (bundle.checkoutMandate.vct !== AP2_CHECKOUT_MANDATE_VCT) {
|
|
84
|
+
throw new AP2MappingError("UNSUPPORTED_AP2_MANDATE_VERSION", `Unsupported AP2 mandate version: ${bundle.checkoutMandate.vct}.`);
|
|
85
|
+
}
|
|
86
|
+
if (bundle.paymentMandate.vct !== AP2_PAYMENT_MANDATE_VCT) {
|
|
87
|
+
throw new AP2MappingError("UNSUPPORTED_AP2_MANDATE_VERSION", `Unsupported AP2 mandate version: ${bundle.paymentMandate.vct}.`);
|
|
88
|
+
}
|
|
89
|
+
if (bundle.trustedSurface.nonAgentic !== true) {
|
|
90
|
+
throw new AP2MappingError("TRUSTED_SURFACE_MUST_BE_NON_AGENTIC", "AP2 Trusted Surface must be represented as non-agentic.");
|
|
91
|
+
}
|
|
92
|
+
if (bundle.paymentMandate.transaction_id !== bundle.checkoutMandate.checkout_hash) {
|
|
93
|
+
throw new AP2MappingError("MANDATE_REFERENCE_MISMATCH", "AP2 payment mandate transaction_id must match the checkout mandate hash.");
|
|
94
|
+
}
|
|
95
|
+
validateRequiredFields(bundle);
|
|
96
|
+
if (expiryFor(bundle).getTime() <= now.getTime()) {
|
|
97
|
+
throw new AP2MappingError("AP2_MANDATE_EXPIRED", "AP2 mandate expiry is in the past.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function validateRequiredFields(bundle) {
|
|
101
|
+
requireNonEmpty(bundle.checkoutMandate.checkout_jwt, "checkoutMandate.checkout_jwt");
|
|
102
|
+
requireNonEmpty(bundle.checkoutMandate.checkout_hash, "checkoutMandate.checkout_hash");
|
|
103
|
+
requireNonEmpty(bundle.paymentMandate.transaction_id, "paymentMandate.transaction_id");
|
|
104
|
+
requireNonEmpty(bundle.paymentMandate.payee?.id, "paymentMandate.payee.id");
|
|
105
|
+
requireNonEmpty(bundle.paymentMandate.payee?.name, "paymentMandate.payee.name");
|
|
106
|
+
requireNonEmpty(bundle.paymentMandate.payment_instrument?.id, "paymentMandate.payment_instrument.id");
|
|
107
|
+
requireNonEmpty(bundle.paymentMandate.payment_instrument?.type, "paymentMandate.payment_instrument.type");
|
|
108
|
+
requireNonEmpty(bundle.paymentMandate.payment_amount?.currency, "paymentMandate.payment_amount.currency");
|
|
109
|
+
if (typeof bundle.paymentMandate.payment_amount?.amount !== "number" ||
|
|
110
|
+
!Number.isInteger(bundle.paymentMandate.payment_amount.amount) ||
|
|
111
|
+
bundle.paymentMandate.payment_amount.amount < 0) {
|
|
112
|
+
throw new AP2MappingError("INVALID_AP2_MANDATE", "AP2 payment amount must be a non-negative integer.");
|
|
113
|
+
}
|
|
114
|
+
if (!/^[A-Z]{3}$/.test(bundle.paymentMandate.payment_amount.currency)) {
|
|
115
|
+
throw new AP2MappingError("INVALID_AP2_MANDATE", "AP2 payment currency must be an ISO-4217 code.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function expiryFor(bundle) {
|
|
119
|
+
const expiries = [bundle.checkoutMandate.exp, bundle.paymentMandate.exp]
|
|
120
|
+
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
121
|
+
if (expiries.length === 0) {
|
|
122
|
+
throw new AP2MappingError("INVALID_AP2_MANDATE", "AP2 mandates must include an expiry.");
|
|
123
|
+
}
|
|
124
|
+
return new Date(Math.min(...expiries) * 1000);
|
|
125
|
+
}
|
|
126
|
+
function requireNonEmpty(value, path) {
|
|
127
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
128
|
+
throw new AP2MappingError("INVALID_AP2_MANDATE", `AP2 mandate field ${path} is required.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AGENT_TRANSACTION_MANIFEST_VERSION } from "./schema.js";
|
|
2
|
+
export function validManifestFixture() {
|
|
3
|
+
return {
|
|
4
|
+
version: AGENT_TRANSACTION_MANIFEST_VERSION,
|
|
5
|
+
agent: {
|
|
6
|
+
id: "agent:quote-bot",
|
|
7
|
+
address: "0x1111111111111111111111111111111111111111111111111111111111111111",
|
|
8
|
+
},
|
|
9
|
+
owner: {
|
|
10
|
+
id: "owner:alice",
|
|
11
|
+
},
|
|
12
|
+
wallet: {
|
|
13
|
+
walletId: "wallet_demo_1",
|
|
14
|
+
signerRef: "signer_ref_demo_1",
|
|
15
|
+
},
|
|
16
|
+
intent: "Open a verifier-release escrow for quote fulfillment.",
|
|
17
|
+
spend: {
|
|
18
|
+
maxGasBudget: 50_000_000,
|
|
19
|
+
maxPayment: {
|
|
20
|
+
amount: "10.00",
|
|
21
|
+
asset: "USD",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
action: {
|
|
25
|
+
packageId: "0x2222222222222222222222222222222222222222222222222222222222222222",
|
|
26
|
+
module: "escrow",
|
|
27
|
+
functionName: "open_escrow",
|
|
28
|
+
templateId: "escrow_v1",
|
|
29
|
+
templateVersion: "1.0.0",
|
|
30
|
+
displayName: "Open escrow",
|
|
31
|
+
},
|
|
32
|
+
counterparty: {
|
|
33
|
+
id: "provider:quote-service",
|
|
34
|
+
address: "0x3333333333333333333333333333333333333333333333333333333333333333",
|
|
35
|
+
},
|
|
36
|
+
scope: ["contract:escrow", "action:open_escrow"],
|
|
37
|
+
expiresAt: "2026-06-10T13:00:00.000Z",
|
|
38
|
+
idempotencyKey: "idem_quote_bot_20260610_0001",
|
|
39
|
+
simulation: {
|
|
40
|
+
required: true,
|
|
41
|
+
status: "passed",
|
|
42
|
+
hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
43
|
+
},
|
|
44
|
+
receipt: {
|
|
45
|
+
required: true,
|
|
46
|
+
templateId: "receipt:escrow:v1",
|
|
47
|
+
},
|
|
48
|
+
humanMandate: {
|
|
49
|
+
required: false,
|
|
50
|
+
},
|
|
51
|
+
refundPolicy: {
|
|
52
|
+
type: "refund_to_owner",
|
|
53
|
+
},
|
|
54
|
+
metadata: {
|
|
55
|
+
purpose: "test-fixture",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const AGENT_TRANSACTION_MANIFEST_VERSION: "agent-tx-manifest/v1";
|
|
2
|
+
export interface ManifestParty {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly address?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ManifestSpend {
|
|
7
|
+
readonly maxGasBudget: number;
|
|
8
|
+
readonly maxPayment?: {
|
|
9
|
+
readonly amount: string;
|
|
10
|
+
readonly asset: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface ManifestAction {
|
|
14
|
+
readonly packageId: string;
|
|
15
|
+
readonly module?: string;
|
|
16
|
+
readonly functionName: string;
|
|
17
|
+
readonly templateId?: string;
|
|
18
|
+
readonly templateVersion?: string;
|
|
19
|
+
readonly displayName?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ManifestSimulation {
|
|
22
|
+
readonly required: boolean;
|
|
23
|
+
readonly status?: "pending" | "passed" | "failed";
|
|
24
|
+
readonly hash?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ManifestReceiptRequirement {
|
|
27
|
+
readonly required: boolean;
|
|
28
|
+
readonly templateId?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface AgentTransactionManifest {
|
|
31
|
+
readonly version: typeof AGENT_TRANSACTION_MANIFEST_VERSION;
|
|
32
|
+
readonly agent: ManifestParty;
|
|
33
|
+
readonly owner: ManifestParty;
|
|
34
|
+
readonly wallet?: {
|
|
35
|
+
readonly walletId?: string;
|
|
36
|
+
readonly signerRef?: string;
|
|
37
|
+
};
|
|
38
|
+
readonly intent: string;
|
|
39
|
+
readonly spend: ManifestSpend;
|
|
40
|
+
readonly action: ManifestAction;
|
|
41
|
+
readonly counterparty: ManifestParty;
|
|
42
|
+
readonly scope: readonly string[];
|
|
43
|
+
readonly expiresAt: string;
|
|
44
|
+
readonly idempotencyKey: string;
|
|
45
|
+
readonly simulation: ManifestSimulation;
|
|
46
|
+
readonly receipt: ManifestReceiptRequirement;
|
|
47
|
+
readonly humanMandate?: {
|
|
48
|
+
readonly required: boolean;
|
|
49
|
+
readonly mandateId?: string;
|
|
50
|
+
};
|
|
51
|
+
readonly refundPolicy?: {
|
|
52
|
+
readonly type: "none" | "refund_to_owner" | "refund_to_agent_wallet";
|
|
53
|
+
};
|
|
54
|
+
readonly metadata?: Record<string, string>;
|
|
55
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AGENT_TRANSACTION_MANIFEST_VERSION = "agent-tx-manifest/v1";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AgentTransactionManifest } from "./schema.js";
|
|
2
|
+
export type ManifestValidationErrorCode = "MANIFEST_NOT_OBJECT" | "UNSUPPORTED_VERSION" | "REQUIRED_FIELD_MISSING" | "FIELD_INVALID" | "MANIFEST_EXPIRED" | "MANIFEST_TOO_LARGE" | "SECRET_FIELD_NOT_ALLOWED";
|
|
3
|
+
export interface ManifestValidationError {
|
|
4
|
+
readonly code: ManifestValidationErrorCode;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}
|
|
8
|
+
export type ManifestValidationResult = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly manifest: AgentTransactionManifest;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly errors: readonly ManifestValidationError[];
|
|
14
|
+
};
|
|
15
|
+
export interface ManifestValidationOptions {
|
|
16
|
+
readonly now?: Date;
|
|
17
|
+
readonly maxBytes?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function validateAgentTransactionManifest(value: unknown, options?: ManifestValidationOptions): ManifestValidationResult;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { AGENT_TRANSACTION_MANIFEST_VERSION } from "./schema.js";
|
|
2
|
+
const DEFAULT_MAX_BYTES = 16 * 1024;
|
|
3
|
+
export function validateAgentTransactionManifest(value, options = {}) {
|
|
4
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
5
|
+
if (byteLength(value) > maxBytes) {
|
|
6
|
+
return fail("MANIFEST_TOO_LARGE", "$", `Manifest exceeds ${maxBytes} bytes.`);
|
|
7
|
+
}
|
|
8
|
+
if (!isRecord(value)) {
|
|
9
|
+
return fail("MANIFEST_NOT_OBJECT", "$", "Manifest must be a JSON object.");
|
|
10
|
+
}
|
|
11
|
+
if (value.version !== AGENT_TRANSACTION_MANIFEST_VERSION) {
|
|
12
|
+
return fail("UNSUPPORTED_VERSION", "$.version", "Manifest version is unsupported.");
|
|
13
|
+
}
|
|
14
|
+
const manifest = value;
|
|
15
|
+
const errors = [];
|
|
16
|
+
rejectSecretFields(errors, value);
|
|
17
|
+
requireParty(errors, manifest.agent, "$.agent");
|
|
18
|
+
requireParty(errors, manifest.owner, "$.owner");
|
|
19
|
+
requireParty(errors, manifest.counterparty, "$.counterparty");
|
|
20
|
+
requireNonEmptyString(errors, manifest.intent, "$.intent");
|
|
21
|
+
requirePositiveNumber(errors, manifest.spend?.maxGasBudget, "$.spend.maxGasBudget");
|
|
22
|
+
requireNonEmptyString(errors, manifest.action?.packageId, "$.action.packageId");
|
|
23
|
+
requireNonEmptyString(errors, manifest.action?.functionName, "$.action.functionName");
|
|
24
|
+
requireOptionalNonEmptyString(errors, manifest.action?.templateId, "$.action.templateId");
|
|
25
|
+
requireOptionalNonEmptyString(errors, manifest.action?.templateVersion, "$.action.templateVersion");
|
|
26
|
+
requireNonEmptyString(errors, manifest.idempotencyKey, "$.idempotencyKey");
|
|
27
|
+
requireNonEmptyString(errors, manifest.expiresAt, "$.expiresAt");
|
|
28
|
+
requireArrayOfStrings(errors, manifest.scope, "$.scope");
|
|
29
|
+
if (typeof manifest.expiresAt === "string" && manifest.expiresAt.trim() !== "") {
|
|
30
|
+
const expiresAt = Date.parse(manifest.expiresAt);
|
|
31
|
+
if (Number.isNaN(expiresAt)) {
|
|
32
|
+
push(errors, "FIELD_INVALID", "$.expiresAt", "Expiry must be an ISO timestamp.");
|
|
33
|
+
}
|
|
34
|
+
else if (expiresAt <= (options.now ?? new Date()).getTime()) {
|
|
35
|
+
push(errors, "MANIFEST_EXPIRED", "$.expiresAt", "Manifest is expired.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof manifest.simulation?.required !== "boolean") {
|
|
39
|
+
push(errors, "REQUIRED_FIELD_MISSING", "$.simulation.required", "Simulation requirement is required.");
|
|
40
|
+
}
|
|
41
|
+
if (typeof manifest.receipt?.required !== "boolean") {
|
|
42
|
+
push(errors, "REQUIRED_FIELD_MISSING", "$.receipt.required", "Receipt requirement is required.");
|
|
43
|
+
}
|
|
44
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, manifest };
|
|
45
|
+
}
|
|
46
|
+
function rejectSecretFields(errors, value, path = "$") {
|
|
47
|
+
for (const [key, child] of Object.entries(value)) {
|
|
48
|
+
const childPath = `${path}.${key}`;
|
|
49
|
+
if (isSecretField(key)) {
|
|
50
|
+
push(errors, "SECRET_FIELD_NOT_ALLOWED", childPath, "Manifest must not contain secret or raw signing material.");
|
|
51
|
+
}
|
|
52
|
+
if (isRecord(child)) {
|
|
53
|
+
rejectSecretFields(errors, child, childPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function requireParty(errors, value, path) {
|
|
58
|
+
if (!isRecord(value)) {
|
|
59
|
+
push(errors, "REQUIRED_FIELD_MISSING", path, "Party object is required.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
requireNonEmptyString(errors, value.id, `${path}.id`);
|
|
63
|
+
}
|
|
64
|
+
function requireNonEmptyString(errors, value, path) {
|
|
65
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
66
|
+
push(errors, "REQUIRED_FIELD_MISSING", path, "Required string field is missing.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function requireOptionalNonEmptyString(errors, value, path) {
|
|
70
|
+
if (value !== undefined && (typeof value !== "string" || value.trim() === "")) {
|
|
71
|
+
push(errors, "FIELD_INVALID", path, "Optional string field must be non-empty when present.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function requirePositiveNumber(errors, value, path) {
|
|
75
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
76
|
+
push(errors, "REQUIRED_FIELD_MISSING", path, "Required positive number field is missing.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function requireArrayOfStrings(errors, value, path) {
|
|
80
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
81
|
+
push(errors, "FIELD_INVALID", path, "Field must be an array of non-empty strings.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function fail(code, path, message) {
|
|
85
|
+
return { ok: false, errors: [{ code, path, message }] };
|
|
86
|
+
}
|
|
87
|
+
function push(errors, code, path, message) {
|
|
88
|
+
errors.push({ code, path, message });
|
|
89
|
+
}
|
|
90
|
+
function isRecord(value) {
|
|
91
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
function byteLength(value) {
|
|
94
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8");
|
|
95
|
+
}
|
|
96
|
+
function isSecretField(key) {
|
|
97
|
+
return /^(seed|mnemonic|privateKey|private_key|rawKeypair|raw_keypair|rawTransactionBytes|raw_transaction_bytes|userSignature|user_signature|sponsorKey|sponsor_key|appApiKey|app_api_key|bearerToken|bearer_token|paymentCredential|payment_credential)$/i.test(key);
|
|
98
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type AgentTransactionManifest, type ManifestParty } from "./schema.js";
|
|
2
|
+
export declare const X402_SUPPORTED_VERSION: 2;
|
|
3
|
+
export interface X402ResourceInfo {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
readonly mimeType?: string;
|
|
7
|
+
readonly serviceName?: string;
|
|
8
|
+
readonly tags?: readonly string[];
|
|
9
|
+
readonly iconUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface X402PaymentRequirements {
|
|
12
|
+
readonly scheme: string;
|
|
13
|
+
readonly network: string;
|
|
14
|
+
readonly asset: string;
|
|
15
|
+
readonly amount: string;
|
|
16
|
+
readonly payTo: string;
|
|
17
|
+
readonly maxTimeoutSeconds: number;
|
|
18
|
+
readonly extra: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface X402PaymentRequired {
|
|
21
|
+
readonly x402Version: number;
|
|
22
|
+
readonly error?: string;
|
|
23
|
+
readonly resource: X402ResourceInfo;
|
|
24
|
+
readonly accepts: readonly X402PaymentRequirements[];
|
|
25
|
+
readonly extensions?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface X402ManifestMappingContext {
|
|
28
|
+
readonly agent: ManifestParty;
|
|
29
|
+
readonly owner: ManifestParty;
|
|
30
|
+
readonly wallet?: AgentTransactionManifest["wallet"];
|
|
31
|
+
readonly packageId: string;
|
|
32
|
+
readonly module?: string;
|
|
33
|
+
readonly functionName: string;
|
|
34
|
+
readonly templateId?: string;
|
|
35
|
+
readonly templateVersion?: string;
|
|
36
|
+
readonly displayName?: string;
|
|
37
|
+
readonly maxGasBudget: number;
|
|
38
|
+
readonly idempotencyKey: string;
|
|
39
|
+
readonly now?: Date;
|
|
40
|
+
readonly acceptedIndex?: number;
|
|
41
|
+
readonly supportedSchemes?: readonly string[];
|
|
42
|
+
readonly simulationHash?: string;
|
|
43
|
+
}
|
|
44
|
+
export type X402MappingErrorCode = "UNSUPPORTED_X402_VERSION" | "UNSUPPORTED_X402_SCHEME" | "NO_SUPPORTED_PAYMENT_REQUIREMENT" | "INVALID_X402_REQUIREMENT";
|
|
45
|
+
export declare class X402MappingError extends Error {
|
|
46
|
+
readonly code: X402MappingErrorCode;
|
|
47
|
+
constructor(code: X402MappingErrorCode, message: string);
|
|
48
|
+
}
|
|
49
|
+
export declare function mapX402PaymentRequiredToManifest(paymentRequired: X402PaymentRequired, context: X402ManifestMappingContext): AgentTransactionManifest;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AGENT_TRANSACTION_MANIFEST_VERSION, } from "./schema.js";
|
|
2
|
+
export const X402_SUPPORTED_VERSION = 2;
|
|
3
|
+
export class X402MappingError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "X402MappingError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function mapX402PaymentRequiredToManifest(paymentRequired, context) {
|
|
12
|
+
if (paymentRequired.x402Version !== X402_SUPPORTED_VERSION) {
|
|
13
|
+
throw new X402MappingError("UNSUPPORTED_X402_VERSION", `Unsupported x402 protocol version: ${paymentRequired.x402Version}.`);
|
|
14
|
+
}
|
|
15
|
+
const acceptedIndex = context.acceptedIndex ?? 0;
|
|
16
|
+
if (!Number.isInteger(acceptedIndex) || acceptedIndex < 0) {
|
|
17
|
+
throw new X402MappingError("INVALID_X402_REQUIREMENT", "x402 accepted payment requirement index is invalid.");
|
|
18
|
+
}
|
|
19
|
+
const requirement = paymentRequired.accepts.at(acceptedIndex);
|
|
20
|
+
if (!requirement) {
|
|
21
|
+
throw new X402MappingError("NO_SUPPORTED_PAYMENT_REQUIREMENT", "x402 response did not include a payment requirement.");
|
|
22
|
+
}
|
|
23
|
+
validateRequirement(paymentRequired.resource, requirement);
|
|
24
|
+
validateSupportedScheme(requirement, context.supportedSchemes ?? ["exact"]);
|
|
25
|
+
const action = {
|
|
26
|
+
packageId: context.packageId,
|
|
27
|
+
...(context.module ? { module: context.module } : {}),
|
|
28
|
+
functionName: context.functionName,
|
|
29
|
+
...(context.templateId ? { templateId: context.templateId } : {}),
|
|
30
|
+
...(context.templateVersion ? { templateVersion: context.templateVersion } : {}),
|
|
31
|
+
...(context.displayName ? { displayName: context.displayName } : {}),
|
|
32
|
+
};
|
|
33
|
+
const now = context.now ?? new Date();
|
|
34
|
+
return {
|
|
35
|
+
version: AGENT_TRANSACTION_MANIFEST_VERSION,
|
|
36
|
+
agent: context.agent,
|
|
37
|
+
owner: context.owner,
|
|
38
|
+
...(context.wallet ? { wallet: context.wallet } : {}),
|
|
39
|
+
intent: `Pay x402 resource ${paymentRequired.resource.url}`,
|
|
40
|
+
spend: {
|
|
41
|
+
maxGasBudget: context.maxGasBudget,
|
|
42
|
+
maxPayment: {
|
|
43
|
+
amount: requirement.amount,
|
|
44
|
+
asset: `${requirement.network}/${requirement.asset}`,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
action,
|
|
48
|
+
counterparty: {
|
|
49
|
+
id: `x402:${requirement.payTo}`,
|
|
50
|
+
address: requirement.payTo,
|
|
51
|
+
},
|
|
52
|
+
scope: [
|
|
53
|
+
"standard:x402",
|
|
54
|
+
`x402:scheme:${requirement.scheme}`,
|
|
55
|
+
`x402:network:${requirement.network}`,
|
|
56
|
+
`x402:resource:${paymentRequired.resource.url}`,
|
|
57
|
+
],
|
|
58
|
+
expiresAt: new Date(now.getTime() + requirement.maxTimeoutSeconds * 1000).toISOString(),
|
|
59
|
+
idempotencyKey: context.idempotencyKey,
|
|
60
|
+
simulation: {
|
|
61
|
+
required: true,
|
|
62
|
+
status: "passed",
|
|
63
|
+
hash: context.simulationHash ?? "sha256:x402-mock-simulation",
|
|
64
|
+
},
|
|
65
|
+
receipt: {
|
|
66
|
+
required: true,
|
|
67
|
+
templateId: "receipt:x402:v2",
|
|
68
|
+
},
|
|
69
|
+
humanMandate: {
|
|
70
|
+
required: false,
|
|
71
|
+
},
|
|
72
|
+
refundPolicy: {
|
|
73
|
+
type: "refund_to_owner",
|
|
74
|
+
},
|
|
75
|
+
metadata: {
|
|
76
|
+
x402Version: String(paymentRequired.x402Version),
|
|
77
|
+
x402Scheme: requirement.scheme,
|
|
78
|
+
x402Network: requirement.network,
|
|
79
|
+
x402Asset: requirement.asset,
|
|
80
|
+
x402ResourceUrl: paymentRequired.resource.url,
|
|
81
|
+
x402PayTo: requirement.payTo,
|
|
82
|
+
x402PaymentAmount: requirement.amount,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function validateRequirement(resource, requirement) {
|
|
87
|
+
requireNonEmpty(resource.url, "resource.url");
|
|
88
|
+
requireNonEmpty(requirement.scheme, "accepts[].scheme");
|
|
89
|
+
requireNonEmpty(requirement.network, "accepts[].network");
|
|
90
|
+
requireNonEmpty(requirement.asset, "accepts[].asset");
|
|
91
|
+
requireNonEmpty(requirement.amount, "accepts[].amount");
|
|
92
|
+
requireNonEmpty(requirement.payTo, "accepts[].payTo");
|
|
93
|
+
if (!/^[a-z0-9]+:[A-Za-z0-9.-]+$/.test(requirement.network)) {
|
|
94
|
+
throw new X402MappingError("INVALID_X402_REQUIREMENT", "x402 payment requirement network must be a CAIP-2 network identifier.");
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isFinite(requirement.maxTimeoutSeconds) || requirement.maxTimeoutSeconds <= 0) {
|
|
97
|
+
throw new X402MappingError("INVALID_X402_REQUIREMENT", "x402 payment requirement maxTimeoutSeconds must be a positive number.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function validateSupportedScheme(requirement, supportedSchemes) {
|
|
101
|
+
if (!supportedSchemes.includes(requirement.scheme)) {
|
|
102
|
+
throw new X402MappingError("UNSUPPORTED_X402_SCHEME", `Unsupported x402 payment scheme: ${requirement.scheme}.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function requireNonEmpty(value, path) {
|
|
106
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
107
|
+
throw new X402MappingError("INVALID_X402_REQUIREMENT", `x402 payment requirement ${path} is required.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vallum/manifest",
|
|
3
|
+
"version": "0.0.0-prerelease",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"description": "Versioned Agent Transaction Manifest schema and validator for Vallum.",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/**/*.js",
|
|
17
|
+
"dist/**/*.d.ts",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"tag": "next"
|
|
31
|
+
}
|
|
32
|
+
}
|