@totemsdk/edge-adapters 0.2.0 → 0.3.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/artifact-cache.d.ts +36 -0
- package/dist/artifact-cache.js +79 -0
- package/dist/cjs/artifact-cache.d.ts +36 -0
- package/dist/cjs/artifact-cache.js +82 -0
- package/dist/cjs/identity.d.ts +19 -0
- package/dist/cjs/identity.js +61 -0
- package/dist/cjs/index.d.ts +25 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/liquidity.d.ts +15 -0
- package/dist/cjs/liquidity.js +45 -0
- package/dist/cjs/location.d.ts +48 -0
- package/dist/cjs/location.js +208 -0
- package/dist/cjs/lookup.d.ts +13 -0
- package/dist/cjs/lookup.js +96 -0
- package/dist/cjs/manifest.d.ts +11 -0
- package/dist/cjs/manifest.js +36 -0
- package/dist/cjs/negotiation-transport.d.ts +50 -0
- package/dist/cjs/negotiation-transport.js +122 -0
- package/dist/cjs/omnia.d.ts +7 -0
- package/dist/cjs/omnia.js +46 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/payment-l1.d.ts +38 -0
- package/dist/cjs/payment-l1.js +46 -0
- package/dist/cjs/payment-l2.d.ts +39 -0
- package/dist/cjs/payment-l2.js +54 -0
- package/dist/cjs/policy.d.ts +13 -0
- package/dist/cjs/policy.js +95 -0
- package/dist/cjs/proof.d.ts +48 -0
- package/dist/cjs/proof.js +88 -0
- package/dist/cjs/pubsub.d.ts +11 -0
- package/dist/cjs/pubsub.js +30 -0
- package/dist/cjs/purchase-authority.d.ts +42 -0
- package/dist/cjs/purchase-authority.js +79 -0
- package/dist/cjs/purchase-lookup.d.ts +44 -0
- package/dist/cjs/purchase-lookup.js +60 -0
- package/dist/cjs/purchase-payment.d.ts +78 -0
- package/dist/cjs/purchase-payment.js +108 -0
- package/dist/cjs/sqlite-commerce-store.d.ts +69 -0
- package/dist/cjs/sqlite-commerce-store.js +416 -0
- package/dist/cjs/stream.d.ts +7 -0
- package/dist/cjs/stream.js +26 -0
- package/dist/identity.js +4 -7
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -27
- package/dist/liquidity.js +1 -4
- package/dist/location.js +10 -13
- package/dist/lookup.js +3 -6
- package/dist/manifest.js +5 -7
- package/dist/negotiation-transport.d.ts +50 -0
- package/dist/negotiation-transport.js +118 -0
- package/dist/omnia.js +1 -4
- package/dist/payment-l1.js +1 -4
- package/dist/payment-l2.js +5 -8
- package/dist/policy.js +1 -4
- package/dist/proof.js +7 -10
- package/dist/pubsub.js +1 -4
- package/dist/purchase-authority.d.ts +42 -0
- package/dist/purchase-authority.js +76 -0
- package/dist/purchase-lookup.d.ts +44 -0
- package/dist/purchase-lookup.js +57 -0
- package/dist/purchase-payment.d.ts +78 -0
- package/dist/purchase-payment.js +105 -0
- package/dist/sqlite-commerce-store.d.ts +69 -0
- package/dist/sqlite-commerce-store.js +411 -0
- package/dist/stream.js +1 -4
- package/package.json +37 -26
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-authority.ts — AuthorityPort adapter over @totemsdk/agent-policy.
|
|
3
|
+
*
|
|
4
|
+
* Maps a machine-commerce TradeAgreement + PurchaseIntent to an AgentProposal
|
|
5
|
+
* and evaluates it through a ComposablePolicy (or any AgentPolicy).
|
|
6
|
+
*
|
|
7
|
+
* Architectural boundary preserved:
|
|
8
|
+
* TradeProposal = machine ↔ machine bargaining (peer-to-peer)
|
|
9
|
+
* AgentProposal = agent → trusted local authority (wallet/policy)
|
|
10
|
+
*
|
|
11
|
+
* The machine negotiation strategy never receives a private key. This adapter
|
|
12
|
+
* is the trusted boundary that decides whether the local authority approves
|
|
13
|
+
* the economic commitment.
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentPolicy, PolicyMiddleware } from '@totemsdk/agent-policy';
|
|
16
|
+
import type { EdgeOperationResult } from '@totemsdk/edge';
|
|
17
|
+
import type { PurchaseIntent, TradeAgreement } from '@totemsdk/edge';
|
|
18
|
+
type PolicyLike = AgentPolicy | PolicyMiddleware;
|
|
19
|
+
export interface PurchaseAuthorityAdapterConfig {
|
|
20
|
+
/** The local policy (ComposablePolicy, AgentPolicy, or PolicyMiddleware). */
|
|
21
|
+
policy: PolicyLike;
|
|
22
|
+
/** Agent identifier used in the AgentProposal (e.g. 'edge-purchase-agent'). */
|
|
23
|
+
agentId?: string;
|
|
24
|
+
/** Optional risk level for the proposal. */
|
|
25
|
+
risk?: 'low' | 'medium' | 'high';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an AuthorityPort adapter over an agent-policy.
|
|
29
|
+
*
|
|
30
|
+
* approve() builds an AgentProposal from the agreement + intent and evaluates
|
|
31
|
+
* it through the policy. Returns `{ allowed, reason }`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createPurchaseAuthorityAdapter(config: PurchaseAuthorityAdapterConfig): {
|
|
34
|
+
approve(params: {
|
|
35
|
+
agreement: TradeAgreement;
|
|
36
|
+
intent: PurchaseIntent;
|
|
37
|
+
}): Promise<EdgeOperationResult<{
|
|
38
|
+
allowed: boolean;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}>>;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-authority.ts — AuthorityPort adapter over @totemsdk/agent-policy.
|
|
3
|
+
*
|
|
4
|
+
* Maps a machine-commerce TradeAgreement + PurchaseIntent to an AgentProposal
|
|
5
|
+
* and evaluates it through a ComposablePolicy (or any AgentPolicy).
|
|
6
|
+
*
|
|
7
|
+
* Architectural boundary preserved:
|
|
8
|
+
* TradeProposal = machine ↔ machine bargaining (peer-to-peer)
|
|
9
|
+
* AgentProposal = agent → trusted local authority (wallet/policy)
|
|
10
|
+
*
|
|
11
|
+
* The machine negotiation strategy never receives a private key. This adapter
|
|
12
|
+
* is the trusted boundary that decides whether the local authority approves
|
|
13
|
+
* the economic commitment.
|
|
14
|
+
*/
|
|
15
|
+
function isMiddleware(p) {
|
|
16
|
+
return 'evaluate' in p && typeof p.evaluate === 'function';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an AuthorityPort adapter over an agent-policy.
|
|
20
|
+
*
|
|
21
|
+
* approve() builds an AgentProposal from the agreement + intent and evaluates
|
|
22
|
+
* it through the policy. Returns `{ allowed, reason }`.
|
|
23
|
+
*/
|
|
24
|
+
export function createPurchaseAuthorityAdapter(config) {
|
|
25
|
+
const { policy } = config;
|
|
26
|
+
const agentId = config.agentId ?? 'edge-purchase-agent';
|
|
27
|
+
const risk = config.risk ?? 'medium';
|
|
28
|
+
return {
|
|
29
|
+
async approve(params) {
|
|
30
|
+
try {
|
|
31
|
+
const { agreement, intent } = params;
|
|
32
|
+
const proposal = {
|
|
33
|
+
id: `edge-purchase:${agreement.agreementId}`,
|
|
34
|
+
agentId,
|
|
35
|
+
intent: {
|
|
36
|
+
type: 'payment',
|
|
37
|
+
amount: agreement.terms.price,
|
|
38
|
+
tokenId: agreement.terms.tokenId,
|
|
39
|
+
recipient: agreement.seller,
|
|
40
|
+
reason: `purchase ${intent.resource} from ${agreement.seller}`,
|
|
41
|
+
risk,
|
|
42
|
+
metadata: {
|
|
43
|
+
agreementId: agreement.agreementId,
|
|
44
|
+
negotiationId: agreement.negotiationId,
|
|
45
|
+
resource: intent.resource,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
explanation: `Machine purchase of ${intent.resource} for ${agreement.terms.price} ${agreement.terms.tokenId ?? '0x00'} from ${agreement.seller}`,
|
|
49
|
+
confidence: 1,
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
if (isMiddleware(policy)) {
|
|
53
|
+
const result = await policy.evaluate(proposal);
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
data: {
|
|
57
|
+
allowed: result.outcome === 'approved',
|
|
58
|
+
reason: result.reason,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const allowed = await policy.canAutoApprove(proposal);
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
data: {
|
|
66
|
+
allowed,
|
|
67
|
+
reason: allowed ? undefined : 'Purchase requires user approval per configured policy',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-lookup.ts — PurchaseLookupPort adapter over @totemsdk/lookup-client.
|
|
3
|
+
*
|
|
4
|
+
* Maps a machine-commerce `resource` (e.g. "compute", "storage") to a
|
|
5
|
+
* capability query on the Lookup Node, returning signed manifests for the
|
|
6
|
+
* buyer's discovery flow.
|
|
7
|
+
*
|
|
8
|
+
* Discovery is publication, not authority: the buyer MUST still verify each
|
|
9
|
+
* returned manifest's signature, identity binding, expiry, and capabilities
|
|
10
|
+
* before negotiation/payment. This adapter only fetches candidates.
|
|
11
|
+
*/
|
|
12
|
+
import type { LookupClient } from '@totemsdk/lookup-client';
|
|
13
|
+
import type { EdgeOperationResult } from '@totemsdk/edge';
|
|
14
|
+
export interface PurchaseLookupAdapterConfig {
|
|
15
|
+
client: LookupClient;
|
|
16
|
+
/** Optional provider address filter (authorAddress for apps). */
|
|
17
|
+
provider?: string;
|
|
18
|
+
/** Optional max price filter (agents). */
|
|
19
|
+
maxPricePerCall?: number;
|
|
20
|
+
/** Optional max latency filter (agents). */
|
|
21
|
+
maxLatencyMs?: number;
|
|
22
|
+
/** Optional result limit. */
|
|
23
|
+
limit?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a PurchaseLookupPort adapter over a LookupClient.
|
|
27
|
+
*
|
|
28
|
+
* `resource` is treated as a capability name for agent manifests (the
|
|
29
|
+
* machine-commerce case). If the resource matches a known app category, it
|
|
30
|
+
* falls back to app queries. Returns `{ id, manifest: Uint8Array, nodeId }`
|
|
31
|
+
* candidates for the buyer's manifest-verification pipeline.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createPurchaseLookupAdapter(config: PurchaseLookupAdapterConfig): {
|
|
34
|
+
query(params: {
|
|
35
|
+
resource: string;
|
|
36
|
+
provider?: string;
|
|
37
|
+
}): Promise<EdgeOperationResult<{
|
|
38
|
+
results: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
manifest: Uint8Array;
|
|
41
|
+
nodeId: string;
|
|
42
|
+
}>;
|
|
43
|
+
}>>;
|
|
44
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-lookup.ts — PurchaseLookupPort adapter over @totemsdk/lookup-client.
|
|
3
|
+
*
|
|
4
|
+
* Maps a machine-commerce `resource` (e.g. "compute", "storage") to a
|
|
5
|
+
* capability query on the Lookup Node, returning signed manifests for the
|
|
6
|
+
* buyer's discovery flow.
|
|
7
|
+
*
|
|
8
|
+
* Discovery is publication, not authority: the buyer MUST still verify each
|
|
9
|
+
* returned manifest's signature, identity binding, expiry, and capabilities
|
|
10
|
+
* before negotiation/payment. This adapter only fetches candidates.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Create a PurchaseLookupPort adapter over a LookupClient.
|
|
14
|
+
*
|
|
15
|
+
* `resource` is treated as a capability name for agent manifests (the
|
|
16
|
+
* machine-commerce case). If the resource matches a known app category, it
|
|
17
|
+
* falls back to app queries. Returns `{ id, manifest: Uint8Array, nodeId }`
|
|
18
|
+
* candidates for the buyer's manifest-verification pipeline.
|
|
19
|
+
*/
|
|
20
|
+
export function createPurchaseLookupAdapter(config) {
|
|
21
|
+
const { client } = config;
|
|
22
|
+
return {
|
|
23
|
+
async query(params) {
|
|
24
|
+
try {
|
|
25
|
+
const provider = params.provider ?? config.provider;
|
|
26
|
+
const limit = config.limit ?? 20;
|
|
27
|
+
// Agent capability query (machine-commerce primary path).
|
|
28
|
+
const agents = await client.queryAgents({
|
|
29
|
+
capabilityName: params.resource,
|
|
30
|
+
maxPricePerCall: config.maxPricePerCall,
|
|
31
|
+
maxLatencyMs: config.maxLatencyMs,
|
|
32
|
+
limit,
|
|
33
|
+
});
|
|
34
|
+
const results = agents.map((a) => ({
|
|
35
|
+
id: a.capabilityId,
|
|
36
|
+
manifest: a.manifest,
|
|
37
|
+
nodeId: a.nodeId,
|
|
38
|
+
}));
|
|
39
|
+
// If no agent results and a provider is specified, also try app query
|
|
40
|
+
// filtered by author address (provider = operator).
|
|
41
|
+
if (results.length === 0 && provider) {
|
|
42
|
+
const apps = await client.queryApps({
|
|
43
|
+
authorAddress: provider,
|
|
44
|
+
limit,
|
|
45
|
+
});
|
|
46
|
+
for (const a of apps) {
|
|
47
|
+
results.push({ id: a.appId, manifest: a.manifest, nodeId: a.nodeId });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, data: { results } };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-payment.ts — PurchasePaymentPort adapter with an atomic claim.
|
|
3
|
+
*
|
|
4
|
+
* Wraps any EdgePaymentPort (L1 Minima, L2 Omnia, hosted) and adds a stable
|
|
5
|
+
* idempotency-key claim so a retry after a lost response never double-pays.
|
|
6
|
+
*
|
|
7
|
+
* The idempotency key is the stable purchase operation identity
|
|
8
|
+
* (`purchaseId:agreementId:payment`) supplied by the purchasing layer. The
|
|
9
|
+
* wrapper records an atomic pending marker (claim) *before* it invokes the
|
|
10
|
+
* underlying payment port; only the caller that wins the claim pays. A retry
|
|
11
|
+
* with the same key returns the prior result without re-invoking the port, and
|
|
12
|
+
* a concurrent duplicate never double-pays — it either replays the completed
|
|
13
|
+
* outcome or surfaces `PAYMENT_STATE_UNKNOWN` for an in-flight claim.
|
|
14
|
+
*
|
|
15
|
+
* Final exactly-once still depends on the payment system's own reconciliation:
|
|
16
|
+
* if the process dies between the port accepting the payment and the outcome
|
|
17
|
+
* being recorded, the key stays `pending`, and retries return
|
|
18
|
+
* `PAYMENT_STATE_UNKNOWN` rather than risking a second payment. The owner
|
|
19
|
+
* reconciles that window against the payment ledger.
|
|
20
|
+
*/
|
|
21
|
+
import type { EdgeOperationResult } from '@totemsdk/edge';
|
|
22
|
+
import { type CasStore, type StorageAdapterWithCapabilities, type WriteAckMode } from '@totemsdk/storage';
|
|
23
|
+
export interface PaymentResult {
|
|
24
|
+
txpowId?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PaymentPortLike {
|
|
27
|
+
pay(params: {
|
|
28
|
+
recipient: string;
|
|
29
|
+
amount: string;
|
|
30
|
+
tokenId?: string;
|
|
31
|
+
memo?: string;
|
|
32
|
+
idempotencyKey?: string;
|
|
33
|
+
}): Promise<EdgeOperationResult<PaymentResult>>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Idempotency claim record. `pending` is written atomically before the port
|
|
37
|
+
* call; `completed` records the outcome. A `pending` key is never re-claimed
|
|
38
|
+
* automatically — that is the double-pay guard.
|
|
39
|
+
*/
|
|
40
|
+
export type PurchasePaymentClaimRecord = {
|
|
41
|
+
phase: 'pending';
|
|
42
|
+
claimedAt: number;
|
|
43
|
+
} | {
|
|
44
|
+
phase: 'completed';
|
|
45
|
+
result: EdgeOperationResult<PaymentResult>;
|
|
46
|
+
completedAt: number;
|
|
47
|
+
};
|
|
48
|
+
/** The durable idempotency store must support conditional (CAS) writes. */
|
|
49
|
+
export type PurchasePaymentStore = StorageAdapterWithCapabilities & CasStore;
|
|
50
|
+
export interface PurchasePaymentAdapterConfig {
|
|
51
|
+
/** The underlying payment port (L1/L2/hosted). */
|
|
52
|
+
port: PaymentPortLike;
|
|
53
|
+
/**
|
|
54
|
+
* Optional claim store. When omitted, a dev in-memory store is used (no
|
|
55
|
+
* crash guarantees and no cross-instance dedup). A durable CAS-capable
|
|
56
|
+
* store (e.g. a `@totemsdk/storage` FileStore/SqliteStore) makes retries
|
|
57
|
+
* safe across restarts and processes. No silent downgrade: a supplied
|
|
58
|
+
* store is asserted CAS-capable and `durably-acknowledged` unless
|
|
59
|
+
* `requireAckMode` opts into a weaker acknowledgment.
|
|
60
|
+
*/
|
|
61
|
+
store?: PurchasePaymentStore;
|
|
62
|
+
/** Key namespace prefix; default `totem_payment:v1:`. */
|
|
63
|
+
namespace?: string;
|
|
64
|
+
/** Required write acknowledgment for a supplied store; default `durably-acknowledged`. */
|
|
65
|
+
requireAckMode?: WriteAckMode;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a PurchasePaymentPort with an atomic idempotency-key claim.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createPurchasePaymentAdapter(config: PurchasePaymentAdapterConfig): {
|
|
71
|
+
pay(params: {
|
|
72
|
+
recipient: string;
|
|
73
|
+
amount: string;
|
|
74
|
+
tokenId?: string;
|
|
75
|
+
memo?: string;
|
|
76
|
+
idempotencyKey?: string;
|
|
77
|
+
}): Promise<EdgeOperationResult<PaymentResult>>;
|
|
78
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* purchase-payment.ts — PurchasePaymentPort adapter with an atomic claim.
|
|
3
|
+
*
|
|
4
|
+
* Wraps any EdgePaymentPort (L1 Minima, L2 Omnia, hosted) and adds a stable
|
|
5
|
+
* idempotency-key claim so a retry after a lost response never double-pays.
|
|
6
|
+
*
|
|
7
|
+
* The idempotency key is the stable purchase operation identity
|
|
8
|
+
* (`purchaseId:agreementId:payment`) supplied by the purchasing layer. The
|
|
9
|
+
* wrapper records an atomic pending marker (claim) *before* it invokes the
|
|
10
|
+
* underlying payment port; only the caller that wins the claim pays. A retry
|
|
11
|
+
* with the same key returns the prior result without re-invoking the port, and
|
|
12
|
+
* a concurrent duplicate never double-pays — it either replays the completed
|
|
13
|
+
* outcome or surfaces `PAYMENT_STATE_UNKNOWN` for an in-flight claim.
|
|
14
|
+
*
|
|
15
|
+
* Final exactly-once still depends on the payment system's own reconciliation:
|
|
16
|
+
* if the process dies between the port accepting the payment and the outcome
|
|
17
|
+
* being recorded, the key stays `pending`, and retries return
|
|
18
|
+
* `PAYMENT_STATE_UNKNOWN` rather than risking a second payment. The owner
|
|
19
|
+
* reconciles that window against the payment ledger.
|
|
20
|
+
*/
|
|
21
|
+
import { MemoryStore, assertCapabilities, } from '@totemsdk/storage';
|
|
22
|
+
const DEFAULT_NAMESPACE = 'totem_payment:v1:';
|
|
23
|
+
/** In-process serialization so concurrent claims never double-pay on one store. */
|
|
24
|
+
const locks = new WeakMap();
|
|
25
|
+
function withPaymentLock(store, fn) {
|
|
26
|
+
const tail = locks.get(store);
|
|
27
|
+
const next = tail ? tail.then(fn, fn) : fn();
|
|
28
|
+
const settled = next.catch(() => undefined);
|
|
29
|
+
locks.set(store, settled);
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a PurchasePaymentPort with an atomic idempotency-key claim.
|
|
34
|
+
*/
|
|
35
|
+
export function createPurchasePaymentAdapter(config) {
|
|
36
|
+
const { port } = config;
|
|
37
|
+
const namespace = config.namespace ?? DEFAULT_NAMESPACE;
|
|
38
|
+
const store = config.store ??
|
|
39
|
+
new MemoryStore(); // dev mode — volatile, process-local
|
|
40
|
+
if (config.store) {
|
|
41
|
+
// No silent downgrade: a consumer-supplied store must be able to claim
|
|
42
|
+
// (CAS) and acknowledge durable writes unless explicitly opted out.
|
|
43
|
+
assertCapabilities(store, {
|
|
44
|
+
conditional: true,
|
|
45
|
+
acknowledge: config.requireAckMode ?? 'durably-acknowledged',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const keyFor = (idempotencyKey) => namespace + idempotencyKey;
|
|
49
|
+
return {
|
|
50
|
+
async pay(params) {
|
|
51
|
+
const key = params.idempotencyKey;
|
|
52
|
+
if (!key) {
|
|
53
|
+
// No idempotency possible without a stable key — pass straight through.
|
|
54
|
+
return port.pay(params);
|
|
55
|
+
}
|
|
56
|
+
// The whole claim→pay→complete path is serialized per store so that N
|
|
57
|
+
// concurrent same-key callers resolve consistently: the winner pays, and
|
|
58
|
+
// every loser replays the completed outcome rather than racing the
|
|
59
|
+
// in-flight window.
|
|
60
|
+
return withPaymentLock(store, async () => {
|
|
61
|
+
const claimKey = keyFor(key);
|
|
62
|
+
// Atomic claim: exactly one caller wins `pending`; concurrent duplicates
|
|
63
|
+
// either replay a completed outcome or get PAYMENT_STATE_UNKNOWN.
|
|
64
|
+
const claim = await store.conditionalUpdate(claimKey, (current) => {
|
|
65
|
+
if (current === null || current === undefined) {
|
|
66
|
+
return { next: { phase: 'pending', claimedAt: Date.now() } };
|
|
67
|
+
}
|
|
68
|
+
return { abort: 'already-claimed' };
|
|
69
|
+
});
|
|
70
|
+
if (!claim.applied) {
|
|
71
|
+
const existing = await store.get(claimKey);
|
|
72
|
+
if (existing && existing.phase === 'completed') {
|
|
73
|
+
return existing.result;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
errorCode: 'PAYMENT_STATE_UNKNOWN',
|
|
78
|
+
error: `payment for idempotency key '${key}' is already in flight`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await port.pay(params);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
result = {
|
|
87
|
+
ok: false,
|
|
88
|
+
errorCode: 'PAYMENT_STATE_UNKNOWN',
|
|
89
|
+
error: `payment port threw: ${err.message}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Record the outcome only if we still own the pending claim.
|
|
93
|
+
await store.conditionalUpdate(claimKey, (current) => {
|
|
94
|
+
if (current !== null && current !== undefined && current.phase === 'pending') {
|
|
95
|
+
return {
|
|
96
|
+
next: { phase: 'completed', result, completedAt: Date.now() },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { abort: 'gone' };
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite-commerce-store.ts — Production SQLite-backed CommerceStore.
|
|
3
|
+
*
|
|
4
|
+
* Implements the five durable commerce contracts over ONE SQLite database:
|
|
5
|
+
* NegotiationStore, PurchaseStore, ReplayLedger, PrincipalNegotiationStore,
|
|
6
|
+
* OutboxStore.
|
|
7
|
+
*
|
|
8
|
+
* The database connection, journal/busy pragmas, transaction lifecycle, and
|
|
9
|
+
* CAS kinematics are owned by the shared `SqliteStore` primitive from
|
|
10
|
+
* `@totemsdk/storage/sqlite` (RFC-007 §4.2). All consumer mutations run through
|
|
11
|
+
* `transactionalSync` (atomic multi-statement batches) or the generalized
|
|
12
|
+
* `transitionAndEnqueue` (revision-CAS update + outbox-style enqueue in one
|
|
13
|
+
* commit). Table schemas and row shapes are unchanged ("records unchanged in
|
|
14
|
+
* shape").
|
|
15
|
+
*
|
|
16
|
+
* The critical guarantee — `transitionAndEnqueue` (negotiation CAS + outbox
|
|
17
|
+
* enqueue) — maps to ONE transaction on the shared primitive:
|
|
18
|
+
*
|
|
19
|
+
* BEGIN
|
|
20
|
+
* UPDATE negotiations SET ... WHERE negotiation_id=? AND revision=?
|
|
21
|
+
* INSERT INTO outbox (...) VALUES (...)
|
|
22
|
+
* COMMIT (or ROLLBACK on CAS miss — nothing is enqueued)
|
|
23
|
+
*
|
|
24
|
+
* Uses WAL mode + busy_timeout for concurrent runtime use. Does NOT persist
|
|
25
|
+
* private keys — only stable references/IDs.
|
|
26
|
+
*
|
|
27
|
+
* All statements use positional `?` parameters so the same SQL runs on the
|
|
28
|
+
* real better-sqlite3 native binding and the node:sqlite jest mock.
|
|
29
|
+
*/
|
|
30
|
+
import type { NegotiationStore, PrincipalNegotiationStore, PurchaseStore } from '@totemsdk/edge';
|
|
31
|
+
import type { ReplayLedger } from '@totemsdk/edge';
|
|
32
|
+
import type { OutboxStore } from '@totemsdk/edge';
|
|
33
|
+
/** Aggregated durable commerce store. */
|
|
34
|
+
export interface CommerceStore {
|
|
35
|
+
negotiations: NegotiationStore;
|
|
36
|
+
purchases: PurchaseStore;
|
|
37
|
+
replay: ReplayLedger;
|
|
38
|
+
principals: PrincipalNegotiationStore;
|
|
39
|
+
outbox: OutboxStore;
|
|
40
|
+
}
|
|
41
|
+
export interface SQLiteCommerceStoreConfig {
|
|
42
|
+
/** File path, or ':memory:' for ephemeral. */
|
|
43
|
+
filename: string;
|
|
44
|
+
/** WAL mode (default true). */
|
|
45
|
+
wal?: boolean;
|
|
46
|
+
/** Busy timeout ms (default 5000). */
|
|
47
|
+
busyTimeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
export declare class SQLiteCommerceStore implements CommerceStore {
|
|
50
|
+
readonly negotiations: NegotiationStore;
|
|
51
|
+
readonly purchases: PurchaseStore;
|
|
52
|
+
readonly replay: ReplayLedger;
|
|
53
|
+
readonly principals: PrincipalNegotiationStore;
|
|
54
|
+
readonly outbox: OutboxStore;
|
|
55
|
+
private readonly store;
|
|
56
|
+
constructor(config: SQLiteCommerceStoreConfig);
|
|
57
|
+
close(): void;
|
|
58
|
+
private createNegotiationStore;
|
|
59
|
+
private createPurchaseStore;
|
|
60
|
+
private createReplayLedger;
|
|
61
|
+
private createPrincipalStore;
|
|
62
|
+
private createOutboxStore;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a SQLite-backed CommerceStore.
|
|
66
|
+
*
|
|
67
|
+
* @param config filename (or ':memory:') + WAL/busy options.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSQLiteCommerceStore(config: SQLiteCommerceStoreConfig): SQLiteCommerceStore;
|