@vallum/receipts 0.0.0-prerelease → 0.1.0

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/dist/index.d.ts CHANGED
@@ -24,6 +24,33 @@ export interface EscrowReceipt {
24
24
  readonly events: readonly ReceiptEvent[];
25
25
  readonly externalPayment?: LinkedReceiptState;
26
26
  readonly iotaReceipt?: LinkedReceiptState;
27
+ readonly escrowSettlement?: EscrowSettlementState;
28
+ }
29
+ export type EscrowSettlementRail = "local" | "iota-testnet" | "iota-mainnet";
30
+ export type EscrowSettlementReleaseMode = "proof" | "acceptance" | "review";
31
+ export interface EscrowSettlementState {
32
+ readonly status: "open" | "released" | "refunded";
33
+ readonly settlementRail: EscrowSettlementRail;
34
+ readonly escrowId: string;
35
+ readonly releaseMode: EscrowSettlementReleaseMode;
36
+ readonly invocationId: string;
37
+ readonly actionId: string;
38
+ readonly actionContractId: string;
39
+ readonly actionContractVersion: string;
40
+ readonly providerPayoutRef: string;
41
+ readonly platformFeeRef: string;
42
+ readonly refundDestinationRef: string;
43
+ readonly providerNetAmount: ReceiptAmount;
44
+ readonly platformFeeAmount: ReceiptAmount;
45
+ readonly openedTransactionDigest: string;
46
+ readonly providerExecutionReceiptHash?: string;
47
+ readonly evidenceAttestationHash?: string;
48
+ readonly settlementReceiptHash?: string;
49
+ readonly buyerFacingReceiptHash?: string;
50
+ readonly settlementTransactionDigest?: string;
51
+ readonly releaseProofHash?: string;
52
+ readonly refundReason?: string;
53
+ readonly platformFeePaid?: boolean;
27
54
  }
