@envarai/envarpay 0.1.0-alpha.3 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +56 -146
  2. package/dist/commerce/buyer-store.d.ts +93 -0
  3. package/dist/commerce/buyer-store.js +168 -0
  4. package/dist/commerce/buyer.d.ts +129 -0
  5. package/dist/commerce/buyer.js +631 -0
  6. package/dist/commerce/card.d.ts +5 -0
  7. package/dist/commerce/card.js +29 -0
  8. package/dist/commerce/chain.d.ts +24 -0
  9. package/dist/commerce/chain.js +146 -0
  10. package/dist/commerce/cli.d.ts +2 -0
  11. package/dist/commerce/cli.js +268 -0
  12. package/dist/commerce/client-runtime.d.ts +8 -0
  13. package/dist/commerce/client-runtime.js +7 -0
  14. package/dist/commerce/config.d.ts +7 -0
  15. package/dist/commerce/config.js +164 -0
  16. package/dist/commerce/envar-runtime.d.ts +46 -0
  17. package/dist/commerce/envar-runtime.js +256 -0
  18. package/dist/commerce/envar.d.ts +126 -0
  19. package/dist/commerce/envar.js +647 -0
  20. package/dist/commerce/http.d.ts +2 -0
  21. package/dist/commerce/http.js +34 -0
  22. package/dist/commerce/index.d.ts +4 -0
  23. package/dist/commerce/index.js +4 -0
  24. package/dist/commerce/management.d.ts +14 -0
  25. package/dist/commerce/management.js +138 -0
  26. package/dist/commerce/mpp-client.d.ts +59 -0
  27. package/dist/commerce/mpp-client.js +24 -0
  28. package/dist/commerce/mpp-runtime.d.ts +3 -0
  29. package/dist/commerce/mpp-runtime.js +3 -0
  30. package/dist/commerce/mpp.d.ts +53 -0
  31. package/dist/commerce/mpp.js +163 -0
  32. package/dist/commerce/peer-proxy.d.ts +44 -0
  33. package/dist/commerce/peer-proxy.js +667 -0
  34. package/dist/commerce/pricing.d.ts +12 -0
  35. package/dist/commerce/pricing.js +60 -0
  36. package/dist/commerce/runtime.d.ts +8 -0
  37. package/dist/commerce/runtime.js +8 -0
  38. package/dist/commerce/server.d.ts +58 -0
  39. package/dist/commerce/server.js +485 -0
  40. package/dist/commerce/store.d.ts +86 -0
  41. package/dist/commerce/store.js +466 -0
  42. package/dist/commerce/stripe-provider.d.ts +9 -0
  43. package/dist/commerce/stripe-provider.js +97 -0
  44. package/dist/commerce/types.d.ts +113 -0
  45. package/dist/commerce/types.js +8 -0
  46. package/dist/commerce/upstream.d.ts +6 -0
  47. package/dist/commerce/upstream.js +103 -0
  48. package/dist/commerce/vault.d.ts +8 -0
  49. package/dist/commerce/vault.js +55 -0
  50. package/dist/commerce/x402.d.ts +37 -0
  51. package/dist/commerce/x402.js +174 -0
  52. package/examples/buyer-policy.json +18 -0
  53. package/examples/envar-integration-policy.json +18 -0
  54. package/examples/seller.json +52 -0
  55. package/package.json +75 -10
  56. package/schemas/buyer-policy.schema.json +148 -0
  57. package/schemas/envarpay.config.schema.json +376 -0
  58. package/schemas/quote.schema.json +91 -0
