@envarai/envarpay 0.2.0-alpha.18 → 0.2.0-alpha.19

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.
@@ -14,7 +14,7 @@ export function createOfferCard(config, serviceId, offerId, origin) {
14
14
  if (!service || !offer)
15
15
  throw new CommerceError('offer_not_found', 'Service offer not found');
16
16
  const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
17
- const payment = profile?.adapter === 'x402' ? { adapter: 'x402', network: profile.network, asset: profile.asset, payTo: profile.payTo } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
17
+ const payment = profile?.adapter === 'x402' ? { adapter: 'x402', network: profile.network, asset: profile.asset, payTo: profile.payTo } : profile?.adapter === 'envar_balance' ? { adapter: 'envar_balance', currency: 'usd', accountRef: profile.accountRef } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
18
18
  return AgentCard.fromJSON({
19
19
  name: service.name,
20
20
  description: `Deliverables: ${service.contract.deliverables.join(', ')}`,
@@ -17,6 +17,7 @@ import { EnvarSellerRuntime, PinnedUpstreams, restoreEnvarConfig } from './envar
17
17
  import { listenCommerce } from './http.js';
18
18
  import { CredentialVault } from './vault.js';
19
19
  import { X402Gate } from './x402.js';
20
+ import { EnvarBalanceGate } from './envar-balance.js';
20
21
  import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
21
22
  import { CommerceError } from './types.js';
22
23
  import { createNativeSkillExecutor } from './native-skills.js';
@@ -311,6 +312,11 @@ async function run() {
311
312
  gates.mpp = gate;
312
313
  stripeRecipientFor = ref => gate.merchantRecipient(ref);
313
314
  }
315
+ if (protocols.has('envar_balance')) {
316
+ if (!integration)
317
+ throw new CommerceError('balance_configuration', 'Envar balance requires an authenticated Envar seller integration');
318
+ gates.envar_balance = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
319
+ }
314
320
  const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
315
321
  const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
316
322
  if (nativeSkills)
@@ -325,6 +331,8 @@ async function run() {
325
331
  throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
326
332
  if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
327
333
  throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
334
+ if (profile.adapter === 'envar_balance' && !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
335
+ throw new CommerceError('balance_configuration', 'Allow this balance receiving account locally');
328
336
  }
329
337
  };
330
338
  validateAdapters(config);
@@ -135,6 +135,8 @@ export function loadCommerceConfig(value) {
135
135
  throw new CommerceError('invalid_amount', 'Maximum total exceeds supported integer range');
136
136
  if (profile.adapter === 'mpp' && low < 50n)
137
137
  throw new CommerceError('card_minimum', 'USD card orders must be at least 50 cents');
138
+ if (profile.adapter === 'envar_balance' && high > 2n ** 63n - 1n)
139
+ throw new CommerceError('balance_amount', 'Envar balance prices must fit signed micro-USD');
138
140
  }
139
141
  }
140
142
  return config;
@@ -0,0 +1,25 @@
1
+ import { type PriceQuote } from './types.js';
2
+ import { CommerceStore, type OrderRecord } from './store.js';
3
+ import { CredentialVault } from './vault.js';
4
+ export interface BalanceClaimArgs {
5
+ platformOrderId: string;
6
+ claimToken: string;
7
+ taskId: string;
8
+ quote: PriceQuote;
9
+ }
10
+ export interface EnvarBalanceGateOptions {
11
+ store: CommerceStore;
12
+ vault: CredentialVault;
13
+ claim: (args: BalanceClaimArgs) => Promise<Record<string, unknown>>;
14
+ }
15
+ export declare class EnvarBalanceGate {
16
+ private readonly options;
17
+ constructor(options: EnvarBalanceGateOptions);
18
+ private assertReceipt;
19
+ private settle;
20
+ handle(request: Request, order: OrderRecord): Promise<{
21
+ response: Response;
22
+ } | {
23
+ headers: Record<string, string>;
24
+ }>;
25
+ }
@@ -0,0 +1,65 @@
1
+ /** Opt-in seller gate for a captured Envar account-balance claim. No x402 or MPP impersonation. */
2
+ import { digest } from './config.js';
3
+ import { CommerceError } from './types.js';
4
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5
+ export class EnvarBalanceGate {
6
+ options;
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ assertReceipt(order, platformOrderId, receipt) {
11
+ const quote = order.quote, profile = quote.paymentProfile;
12
+ if (profile?.adapter !== 'envar_balance')
13
+ throw new CommerceError('balance_profile', 'Original quote is not an Envar balance offer');
14
+ if (receipt.state !== 'captured' || receipt.order_id !== platformOrderId ||
15
+ receipt.amount !== quote.amount || receipt.currency !== 'usd' || receipt.recipient !== profile?.accountRef ||
16
+ receipt.quote_id !== quote.quoteId || receipt.message_id !== quote.messageId ||
17
+ receipt.task_id !== order.taskId ||
18
+ receipt.service_id !== quote.serviceId || receipt.service_revision !== quote.serviceRevision ||
19
+ receipt.offer_id !== quote.offerId || receipt.input_digest !== quote.inputDigest ||
20
+ receipt.terms_digest !== quote.termsDigest ||
21
+ typeof receipt.economic_id !== 'string' || !/^ledger:[0-9a-f-]{36}$/i.test(receipt.economic_id))
22
+ throw new CommerceError('balance_receipt_mismatch', 'Platform capture differs from the frozen service order');
23
+ }
24
+ async settle(order, attemptId, saved) {
25
+ if (saved.quoteId !== order.quote.quoteId)
26
+ throw new CommerceError('balance_recovery_conflict', 'Original balance claim belongs to another quote');
27
+ let receipt;
28
+ try {
29
+ receipt = await this.options.claim({ platformOrderId: saved.platformOrderId, claimToken: saved.claimToken, taskId: order.taskId, quote: order.quote });
30
+ this.assertReceipt(order, saved.platformOrderId, receipt);
31
+ }
32
+ catch {
33
+ this.options.store.recordSettlement(attemptId, 'unknown', {});
34
+ return { response: Response.json({ error: 'balance_claim_unknown', taskId: order.taskId }, { status: 503, headers: { 'Cache-Control': 'no-store' } }) };
35
+ }
36
+ this.options.store.recordSettlement(attemptId, 'confirmed', receipt);
37
+ return { headers: { 'ENVAR-BALANCE-RECEIPT': String(receipt.economic_id) } };
38
+ }
39
+ async handle(request, order) {
40
+ const quote = order.quote, profile = quote.paymentProfile;
41
+ if (profile?.adapter !== 'envar_balance' || quote.currency !== 'usd' || quote.recipient !== profile.accountRef)
42
+ throw new CommerceError('balance_profile', 'This offer does not accept Envar account balance');
43
+ const prior = this.options.store.paymentForOrder(order.id);
44
+ if (order.paymentState === 'confirmed' && prior?.receipt)
45
+ return { headers: { 'ENVAR-BALANCE-RECEIPT': String(prior.receipt.economic_id) } };
46
+ if (['settling', 'unknown'].includes(order.paymentState)) {
47
+ if (!prior)
48
+ throw new CommerceError('balance_recovery_conflict', 'Original balance attempt is missing');
49
+ const saved = this.options.vault.get(prior.credentialRef);
50
+ return this.settle(order, prior.id, saved);
51
+ }
52
+ if (Date.now() >= Date.parse(quote.expiresAt))
53
+ return { response: Response.json({ error: 'quote_expired' }, { status: 409, headers: { 'Cache-Control': 'no-store' } }) };
54
+ const platformOrderId = request.headers.get('Envar-Balance-Order') ?? '';
55
+ const claimToken = request.headers.get('Envar-Balance-Claim') ?? '';
56
+ if (!platformOrderId && !claimToken)
57
+ return { response: Response.json({ error: 'envar_balance_required', taskId: order.taskId, quote: { quoteId: quote.quoteId, messageId: quote.messageId, serviceId: quote.serviceId, serviceRevision: quote.serviceRevision, offerId: quote.offerId, inputDigest: quote.inputDigest, termsDigest: quote.termsDigest, amount: quote.amount, currency: quote.currency, recipient: quote.recipient, expiresAt: quote.expiresAt } }, { status: 402, headers: { 'Cache-Control': 'no-store' } }) };
58
+ if (!UUID.test(platformOrderId) || claimToken.length < 32 || claimToken.length > 4096 || /\s/.test(claimToken))
59
+ throw new CommerceError('balance_claim', 'Use the original bounded platform balance claim');
60
+ const credentialRef = this.options.vault.put({ platformOrderId, claimToken, quoteId: quote.quoteId });
61
+ const economicKey = digest({ adapter: 'envar_balance', platformOrderId });
62
+ const attempt = this.options.store.reservePayment(order.id, order.caller, economicKey, credentialRef);
63
+ return this.settle(order, attempt, { platformOrderId, claimToken, quoteId: quote.quoteId });
64
+ }
65
+ }
@@ -16,6 +16,8 @@ export interface EnvarPolicy {
16
16
  facilitatorOrigin: string;
17
17
  }[];
18
18
  allowedMppAccounts: string[];
19
+ /** Explicit seller account references allowed to accept platform balance. */
20
+ allowedBalanceAccounts?: string[];
19
21
  }
20
22
  export interface EnvarCandidate {
21
23
  id: string;
@@ -81,6 +83,12 @@ export declare class EnvarIntegration {
81
83
  close(): void;
82
84
  private open;
83
85
  private request;
86
+ claimBalancePayment(args: {
87
+ platformOrderId: string;
88
+ claimToken: string;
89
+ taskId: string;
90
+ quote: PriceQuote;
91
+ }): Promise<Record<string, unknown>>;
84
92
  allowInstalledService(name: string): void;
85
93
  reportRuntime(value: unknown): Promise<{
86
94
  name: string;
@@ -136,6 +136,9 @@ export class EnvarIntegration {
136
136
  });
137
137
  if (this.policy.allowedMppAccounts.some((id) => !ID.test(id)))
138
138
  fail("envar_policy", "Configure explicit merchant account references");
139
+ this.policy.allowedBalanceAccounts ??= [];
140
+ if (this.policy.allowedBalanceAccounts.some((id) => !ID.test(id)))
141
+ fail("envar_policy", "Configure explicit balance account references");
139
142
  this.timeoutMs = options.timeoutMs ?? 10000;
140
143
  this.limitBytes = options.responseLimitBytes ?? 1048576;
141
144
  if (!Number.isInteger(this.timeoutMs) ||
@@ -214,12 +217,12 @@ export class EnvarIntegration {
214
217
  }
215
218
  async request(path, payload, eventId) {
216
219
  this.open();
217
- if (![
220
+ if (!([
218
221
  "/api/v1/agent-config/candidates",
219
222
  "/api/v1/agent-config/acknowledgments",
220
223
  "/api/v1/commerce-reports",
221
224
  "/api/v1/agent-config/runtime",
222
- ].includes(path))
225
+ ].includes(path) || /^\/api\/v1\/commerce\/payment-claims\/[0-9a-f-]{36}\/claim$/i.test(path)))
223
226
  fail("envar_path", "Unsupported platform integration operation");
224
227
  const token = await this.options.token();
225
228
  if (typeof token !== "string" ||
@@ -298,6 +301,19 @@ export class EnvarIntegration {
298
301
  fail("envar_response_json", "Platform response must be an object");
299
302
  return value;
300
303
  }
304
+ async claimBalancePayment(args) {
305
+ uuid(args.platformOrderId);
306
+ if (typeof args.claimToken !== 'string' || args.claimToken.length < 32 || args.claimToken.length > 4096 || /\s/.test(args.claimToken))
307
+ fail('envar_balance_claim', 'Use the original bounded platform claim');
308
+ return this.request(`/api/v1/commerce/payment-claims/${args.platformOrderId}/claim`, {
309
+ claim_token: args.claimToken, task_id: args.taskId,
310
+ quote_id: args.quote.quoteId, message_id: args.quote.messageId,
311
+ service_id: args.quote.serviceId, service_revision: args.quote.serviceRevision,
312
+ offer_id: args.quote.offerId, input_digest: args.quote.inputDigest,
313
+ terms_digest: args.quote.termsDigest, amount: args.quote.amount,
314
+ currency: args.quote.currency, recipient: args.quote.recipient,
315
+ }, args.platformOrderId);
316
+ }
301
317
  allowInstalledService(name) {
302
318
  if (!ID.test(name))
303
319
  fail('envar_policy', 'Invalid installed Skill name');
@@ -348,8 +364,10 @@ export class EnvarIntegration {
348
364
  a.facilitatorOrigin === new URL(profile.facilitatorUrl).origin))
349
365
  fail("envar_recipient_denied", "Receiving identity or facilitator is outside the local update grant");
350
366
  }
351
- else if (!this.policy.allowedMppAccounts.includes(profile.accountRef))
367
+ else if (profile.adapter === "mpp" && !this.policy.allowedMppAccounts.includes(profile.accountRef))
352
368
  fail("envar_merchant_denied", "Merchant account is outside the local update grant");
369
+ else if (profile.adapter === "envar_balance" && !this.policy.allowedBalanceAccounts.includes(profile.accountRef))
370
+ fail("envar_balance_denied", "Balance receiving account is outside the local update grant");
353
371
  }
354
372
  const prior = this.db
355
373
  .prepare("SELECT * FROM revisions WHERE id=? OR (local_service=? AND revision=?)")
@@ -33,7 +33,7 @@ export function buildQuote(config, args) {
33
33
  if (amount >= 2n ** 256n)
34
34
  throw new CommerceError('invalid_amount', 'Quoted amount is too large');
35
35
  const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
36
- const recipient = profile?.adapter === 'x402' ? profile.payTo : profile ? args.stripeRecipient : null;
36
+ const recipient = profile?.adapter === 'x402' ? profile.payTo : profile?.adapter === 'envar_balance' ? profile.accountRef : profile ? args.stripeRecipient : null;
37
37
  if (profile?.adapter === 'mpp' && !recipient)
38
38
  throw new CommerceError('merchant_not_ready', 'Resolve and verify the Stripe merchant before quoting');
39
39
  const now = args.now ?? new Date();
@@ -6,6 +6,7 @@ export * from './skill-gate.js';
6
6
  export * from './native-skills.js';
7
7
  export * from './http.js';
8
8
  export * from './x402.js';
9
+ export * from './envar-balance.js';
9
10
  export * from './vault.js';
10
11
  export * from './chain.js';
11
12
  export * from './external-agent.js';
@@ -6,6 +6,7 @@ export * from './skill-gate.js';
6
6
  export * from './native-skills.js';
7
7
  export * from './http.js';
8
8
  export * from './x402.js';
9
+ export * from './envar-balance.js';
9
10
  export * from './vault.js';
10
11
  export * from './chain.js';
11
12
  export * from './external-agent.js';
@@ -39,6 +39,10 @@ export type PaymentProfile = {
39
39
  intent: 'charge';
40
40
  currency: 'usd';
41
41
  accountRef: string;
42
+ } | {
43
+ adapter: 'envar_balance';
44
+ currency: 'usd';
45
+ accountRef: string;
42
46
  };
43
47
  export interface Service {
44
48
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envarai/envarpay",
3
- "version": "0.2.0-alpha.18",
3
+ "version": "0.2.0-alpha.19",
4
4
  "description": "Independent A2A service commerce, budgeted buyers and x402/MPP payments",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,9 @@
45
45
  },
46
46
  {
47
47
  "$ref": "#/$defs/mpp"
48
+ },
49
+ {
50
+ "$ref": "#/$defs/envarBalance"
48
51
  }
49
52
  ]
50
53
  }
@@ -140,6 +143,16 @@
140
143
  }
141
144
  }
142
145
  },
146
+ "envarBalance": {
147
+ "type": "object",
148
+ "additionalProperties": false,
149
+ "required": ["adapter", "currency", "accountRef"],
150
+ "properties": {
151
+ "adapter": { "const": "envar_balance" },
152
+ "currency": { "const": "usd" },
153
+ "accountRef": { "$ref": "#/$defs/id" }
154
+ }
155
+ },
143
156
  "service": {
144
157
  "type": "object",
145
158
  "additionalProperties": false,