28
55
  export interface PayPerCallReceipt {
29
56
  readonly receiptId: string;
@@ -228,6 +255,40 @@ export interface CreateSubscriptionReceiptInput {
228
255
  export interface TransitionOptions {
229
256
  readonly at: Date;
230
257
  }
258
+ export interface RecordEscrowSettlementOpenOptions extends TransitionOptions {
259
+ readonly settlementRail: EscrowSettlementRail;
260
+ readonly escrowId: string;
261
+ readonly releaseMode: EscrowSettlementReleaseMode;
262
+ readonly invocationId: string;
263
+ readonly actionId: string;
264
+ readonly actionContractId: string;
265
+ readonly actionContractVersion: string;
266
+ readonly providerPayoutRef: string;
267
+ readonly platformFeeRef: string;
268
+ readonly refundDestinationRef: string;
269
+ readonly providerNetAmount: ReceiptAmount;
270
+ readonly platformFeeAmount: ReceiptAmount;
271
+ readonly transactionDigest: string;
272
+ }
273
+ export interface RecordEscrowSettlementReleaseOptions extends TransitionOptions {
274
+ readonly verifierId: string;
275
+ readonly escrowId: string;
276
+ readonly invocationId: string;
277
+ readonly releaseProofHash: string;
278
+ readonly providerExecutionReceiptHash: string;
279
+ readonly evidenceAttestationHash: string;
280
+ readonly settlementReceiptHash: string;
281
+ readonly buyerFacingReceiptHash: string;
282
+ readonly transactionDigest: string;
283
+ }
284
+ export interface RecordEscrowSettlementRefundOptions extends TransitionOptions {
285
+ readonly escrowId: string;
286
+ readonly invocationId: string;
287
+ readonly reason: string;
288
+ readonly settlementReceiptHash: string;
289
+ readonly buyerFacingReceiptHash: string;
290
+ readonly transactionDigest: string;
291
+ }
231
292
  export declare class ReceiptTransitionError extends Error {
232
293
  readonly code: "INVALID_TRANSITION" | "UNAUTHORIZED_VERIFIER";
233
294
  constructor(code: "INVALID_TRANSITION" | "UNAUTHORIZED_VERIFIER", message: string);
@@ -363,6 +424,9 @@ export declare function refundEscrow(receipt: EscrowReceipt, options: Transition
363
424
  export declare function expireEscrow(receipt: EscrowReceipt, options: TransitionOptions & {
364
425
  readonly reason: string;
365
426
  }): EscrowReceipt;
427
+ export declare function recordEscrowSettlementOpen(receipt: EscrowReceipt, options: RecordEscrowSettlementOpenOptions): EscrowReceipt;
428
+ export declare function recordEscrowSettlementRelease(receipt: EscrowReceipt, options: RecordEscrowSettlementReleaseOptions): EscrowReceipt;
429
+ export declare function recordEscrowSettlementRefund(receipt: EscrowReceipt, options: RecordEscrowSettlementRefundOptions): EscrowReceipt;
366
430
  export declare function linkExternalPaymentState(receipt: EscrowReceipt, state: LinkedReceiptState): EscrowReceipt;
367
431
  export declare function linkIotaReceiptState(receipt: EscrowReceipt, state: LinkedReceiptState): EscrowReceipt;
368
432
  export * from "./x402Receipt.js";
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "./ap2Receipt.js";
2
+ const escrowSettlementRails = ["local", "iota-testnet", "iota-mainnet"];
3
+ const escrowSettlementReleaseModes = ["proof", "acceptance", "review"];
2
4
  export class ReceiptTransitionError extends Error {
3
5
  code;
4
6
  constructor(code, message) {
@@ -457,6 +459,97 @@ export function expireEscrow(receipt, options) {
457
459
  refundReason: options.reason,
458
460
  }, options.reason);
459
461
  }
462
+ export function recordEscrowSettlementOpen(receipt, options) {
463
+ requireReceiptStatus(receipt, ["sponsored"], "record escrow settlement open");
464
+ if (receipt.escrowSettlement) {
465
+ throw new ReceiptTransitionError("INVALID_TRANSITION", "Escrow settlement is already recorded.");
466
+ }
467
+ requireEscrowSettlementRail(options.settlementRail);
468
+ requireEscrowSettlementReleaseMode(options.releaseMode);
469
+ requireNonEmpty(options.escrowId, "escrowId");
470
+ requireNonEmpty(options.invocationId, "invocationId");
471
+ requireNonEmpty(options.actionId, "actionId");
472
+ requireNonEmpty(options.actionContractId, "actionContractId");
473
+ requireNonEmpty(options.actionContractVersion, "actionContractVersion");
474
+ requireNonEmpty(options.transactionDigest, "transactionDigest");
475
+ requireSafeSettlementReference(options.providerPayoutRef, "providerPayoutRef");
476
+ requireSafeSettlementReference(options.platformFeeRef, "platformFeeRef");
477
+ requireSafeSettlementReference(options.refundDestinationRef, "refundDestinationRef");
478
+ requireFeeSplit(receipt.amount, options.providerNetAmount, options.platformFeeAmount);
479
+ return withReceiptEvent(receipt, "submitted", options.at, {
480
+ status: "submitted",
481
+ transactionDigest: options.transactionDigest,
482
+ escrowSettlement: {
483
+ status: "open",
484
+ settlementRail: options.settlementRail,
485
+ escrowId: options.escrowId,
486
+ releaseMode: options.releaseMode,
487
+ invocationId: options.invocationId,
488
+ actionId: options.actionId,
489
+ actionContractId: options.actionContractId,
490
+ actionContractVersion: options.actionContractVersion,
491
+ providerPayoutRef: options.providerPayoutRef,
492
+ platformFeeRef: options.platformFeeRef,
493
+ refundDestinationRef: options.refundDestinationRef,
494
+ providerNetAmount: options.providerNetAmount,
495
+ platformFeeAmount: options.platformFeeAmount,
496
+ openedTransactionDigest: options.transactionDigest,
497
+ },
498
+ });
499
+ }
500
+ export function recordEscrowSettlementRelease(receipt, options) {
501
+ const settlement = requireOpenEscrowSettlement(receipt);
502
+ requireSettlementBinding(settlement, options.escrowId, options.invocationId);
503
+ requireSafeHashReference(options.releaseProofHash, "releaseProofHash");
504
+ requireSafeHashReference(options.providerExecutionReceiptHash, "providerExecutionReceiptHash");
505
+ requireSafeHashReference(options.evidenceAttestationHash, "evidenceAttestationHash");
506
+ requireSafeHashReference(options.settlementReceiptHash, "settlementReceiptHash");
507
+ requireSafeHashReference(options.buyerFacingReceiptHash, "buyerFacingReceiptHash");
508
+ requireNonEmpty(options.transactionDigest, "transactionDigest");
509
+ const released = releaseEscrow(receipt, {
510
+ at: options.at,
511
+ verifierId: options.verifierId,
512
+ releaseProofHash: options.releaseProofHash,
513
+ });
514
+ return {
515
+ ...released,
516
+ escrowSettlement: {
517
+ ...settlement,
518
+ status: "released",
519
+ releaseProofHash: options.releaseProofHash,
520
+ providerExecutionReceiptHash: options.providerExecutionReceiptHash,
521
+ evidenceAttestationHash: options.evidenceAttestationHash,
522
+ settlementReceiptHash: options.settlementReceiptHash,
523
+ buyerFacingReceiptHash: options.buyerFacingReceiptHash,
524
+ settlementTransactionDigest: options.transactionDigest,
525
+ platformFeePaid: true,
526
+ },
527
+ };
528
+ }
529
+ export function recordEscrowSettlementRefund(receipt, options) {
530
+ const settlement = requireOpenEscrowSettlement(receipt);
531
+ requireSettlementBinding(settlement, options.escrowId, options.invocationId);
532
+ requireNonEmpty(options.reason, "reason");
533
+ requireSafeHashReference(options.settlementReceiptHash, "settlementReceiptHash");
534
+ requireSafeHashReference(options.buyerFacingReceiptHash, "buyerFacingReceiptHash");
535
+ requireNonEmpty(options.transactionDigest, "transactionDigest");
536
+ const refunded = refundEscrow(receipt, {
537
+ at: options.at,
538
+ reason: options.reason,
539
+ });
540
+ return {
541
+ ...refunded,
542
+ escrowSettlement: {
543
+ ...settlement,
544
+ status: "refunded",
545
+ settlementReceiptHash: options.settlementReceiptHash,
546
+ buyerFacingReceiptHash: options.buyerFacingReceiptHash,
547
+ settlementTransactionDigest: options.transactionDigest,
548
+ refundReason: options.reason,
549
+ platformFeePaid: false,
550
+ },
551
+ };
552
+ }
460
553
  export function linkExternalPaymentState(receipt, state) {
461
554
  return {
462
555
  ...receipt,
@@ -480,13 +573,35 @@ function requireEscrowOpen(receipt, action) {
480
573
  throw new ReceiptTransitionError("INVALID_TRANSITION", `Cannot ${action} escrow from ${receipt.escrow.status}.`);
481
574
  }
482
575
  }
576
+ function requireOpenEscrowSettlement(receipt) {
577
+ const settlement = receipt.escrowSettlement;
578
+ if (!settlement || settlement.status !== "open") {
579
+ throw new ReceiptTransitionError("INVALID_TRANSITION", "Escrow settlement must be open.");
580
+ }
581
+ return settlement;
582
+ }
583
+ function requireSettlementBinding(settlement, escrowId, invocationId) {
584
+ if (settlement.escrowId !== escrowId || settlement.invocationId !== invocationId) {
585
+ throw new ReceiptInputError("FIELD_REQUIRED", "Escrow settlement binding does not match the receipt.");
586
+ }
587
+ }
588
+ function requireEscrowSettlementRail(value) {
589
+ if (!escrowSettlementRails.includes(value)) {
590
+ throw new ReceiptInputError("FIELD_REQUIRED", "settlementRail must be a supported escrow settlement rail.");
591
+ }
592
+ }
593
+ function requireEscrowSettlementReleaseMode(value) {
594
+ if (!escrowSettlementReleaseModes.includes(value)) {
595
+ throw new ReceiptInputError("FIELD_REQUIRED", "releaseMode must be a supported escrow settlement release mode.");
596
+ }
597
+ }
483
598
  function requireReceiptStatus(receipt, allowed, action) {
484
599
  if (!allowed.includes(receipt.status)) {
485
600
  throw new ReceiptTransitionError("INVALID_TRANSITION", `Cannot ${action} receipt from ${receipt.status}.`);
486
601
  }
487
602
  }
488
603
  function requireNonEmpty(value, field) {
489
- if (value.trim() === "") {
604
+ if (typeof value !== "string" || value.trim() === "") {
490
605
  throw new ReceiptInputError("FIELD_REQUIRED", `${field} is required.`);
491
606
  }
492
607
  }
@@ -497,6 +612,60 @@ function requireSafeHashReference(value, field) {
497
612
  throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be a safe sha256 reference.`);
498
613
  }
499
614
  }
615
+ function requireSafeSettlementReference(value, field) {
616
+ requireNonEmpty(value, field);
617
+ if (value.length > 160 ||
618
+ !/^[A-Za-z0-9._:-]+$/.test(value) ||
619
+ /^0x[a-f0-9]{16,}$/i.test(value) ||
620
+ /^(iota|sui)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{20,}$/i.test(value) ||
621
+ /(private prompt|review payload|bearer|access-token|signer_ref|payment credential|privateKey|mnemonic|seed|raw transaction|user signature)/i.test(value)) {
622
+ throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be an opaque public reference, not raw settlement material.`);
623
+ }
624
+ }
625
+ function requireFeeSplit(gross, providerNet, platformFee) {
626
+ requireReceiptAmount(gross, "amount");
627
+ requireReceiptAmount(providerNet, "providerNetAmount");
628
+ requireReceiptAmount(platformFee, "platformFeeAmount");
629
+ if (gross.asset !== providerNet.asset || gross.asset !== platformFee.asset) {
630
+ throw new ReceiptInputError("FIELD_REQUIRED", "Escrow fee split assets must match the gross amount asset.");
631
+ }
632
+ const grossAmount = parseDecimalAmount(gross.amount, "amount");
633
+ const providerNetAmount = parseDecimalAmount(providerNet.amount, "providerNetAmount");
634
+ const platformFeeAmount = parseDecimalAmount(platformFee.amount, "platformFeeAmount");
635
+ const scale = Math.max(grossAmount.scale, providerNetAmount.scale, platformFeeAmount.scale);
636
+ const grossUnits = scaleDecimal(grossAmount, scale);
637
+ const providerUnits = scaleDecimal(providerNetAmount, scale);
638
+ const platformUnits = scaleDecimal(platformFeeAmount, scale);
639
+ if (providerUnits + platformUnits !== grossUnits) {
640
+ throw new ReceiptInputError("FIELD_REQUIRED", "Escrow fee split must equal the gross receipt amount.");
641
+ }
642
+ }
643
+ function requireReceiptAmount(value, field) {
644
+ if (!value || typeof value !== "object") {
645
+ throw new ReceiptInputError("FIELD_REQUIRED", `${field} is required.`);
646
+ }
647
+ requireNonEmpty(value.amount, `${field}.amount`);
648
+ requireNonEmpty(value.asset, `${field}.asset`);
649
+ }
650
+ function parseDecimalAmount(value, field) {
651
+ if (value.length > 80) {
652
+ throw new ReceiptInputError("FIELD_REQUIRED", `${field} is too large.`);
653
+ }
654
+ if (!/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(value)) {
655
+ throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be a non-negative decimal amount.`);
656
+ }
657
+ const [whole, fraction = ""] = value.split(".");
658
+ if (fraction.length > 18) {
659
+ throw new ReceiptInputError("FIELD_REQUIRED", `${field} has too many decimal places.`);
660
+ }
661
+ return {
662
+ units: BigInt(`${whole}${fraction}`),
663
+ scale: fraction.length,
664
+ };
665
+ }
666
+ function scaleDecimal(value, targetScale) {
667
+ return value.units * (10n ** BigInt(targetScale - value.scale));
668
+ }
500
669
  function requireMatchingField(left, right, leftField, rightField) {
501
670
  if (left !== right) {
502
671
  throw new ReceiptInputError("FIELD_REQUIRED", `${leftField} must match ${rightField}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vallum/receipts",
3
- "version": "0.0.0-prerelease",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,6 @@
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public",
30
- "tag": "next"
30
+ "tag": "latest"
31
31
  }
32
32
  }