@@ -0,0 +1,164 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { Ajv2020 } from 'ajv/dist/2020.js';
4
+ import addFormatsImport from 'ajv-formats';
5
+ import canonicalizeImport from 'canonicalize';
6
+ import { CommerceError } from './types.js';
7
+ const canonicalize = canonicalizeImport;
8
+ const addFormats = addFormatsImport;
9
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
10
+ addFormats(ajv);
11
+ const schema = (name) => JSON.parse(readFileSync(new URL(`../../schemas/${name}`, import.meta.url), 'utf8'));
12
+ const sellerValidator = ajv.compile(schema('envarpay.config.schema.json'));
13
+ const buyerValidator = ajv.compile(schema('buyer-policy.schema.json'));
14
+ const USDC = {
15
+ 'eip155:84532': '0x036cbd53842c5426634e7929541ec2318f3dcf7e',
16
+ 'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
17
+ };
18
+ export function atomic(value) {
19
+ if (typeof value !== 'string' || !/^[1-9][0-9]{0,77}$/.test(value))
20
+ throw new CommerceError('invalid_amount', 'Use a positive integer string in smallest currency units');
21
+ const n = BigInt(value);
22
+ if (n >= 2n ** 256n)
23
+ throw new CommerceError('invalid_amount', 'Amount exceeds supported integer range');
24
+ return n;
25
+ }
26
+ function noExternalRefs(value) {
27
+ if (Array.isArray(value)) {
28
+ value.forEach(noExternalRefs);
29
+ return;
30
+ }
31
+ if (!value || typeof value !== 'object')
32
+ return;
33
+ for (const [key, child] of Object.entries(value)) {
34
+ if (['$ref', '$dynamicRef'].includes(key) && (typeof child !== 'string' || !child.startsWith('#')))
35
+ throw new CommerceError('external_schema_ref', 'Input schemas must contain only local references');
36
+ noExternalRefs(child);
37
+ }
38
+ }
39
+ export function validateUrl(value, allowPrivate = false) {
40
+ const url = new URL(value);
41
+ if (!['https:', ...(allowPrivate ? ['http:'] : [])].includes(url.protocol) || !url.hostname || url.username || url.password || url.hash)
42
+ throw new CommerceError('invalid_url', 'Use an HTTP(S) URL without embedded credentials or fragment');
43
+ return url;
44
+ }
45
+ export function currencyOf(profile) {
46
+ return profile.adapter === 'x402' ? `${profile.network}/erc20:${profile.asset.toLowerCase()}` : profile.currency;
47
+ }
48
+ function assertJson(value) {
49
+ if (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
50
+ throw new CommerceError('unsafe_json_number', 'Use strings for integers outside the safe JSON number range');
51
+ if (value === undefined || typeof value === 'function' || typeof value === 'bigint' || typeof value === 'symbol')
52
+ throw new CommerceError('invalid_json', 'Value must be JSON');
53
+ if (value && typeof value === 'object') {
54
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
55
+ throw new CommerceError('invalid_json', 'Only plain JSON objects are supported');
56
+ for (const child of Object.values(value))
57
+ assertJson(child);
58
+ }
59
+ }
60
+ export function digest(value) {
61
+ assertJson(value);
62
+ const serialized = canonicalize(value);
63
+ if (serialized === undefined)
64
+ throw new CommerceError('invalid_json', 'Cannot canonicalize input');
65
+ return createHash('sha256').update(serialized).digest('hex');
66
+ }
67
+ export function loadCommerceConfig(value) {
68
+ if (!sellerValidator(value))
69
+ throw new CommerceError('invalid_config', ajv.errorsText(sellerValidator.errors));
70
+ const config = structuredClone(value);
71
+ const services = new Set();
72
+ for (const profile of Object.values(config.paymentProfiles)) {
73
+ if (profile.adapter === 'x402') {
74
+ if (USDC[profile.network] !== profile.asset.toLowerCase() || BigInt(profile.payTo) === 0n)
75
+ throw new CommerceError('unsupported_payment_profile', 'MVP accepts official Base or Base Sepolia USDC and a nonzero receiving address');
76
+ validateUrl(profile.facilitatorUrl);
77
+ }
78
+ }
79
+ for (const service of config.services) {
80
+ if (services.has(service.id))
81
+ throw new CommerceError('duplicate_service', 'Service IDs must be unique');
82
+ services.add(service.id);
83
+ validateUrl(service.execution.cardUrl, true);
84
+ noExternalRefs(service.contract.inputSchema);
85
+ const inputAjv = new Ajv2020({ strict: true });
86
+ addFormats(inputAjv);
87
+ inputAjv.compile(service.contract.inputSchema);
88
+ const offers = new Set();
89
+ for (const offer of service.offers) {
90
+ if (offers.has(offer.id))
91
+ throw new CommerceError('duplicate_offer', 'Offer IDs must be unique within a service');
92
+ offers.add(offer.id);
93
+ if (offer.pricing.kind === 'free')
94
+ continue;
95
+ const profile = config.paymentProfiles[offer.paymentProfile];
96
+ if (!profile)
97
+ throw new CommerceError('missing_payment_profile', 'Offer refers to an unknown payment profile');
98
+ let low, high;
99
+ if (offer.pricing.kind === 'fixed')
100
+ low = high = atomic(offer.pricing.amount);
101
+ else {
102
+ const q = offer.pricing.quantity;
103
+ if (q.min > q.max)
104
+ throw new CommerceError('invalid_quantity', 'Quantity bounds are reversed');
105
+ let node = service.contract.inputSchema;
106
+ for (const part of q.pointer.slice(1).split('/')) {
107
+ const key = part.replace(/~1/g, '/').replace(/~0/g, '~');
108
+ const props = node.properties;
109
+ node = props && Object.hasOwn(props, key) ? props[key] : {};
110
+ }
111
+ if (node.type !== 'array' || q.min < Number(node.minItems ?? 0) || q.max > Number(node.maxItems ?? 10000))
112
+ throw new CommerceError('invalid_quantity', 'Quantity must reference a bounded declared array');
113
+ low = atomic(offer.pricing.unitAmount) * BigInt(q.min);
114
+ high = atomic(offer.pricing.unitAmount) * BigInt(q.max);
115
+ }
116
+ if (high >= 2n ** 256n)
117
+ throw new CommerceError('invalid_amount', 'Maximum total exceeds supported integer range');
118
+ if (profile.adapter === 'mpp' && low < 50n)
119
+ throw new CommerceError('card_minimum', 'USD card orders must be at least 50 cents');
120
+ }
121
+ }
122
+ return config;
123
+ }
124
+ export function loadBuyerPolicy(value) {
125
+ if (!buyerValidator(value))
126
+ throw new CommerceError('invalid_policy', ajv.errorsText(buyerValidator.errors));
127
+ const policy = structuredClone(value);
128
+ const normalizedCurrency = (currency) => {
129
+ if (currency === 'usd')
130
+ return currency;
131
+ const match = /^(eip155:[1-9][0-9]*)\/erc20:(0x[0-9a-fA-F]{40})$/.exec(currency);
132
+ if (!match || USDC[match[1]] !== match[2].toLowerCase())
133
+ throw new CommerceError('unsupported_currency', 'Buyer budget must use USD or an official supported USDC asset');
134
+ return `${match[1]}/erc20:${match[2].toLowerCase()}`;
135
+ };
136
+ const limits = new Map();
137
+ for (const b of policy.budgets) {
138
+ b.currency = normalizedCurrency(b.currency);
139
+ if (limits.has(b.currency))
140
+ throw new CommerceError('duplicate_budget', 'Only one budget per currency is allowed');
141
+ limits.set(b.currency, atomic(b.maxTotal));
142
+ }
143
+ const ids = new Set();
144
+ for (const peer of policy.peers) {
145
+ validateUrl(peer.cardUrl);
146
+ if (ids.has(peer.id))
147
+ throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
148
+ ids.add(peer.id);
149
+ if (peer.protocol === 'free')
150
+ continue;
151
+ peer.currency = normalizedCurrency(peer.currency);
152
+ if (peer.protocol === 'x402') {
153
+ if (!/^0x[0-9a-fA-F]{40}$/.test(peer.recipient) || BigInt(peer.recipient) === 0n || peer.currency === 'usd')
154
+ throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
155
+ peer.recipient = peer.recipient.toLowerCase();
156
+ }
157
+ else if (peer.currency !== 'usd' || !/^profile_[A-Za-z0-9]+$/.test(peer.recipient))
158
+ throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
159
+ const limit = limits.get(peer.currency);
160
+ if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
161
+ throw new CommerceError('invalid_budget', 'Each peer needs a cumulative budget at least as large as its per-purchase limit');
162
+ }
163
+ return policy;
164
+ }
@@ -0,0 +1,46 @@
1
+ import { type CommerceConfig, type Service } from "./types.js";
2
+ import { CommerceServer, type ExecuteOrder } from "./server.js";
3
+ import { EnvarIntegration, type EnvarCandidate } from "./envar.js";
4
+ /** The credential and input encoding for an original revision stay fixed across updates/restarts. */
5
+ export declare class PinnedUpstreams {
6
+ private readonly path;
7
+ private readonly credentials;
8
+ private readonly inputEncoding;
9
+ private readonly fetchImpl;
10
+ private readonly signal?;
11
+ private bindings;
12
+ private readonly executors;
13
+ readonly execute: ExecuteOrder;
14
+ constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined);
15
+ prepare(config: CommerceConfig, history?: readonly Service[]): void;
16
+ private executor;
17
+ }
18
+ /** Merge a single published service without silently renaming/conflicting shared payment profiles. */
19
+ export declare function mergeEnvarService(base: CommerceConfig, candidate: EnvarCandidate): CommerceConfig;
20
+ /** Restore acknowledged local revisions before opening the listener, without a network request. */
21
+ export declare function restoreEnvarConfig(base: CommerceConfig, integration: EnvarIntegration, history?: readonly {
22
+ service: Service;
23
+ profiles: CommerceConfig["paymentProfiles"];
24
+ }[]): CommerceConfig;
25
+ export interface EnvarSellerRuntimeOptions {
26
+ integration: EnvarIntegration;
27
+ server: CommerceServer;
28
+ upstreams: PinnedUpstreams;
29
+ pollIntervalMs?: number;
30
+ /** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
31
+ validateConfig?: (config: CommerceConfig) => void;
32
+ onError?: (code: string) => void;
33
+ }
34
+ export declare class EnvarSellerRuntime {
35
+ private readonly options;
36
+ private timer?;
37
+ private running?;
38
+ private stopped;
39
+ private failures;
40
+ private readonly interval;
41
+ constructor(options: EnvarSellerRuntimeOptions);
42
+ private apply;
43
+ syncOnce(): Promise<void>;
44
+ start(): void;
45
+ stop(): Promise<void>;
46
+ }
@@ -0,0 +1,256 @@
1
+ /** Seller runtime wiring for the optional directory connection; never used by independent purchase paths. */
2
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { digest, loadCommerceConfig } from "./config.js";
6
+ import { CommerceError } from "./types.js";
7
+ import { nativeA2AExecutor } from "./upstream.js";
8
+ function privateJson(path) {
9
+ const stat = lstatSync(path);
10
+ if (!stat.isFile() ||
11
+ stat.isSymbolicLink() ||
12
+ (stat.mode & 0o077) !== 0 ||
13
+ stat.size > 1024 * 1024)
14
+ throw new CommerceError("upstream_binding_file", "Use a bounded owner-only upstream binding file");
15
+ return JSON.parse(readFileSync(path, "utf8"));
16
+ }
17
+ function writePrivate(path, value) {
18
+ const folder = dirname(path);
19
+ mkdirSync(folder, { recursive: true, mode: 0o700 });
20
+ const stat = lstatSync(folder);
21
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0)
22
+ throw new CommerceError("upstream_binding_directory", "Use an owner-only runtime state directory");
23
+ if (existsSync(path))
24
+ privateJson(path);
25
+ const temporary = `${path}.${randomUUID()}.tmp`;
26
+ try {
27
+ writeFileSync(temporary, JSON.stringify(value), {
28
+ mode: 0o600,
29
+ flag: "wx",
30
+ flush: true,
31
+ });
32
+ renameSync(temporary, path);
33
+ const fd = openSync(folder, "r");
34
+ try {
35
+ fsyncSync(fd);
36
+ }
37
+ finally {
38
+ closeSync(fd);
39
+ }
40
+ }
41
+ finally {
42
+ if (existsSync(temporary))
43
+ unlinkSync(temporary);
44
+ }
45
+ }
46
+ /** The credential and input encoding for an original revision stay fixed across updates/restarts. */
47
+ export class PinnedUpstreams {
48
+ path;
49
+ credentials;
50
+ inputEncoding;
51
+ fetchImpl;
52
+ signal;
53
+ bindings = {};
54
+ executors = new Map();
55
+ execute;
56
+ constructor(path, credentials, inputEncoding = {}, fetchImpl = fetch, signal) {
57
+ this.path = path;
58
+ this.credentials = credentials;
59
+ this.inputEncoding = inputEncoding;
60
+ this.fetchImpl = fetchImpl;
61
+ this.signal = signal;
62
+ if (existsSync(path)) {
63
+ const value = privateJson(path);
64
+ if (value.version !== 1 ||
65
+ !value.bindings ||
66
+ typeof value.bindings !== "object" ||
67
+ Array.isArray(value.bindings))
68
+ throw new CommerceError("upstream_bindings", "Invalid private upstream binding state");
69
+ for (const [key, binding] of Object.entries(value.bindings))
70
+ if (!/^[a-z][a-z0-9-]{0,63}:[1-9][0-9]*$/.test(key) ||
71
+ !binding ||
72
+ typeof binding.cardUrl !== "string" ||
73
+ typeof binding.token !== "string" ||
74
+ binding.token.length < 16 ||
75
+ !["data", "json-text"].includes(binding.inputEncoding))
76
+ throw new CommerceError("upstream_bindings", "Invalid private upstream binding state");
77
+ this.bindings = value.bindings;
78
+ }
79
+ const self = this;
80
+ this.execute = async function* (order, service) {
81
+ yield* self.executor(service)(order, service);
82
+ };
83
+ this.execute.continue = async function* (order, service, continuation, remote) {
84
+ yield* self.executor(service).continue(order, service, continuation, remote);
85
+ };
86
+ this.execute.recover = (order, service, remote) => self.executor(service).recover(order, service, remote);
87
+ this.execute.remoteInterface = (task) => {
88
+ for (const executor of self.executors.values()) {
89
+ const value = executor.remoteInterface?.(task);
90
+ if (value)
91
+ return value;
92
+ }
93
+ return undefined;
94
+ };
95
+ }
96
+ prepare(config, history = []) {
97
+ const next = structuredClone(this.bindings), current = new Set(config.services.map((s) => `${s.id}:${s.revision}`));
98
+ for (const service of [...history, ...config.services]) {
99
+ const key = `${service.id}:${service.revision}`, saved = next[key], explicit = this.credentials[key];
100
+ if (saved) {
101
+ if (saved.cardUrl !== service.execution.cardUrl ||
102
+ (explicit !== undefined && explicit !== saved.token))
103
+ throw new CommerceError("upstream_binding_changed", "Original runtime origin/credential differs from its private revision binding");
104
+ continue;
105
+ }
106
+ if (!current.has(key) && !explicit)
107
+ throw new CommerceError("historical_upstream_credentials", "Provide an explicit serviceId:revision credential for historical Tasks before first binding migration");
108
+ const token = explicit ?? this.credentials[service.id], encoding = this.inputEncoding[key] ?? this.inputEncoding[service.id] ?? "data";
109
+ if (!token ||
110
+ token.length < 16 ||
111
+ !["data", "json-text"].includes(encoding))
112
+ throw new CommerceError("upstream_credentials", "Provide local upstream credentials and explicit input encoding");
113
+ next[key] = {
114
+ cardUrl: service.execution.cardUrl,
115
+ token,
116
+ inputEncoding: encoding,
117
+ };
118
+ }
119
+ writePrivate(resolve(this.path), { version: 1, bindings: next });
120
+ this.bindings = next;
121
+ }
122
+ executor(service) {
123
+ const key = `${service.id}:${service.revision}`, binding = this.bindings[key];
124
+ if (!binding || binding.cardUrl !== service.execution.cardUrl)
125
+ throw new CommerceError("upstream_binding_missing", "Original runtime credential binding is unavailable");
126
+ let executor = this.executors.get(key);
127
+ if (!executor) {
128
+ executor = nativeA2AExecutor({ [key]: binding.token }, this.fetchImpl, this.signal, { inputEncoding: { [service.id]: binding.inputEncoding } });
129
+ this.executors.set(key, executor);
130
+ }
131
+ return executor;
132
+ }
133
+ }
134
+ /** Merge a single published service without silently renaming/conflicting shared payment profiles. */
135
+ export function mergeEnvarService(base, candidate) {
136
+ const incoming = loadCommerceConfig(candidate.config), service = incoming.services[0];
137
+ if (incoming.agent.id !== base.agent.id || incoming.services.length !== 1)
138
+ throw new CommerceError("envar_runtime_agent", "Candidate does not match this runtime");
139
+ const prior = base.services.find((s) => s.id === service.id);
140
+ if (prior && service.revision < prior.revision)
141
+ throw new CommerceError("envar_stale_revision", "An older candidate cannot replace the active service");
142
+ const retained = base.services.filter((s) => s.id !== service.id), references = new Set(retained.flatMap((s) => s.offers.map((o) => o.paymentProfile).filter((p) => !!p)));
143
+ for (const [key, profile] of Object.entries(incoming.paymentProfiles))
144
+ if (references.has(key) &&
145
+ digest(profile) !== digest(base.paymentProfiles[key]))
146
+ throw new CommerceError("envar_profile_conflict", "Use a unique payment profile ID when its receiving identity differs from another active service");
147
+ const profiles = { ...base.paymentProfiles, ...incoming.paymentProfiles }, services = [...retained, service], used = new Set(services.flatMap((s) => s.offers.map((o) => o.paymentProfile).filter((p) => !!p)));
148
+ return loadCommerceConfig({
149
+ ...base,
150
+ paymentProfiles: Object.fromEntries(Object.entries(profiles).filter(([key]) => used.has(key))),
151
+ services,
152
+ });
153
+ }
154
+ /** Restore acknowledged local revisions before opening the listener, without a network request. */
155
+ export function restoreEnvarConfig(base, integration, history = []) {
156
+ const registered = history.map((snapshot) => ({
157
+ id: snapshot.service.id,
158
+ revision: snapshot.service.revision,
159
+ digest: digest(snapshot),
160
+ }));
161
+ let result = base;
162
+ for (const candidate of [
163
+ ...integration.appliedCandidates(),
164
+ ...integration.pendingCandidates(registered),
165
+ ].sort((a, b) => a.revision - b.revision)) {
166
+ const current = result.services.find((s) => s.id === candidate.config.services[0].id);
167
+ if (current && current.revision > candidate.revision)
168
+ continue;
169
+ result = mergeEnvarService(result, candidate);
170
+ }
171
+ return result;
172
+ }
173
+ export class EnvarSellerRuntime {
174
+ options;
175
+ timer;
176
+ running;
177
+ stopped = false;
178
+ failures = 0;
179
+ interval;
180
+ constructor(options) {
181
+ this.options = options;
182
+ this.interval = options.pollIntervalMs ?? 10000;
183
+ if (!Number.isInteger(this.interval) ||
184
+ this.interval < 1000 ||
185
+ this.interval > 300000)
186
+ throw new CommerceError("envar_poll_interval", "Poll interval must be between one second and five minutes");
187
+ }
188
+ async apply(candidate) {
189
+ const config = mergeEnvarService(this.options.server.config, candidate);
190
+ this.options.validateConfig?.(config);
191
+ this.options.upstreams.prepare(config, this.options.server.store.catalogHistory().map((entry) => entry.service));
192
+ this.options.server.applyConfig(config);
193
+ const expected = candidate.config.services[0], actual = this.options.server.config.services.find((s) => s.id === expected.id);
194
+ const stored = this.options.server.store
195
+ .catalogHistory()
196
+ .find((entry) => entry.service.id === expected.id &&
197
+ entry.service.revision === expected.revision);
198
+ if (!actual || !stored || digest(actual) !== digest(expected))
199
+ throw new CommerceError("envar_application_proof", "Active runtime and persisted service do not match the candidate");
200
+ return {
201
+ digest: candidate.digest,
202
+ services: [
203
+ { id: actual.id, revision: actual.revision, digest: digest(stored) },
204
+ ],
205
+ };
206
+ }
207
+ syncOnce() {
208
+ if (this.stopped)
209
+ return Promise.resolve();
210
+ if (this.running)
211
+ return this.running;
212
+ this.running = (async () => {
213
+ try {
214
+ for (const candidate of await this.options.integration.pullCandidates())
215
+ await this.options.integration.applyCandidate(candidate, (c) => this.apply(c));
216
+ }
217
+ finally {
218
+ await this.options.integration.flush({ limit: 1 });
219
+ }
220
+ })().finally(() => {
221
+ this.running = undefined;
222
+ });
223
+ return this.running;
224
+ }
225
+ start() {
226
+ if (this.timer || this.running || this.stopped)
227
+ return;
228
+ const tick = async () => {
229
+ try {
230
+ await this.syncOnce();
231
+ this.failures = 0;
232
+ }
233
+ catch (error) {
234
+ this.failures++;
235
+ this.options.onError?.(error instanceof CommerceError ? error.code : "envar_sync_failed");
236
+ }
237
+ finally {
238
+ if (!this.stopped) {
239
+ this.timer = setTimeout(() => {
240
+ this.timer = undefined;
241
+ void tick();
242
+ }, Math.min(300000, this.interval * 2 ** Math.min(this.failures, 5)));
243
+ this.timer.unref();
244
+ }
245
+ }
246
+ };
247
+ void tick();
248
+ }
249
+ async stop() {
250
+ this.stopped = true;
251
+ if (this.timer)
252
+ clearTimeout(this.timer);
253
+ this.timer = undefined;
254
+ await this.running?.catch(() => undefined);
255
+ }
256
+ }
@@ -0,0 +1,126 @@
1
+ import { type CommerceConfig, type PriceQuote } from "./types.js";
2
+ declare const STATUS_KINDS: readonly ["seller_started", "seller_completed", "seller_failed", "buyer_received", "buyer_failed", "execution_unknown"];
3
+ export type EnvarStatusKind = (typeof STATUS_KINDS)[number];
4
+ export interface EnvarPolicy {
5
+ enabled: true;
6
+ platformOrigin: string;
7
+ agentId: string;
8
+ runtimeAgentId: string;
9
+ acceptUpdates: boolean;
10
+ allowedServices: string[];
11
+ allowedUpstreamOrigins: string[];
12
+ allowedX402: {
13
+ network: string;
14
+ asset: string;
15
+ payTo: string;
16
+ facilitatorOrigin: string;
17
+ }[];
18
+ allowedMppAccounts: string[];
19
+ }
20
+ export interface EnvarCandidate {
21
+ id: string;
22
+ service_id: string;
23
+ revision: number;
24
+ digest: string;
25
+ config: CommerceConfig;
26
+ publication: {
27
+ state: string;
28
+ desired_digest: string;
29
+ };
30
+ }
31
+ export interface EnvarApplicationProof {
32
+ digest: string;
33
+ services: {
34
+ id: string;
35
+ revision: number;
36
+ digest: string;
37
+ }[];
38
+ }
39
+ export interface EnvarIntegrationOptions {
40
+ policy: EnvarPolicy;
41
+ stateDirectory: string;
42
+ configDirectory: string;
43
+ /** Dedicated Agent-scoped credential, read locally; never persisted here. */
44
+ token: () => string | Promise<string>;
45
+ fetch?: typeof fetch;
46
+ timeoutMs?: number;
47
+ responseLimitBytes?: number;
48
+ now?: () => number;
49
+ }
50
+ export interface EnvarX402Evidence {
51
+ network: string;
52
+ asset: string;
53
+ payer: string;
54
+ recipient: string;
55
+ amount: string;
56
+ nonce: string;
57
+ transaction: string;
58
+ }
59
+ export interface EnvarMppEvidence {
60
+ reference: string;
61
+ amount: string;
62
+ currency: string;
63
+ }
64
+ /** Same frozen fingerprint as CommerceStore.registerCatalog; callers must read actual persisted state. */
65
+ export declare function envarServiceProofs(config: CommerceConfig): EnvarApplicationProof["services"];
66
+ export declare class EnvarIntegration {
67
+ private readonly options;
68
+ readonly sourceInstance: string;
69
+ private readonly policy;
70
+ private readonly db;
71
+ private readonly lock;
72
+ private readonly configDirectory;
73
+ private readonly fetcher;
74
+ private readonly now;
75
+ private readonly timeoutMs;
76
+ private readonly limitBytes;
77
+ private flushing;
78
+ private applying;
79
+ private closed;
80
+ constructor(options: EnvarIntegrationOptions);
81
+ close(): void;
82
+ private open;
83
+ private request;
84
+ private candidate;
85
+ /** Latest locally acknowledged revisions; startup recovery never requires platform connectivity. */
86
+ appliedCandidates(): EnvarCandidate[];
87
+ /** Recover only pending applications already proven by the runtime's durable service catalog. */
88
+ pendingCandidates(registered: EnvarApplicationProof["services"]): EnvarCandidate[];
89
+ pullCandidates(): Promise<EnvarCandidate[]>;
90
+ applyCandidate(raw: EnvarCandidate, apply: (candidate: EnvarCandidate) => Promise<EnvarApplicationProof>): Promise<{
91
+ ackEventId: string;
92
+ configPath: string;
93
+ }>;
94
+ private enqueueAck;
95
+ private enqueue;
96
+ enqueueStatus(dedupeKey: string, report: {
97
+ orderId: string;
98
+ kind: EnvarStatusKind;
99
+ }): string;
100
+ enqueueX402Payment(dedupeKey: string, report: {
101
+ orderId: string;
102
+ quote: PriceQuote;
103
+ payment: EnvarX402Evidence;
104
+ }): string;
105
+ enqueueMppPayment(dedupeKey: string, report: {
106
+ orderId: string;
107
+ quote: PriceQuote;
108
+ payment: EnvarMppEvidence;
109
+ }): string;
110
+ queueStatus(): {
111
+ pending: number;
112
+ sent: number;
113
+ errors: {
114
+ eventId: string;
115
+ code: string;
116
+ attempts: number;
117
+ }[];
118
+ };
119
+ flush(options?: {
120
+ limit?: number;
121
+ }): Promise<{
122
+ sent: number;
123
+ pending: number;
124
+ }>;
125
+ }
126
+ export {};