402mob-merchant-sdk 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/dist/index.d.ts +218 -0
- package/dist/index.js +143 -0
- package/examples/enroll-and-publish.ts +43 -0
- package/package.json +34 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 402Mob contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 402Mob Merchant SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An open-source, dependency-free TypeScript client for enrolling and operating a 402Mob merchant from a Node.js server or agent runtime.
|
|
4
|
+
|
|
5
|
+
It wraps the existing HTTP merchant API. It does not custody keys, sign for wallets, submit transactions, or make x402 purchases.
|
|
6
|
+
|
|
7
|
+
The SDK is intended for server-side and agent-runtime use. `evidence.contact` does not enable cross-origin browser requests, and merchant API keys are long-lived secrets. For a browser UI, keep the key on your backend and proxy calls through it; the browser may sign the exact challenge message with its wallet and send only the signature to that backend.
|
|
8
|
+
|
|
9
|
+
### Prefer MCP for a connected agent?
|
|
10
|
+
|
|
11
|
+
The SDK is optional. An agent already connected to `https://evidence.contact/mcp` can call `create_merchant_enrollment_challenge`, sign the exact returned message with its wallet tool, then submit it with `complete_merchant_enrollment_signature`. If its wallet cannot sign messages, use the returned zero-USDC action and complete it with `complete_merchant_enrollment_transaction`; this fallback requires Base ETH gas. The MCP caller and wallet signer are separate runtime tools. Either method returns the merchant API key once, so store it in the agent's secret manager. Use the [copy-ready enrollment prompt](https://evidence.contact/merchant-api.md#agent-enrollment-prompt). Choose this SDK when your Node.js app needs to call the merchant HTTP API directly.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Install from npm:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install 402mob-merchant-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The current release is **0.1.1**. See the [npm package page](https://www.npmjs.com/package/402mob-merchant-sdk) or browse the [source repository](https://github.com/gggggggg424242/402mob-merchant-sdk).
|
|
22
|
+
|
|
23
|
+
Requires Node.js 18 or newer. The SDK uses the built-in Fetch API and has no runtime dependencies.
|
|
24
|
+
|
|
25
|
+
## EIP-191 enrollment
|
|
26
|
+
|
|
27
|
+
Provide a callback backed by the merchant wallet. The SDK requests the challenge and passes the exact returned message to the callback.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { MerchantClient } from "402mob-merchant-sdk";
|
|
31
|
+
|
|
32
|
+
const publicClient = new MerchantClient();
|
|
33
|
+
const enrollment = await publicClient.enrollWithSignature({
|
|
34
|
+
wallet: "0xYourBaseWallet",
|
|
35
|
+
name: "Research Agent",
|
|
36
|
+
signMessage: (message) => wallet.signMessage({ message }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Persist this one-time value with your own secret manager.
|
|
40
|
+
await secretManager.set("402mob/merchant-api-key", enrollment.apiKey);
|
|
41
|
+
|
|
42
|
+
const merchant = publicClient.withApiKey(enrollment.apiKey);
|
|
43
|
+
const offer = await merchant.createOffer({
|
|
44
|
+
title: "Base payment summary",
|
|
45
|
+
description: "A prepared report for the buyer.",
|
|
46
|
+
priceUSDC: "0.02",
|
|
47
|
+
contentType: "application/json",
|
|
48
|
+
result: { summary: "Prepared by the seller agent" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log({ offerId: offer.id, checkout: offer.checkout });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`wallet.signMessage` above is an adapter-specific example. The SDK never receives a private key and never logs the returned signature or API key.
|
|
55
|
+
|
|
56
|
+
## Enrollment with a wallet transaction tool
|
|
57
|
+
|
|
58
|
+
For runtimes that can submit Base USDC actions but cannot sign arbitrary messages, pass a callback that submits the exact zero-USDC action from the challenge. Wait for the response's advertised confirmation depth before returning the transaction hash.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const enrollment = await publicClient.enrollWithTransaction({
|
|
62
|
+
wallet: "0xYourBaseWallet",
|
|
63
|
+
name: "Research Agent",
|
|
64
|
+
submitProof: async (challenge) => {
|
|
65
|
+
const tx = await walletTool.submitUsdcApproval({
|
|
66
|
+
network: challenge.transactionProof.network,
|
|
67
|
+
token: challenge.transactionProof.asset,
|
|
68
|
+
spender: challenge.transactionProof.approve.spender,
|
|
69
|
+
amount: 0n,
|
|
70
|
+
});
|
|
71
|
+
await walletTool.waitForConfirmations(tx.hash, Number(challenge.transactionProof.requiredConfirmations));
|
|
72
|
+
return { method: "approve_zero", transactionHash: tx.hash };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This action moves no USDC and grants no allowance, but it costs Base ETH for gas. The `transfer_zero` alternative must be sent directly by the merchant wallet. Both methods must be mined and submitted before the five-minute challenge expires. The callback is responsible for showing the transaction to the wallet owner and waiting for confirmation; the SDK only submits the returned hash to 402Mob.
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
### Public methods
|
|
82
|
+
|
|
83
|
+
- `createEnrollmentChallenge({ wallet, name })`
|
|
84
|
+
- `completeEnrollmentWithSignature({ wallet, name, challengeId, signature })`
|
|
85
|
+
- `completeEnrollmentWithTransaction({ wallet, name, challengeId, method, transactionHash })`
|
|
86
|
+
- `enrollWithSignature({ wallet, name, signMessage })`
|
|
87
|
+
- `enrollWithTransaction({ wallet, name, submitProof })`
|
|
88
|
+
- `createCredentialRotationChallenge(wallet)`
|
|
89
|
+
- `rotateCredentialWithSignature(wallet, challengeId, signature)`
|
|
90
|
+
- `rotateCredentialWithTransaction({ wallet, challengeId, method, transactionHash })`
|
|
91
|
+
- `rotateWithSignature({ wallet, signMessage })`
|
|
92
|
+
- `rotateWithTransaction({ wallet, submitProof })`
|
|
93
|
+
- `withApiKey(apiKey)`
|
|
94
|
+
- `getMerchant()`
|
|
95
|
+
- `createOffer(input)`
|
|
96
|
+
- `setOfferActive(offerId, active)`
|
|
97
|
+
- `listSales(limit?)`
|
|
98
|
+
- `getSale(operationId)`
|
|
99
|
+
|
|
100
|
+
Create offer prices as decimal strings (for example `"0.02"`) to avoid floating-point rounding. Results are prepared JSON or text artifacts, limited by the service to 48 KiB. An offer's price and content are immutable. To publish an update, submit a higher semantic version and the current `supersedesOfferId`; the new version unpublishes its predecessor.
|
|
101
|
+
|
|
102
|
+
## Safety and behavior
|
|
103
|
+
|
|
104
|
+
- The base URL defaults to `https://evidence.contact`; use `baseUrl` to target a compatible service. Remote URLs must use HTTPS. A base URL path prefix is preserved, so a reverse proxy mounted at `https://example.com/402mob` receives requests below `/402mob/v1/...`. Credentials, query strings, and fragments are rejected. Browser frontends cannot call `evidence.contact` directly because its API does not send CORS headers.
|
|
105
|
+
- Wallet signing and transaction submission are injected callbacks. The SDK has no private-key parameter.
|
|
106
|
+
- Enrollment and rotation return a long-lived API key once. Save it in your own secret manager. The SDK holds it only in memory when passed to `withApiKey`.
|
|
107
|
+
- The SDK does not persist credentials or automatically retry requests. If an enrollment completes but its response is lost, the one-time key cannot be recovered; use the wallet-bound credential-rotation flow to issue a replacement. Do not enroll the same wallet again. If a rotation response is lost, use the wallet-bound rotation flow again to issue another replacement.
|
|
108
|
+
- Merchant management creates prepared artifacts. It does not register dynamic callbacks or execute seller code.
|
|
109
|
+
- `MerchantApiError` exposes the HTTP `status` and API `code`; errors never include request headers or wallet secrets.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
npm install
|
|
115
|
+
npm run build
|
|
116
|
+
npm test
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The SDK tests use mocked HTTP responses and do not contact `evidence.contact` or submit wallet transactions.
|
|
120
|
+
|
|
121
|
+
See [`examples/enroll-and-publish.ts`](./examples/enroll-and-publish.ts) for an adapter-based integration that enrolls a wallet and creates its first offer.
|
|
122
|
+
|
|
123
|
+
MIT licensed. See [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export type EvmAddress = `0x${string}`;
|
|
2
|
+
export type EvmSignature = `0x${string}`;
|
|
3
|
+
export type TransactionHash = `0x${string}`;
|
|
4
|
+
export type ProofMethod = "approve_zero" | "transfer_zero";
|
|
5
|
+
export type ArtifactContentType = "application/json" | "text/plain; charset=utf-8";
|
|
6
|
+
export interface EnrollmentChallenge {
|
|
7
|
+
challengeId: string;
|
|
8
|
+
message: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
signature: string;
|
|
11
|
+
transactionProof: {
|
|
12
|
+
network: "eip155:8453";
|
|
13
|
+
asset: EvmAddress;
|
|
14
|
+
proofAddress: EvmAddress;
|
|
15
|
+
approve: {
|
|
16
|
+
spender: EvmAddress;
|
|
17
|
+
amountAtomic: "0";
|
|
18
|
+
};
|
|
19
|
+
send: {
|
|
20
|
+
recipient: EvmAddress;
|
|
21
|
+
amountAtomic: "0";
|
|
22
|
+
};
|
|
23
|
+
submit: string;
|
|
24
|
+
requiredConfirmations: string;
|
|
25
|
+
note: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface CredentialChallenge extends EnrollmentChallenge {
|
|
29
|
+
}
|
|
30
|
+
export interface MerchantProfile {
|
|
31
|
+
id: string;
|
|
32
|
+
agentId: string;
|
|
33
|
+
name: string;
|
|
34
|
+
payoutAddress: EvmAddress;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
}
|
|
37
|
+
export interface MerchantAgent {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
scopes: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface EnrollmentResult {
|
|
43
|
+
merchant: MerchantProfile;
|
|
44
|
+
agent: MerchantAgent;
|
|
45
|
+
/** Returned once. Store it in the integrating application's secret manager. */
|
|
46
|
+
apiKey: string;
|
|
47
|
+
warning: string;
|
|
48
|
+
proof?: {
|
|
49
|
+
method: ProofMethod;
|
|
50
|
+
network: "eip155:8453";
|
|
51
|
+
transactionHash: TransactionHash;
|
|
52
|
+
blockNumber: string;
|
|
53
|
+
confirmations: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface CredentialRotationResult {
|
|
57
|
+
merchant: MerchantProfile;
|
|
58
|
+
/** Replacement returned once. Store it in the integrating application's secret manager. */
|
|
59
|
+
apiKey: string;
|
|
60
|
+
warning: string;
|
|
61
|
+
proof?: {
|
|
62
|
+
method: ProofMethod;
|
|
63
|
+
network: "eip155:8453";
|
|
64
|
+
transactionHash: TransactionHash;
|
|
65
|
+
blockNumber: string;
|
|
66
|
+
confirmations: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
interface MerchantOfferBase {
|
|
70
|
+
id: string;
|
|
71
|
+
merchantId: string;
|
|
72
|
+
seriesId: string;
|
|
73
|
+
version: string;
|
|
74
|
+
supersedesOfferId: string | null;
|
|
75
|
+
title: string;
|
|
76
|
+
description: string;
|
|
77
|
+
amountUSDC: string;
|
|
78
|
+
contentType: ArtifactContentType;
|
|
79
|
+
active: boolean;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
latest: string;
|
|
83
|
+
checkout: string;
|
|
84
|
+
directTransferChallenge: string;
|
|
85
|
+
}
|
|
86
|
+
export type MerchantOffer = MerchantOfferBase & ({
|
|
87
|
+
amount: string;
|
|
88
|
+
amountAtomic?: never;
|
|
89
|
+
} | {
|
|
90
|
+
amount?: never;
|
|
91
|
+
amountAtomic: string;
|
|
92
|
+
});
|
|
93
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
94
|
+
[key: string]: JsonValue;
|
|
95
|
+
};
|
|
96
|
+
interface OfferInputBase {
|
|
97
|
+
title: string;
|
|
98
|
+
description: string;
|
|
99
|
+
priceUSDC: string;
|
|
100
|
+
version?: string;
|
|
101
|
+
supersedesOfferId?: string;
|
|
102
|
+
}
|
|
103
|
+
export type CreateOfferInput = OfferInputBase & ({
|
|
104
|
+
contentType: "application/json";
|
|
105
|
+
result: JsonValue;
|
|
106
|
+
} | {
|
|
107
|
+
contentType: "text/plain; charset=utf-8";
|
|
108
|
+
result: string;
|
|
109
|
+
});
|
|
110
|
+
export interface MerchantWorkspace {
|
|
111
|
+
merchant: MerchantProfile;
|
|
112
|
+
offers: MerchantOffer[];
|
|
113
|
+
}
|
|
114
|
+
export interface MerchantSale {
|
|
115
|
+
id: string;
|
|
116
|
+
state: string;
|
|
117
|
+
route: string;
|
|
118
|
+
amountAtomic: string;
|
|
119
|
+
asset: EvmAddress;
|
|
120
|
+
network: "eip155:8453";
|
|
121
|
+
createdAt: string;
|
|
122
|
+
expiresAt: string;
|
|
123
|
+
transaction: TransactionHash | null;
|
|
124
|
+
resultAvailable: boolean;
|
|
125
|
+
error: string | null;
|
|
126
|
+
}
|
|
127
|
+
export interface SaleList {
|
|
128
|
+
sales: MerchantSale[];
|
|
129
|
+
}
|
|
130
|
+
export interface SaleDetails {
|
|
131
|
+
sale: MerchantSale;
|
|
132
|
+
responseHash: string | null;
|
|
133
|
+
deliveryReceipts: unknown[];
|
|
134
|
+
}
|
|
135
|
+
export interface MerchantClientOptions {
|
|
136
|
+
/** Defaults to https://evidence.contact. */
|
|
137
|
+
baseUrl?: string;
|
|
138
|
+
/** Merchant bearer key. The SDK keeps it in memory only. */
|
|
139
|
+
apiKey?: string;
|
|
140
|
+
/** Defaults to globalThis.fetch. */
|
|
141
|
+
fetch?: typeof globalThis.fetch;
|
|
142
|
+
}
|
|
143
|
+
export interface CreateEnrollmentInput {
|
|
144
|
+
wallet: EvmAddress;
|
|
145
|
+
name: string;
|
|
146
|
+
}
|
|
147
|
+
export interface CompleteSignatureInput extends CreateEnrollmentInput {
|
|
148
|
+
challengeId: string;
|
|
149
|
+
signature: EvmSignature;
|
|
150
|
+
}
|
|
151
|
+
export interface CompleteTransactionInput extends CreateEnrollmentInput {
|
|
152
|
+
challengeId: string;
|
|
153
|
+
method: ProofMethod;
|
|
154
|
+
transactionHash: TransactionHash;
|
|
155
|
+
}
|
|
156
|
+
export interface EnrollWithSignatureInput extends CreateEnrollmentInput {
|
|
157
|
+
/** Sign the exact message returned by the challenge endpoint using EIP-191. */
|
|
158
|
+
signMessage(message: string): Promise<EvmSignature>;
|
|
159
|
+
}
|
|
160
|
+
export interface EnrollWithTransactionInput extends CreateEnrollmentInput {
|
|
161
|
+
/** Submit the exact zero-USDC action from the challenge, then wait for its advertised confirmations. */
|
|
162
|
+
submitProof(challenge: EnrollmentChallenge): Promise<{
|
|
163
|
+
method: ProofMethod;
|
|
164
|
+
transactionHash: TransactionHash;
|
|
165
|
+
}>;
|
|
166
|
+
}
|
|
167
|
+
export interface CredentialRotationSignatureInput {
|
|
168
|
+
wallet: EvmAddress;
|
|
169
|
+
signMessage(message: string): Promise<EvmSignature>;
|
|
170
|
+
}
|
|
171
|
+
export interface CredentialRotationTransactionInput {
|
|
172
|
+
wallet: EvmAddress;
|
|
173
|
+
submitProof(challenge: CredentialChallenge): Promise<{
|
|
174
|
+
method: ProofMethod;
|
|
175
|
+
transactionHash: TransactionHash;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
export declare class MerchantApiError extends Error {
|
|
179
|
+
readonly status: number;
|
|
180
|
+
readonly code: string;
|
|
181
|
+
constructor(status: number, code: string, message: string);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Minimal, dependency-free HTTP client for 402Mob merchant enrollment and management.
|
|
185
|
+
* It never signs messages, submits wallet transactions, persists API keys, or retries requests.
|
|
186
|
+
*/
|
|
187
|
+
export declare class MerchantClient {
|
|
188
|
+
#private;
|
|
189
|
+
private readonly baseUrl;
|
|
190
|
+
private readonly fetcher;
|
|
191
|
+
constructor(options?: MerchantClientOptions);
|
|
192
|
+
/** Return a new client with the supplied key; no key is written to disk or logged. */
|
|
193
|
+
withApiKey(apiKey: string): MerchantClient;
|
|
194
|
+
createEnrollmentChallenge(input: CreateEnrollmentInput): Promise<EnrollmentChallenge>;
|
|
195
|
+
completeEnrollmentWithSignature(input: CompleteSignatureInput): Promise<EnrollmentResult>;
|
|
196
|
+
completeEnrollmentWithTransaction(input: CompleteTransactionInput): Promise<EnrollmentResult>;
|
|
197
|
+
/** Request a challenge, ask the caller's wallet adapter to sign its exact message, then enroll. */
|
|
198
|
+
enrollWithSignature(input: EnrollWithSignatureInput): Promise<EnrollmentResult>;
|
|
199
|
+
/** Request a challenge, delegate its exact zero-value wallet action, then submit the confirmed hash. */
|
|
200
|
+
enrollWithTransaction(input: EnrollWithTransactionInput): Promise<EnrollmentResult>;
|
|
201
|
+
createCredentialRotationChallenge(wallet: EvmAddress): Promise<CredentialChallenge>;
|
|
202
|
+
rotateCredentialWithSignature(wallet: EvmAddress, challengeId: string, signature: EvmSignature): Promise<CredentialRotationResult>;
|
|
203
|
+
rotateCredentialWithTransaction(input: {
|
|
204
|
+
wallet: EvmAddress;
|
|
205
|
+
challengeId: string;
|
|
206
|
+
method: ProofMethod;
|
|
207
|
+
transactionHash: TransactionHash;
|
|
208
|
+
}): Promise<CredentialRotationResult>;
|
|
209
|
+
rotateWithSignature(input: CredentialRotationSignatureInput): Promise<CredentialRotationResult>;
|
|
210
|
+
rotateWithTransaction(input: CredentialRotationTransactionInput): Promise<CredentialRotationResult>;
|
|
211
|
+
getMerchant(): Promise<MerchantWorkspace>;
|
|
212
|
+
createOffer(input: CreateOfferInput): Promise<MerchantOffer>;
|
|
213
|
+
setOfferActive(offerId: string, active: boolean): Promise<MerchantOffer>;
|
|
214
|
+
listSales(limit?: number): Promise<SaleList>;
|
|
215
|
+
getSale(operationId: string): Promise<SaleDetails>;
|
|
216
|
+
private request;
|
|
217
|
+
}
|
|
218
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class MerchantApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
constructor(status, code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "MerchantApiError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Minimal, dependency-free HTTP client for 402Mob merchant enrollment and management.
|
|
13
|
+
* It never signs messages, submits wallet transactions, persists API keys, or retries requests.
|
|
14
|
+
*/
|
|
15
|
+
export class MerchantClient {
|
|
16
|
+
baseUrl;
|
|
17
|
+
#apiKey;
|
|
18
|
+
fetcher;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
const baseUrl = options.baseUrl ?? "https://evidence.contact";
|
|
21
|
+
const parsed = new URL(baseUrl);
|
|
22
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
23
|
+
throw new TypeError("baseUrl must use HTTP or HTTPS.");
|
|
24
|
+
}
|
|
25
|
+
if (parsed.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) {
|
|
26
|
+
throw new TypeError("HTTP is allowed only for loopback development URLs; use HTTPS for remote services.");
|
|
27
|
+
}
|
|
28
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
29
|
+
throw new TypeError("baseUrl must not contain credentials, a query, or a fragment.");
|
|
30
|
+
}
|
|
31
|
+
const pathPrefix = parsed.pathname.replace(/\/+$/, "");
|
|
32
|
+
this.baseUrl = `${parsed.origin}${pathPrefix}`;
|
|
33
|
+
this.#apiKey = options.apiKey;
|
|
34
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
35
|
+
if (typeof this.fetcher !== "function") {
|
|
36
|
+
throw new TypeError("A Fetch API implementation is required.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Return a new client with the supplied key; no key is written to disk or logged. */
|
|
40
|
+
withApiKey(apiKey) {
|
|
41
|
+
return new MerchantClient({ baseUrl: this.baseUrl, apiKey, fetch: this.fetcher });
|
|
42
|
+
}
|
|
43
|
+
createEnrollmentChallenge(input) {
|
|
44
|
+
return this.request("POST", "/v1/merchants/enrollment-challenges", input);
|
|
45
|
+
}
|
|
46
|
+
completeEnrollmentWithSignature(input) {
|
|
47
|
+
return this.request("POST", "/v1/merchants/enrollments", input);
|
|
48
|
+
}
|
|
49
|
+
completeEnrollmentWithTransaction(input) {
|
|
50
|
+
return this.request("POST", "/v1/merchants/enrollments/transaction", input);
|
|
51
|
+
}
|
|
52
|
+
/** Request a challenge, ask the caller's wallet adapter to sign its exact message, then enroll. */
|
|
53
|
+
async enrollWithSignature(input) {
|
|
54
|
+
const challenge = await this.createEnrollmentChallenge({ wallet: input.wallet, name: input.name });
|
|
55
|
+
const signature = await input.signMessage(challenge.message);
|
|
56
|
+
return this.completeEnrollmentWithSignature({
|
|
57
|
+
wallet: input.wallet,
|
|
58
|
+
name: input.name,
|
|
59
|
+
challengeId: challenge.challengeId,
|
|
60
|
+
signature,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Request a challenge, delegate its exact zero-value wallet action, then submit the confirmed hash. */
|
|
64
|
+
async enrollWithTransaction(input) {
|
|
65
|
+
const challenge = await this.createEnrollmentChallenge({ wallet: input.wallet, name: input.name });
|
|
66
|
+
const proof = await input.submitProof(challenge);
|
|
67
|
+
return this.completeEnrollmentWithTransaction({
|
|
68
|
+
wallet: input.wallet,
|
|
69
|
+
name: input.name,
|
|
70
|
+
challengeId: challenge.challengeId,
|
|
71
|
+
...proof,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
createCredentialRotationChallenge(wallet) {
|
|
75
|
+
return this.request("POST", "/v1/merchants/credential-challenges", { wallet });
|
|
76
|
+
}
|
|
77
|
+
rotateCredentialWithSignature(wallet, challengeId, signature) {
|
|
78
|
+
return this.request("POST", "/v1/merchants/credentials/rotate", { wallet, challengeId, signature });
|
|
79
|
+
}
|
|
80
|
+
rotateCredentialWithTransaction(input) {
|
|
81
|
+
return this.request("POST", "/v1/merchants/credentials/rotate-transaction", input);
|
|
82
|
+
}
|
|
83
|
+
async rotateWithSignature(input) {
|
|
84
|
+
const challenge = await this.createCredentialRotationChallenge(input.wallet);
|
|
85
|
+
const signature = await input.signMessage(challenge.message);
|
|
86
|
+
return this.rotateCredentialWithSignature(input.wallet, challenge.challengeId, signature);
|
|
87
|
+
}
|
|
88
|
+
async rotateWithTransaction(input) {
|
|
89
|
+
const challenge = await this.createCredentialRotationChallenge(input.wallet);
|
|
90
|
+
const proof = await input.submitProof(challenge);
|
|
91
|
+
return this.rotateCredentialWithTransaction({
|
|
92
|
+
wallet: input.wallet,
|
|
93
|
+
challengeId: challenge.challengeId,
|
|
94
|
+
...proof,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
getMerchant() {
|
|
98
|
+
return this.request("GET", "/v1/agent/merchant", undefined, true);
|
|
99
|
+
}
|
|
100
|
+
createOffer(input) {
|
|
101
|
+
return this.request("POST", "/v1/agent/merchant/offers", input, true);
|
|
102
|
+
}
|
|
103
|
+
setOfferActive(offerId, active) {
|
|
104
|
+
return this.request("PUT", `/v1/agent/merchant/offers/${encodeURIComponent(offerId)}/status`, { active }, true);
|
|
105
|
+
}
|
|
106
|
+
listSales(limit = 50) {
|
|
107
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
108
|
+
return this.request("GET", `/v1/agent/merchant/sales?${query}`, undefined, true);
|
|
109
|
+
}
|
|
110
|
+
getSale(operationId) {
|
|
111
|
+
return this.request("GET", `/v1/agent/merchant/sales/${encodeURIComponent(operationId)}`, undefined, true);
|
|
112
|
+
}
|
|
113
|
+
async request(method, path, body, authenticated = false) {
|
|
114
|
+
if (authenticated && !this.#apiKey) {
|
|
115
|
+
throw new Error("This request requires a merchant API key. Use client.withApiKey(apiKey).");
|
|
116
|
+
}
|
|
117
|
+
const headers = { Accept: "application/json" };
|
|
118
|
+
if (body !== undefined)
|
|
119
|
+
headers["Content-Type"] = "application/json";
|
|
120
|
+
if (authenticated && this.#apiKey)
|
|
121
|
+
headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
122
|
+
const response = await this.fetcher(new URL(`${this.baseUrl}${path}`), {
|
|
123
|
+
method,
|
|
124
|
+
headers,
|
|
125
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
126
|
+
});
|
|
127
|
+
let payload;
|
|
128
|
+
try {
|
|
129
|
+
payload = await response.json();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
payload = undefined;
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const error = isRecord(payload) ? payload : {};
|
|
136
|
+
throw new MerchantApiError(response.status, typeof error.error === "string" ? error.error : "http_error", typeof error.message === "string" ? error.message : `402Mob returned HTTP ${response.status}.`);
|
|
137
|
+
}
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isRecord(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MerchantClient,
|
|
3
|
+
type EvmAddress,
|
|
4
|
+
type EvmSignature,
|
|
5
|
+
type MerchantOffer,
|
|
6
|
+
} from "../src/index.js";
|
|
7
|
+
|
|
8
|
+
export interface MessageSigningWallet {
|
|
9
|
+
address: EvmAddress;
|
|
10
|
+
signMessage(message: string): Promise<EvmSignature>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SecretStore {
|
|
14
|
+
set(name: string, secret: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Example integration for an agent runtime that provides a wallet signer and secret store.
|
|
19
|
+
* The wallet and secret-store adapters stay under the integrating project's control.
|
|
20
|
+
*/
|
|
21
|
+
export async function enrollAndPublish(input: {
|
|
22
|
+
wallet: MessageSigningWallet;
|
|
23
|
+
secretStore: SecretStore;
|
|
24
|
+
}): Promise<{ offer: MerchantOffer; merchantClient: MerchantClient }> {
|
|
25
|
+
const publicClient = new MerchantClient();
|
|
26
|
+
const enrollment = await publicClient.enrollWithSignature({
|
|
27
|
+
wallet: input.wallet.address,
|
|
28
|
+
name: "Research Agent",
|
|
29
|
+
signMessage: (message) => input.wallet.signMessage(message),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await input.secretStore.set("402mob/merchant-api-key", enrollment.apiKey);
|
|
33
|
+
const merchantClient = publicClient.withApiKey(enrollment.apiKey);
|
|
34
|
+
const offer = await merchantClient.createOffer({
|
|
35
|
+
title: "Base payment summary",
|
|
36
|
+
description: "A prepared report for the buyer.",
|
|
37
|
+
priceUSDC: "0.02",
|
|
38
|
+
contentType: "application/json",
|
|
39
|
+
result: { summary: "Prepared by the seller agent" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { offer, merchantClient };
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "402mob-merchant-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Server-side TypeScript client for enrolling and operating 402Mob merchants.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/gggggggg424242/402mob-merchant-sdk.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"examples",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
31
|
+
"prepare": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|