@versenco/vcoin-client 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/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/index.cjs +352 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +158 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Versenco
|
|
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
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @versenco/vcoin-client
|
|
2
|
+
|
|
3
|
+
TypeScript client for Versenco's vCoin API — the shared in-app currency used
|
|
4
|
+
across the Versenco ecosystem (vChat, VersenEducation, versen-pay, and more).
|
|
5
|
+
|
|
6
|
+
Two entry points, matching the two trust domains of the underlying API:
|
|
7
|
+
|
|
8
|
+
- **`createVCoinServerClient`** — needs your app's `client_secret`. Runs
|
|
9
|
+
**server-side only**. Grants and debits vCoins, issues refunds, reads any
|
|
10
|
+
user's balance, reads platform config.
|
|
11
|
+
- **`createVCoinUserClient`** — no secret. Takes the *user's own*
|
|
12
|
+
`accessToken` per call. Safe to use in the browser: reads the current
|
|
13
|
+
user's balance, verifies/executes a peer-to-peer transfer, starts a top-up
|
|
14
|
+
checkout.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @versenco/vcoin-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Server usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createVCoinServerClient, idempotencyKey } from "@versenco/vcoin-client";
|
|
26
|
+
|
|
27
|
+
const vcoin = createVCoinServerClient({
|
|
28
|
+
appId: "your-app-id",
|
|
29
|
+
clientSecret: process.env.VCOIN_CLIENT_SECRET!,
|
|
30
|
+
baseUrl: "https://wnziizrtaocclnctqkll.supabase.co",
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This is your versen-connect Supabase project's URL (not the `auth.versenco.com`
|
|
35
|
+
app domain, which only serves the login UI) — edge functions live at
|
|
36
|
+
`<supabase-project-url>/functions/v1/*`.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const result = await vcoin.earn({
|
|
40
|
+
userId,
|
|
41
|
+
amount: 50,
|
|
42
|
+
type: "upload_reward",
|
|
43
|
+
description: `Uploaded "${title}"`,
|
|
44
|
+
referenceId: documentId,
|
|
45
|
+
idempotencyKey: idempotencyKey("upload", documentId, userId),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!result.ok) {
|
|
49
|
+
// result.error is a typed string; some errors (insufficient_balance,
|
|
50
|
+
// insufficient_balance_for_refund, daily_limit_exceeded) carry extra fields.
|
|
51
|
+
console.error(result.error);
|
|
52
|
+
} else if (result.data.idempotent) {
|
|
53
|
+
// this exact idempotencyKey was already processed — nothing new happened.
|
|
54
|
+
} else {
|
|
55
|
+
console.log(result.data.balance, result.data.transactionId);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## User usage (safe in the browser)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createVCoinUserClient } from "@versenco/vcoin-client";
|
|
63
|
+
|
|
64
|
+
const vcoin = createVCoinUserClient({ baseUrl: "https://wnziizrtaocclnctqkll.supabase.co" });
|
|
65
|
+
|
|
66
|
+
const balance = await vcoin.balance({ accessToken });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The `accessToken` is the access token from your app's own versen-connect SSO
|
|
70
|
+
session — not a vCoin-specific credential.
|
|
71
|
+
|
|
72
|
+
## Getting a `client_secret`
|
|
73
|
+
|
|
74
|
+
Server-side access is gated per app. Contact Versenco to register your app
|
|
75
|
+
and receive an app id / client secret pair (the `appId` / `clientSecret`
|
|
76
|
+
fields of `VCoinServerConfig`).
|
|
77
|
+
|
|
78
|
+
## All methods
|
|
79
|
+
|
|
80
|
+
**Server** (`createVCoinServerClient`, needs `clientSecret`):
|
|
81
|
+
|
|
82
|
+
- `earn(input)` — credit vCoins to a user; idempotent via `idempotencyKey`.
|
|
83
|
+
- `spend(input)` — debit vCoins from a user; idempotent via `idempotencyKey`.
|
|
84
|
+
- `refund(input)` — reverse a prior spend by transaction id.
|
|
85
|
+
- `balance(input)` — read any user's balance and recent transactions.
|
|
86
|
+
- `getConfig(key?)` — read platform config (e.g. the vCoin-to-XOF ratio).
|
|
87
|
+
|
|
88
|
+
**User** (`createVCoinUserClient`, no secret, takes `accessToken` per call):
|
|
89
|
+
|
|
90
|
+
- `balance(input)` — read the current user's own balance and transactions.
|
|
91
|
+
- `verifyRecipient(input)` — look up a transfer recipient before sending.
|
|
92
|
+
- `transfer(input)` — send vCoins to another user; requires an
|
|
93
|
+
`idempotencyKey` (see the idempotency section below — always include your
|
|
94
|
+
app id as one of the parts, since keys are unique across the whole vCoin
|
|
95
|
+
system, not just your app).
|
|
96
|
+
- `createCheckout(input)` — start a vCoin top-up checkout session.
|
|
97
|
+
|
|
98
|
+
## Idempotency keys
|
|
99
|
+
|
|
100
|
+
`idempotencyKey(...)` builds a stable key from one or more parts. Keys are
|
|
101
|
+
unique across the ENTIRE vCoin system — all apps and all users share one
|
|
102
|
+
keyspace at the database level — so always include your own app id (or an
|
|
103
|
+
equally unique prefix) as one of the parts you pass in, to avoid colliding
|
|
104
|
+
with another app's key.
|
|
105
|
+
|
|
106
|
+
## Errors
|
|
107
|
+
|
|
108
|
+
Two different failure shapes:
|
|
109
|
+
|
|
110
|
+
- A thrown `VCoinNetworkError` — unexpected failures: the network is down,
|
|
111
|
+
the response isn't valid JSON, or the API returned something with no
|
|
112
|
+
recognizable error code. Callers should generally let this propagate or
|
|
113
|
+
wrap it in their own error handling.
|
|
114
|
+
- A returned `{ ok: false, error, ... }` — expected business outcomes:
|
|
115
|
+
insufficient balance, wallet not found, daily limit exceeded, etc. These
|
|
116
|
+
are part of the normal return type and should be handled inline.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
try {
|
|
120
|
+
const result = await vcoin.spend({ userId, amount: 10, type: "decoration" });
|
|
121
|
+
if (!result.ok) {
|
|
122
|
+
// expected business outcome, e.g. result.error === "insufficient_balance"
|
|
123
|
+
console.error(result.error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
console.log(result.data.balance);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// unexpected failure (network, malformed response, etc.)
|
|
129
|
+
console.error(err);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
VCOIN_CLIENT_VERSION: () => VCOIN_CLIENT_VERSION,
|
|
24
|
+
VCoinNetworkError: () => VCoinNetworkError,
|
|
25
|
+
createVCoinServerClient: () => createVCoinServerClient,
|
|
26
|
+
createVCoinUserClient: () => createVCoinUserClient,
|
|
27
|
+
idempotencyKey: () => idempotencyKey
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/types.ts
|
|
32
|
+
var VCoinNetworkError = class extends Error {
|
|
33
|
+
constructor(message, status, cause) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.cause = cause;
|
|
37
|
+
this.name = "VCoinNetworkError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/fetcher.ts
|
|
42
|
+
function functionUrl(baseUrl, fn) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(baseUrl);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error(`vCoin baseUrl must be a valid URL (got: ${baseUrl})`);
|
|
48
|
+
}
|
|
49
|
+
const isLocal = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
50
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
51
|
+
throw new Error(`vCoin baseUrl must use https:// (got: ${baseUrl})`);
|
|
52
|
+
}
|
|
53
|
+
return `${baseUrl.replace(/\/+$/, "")}/functions/v1/${fn}`;
|
|
54
|
+
}
|
|
55
|
+
async function vcoinFetch(url, init) {
|
|
56
|
+
let res;
|
|
57
|
+
try {
|
|
58
|
+
res = await fetch(url, {
|
|
59
|
+
method: init.method,
|
|
60
|
+
headers: { "Content-Type": "application/json", ...init.headers },
|
|
61
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new VCoinNetworkError("Network error calling the vCoin API", void 0, err);
|
|
65
|
+
}
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
let json;
|
|
68
|
+
try {
|
|
69
|
+
json = text.length > 0 ? JSON.parse(text) : {};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new VCoinNetworkError("vCoin API returned a non-JSON response", res.status, err);
|
|
72
|
+
}
|
|
73
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
74
|
+
throw new VCoinNetworkError("vCoin API returned a non-object JSON response", res.status);
|
|
75
|
+
}
|
|
76
|
+
return { status: res.status, ok: res.ok, json };
|
|
77
|
+
}
|
|
78
|
+
function genericErrorResult(raw) {
|
|
79
|
+
const code = typeof raw.json.error === "string" ? raw.json.error : void 0;
|
|
80
|
+
if (!code) {
|
|
81
|
+
if (raw.status === 401) {
|
|
82
|
+
return { ok: false, error: "invalid_token", detail: void 0 };
|
|
83
|
+
}
|
|
84
|
+
throw new VCoinNetworkError(`vCoin API returned ${raw.status} with no recognizable error code`, raw.status);
|
|
85
|
+
}
|
|
86
|
+
const detail = typeof raw.json.detail === "string" ? raw.json.detail : void 0;
|
|
87
|
+
return { ok: false, error: code, detail };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/mappers.ts
|
|
91
|
+
function num(v) {
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
function str(v) {
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
97
|
+
function strOrNull(v) {
|
|
98
|
+
return v ?? null;
|
|
99
|
+
}
|
|
100
|
+
function toTransactionResult(raw) {
|
|
101
|
+
if (raw.ok) {
|
|
102
|
+
if (raw.json.idempotent === true) {
|
|
103
|
+
return { ok: true, data: { idempotent: true } };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
data: {
|
|
108
|
+
idempotent: false,
|
|
109
|
+
balance: num(raw.json.balance),
|
|
110
|
+
transactionId: str(raw.json.transaction_id),
|
|
111
|
+
amount: num(raw.json.amount)
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (raw.json.error === "insufficient_balance") {
|
|
116
|
+
return { ok: false, error: "insufficient_balance", balance: num(raw.json.balance), required: num(raw.json.required) };
|
|
117
|
+
}
|
|
118
|
+
return genericErrorResult(raw);
|
|
119
|
+
}
|
|
120
|
+
function toRefundResult(raw) {
|
|
121
|
+
if (raw.ok) {
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
data: {
|
|
125
|
+
refundTransactionId: str(raw.json.refund_transaction_id),
|
|
126
|
+
originalTransactionId: str(raw.json.original_transaction_id),
|
|
127
|
+
balance: num(raw.json.balance),
|
|
128
|
+
amount: num(raw.json.amount)
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (raw.json.error === "insufficient_balance_for_refund") {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: "insufficient_balance_for_refund",
|
|
136
|
+
balance: num(raw.json.balance),
|
|
137
|
+
required: num(raw.json.required)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return genericErrorResult(raw);
|
|
141
|
+
}
|
|
142
|
+
function toBalanceResult(raw) {
|
|
143
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
144
|
+
const rawTx = raw.json.transactions ?? [];
|
|
145
|
+
const transactions = rawTx.map((t) => ({
|
|
146
|
+
id: str(t.id),
|
|
147
|
+
amount: num(t.amount),
|
|
148
|
+
balanceAfter: num(t.balance_after),
|
|
149
|
+
type: str(t.type),
|
|
150
|
+
description: strOrNull(t.description),
|
|
151
|
+
referenceId: strOrNull(t.reference_id),
|
|
152
|
+
appId: strOrNull(t.app_id),
|
|
153
|
+
createdAt: str(t.created_at)
|
|
154
|
+
}));
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
data: {
|
|
158
|
+
balance: num(raw.json.balance),
|
|
159
|
+
walletId: strOrNull(raw.json.wallet_id),
|
|
160
|
+
updatedAt: strOrNull(raw.json.updated_at),
|
|
161
|
+
transactions
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toConfigResult(raw) {
|
|
166
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
data: { key: str(raw.json.key), value: str(raw.json.value), ratio: num(raw.json.ratio) }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function toRecipientInfo(raw) {
|
|
173
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
data: {
|
|
177
|
+
walletId: strOrNull(raw.json.wallet_id),
|
|
178
|
+
name: str(raw.json.name),
|
|
179
|
+
avatarUrl: strOrNull(raw.json.avatar_url)
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function toTransferResult(raw) {
|
|
184
|
+
if (raw.json.error === "insufficient_balance") {
|
|
185
|
+
return { ok: false, error: "insufficient_balance", balance: num(raw.json.balance), required: num(raw.json.required) };
|
|
186
|
+
}
|
|
187
|
+
if (raw.json.error === "daily_limit_exceeded") {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: "daily_limit_exceeded",
|
|
191
|
+
dailySent: num(raw.json.daily_sent),
|
|
192
|
+
dailyLimit: num(raw.json.daily_limit),
|
|
193
|
+
remaining: num(raw.json.remaining)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!raw.ok || typeof raw.json.error === "string") {
|
|
197
|
+
return genericErrorResult(raw);
|
|
198
|
+
}
|
|
199
|
+
if (raw.json.idempotent === true) {
|
|
200
|
+
return { ok: true, data: { idempotent: true } };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
data: {
|
|
205
|
+
idempotent: false,
|
|
206
|
+
senderBalance: num(raw.json.sender_balance),
|
|
207
|
+
recipientWalletId: str(raw.json.recipient_wallet_id),
|
|
208
|
+
txOutId: str(raw.json.tx_out_id),
|
|
209
|
+
txInId: str(raw.json.tx_in_id),
|
|
210
|
+
amount: num(raw.json.amount)
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function toCheckoutResult(raw) {
|
|
215
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
216
|
+
return { ok: true, data: { checkoutUrl: str(raw.json.checkout_url) } };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/server.ts
|
|
220
|
+
function createVCoinServerClient(config) {
|
|
221
|
+
if (typeof document !== "undefined") {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"createVCoinServerClient must never run in a browser \u2014 it holds a secret. Use createVCoinUserClient for browser code."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
async function earn(input) {
|
|
227
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-earn"), {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
230
|
+
body: {
|
|
231
|
+
user_id: input.userId,
|
|
232
|
+
amount: input.amount,
|
|
233
|
+
type: input.type,
|
|
234
|
+
description: input.description,
|
|
235
|
+
reference_id: input.referenceId,
|
|
236
|
+
app_id: config.appId,
|
|
237
|
+
idempotency_key: input.idempotencyKey
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return toTransactionResult(raw);
|
|
241
|
+
}
|
|
242
|
+
async function spend(input) {
|
|
243
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-spend"), {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
246
|
+
body: {
|
|
247
|
+
user_id: input.userId,
|
|
248
|
+
amount: input.amount,
|
|
249
|
+
type: input.type,
|
|
250
|
+
description: input.description,
|
|
251
|
+
reference_id: input.referenceId,
|
|
252
|
+
app_id: config.appId,
|
|
253
|
+
idempotency_key: input.idempotencyKey
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return toTransactionResult(raw);
|
|
257
|
+
}
|
|
258
|
+
async function refund(input) {
|
|
259
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-refund"), {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
262
|
+
body: { transaction_id: input.transactionId, reason: input.reason, app_id: config.appId }
|
|
263
|
+
});
|
|
264
|
+
return toRefundResult(raw);
|
|
265
|
+
}
|
|
266
|
+
async function balance(input) {
|
|
267
|
+
const url = `${functionUrl(config.baseUrl, "vcoin-balance")}?user_id=${encodeURIComponent(input.userId)}`;
|
|
268
|
+
const raw = await vcoinFetch(url, {
|
|
269
|
+
method: "GET",
|
|
270
|
+
headers: { "x-vcoin-secret": config.clientSecret, "x-app-id": config.appId }
|
|
271
|
+
});
|
|
272
|
+
return toBalanceResult(raw);
|
|
273
|
+
}
|
|
274
|
+
async function getConfig(key) {
|
|
275
|
+
const qs = key ? `?key=${encodeURIComponent(key)}` : "";
|
|
276
|
+
const raw = await vcoinFetch(`${functionUrl(config.baseUrl, "vcoin-config")}${qs}`, {
|
|
277
|
+
method: "GET",
|
|
278
|
+
headers: {}
|
|
279
|
+
});
|
|
280
|
+
return toConfigResult(raw);
|
|
281
|
+
}
|
|
282
|
+
return { earn, spend, refund, balance, getConfig };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/user.ts
|
|
286
|
+
function createVCoinUserClient(config) {
|
|
287
|
+
async function balance(input) {
|
|
288
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-balance"), {
|
|
289
|
+
method: "GET",
|
|
290
|
+
headers: { Authorization: `Bearer ${input.accessToken}` }
|
|
291
|
+
});
|
|
292
|
+
return toBalanceResult(raw);
|
|
293
|
+
}
|
|
294
|
+
async function verifyRecipient(input) {
|
|
295
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-transfer"), {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
298
|
+
body: { action: "verify", recipient: input.recipient }
|
|
299
|
+
});
|
|
300
|
+
return toRecipientInfo(raw);
|
|
301
|
+
}
|
|
302
|
+
async function transfer(input) {
|
|
303
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-transfer"), {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
306
|
+
body: {
|
|
307
|
+
action: "transfer",
|
|
308
|
+
recipient: input.recipient,
|
|
309
|
+
amount: input.amount,
|
|
310
|
+
description: input.description,
|
|
311
|
+
idempotency_key: input.idempotencyKey
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return toTransferResult(raw);
|
|
315
|
+
}
|
|
316
|
+
async function createCheckout(input) {
|
|
317
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-create-checkout"), {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
320
|
+
body: { pack_id: input.packId }
|
|
321
|
+
});
|
|
322
|
+
return toCheckoutResult(raw);
|
|
323
|
+
}
|
|
324
|
+
return { balance, verifyRecipient, transfer, createCheckout };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/idempotency.ts
|
|
328
|
+
function idempotencyKey(...parts) {
|
|
329
|
+
if (parts.length === 0) {
|
|
330
|
+
throw new Error("idempotencyKey requires at least one part");
|
|
331
|
+
}
|
|
332
|
+
for (const part of parts) {
|
|
333
|
+
if (part.includes("::")) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
'idempotencyKey parts must not contain "::" (the separator) \u2014 this would let two different calls silently collide'
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return parts.join("::");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/index.ts
|
|
343
|
+
var VCOIN_CLIENT_VERSION = "0.1.0";
|
|
344
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
345
|
+
0 && (module.exports = {
|
|
346
|
+
VCOIN_CLIENT_VERSION,
|
|
347
|
+
VCoinNetworkError,
|
|
348
|
+
createVCoinServerClient,
|
|
349
|
+
createVCoinUserClient,
|
|
350
|
+
idempotencyKey
|
|
351
|
+
});
|
|
352
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/fetcher.ts","../src/mappers.ts","../src/server.ts","../src/user.ts","../src/idempotency.ts"],"sourcesContent":["export const VCOIN_CLIENT_VERSION = \"0.1.0\";\n\nexport { createVCoinServerClient } from \"./server\";\nexport { createVCoinUserClient } from \"./user\";\nexport { idempotencyKey } from \"./idempotency\";\n\nexport {\n VCoinNetworkError,\n type VCoinResult,\n type VCoinBusinessError,\n type EarnType,\n type SpendType,\n type EarnInput,\n type SpendInput,\n type TransactionResult,\n type RefundInput,\n type RefundResult,\n type BalanceResult,\n type BalanceTransaction,\n type ConfigResult,\n type RecipientInfo,\n type TransferInput,\n type TransferResult,\n type VCoinServerConfig,\n type VCoinUserConfig,\n type VCoinServerClient,\n type VCoinUserClient,\n} from \"./types\";\n","// ── Network-level error ──────────────────────────────────────────────────\n\nexport class VCoinNetworkError extends Error {\n constructor(message: string, readonly status?: number, readonly cause?: unknown) {\n super(message);\n this.name = \"VCoinNetworkError\";\n }\n}\n\n// ── Discriminated result type ────────────────────────────────────────────\n\nexport type VCoinBusinessError =\n | \"wallet_not_found\"\n | \"recipient_wallet_not_found\"\n | \"invalid_recipient\"\n | \"self_transfer\"\n | \"transaction_not_found\"\n | \"already_refunded\"\n | \"forbidden\"\n | \"invalid_token\"\n | \"missing_token\"\n | \"missing_credentials\"\n | \"rate_limited\"\n | \"cannot_spend_for_other_user\"\n | \"admin_deduct_requires_server_credentials\"\n | \"config_not_found\"\n | \"database_error\";\n\nexport type VCoinResult<T> =\n | { ok: true; data: T }\n | { ok: false; error: \"insufficient_balance\"; balance: number; required: number }\n | { ok: false; error: \"insufficient_balance_for_refund\"; balance: number; required: number }\n | { ok: false; error: \"daily_limit_exceeded\"; dailySent: number; dailyLimit: number; remaining: number }\n | { ok: false; error: VCoinBusinessError; detail?: string };\n\n// ── Earn / spend ──────────────────────────────────────────────────────────\n\nexport type EarnType = \"upload_reward\" | \"welcome_bonus\" | \"admin_grant\" | \"referral\";\nexport type SpendType = \"document_purchase\" | \"decoration\" | \"payment\" | \"admin_deduct\";\n\nexport interface EarnInput {\n userId: string;\n amount: number;\n type: EarnType;\n description?: string;\n referenceId?: string;\n idempotencyKey?: string;\n}\n\nexport interface SpendInput {\n userId: string;\n amount: number;\n type: SpendType;\n description?: string;\n referenceId?: string;\n idempotencyKey?: string;\n}\n\nexport type TransactionResult =\n | { idempotent: true }\n | { idempotent: false; balance: number; transactionId: string; amount: number };\n\n// ── Refund ────────────────────────────────────────────────────────────────\n\nexport interface RefundInput {\n transactionId: string;\n reason: string;\n}\n\nexport interface RefundResult {\n refundTransactionId: string;\n originalTransactionId: string;\n balance: number;\n amount: number;\n}\n\n// ── Balance ───────────────────────────────────────────────────────────────\n\nexport interface BalanceTransaction {\n id: string;\n amount: number;\n balanceAfter: number;\n type: string;\n description: string | null;\n referenceId: string | null;\n appId: string | null;\n createdAt: string;\n}\n\nexport interface BalanceResult {\n balance: number;\n walletId: string | null;\n updatedAt: string | null;\n transactions: BalanceTransaction[];\n}\n\n// ── Config ────────────────────────────────────────────────────────────────\n\nexport interface ConfigResult {\n key: string;\n value: string;\n ratio: number;\n}\n\n// ── Transfer ──────────────────────────────────────────────────────────────\n\nexport interface RecipientInfo {\n walletId: string | null;\n name: string;\n avatarUrl: string | null;\n}\n\nexport interface TransferInput {\n accessToken: string;\n recipient: string;\n amount: number;\n description?: string;\n idempotencyKey: string;\n}\n\nexport type TransferResult =\n | { idempotent: true }\n | {\n idempotent: false;\n senderBalance: number;\n recipientWalletId: string;\n txOutId: string;\n txInId: string;\n amount: number;\n };\n\n// ── Client configs ────────────────────────────────────────────────────────\n\nexport interface VCoinServerConfig {\n appId: string;\n clientSecret: string;\n baseUrl: string;\n}\n\nexport interface VCoinUserConfig {\n baseUrl: string;\n}\n\nexport interface VCoinServerClient {\n earn(input: EarnInput): Promise<VCoinResult<TransactionResult>>;\n spend(input: SpendInput): Promise<VCoinResult<TransactionResult>>;\n refund(input: RefundInput): Promise<VCoinResult<RefundResult>>;\n balance(input: { userId: string }): Promise<VCoinResult<BalanceResult>>;\n getConfig(key?: string): Promise<VCoinResult<ConfigResult>>;\n}\n\nexport interface VCoinUserClient {\n balance(input: { accessToken: string }): Promise<VCoinResult<BalanceResult>>;\n verifyRecipient(input: { accessToken: string; recipient: string }): Promise<VCoinResult<RecipientInfo>>;\n transfer(input: TransferInput): Promise<VCoinResult<TransferResult>>;\n createCheckout(input: { accessToken: string; packId: string }): Promise<VCoinResult<{ checkoutUrl: string }>>;\n}\n","import { VCoinNetworkError, type VCoinBusinessError, type VCoinResult } from \"./types\";\n\nexport function functionUrl(baseUrl: string, fn: string): string {\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`vCoin baseUrl must be a valid URL (got: ${baseUrl})`);\n }\n const isLocal =\n parsed.protocol === \"http:\" && (parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\");\n if (parsed.protocol !== \"https:\" && !isLocal) {\n throw new Error(`vCoin baseUrl must use https:// (got: ${baseUrl})`);\n }\n return `${baseUrl.replace(/\\/+$/, \"\")}/functions/v1/${fn}`;\n}\n\nexport interface VCoinRawResponse {\n status: number;\n ok: boolean;\n json: Record<string, unknown>;\n}\n\nexport async function vcoinFetch(\n url: string,\n init: { method: \"GET\" | \"POST\"; headers: Record<string, string>; body?: unknown },\n): Promise<VCoinRawResponse> {\n let res: Response;\n try {\n res = await fetch(url, {\n method: init.method,\n headers: { \"Content-Type\": \"application/json\", ...init.headers },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n });\n } catch (err) {\n throw new VCoinNetworkError(\"Network error calling the vCoin API\", undefined, err);\n }\n\n const text = await res.text();\n let json: Record<string, unknown>;\n try {\n json = text.length > 0 ? JSON.parse(text) : {};\n } catch (err) {\n throw new VCoinNetworkError(\"vCoin API returned a non-JSON response\", res.status, err);\n }\n\n if (json === null || typeof json !== \"object\" || Array.isArray(json)) {\n throw new VCoinNetworkError(\"vCoin API returned a non-object JSON response\", res.status);\n }\n\n return { status: res.status, ok: res.ok, json };\n}\n\nexport function genericErrorResult<T>(raw: VCoinRawResponse): VCoinResult<T> {\n const code = typeof raw.json.error === \"string\" ? raw.json.error : undefined;\n if (!code) {\n if (raw.status === 401) {\n return { ok: false, error: \"invalid_token\", detail: undefined };\n }\n throw new VCoinNetworkError(`vCoin API returned ${raw.status} with no recognizable error code`, raw.status);\n }\n const detail = typeof raw.json.detail === \"string\" ? raw.json.detail : undefined;\n return { ok: false, error: code as VCoinBusinessError, detail };\n}\n","import { genericErrorResult, type VCoinRawResponse } from \"./fetcher\";\nimport type {\n BalanceResult,\n BalanceTransaction,\n ConfigResult,\n RecipientInfo,\n RefundResult,\n TransactionResult,\n TransferResult,\n VCoinResult,\n} from \"./types\";\n\nfunction num(v: unknown): number {\n return v as number;\n}\nfunction str(v: unknown): string {\n return v as string;\n}\nfunction strOrNull(v: unknown): string | null {\n return (v as string | null) ?? null;\n}\n\nexport function toTransactionResult(raw: VCoinRawResponse): VCoinResult<TransactionResult> {\n if (raw.ok) {\n if (raw.json.idempotent === true) {\n return { ok: true, data: { idempotent: true } };\n }\n return {\n ok: true,\n data: {\n idempotent: false,\n balance: num(raw.json.balance),\n transactionId: str(raw.json.transaction_id),\n amount: num(raw.json.amount),\n },\n };\n }\n if (raw.json.error === \"insufficient_balance\") {\n return { ok: false, error: \"insufficient_balance\", balance: num(raw.json.balance), required: num(raw.json.required) };\n }\n return genericErrorResult<TransactionResult>(raw);\n}\n\nexport function toRefundResult(raw: VCoinRawResponse): VCoinResult<RefundResult> {\n if (raw.ok) {\n return {\n ok: true,\n data: {\n refundTransactionId: str(raw.json.refund_transaction_id),\n originalTransactionId: str(raw.json.original_transaction_id),\n balance: num(raw.json.balance),\n amount: num(raw.json.amount),\n },\n };\n }\n if (raw.json.error === \"insufficient_balance_for_refund\") {\n return {\n ok: false,\n error: \"insufficient_balance_for_refund\",\n balance: num(raw.json.balance),\n required: num(raw.json.required),\n };\n }\n return genericErrorResult<RefundResult>(raw);\n}\n\nexport function toBalanceResult(raw: VCoinRawResponse): VCoinResult<BalanceResult> {\n if (!raw.ok) return genericErrorResult<BalanceResult>(raw);\n const rawTx = (raw.json.transactions as Record<string, unknown>[] | undefined) ?? [];\n const transactions: BalanceTransaction[] = rawTx.map((t) => ({\n id: str(t.id),\n amount: num(t.amount),\n balanceAfter: num(t.balance_after),\n type: str(t.type),\n description: strOrNull(t.description),\n referenceId: strOrNull(t.reference_id),\n appId: strOrNull(t.app_id),\n createdAt: str(t.created_at),\n }));\n return {\n ok: true,\n data: {\n balance: num(raw.json.balance),\n walletId: strOrNull(raw.json.wallet_id),\n updatedAt: strOrNull(raw.json.updated_at),\n transactions,\n },\n };\n}\n\nexport function toConfigResult(raw: VCoinRawResponse): VCoinResult<ConfigResult> {\n if (!raw.ok) return genericErrorResult<ConfigResult>(raw);\n return {\n ok: true,\n data: { key: str(raw.json.key), value: str(raw.json.value), ratio: num(raw.json.ratio) },\n };\n}\n\nexport function toRecipientInfo(raw: VCoinRawResponse): VCoinResult<RecipientInfo> {\n if (!raw.ok) return genericErrorResult<RecipientInfo>(raw);\n return {\n ok: true,\n data: {\n walletId: strOrNull(raw.json.wallet_id),\n name: str(raw.json.name),\n avatarUrl: strOrNull(raw.json.avatar_url),\n },\n };\n}\n\nexport function toTransferResult(raw: VCoinRawResponse): VCoinResult<TransferResult> {\n // vcoin-transfer's business errors use real HTTP status codes (402 for\n // insufficient_balance, 429 for daily_limit_exceeded) — but we check\n // raw.json.error before trusting raw.ok as defense in depth, since this\n // endpoint's exact status-code contract isn't guaranteed stable.\n if (raw.json.error === \"insufficient_balance\") {\n return { ok: false, error: \"insufficient_balance\", balance: num(raw.json.balance), required: num(raw.json.required) };\n }\n if (raw.json.error === \"daily_limit_exceeded\") {\n return {\n ok: false,\n error: \"daily_limit_exceeded\",\n dailySent: num(raw.json.daily_sent),\n dailyLimit: num(raw.json.daily_limit),\n remaining: num(raw.json.remaining),\n };\n }\n if (!raw.ok || typeof raw.json.error === \"string\") {\n return genericErrorResult<TransferResult>(raw);\n }\n if (raw.json.idempotent === true) {\n return { ok: true, data: { idempotent: true } };\n }\n return {\n ok: true,\n data: {\n idempotent: false,\n senderBalance: num(raw.json.sender_balance),\n recipientWalletId: str(raw.json.recipient_wallet_id),\n txOutId: str(raw.json.tx_out_id),\n txInId: str(raw.json.tx_in_id),\n amount: num(raw.json.amount),\n },\n };\n}\n\nexport function toCheckoutResult(raw: VCoinRawResponse): VCoinResult<{ checkoutUrl: string }> {\n if (!raw.ok) return genericErrorResult<{ checkoutUrl: string }>(raw);\n return { ok: true, data: { checkoutUrl: str(raw.json.checkout_url) } };\n}\n","import { functionUrl, vcoinFetch } from \"./fetcher\";\nimport {\n toBalanceResult,\n toConfigResult,\n toRefundResult,\n toTransactionResult,\n} from \"./mappers\";\nimport type {\n BalanceResult,\n ConfigResult,\n EarnInput,\n RefundInput,\n RefundResult,\n SpendInput,\n TransactionResult,\n VCoinResult,\n VCoinServerClient,\n VCoinServerConfig,\n} from \"./types\";\n\nexport function createVCoinServerClient(config: VCoinServerConfig): VCoinServerClient {\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"createVCoinServerClient must never run in a browser — it holds a secret. Use createVCoinUserClient for browser code.\",\n );\n }\n\n async function earn(input: EarnInput): Promise<VCoinResult<TransactionResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-earn\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: {\n user_id: input.userId,\n amount: input.amount,\n type: input.type,\n description: input.description,\n reference_id: input.referenceId,\n app_id: config.appId,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransactionResult(raw);\n }\n\n async function spend(input: SpendInput): Promise<VCoinResult<TransactionResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-spend\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: {\n user_id: input.userId,\n amount: input.amount,\n type: input.type,\n description: input.description,\n reference_id: input.referenceId,\n app_id: config.appId,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransactionResult(raw);\n }\n\n async function refund(input: RefundInput): Promise<VCoinResult<RefundResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-refund\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: { transaction_id: input.transactionId, reason: input.reason, app_id: config.appId },\n });\n return toRefundResult(raw);\n }\n\n async function balance(input: { userId: string }): Promise<VCoinResult<BalanceResult>> {\n const url = `${functionUrl(config.baseUrl, \"vcoin-balance\")}?user_id=${encodeURIComponent(input.userId)}`;\n const raw = await vcoinFetch(url, {\n method: \"GET\",\n headers: { \"x-vcoin-secret\": config.clientSecret, \"x-app-id\": config.appId },\n });\n return toBalanceResult(raw);\n }\n\n async function getConfig(key?: string): Promise<VCoinResult<ConfigResult>> {\n const qs = key ? `?key=${encodeURIComponent(key)}` : \"\";\n const raw = await vcoinFetch(`${functionUrl(config.baseUrl, \"vcoin-config\")}${qs}`, {\n method: \"GET\",\n headers: {},\n });\n return toConfigResult(raw);\n }\n\n return { earn, spend, refund, balance, getConfig };\n}\n","import { functionUrl, vcoinFetch } from \"./fetcher\";\nimport { toBalanceResult, toCheckoutResult, toRecipientInfo, toTransferResult } from \"./mappers\";\nimport type {\n BalanceResult,\n RecipientInfo,\n TransferInput,\n TransferResult,\n VCoinResult,\n VCoinUserClient,\n VCoinUserConfig,\n} from \"./types\";\n\nexport function createVCoinUserClient(config: VCoinUserConfig): VCoinUserClient {\n async function balance(input: { accessToken: string }): Promise<VCoinResult<BalanceResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-balance\"), {\n method: \"GET\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n });\n return toBalanceResult(raw);\n }\n\n async function verifyRecipient(input: {\n accessToken: string;\n recipient: string;\n }): Promise<VCoinResult<RecipientInfo>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-transfer\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: { action: \"verify\", recipient: input.recipient },\n });\n return toRecipientInfo(raw);\n }\n\n async function transfer(input: TransferInput): Promise<VCoinResult<TransferResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-transfer\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: {\n action: \"transfer\",\n recipient: input.recipient,\n amount: input.amount,\n description: input.description,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransferResult(raw);\n }\n\n async function createCheckout(input: {\n accessToken: string;\n packId: string;\n }): Promise<VCoinResult<{ checkoutUrl: string }>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-create-checkout\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: { pack_id: input.packId },\n });\n return toCheckoutResult(raw);\n }\n\n return { balance, verifyRecipient, transfer, createCheckout };\n}\n","/**\n * Builds a stable idempotency key from one or more parts (e.g. an action\n * name, a resource id, a user id). Replaces the ad-hoc string\n * concatenation (`upload_${documentId}_${userId}`) that every consuming\n * app currently writes by hand.\n */\nexport function idempotencyKey(...parts: string[]): string {\n if (parts.length === 0) {\n throw new Error(\"idempotencyKey requires at least one part\");\n }\n for (const part of parts) {\n if (part.includes(\"::\")) {\n throw new Error(\n 'idempotencyKey parts must not contain \"::\" (the separator) — this would let two different calls silently collide',\n );\n }\n }\n return parts.join(\"::\");\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
declare class VCoinNetworkError extends Error {
|
|
2
|
+
readonly status?: number | undefined;
|
|
3
|
+
readonly cause?: unknown | undefined;
|
|
4
|
+
constructor(message: string, status?: number | undefined, cause?: unknown | undefined);
|
|
5
|
+
}
|
|
6
|
+
type VCoinBusinessError = "wallet_not_found" | "recipient_wallet_not_found" | "invalid_recipient" | "self_transfer" | "transaction_not_found" | "already_refunded" | "forbidden" | "invalid_token" | "missing_token" | "missing_credentials" | "rate_limited" | "cannot_spend_for_other_user" | "admin_deduct_requires_server_credentials" | "config_not_found" | "database_error";
|
|
7
|
+
type VCoinResult<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
data: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: "insufficient_balance";
|
|
13
|
+
balance: number;
|
|
14
|
+
required: number;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: "insufficient_balance_for_refund";
|
|
18
|
+
balance: number;
|
|
19
|
+
required: number;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: "daily_limit_exceeded";
|
|
23
|
+
dailySent: number;
|
|
24
|
+
dailyLimit: number;
|
|
25
|
+
remaining: number;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: VCoinBusinessError;
|
|
29
|
+
detail?: string;
|
|
30
|
+
};
|
|
31
|
+
type EarnType = "upload_reward" | "welcome_bonus" | "admin_grant" | "referral";
|
|
32
|
+
type SpendType = "document_purchase" | "decoration" | "payment" | "admin_deduct";
|
|
33
|
+
interface EarnInput {
|
|
34
|
+
userId: string;
|
|
35
|
+
amount: number;
|
|
36
|
+
type: EarnType;
|
|
37
|
+
description?: string;
|
|
38
|
+
referenceId?: string;
|
|
39
|
+
idempotencyKey?: string;
|
|
40
|
+
}
|
|
41
|
+
interface SpendInput {
|
|
42
|
+
userId: string;
|
|
43
|
+
amount: number;
|
|
44
|
+
type: SpendType;
|
|
45
|
+
description?: string;
|
|
46
|
+
referenceId?: string;
|
|
47
|
+
idempotencyKey?: string;
|
|
48
|
+
}
|
|
49
|
+
type TransactionResult = {
|
|
50
|
+
idempotent: true;
|
|
51
|
+
} | {
|
|
52
|
+
idempotent: false;
|
|
53
|
+
balance: number;
|
|
54
|
+
transactionId: string;
|
|
55
|
+
amount: number;
|
|
56
|
+
};
|
|
57
|
+
interface RefundInput {
|
|
58
|
+
transactionId: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
interface RefundResult {
|
|
62
|
+
refundTransactionId: string;
|
|
63
|
+
originalTransactionId: string;
|
|
64
|
+
balance: number;
|
|
65
|
+
amount: number;
|
|
66
|
+
}
|
|
67
|
+
interface BalanceTransaction {
|
|
68
|
+
id: string;
|
|
69
|
+
amount: number;
|
|
70
|
+
balanceAfter: number;
|
|
71
|
+
type: string;
|
|
72
|
+
description: string | null;
|
|
73
|
+
referenceId: string | null;
|
|
74
|
+
appId: string | null;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
}
|
|
77
|
+
interface BalanceResult {
|
|
78
|
+
balance: number;
|
|
79
|
+
walletId: string | null;
|
|
80
|
+
updatedAt: string | null;
|
|
81
|
+
transactions: BalanceTransaction[];
|
|
82
|
+
}
|
|
83
|
+
interface ConfigResult {
|
|
84
|
+
key: string;
|
|
85
|
+
value: string;
|
|
86
|
+
ratio: number;
|
|
87
|
+
}
|
|
88
|
+
interface RecipientInfo {
|
|
89
|
+
walletId: string | null;
|
|
90
|
+
name: string;
|
|
91
|
+
avatarUrl: string | null;
|
|
92
|
+
}
|
|
93
|
+
interface TransferInput {
|
|
94
|
+
accessToken: string;
|
|
95
|
+
recipient: string;
|
|
96
|
+
amount: number;
|
|
97
|
+
description?: string;
|
|
98
|
+
idempotencyKey: string;
|
|
99
|
+
}
|
|
100
|
+
type TransferResult = {
|
|
101
|
+
idempotent: true;
|
|
102
|
+
} | {
|
|
103
|
+
idempotent: false;
|
|
104
|
+
senderBalance: number;
|
|
105
|
+
recipientWalletId: string;
|
|
106
|
+
txOutId: string;
|
|
107
|
+
txInId: string;
|
|
108
|
+
amount: number;
|
|
109
|
+
};
|
|
110
|
+
interface VCoinServerConfig {
|
|
111
|
+
appId: string;
|
|
112
|
+
clientSecret: string;
|
|
113
|
+
baseUrl: string;
|
|
114
|
+
}
|
|
115
|
+
interface VCoinUserConfig {
|
|
116
|
+
baseUrl: string;
|
|
117
|
+
}
|
|
118
|
+
interface VCoinServerClient {
|
|
119
|
+
earn(input: EarnInput): Promise<VCoinResult<TransactionResult>>;
|
|
120
|
+
spend(input: SpendInput): Promise<VCoinResult<TransactionResult>>;
|
|
121
|
+
refund(input: RefundInput): Promise<VCoinResult<RefundResult>>;
|
|
122
|
+
balance(input: {
|
|
123
|
+
userId: string;
|
|
124
|
+
}): Promise<VCoinResult<BalanceResult>>;
|
|
125
|
+
getConfig(key?: string): Promise<VCoinResult<ConfigResult>>;
|
|
126
|
+
}
|
|
127
|
+
interface VCoinUserClient {
|
|
128
|
+
balance(input: {
|
|
129
|
+
accessToken: string;
|
|
130
|
+
}): Promise<VCoinResult<BalanceResult>>;
|
|
131
|
+
verifyRecipient(input: {
|
|
132
|
+
accessToken: string;
|
|
133
|
+
recipient: string;
|
|
134
|
+
}): Promise<VCoinResult<RecipientInfo>>;
|
|
135
|
+
transfer(input: TransferInput): Promise<VCoinResult<TransferResult>>;
|
|
136
|
+
createCheckout(input: {
|
|
137
|
+
accessToken: string;
|
|
138
|
+
packId: string;
|
|
139
|
+
}): Promise<VCoinResult<{
|
|
140
|
+
checkoutUrl: string;
|
|
141
|
+
}>>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
declare function createVCoinServerClient(config: VCoinServerConfig): VCoinServerClient;
|
|
145
|
+
|
|
146
|
+
declare function createVCoinUserClient(config: VCoinUserConfig): VCoinUserClient;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds a stable idempotency key from one or more parts (e.g. an action
|
|
150
|
+
* name, a resource id, a user id). Replaces the ad-hoc string
|
|
151
|
+
* concatenation (`upload_${documentId}_${userId}`) that every consuming
|
|
152
|
+
* app currently writes by hand.
|
|
153
|
+
*/
|
|
154
|
+
declare function idempotencyKey(...parts: string[]): string;
|
|
155
|
+
|
|
156
|
+
declare const VCOIN_CLIENT_VERSION = "0.1.0";
|
|
157
|
+
|
|
158
|
+
export { type BalanceResult, type BalanceTransaction, type ConfigResult, type EarnInput, type EarnType, type RecipientInfo, type RefundInput, type RefundResult, type SpendInput, type SpendType, type TransactionResult, type TransferInput, type TransferResult, VCOIN_CLIENT_VERSION, type VCoinBusinessError, VCoinNetworkError, type VCoinResult, type VCoinServerClient, type VCoinServerConfig, type VCoinUserClient, type VCoinUserConfig, createVCoinServerClient, createVCoinUserClient, idempotencyKey };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
declare class VCoinNetworkError extends Error {
|
|
2
|
+
readonly status?: number | undefined;
|
|
3
|
+
readonly cause?: unknown | undefined;
|
|
4
|
+
constructor(message: string, status?: number | undefined, cause?: unknown | undefined);
|
|
5
|
+
}
|
|
6
|
+
type VCoinBusinessError = "wallet_not_found" | "recipient_wallet_not_found" | "invalid_recipient" | "self_transfer" | "transaction_not_found" | "already_refunded" | "forbidden" | "invalid_token" | "missing_token" | "missing_credentials" | "rate_limited" | "cannot_spend_for_other_user" | "admin_deduct_requires_server_credentials" | "config_not_found" | "database_error";
|
|
7
|
+
type VCoinResult<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
data: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: "insufficient_balance";
|
|
13
|
+
balance: number;
|
|
14
|
+
required: number;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: "insufficient_balance_for_refund";
|
|
18
|
+
balance: number;
|
|
19
|
+
required: number;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: "daily_limit_exceeded";
|
|
23
|
+
dailySent: number;
|
|
24
|
+
dailyLimit: number;
|
|
25
|
+
remaining: number;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: VCoinBusinessError;
|
|
29
|
+
detail?: string;
|
|
30
|
+
};
|
|
31
|
+
type EarnType = "upload_reward" | "welcome_bonus" | "admin_grant" | "referral";
|
|
32
|
+
type SpendType = "document_purchase" | "decoration" | "payment" | "admin_deduct";
|
|
33
|
+
interface EarnInput {
|
|
34
|
+
userId: string;
|
|
35
|
+
amount: number;
|
|
36
|
+
type: EarnType;
|
|
37
|
+
description?: string;
|
|
38
|
+
referenceId?: string;
|
|
39
|
+
idempotencyKey?: string;
|
|
40
|
+
}
|
|
41
|
+
interface SpendInput {
|
|
42
|
+
userId: string;
|
|
43
|
+
amount: number;
|
|
44
|
+
type: SpendType;
|
|
45
|
+
description?: string;
|
|
46
|
+
referenceId?: string;
|
|
47
|
+
idempotencyKey?: string;
|
|
48
|
+
}
|
|
49
|
+
type TransactionResult = {
|
|
50
|
+
idempotent: true;
|
|
51
|
+
} | {
|
|
52
|
+
idempotent: false;
|
|
53
|
+
balance: number;
|
|
54
|
+
transactionId: string;
|
|
55
|
+
amount: number;
|
|
56
|
+
};
|
|
57
|
+
interface RefundInput {
|
|
58
|
+
transactionId: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
interface RefundResult {
|
|
62
|
+
refundTransactionId: string;
|
|
63
|
+
originalTransactionId: string;
|
|
64
|
+
balance: number;
|
|
65
|
+
amount: number;
|
|
66
|
+
}
|
|
67
|
+
interface BalanceTransaction {
|
|
68
|
+
id: string;
|
|
69
|
+
amount: number;
|
|
70
|
+
balanceAfter: number;
|
|
71
|
+
type: string;
|
|
72
|
+
description: string | null;
|
|
73
|
+
referenceId: string | null;
|
|
74
|
+
appId: string | null;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
}
|
|
77
|
+
interface BalanceResult {
|
|
78
|
+
balance: number;
|
|
79
|
+
walletId: string | null;
|
|
80
|
+
updatedAt: string | null;
|
|
81
|
+
transactions: BalanceTransaction[];
|
|
82
|
+
}
|
|
83
|
+
interface ConfigResult {
|
|
84
|
+
key: string;
|
|
85
|
+
value: string;
|
|
86
|
+
ratio: number;
|
|
87
|
+
}
|
|
88
|
+
interface RecipientInfo {
|
|
89
|
+
walletId: string | null;
|
|
90
|
+
name: string;
|
|
91
|
+
avatarUrl: string | null;
|
|
92
|
+
}
|
|
93
|
+
interface TransferInput {
|
|
94
|
+
accessToken: string;
|
|
95
|
+
recipient: string;
|
|
96
|
+
amount: number;
|
|
97
|
+
description?: string;
|
|
98
|
+
idempotencyKey: string;
|
|
99
|
+
}
|
|
100
|
+
type TransferResult = {
|
|
101
|
+
idempotent: true;
|
|
102
|
+
} | {
|
|
103
|
+
idempotent: false;
|
|
104
|
+
senderBalance: number;
|
|
105
|
+
recipientWalletId: string;
|
|
106
|
+
txOutId: string;
|
|
107
|
+
txInId: string;
|
|
108
|
+
amount: number;
|
|
109
|
+
};
|
|
110
|
+
interface VCoinServerConfig {
|
|
111
|
+
appId: string;
|
|
112
|
+
clientSecret: string;
|
|
113
|
+
baseUrl: string;
|
|
114
|
+
}
|
|
115
|
+
interface VCoinUserConfig {
|
|
116
|
+
baseUrl: string;
|
|
117
|
+
}
|
|
118
|
+
interface VCoinServerClient {
|
|
119
|
+
earn(input: EarnInput): Promise<VCoinResult<TransactionResult>>;
|
|
120
|
+
spend(input: SpendInput): Promise<VCoinResult<TransactionResult>>;
|
|
121
|
+
refund(input: RefundInput): Promise<VCoinResult<RefundResult>>;
|
|
122
|
+
balance(input: {
|
|
123
|
+
userId: string;
|
|
124
|
+
}): Promise<VCoinResult<BalanceResult>>;
|
|
125
|
+
getConfig(key?: string): Promise<VCoinResult<ConfigResult>>;
|
|
126
|
+
}
|
|
127
|
+
interface VCoinUserClient {
|
|
128
|
+
balance(input: {
|
|
129
|
+
accessToken: string;
|
|
130
|
+
}): Promise<VCoinResult<BalanceResult>>;
|
|
131
|
+
verifyRecipient(input: {
|
|
132
|
+
accessToken: string;
|
|
133
|
+
recipient: string;
|
|
134
|
+
}): Promise<VCoinResult<RecipientInfo>>;
|
|
135
|
+
transfer(input: TransferInput): Promise<VCoinResult<TransferResult>>;
|
|
136
|
+
createCheckout(input: {
|
|
137
|
+
accessToken: string;
|
|
138
|
+
packId: string;
|
|
139
|
+
}): Promise<VCoinResult<{
|
|
140
|
+
checkoutUrl: string;
|
|
141
|
+
}>>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
declare function createVCoinServerClient(config: VCoinServerConfig): VCoinServerClient;
|
|
145
|
+
|
|
146
|
+
declare function createVCoinUserClient(config: VCoinUserConfig): VCoinUserClient;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds a stable idempotency key from one or more parts (e.g. an action
|
|
150
|
+
* name, a resource id, a user id). Replaces the ad-hoc string
|
|
151
|
+
* concatenation (`upload_${documentId}_${userId}`) that every consuming
|
|
152
|
+
* app currently writes by hand.
|
|
153
|
+
*/
|
|
154
|
+
declare function idempotencyKey(...parts: string[]): string;
|
|
155
|
+
|
|
156
|
+
declare const VCOIN_CLIENT_VERSION = "0.1.0";
|
|
157
|
+
|
|
158
|
+
export { type BalanceResult, type BalanceTransaction, type ConfigResult, type EarnInput, type EarnType, type RecipientInfo, type RefundInput, type RefundResult, type SpendInput, type SpendType, type TransactionResult, type TransferInput, type TransferResult, VCOIN_CLIENT_VERSION, type VCoinBusinessError, VCoinNetworkError, type VCoinResult, type VCoinServerClient, type VCoinServerConfig, type VCoinUserClient, type VCoinUserConfig, createVCoinServerClient, createVCoinUserClient, idempotencyKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var VCoinNetworkError = class extends Error {
|
|
3
|
+
constructor(message, status, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "VCoinNetworkError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/fetcher.ts
|
|
12
|
+
function functionUrl(baseUrl, fn) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(baseUrl);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`vCoin baseUrl must be a valid URL (got: ${baseUrl})`);
|
|
18
|
+
}
|
|
19
|
+
const isLocal = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
20
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
21
|
+
throw new Error(`vCoin baseUrl must use https:// (got: ${baseUrl})`);
|
|
22
|
+
}
|
|
23
|
+
return `${baseUrl.replace(/\/+$/, "")}/functions/v1/${fn}`;
|
|
24
|
+
}
|
|
25
|
+
async function vcoinFetch(url, init) {
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetch(url, {
|
|
29
|
+
method: init.method,
|
|
30
|
+
headers: { "Content-Type": "application/json", ...init.headers },
|
|
31
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new VCoinNetworkError("Network error calling the vCoin API", void 0, err);
|
|
35
|
+
}
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
let json;
|
|
38
|
+
try {
|
|
39
|
+
json = text.length > 0 ? JSON.parse(text) : {};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new VCoinNetworkError("vCoin API returned a non-JSON response", res.status, err);
|
|
42
|
+
}
|
|
43
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
44
|
+
throw new VCoinNetworkError("vCoin API returned a non-object JSON response", res.status);
|
|
45
|
+
}
|
|
46
|
+
return { status: res.status, ok: res.ok, json };
|
|
47
|
+
}
|
|
48
|
+
function genericErrorResult(raw) {
|
|
49
|
+
const code = typeof raw.json.error === "string" ? raw.json.error : void 0;
|
|
50
|
+
if (!code) {
|
|
51
|
+
if (raw.status === 401) {
|
|
52
|
+
return { ok: false, error: "invalid_token", detail: void 0 };
|
|
53
|
+
}
|
|
54
|
+
throw new VCoinNetworkError(`vCoin API returned ${raw.status} with no recognizable error code`, raw.status);
|
|
55
|
+
}
|
|
56
|
+
const detail = typeof raw.json.detail === "string" ? raw.json.detail : void 0;
|
|
57
|
+
return { ok: false, error: code, detail };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/mappers.ts
|
|
61
|
+
function num(v) {
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
function str(v) {
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
function strOrNull(v) {
|
|
68
|
+
return v ?? null;
|
|
69
|
+
}
|
|
70
|
+
function toTransactionResult(raw) {
|
|
71
|
+
if (raw.ok) {
|
|
72
|
+
if (raw.json.idempotent === true) {
|
|
73
|
+
return { ok: true, data: { idempotent: true } };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
data: {
|
|
78
|
+
idempotent: false,
|
|
79
|
+
balance: num(raw.json.balance),
|
|
80
|
+
transactionId: str(raw.json.transaction_id),
|
|
81
|
+
amount: num(raw.json.amount)
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (raw.json.error === "insufficient_balance") {
|
|
86
|
+
return { ok: false, error: "insufficient_balance", balance: num(raw.json.balance), required: num(raw.json.required) };
|
|
87
|
+
}
|
|
88
|
+
return genericErrorResult(raw);
|
|
89
|
+
}
|
|
90
|
+
function toRefundResult(raw) {
|
|
91
|
+
if (raw.ok) {
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
data: {
|
|
95
|
+
refundTransactionId: str(raw.json.refund_transaction_id),
|
|
96
|
+
originalTransactionId: str(raw.json.original_transaction_id),
|
|
97
|
+
balance: num(raw.json.balance),
|
|
98
|
+
amount: num(raw.json.amount)
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (raw.json.error === "insufficient_balance_for_refund") {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: "insufficient_balance_for_refund",
|
|
106
|
+
balance: num(raw.json.balance),
|
|
107
|
+
required: num(raw.json.required)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return genericErrorResult(raw);
|
|
111
|
+
}
|
|
112
|
+
function toBalanceResult(raw) {
|
|
113
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
114
|
+
const rawTx = raw.json.transactions ?? [];
|
|
115
|
+
const transactions = rawTx.map((t) => ({
|
|
116
|
+
id: str(t.id),
|
|
117
|
+
amount: num(t.amount),
|
|
118
|
+
balanceAfter: num(t.balance_after),
|
|
119
|
+
type: str(t.type),
|
|
120
|
+
description: strOrNull(t.description),
|
|
121
|
+
referenceId: strOrNull(t.reference_id),
|
|
122
|
+
appId: strOrNull(t.app_id),
|
|
123
|
+
createdAt: str(t.created_at)
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
data: {
|
|
128
|
+
balance: num(raw.json.balance),
|
|
129
|
+
walletId: strOrNull(raw.json.wallet_id),
|
|
130
|
+
updatedAt: strOrNull(raw.json.updated_at),
|
|
131
|
+
transactions
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function toConfigResult(raw) {
|
|
136
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
data: { key: str(raw.json.key), value: str(raw.json.value), ratio: num(raw.json.ratio) }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function toRecipientInfo(raw) {
|
|
143
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
data: {
|
|
147
|
+
walletId: strOrNull(raw.json.wallet_id),
|
|
148
|
+
name: str(raw.json.name),
|
|
149
|
+
avatarUrl: strOrNull(raw.json.avatar_url)
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function toTransferResult(raw) {
|
|
154
|
+
if (raw.json.error === "insufficient_balance") {
|
|
155
|
+
return { ok: false, error: "insufficient_balance", balance: num(raw.json.balance), required: num(raw.json.required) };
|
|
156
|
+
}
|
|
157
|
+
if (raw.json.error === "daily_limit_exceeded") {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: "daily_limit_exceeded",
|
|
161
|
+
dailySent: num(raw.json.daily_sent),
|
|
162
|
+
dailyLimit: num(raw.json.daily_limit),
|
|
163
|
+
remaining: num(raw.json.remaining)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!raw.ok || typeof raw.json.error === "string") {
|
|
167
|
+
return genericErrorResult(raw);
|
|
168
|
+
}
|
|
169
|
+
if (raw.json.idempotent === true) {
|
|
170
|
+
return { ok: true, data: { idempotent: true } };
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
data: {
|
|
175
|
+
idempotent: false,
|
|
176
|
+
senderBalance: num(raw.json.sender_balance),
|
|
177
|
+
recipientWalletId: str(raw.json.recipient_wallet_id),
|
|
178
|
+
txOutId: str(raw.json.tx_out_id),
|
|
179
|
+
txInId: str(raw.json.tx_in_id),
|
|
180
|
+
amount: num(raw.json.amount)
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function toCheckoutResult(raw) {
|
|
185
|
+
if (!raw.ok) return genericErrorResult(raw);
|
|
186
|
+
return { ok: true, data: { checkoutUrl: str(raw.json.checkout_url) } };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/server.ts
|
|
190
|
+
function createVCoinServerClient(config) {
|
|
191
|
+
if (typeof document !== "undefined") {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"createVCoinServerClient must never run in a browser \u2014 it holds a secret. Use createVCoinUserClient for browser code."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
async function earn(input) {
|
|
197
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-earn"), {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
200
|
+
body: {
|
|
201
|
+
user_id: input.userId,
|
|
202
|
+
amount: input.amount,
|
|
203
|
+
type: input.type,
|
|
204
|
+
description: input.description,
|
|
205
|
+
reference_id: input.referenceId,
|
|
206
|
+
app_id: config.appId,
|
|
207
|
+
idempotency_key: input.idempotencyKey
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return toTransactionResult(raw);
|
|
211
|
+
}
|
|
212
|
+
async function spend(input) {
|
|
213
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-spend"), {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
216
|
+
body: {
|
|
217
|
+
user_id: input.userId,
|
|
218
|
+
amount: input.amount,
|
|
219
|
+
type: input.type,
|
|
220
|
+
description: input.description,
|
|
221
|
+
reference_id: input.referenceId,
|
|
222
|
+
app_id: config.appId,
|
|
223
|
+
idempotency_key: input.idempotencyKey
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return toTransactionResult(raw);
|
|
227
|
+
}
|
|
228
|
+
async function refund(input) {
|
|
229
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-refund"), {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: { "x-vcoin-secret": config.clientSecret },
|
|
232
|
+
body: { transaction_id: input.transactionId, reason: input.reason, app_id: config.appId }
|
|
233
|
+
});
|
|
234
|
+
return toRefundResult(raw);
|
|
235
|
+
}
|
|
236
|
+
async function balance(input) {
|
|
237
|
+
const url = `${functionUrl(config.baseUrl, "vcoin-balance")}?user_id=${encodeURIComponent(input.userId)}`;
|
|
238
|
+
const raw = await vcoinFetch(url, {
|
|
239
|
+
method: "GET",
|
|
240
|
+
headers: { "x-vcoin-secret": config.clientSecret, "x-app-id": config.appId }
|
|
241
|
+
});
|
|
242
|
+
return toBalanceResult(raw);
|
|
243
|
+
}
|
|
244
|
+
async function getConfig(key) {
|
|
245
|
+
const qs = key ? `?key=${encodeURIComponent(key)}` : "";
|
|
246
|
+
const raw = await vcoinFetch(`${functionUrl(config.baseUrl, "vcoin-config")}${qs}`, {
|
|
247
|
+
method: "GET",
|
|
248
|
+
headers: {}
|
|
249
|
+
});
|
|
250
|
+
return toConfigResult(raw);
|
|
251
|
+
}
|
|
252
|
+
return { earn, spend, refund, balance, getConfig };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/user.ts
|
|
256
|
+
function createVCoinUserClient(config) {
|
|
257
|
+
async function balance(input) {
|
|
258
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-balance"), {
|
|
259
|
+
method: "GET",
|
|
260
|
+
headers: { Authorization: `Bearer ${input.accessToken}` }
|
|
261
|
+
});
|
|
262
|
+
return toBalanceResult(raw);
|
|
263
|
+
}
|
|
264
|
+
async function verifyRecipient(input) {
|
|
265
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-transfer"), {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
268
|
+
body: { action: "verify", recipient: input.recipient }
|
|
269
|
+
});
|
|
270
|
+
return toRecipientInfo(raw);
|
|
271
|
+
}
|
|
272
|
+
async function transfer(input) {
|
|
273
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-transfer"), {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
276
|
+
body: {
|
|
277
|
+
action: "transfer",
|
|
278
|
+
recipient: input.recipient,
|
|
279
|
+
amount: input.amount,
|
|
280
|
+
description: input.description,
|
|
281
|
+
idempotency_key: input.idempotencyKey
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return toTransferResult(raw);
|
|
285
|
+
}
|
|
286
|
+
async function createCheckout(input) {
|
|
287
|
+
const raw = await vcoinFetch(functionUrl(config.baseUrl, "vcoin-create-checkout"), {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { Authorization: `Bearer ${input.accessToken}` },
|
|
290
|
+
body: { pack_id: input.packId }
|
|
291
|
+
});
|
|
292
|
+
return toCheckoutResult(raw);
|
|
293
|
+
}
|
|
294
|
+
return { balance, verifyRecipient, transfer, createCheckout };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/idempotency.ts
|
|
298
|
+
function idempotencyKey(...parts) {
|
|
299
|
+
if (parts.length === 0) {
|
|
300
|
+
throw new Error("idempotencyKey requires at least one part");
|
|
301
|
+
}
|
|
302
|
+
for (const part of parts) {
|
|
303
|
+
if (part.includes("::")) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
'idempotencyKey parts must not contain "::" (the separator) \u2014 this would let two different calls silently collide'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return parts.join("::");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/index.ts
|
|
313
|
+
var VCOIN_CLIENT_VERSION = "0.1.0";
|
|
314
|
+
export {
|
|
315
|
+
VCOIN_CLIENT_VERSION,
|
|
316
|
+
VCoinNetworkError,
|
|
317
|
+
createVCoinServerClient,
|
|
318
|
+
createVCoinUserClient,
|
|
319
|
+
idempotencyKey
|
|
320
|
+
};
|
|
321
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/fetcher.ts","../src/mappers.ts","../src/server.ts","../src/user.ts","../src/idempotency.ts","../src/index.ts"],"sourcesContent":["// ── Network-level error ──────────────────────────────────────────────────\n\nexport class VCoinNetworkError extends Error {\n constructor(message: string, readonly status?: number, readonly cause?: unknown) {\n super(message);\n this.name = \"VCoinNetworkError\";\n }\n}\n\n// ── Discriminated result type ────────────────────────────────────────────\n\nexport type VCoinBusinessError =\n | \"wallet_not_found\"\n | \"recipient_wallet_not_found\"\n | \"invalid_recipient\"\n | \"self_transfer\"\n | \"transaction_not_found\"\n | \"already_refunded\"\n | \"forbidden\"\n | \"invalid_token\"\n | \"missing_token\"\n | \"missing_credentials\"\n | \"rate_limited\"\n | \"cannot_spend_for_other_user\"\n | \"admin_deduct_requires_server_credentials\"\n | \"config_not_found\"\n | \"database_error\";\n\nexport type VCoinResult<T> =\n | { ok: true; data: T }\n | { ok: false; error: \"insufficient_balance\"; balance: number; required: number }\n | { ok: false; error: \"insufficient_balance_for_refund\"; balance: number; required: number }\n | { ok: false; error: \"daily_limit_exceeded\"; dailySent: number; dailyLimit: number; remaining: number }\n | { ok: false; error: VCoinBusinessError; detail?: string };\n\n// ── Earn / spend ──────────────────────────────────────────────────────────\n\nexport type EarnType = \"upload_reward\" | \"welcome_bonus\" | \"admin_grant\" | \"referral\";\nexport type SpendType = \"document_purchase\" | \"decoration\" | \"payment\" | \"admin_deduct\";\n\nexport interface EarnInput {\n userId: string;\n amount: number;\n type: EarnType;\n description?: string;\n referenceId?: string;\n idempotencyKey?: string;\n}\n\nexport interface SpendInput {\n userId: string;\n amount: number;\n type: SpendType;\n description?: string;\n referenceId?: string;\n idempotencyKey?: string;\n}\n\nexport type TransactionResult =\n | { idempotent: true }\n | { idempotent: false; balance: number; transactionId: string; amount: number };\n\n// ── Refund ────────────────────────────────────────────────────────────────\n\nexport interface RefundInput {\n transactionId: string;\n reason: string;\n}\n\nexport interface RefundResult {\n refundTransactionId: string;\n originalTransactionId: string;\n balance: number;\n amount: number;\n}\n\n// ── Balance ───────────────────────────────────────────────────────────────\n\nexport interface BalanceTransaction {\n id: string;\n amount: number;\n balanceAfter: number;\n type: string;\n description: string | null;\n referenceId: string | null;\n appId: string | null;\n createdAt: string;\n}\n\nexport interface BalanceResult {\n balance: number;\n walletId: string | null;\n updatedAt: string | null;\n transactions: BalanceTransaction[];\n}\n\n// ── Config ────────────────────────────────────────────────────────────────\n\nexport interface ConfigResult {\n key: string;\n value: string;\n ratio: number;\n}\n\n// ── Transfer ──────────────────────────────────────────────────────────────\n\nexport interface RecipientInfo {\n walletId: string | null;\n name: string;\n avatarUrl: string | null;\n}\n\nexport interface TransferInput {\n accessToken: string;\n recipient: string;\n amount: number;\n description?: string;\n idempotencyKey: string;\n}\n\nexport type TransferResult =\n | { idempotent: true }\n | {\n idempotent: false;\n senderBalance: number;\n recipientWalletId: string;\n txOutId: string;\n txInId: string;\n amount: number;\n };\n\n// ── Client configs ────────────────────────────────────────────────────────\n\nexport interface VCoinServerConfig {\n appId: string;\n clientSecret: string;\n baseUrl: string;\n}\n\nexport interface VCoinUserConfig {\n baseUrl: string;\n}\n\nexport interface VCoinServerClient {\n earn(input: EarnInput): Promise<VCoinResult<TransactionResult>>;\n spend(input: SpendInput): Promise<VCoinResult<TransactionResult>>;\n refund(input: RefundInput): Promise<VCoinResult<RefundResult>>;\n balance(input: { userId: string }): Promise<VCoinResult<BalanceResult>>;\n getConfig(key?: string): Promise<VCoinResult<ConfigResult>>;\n}\n\nexport interface VCoinUserClient {\n balance(input: { accessToken: string }): Promise<VCoinResult<BalanceResult>>;\n verifyRecipient(input: { accessToken: string; recipient: string }): Promise<VCoinResult<RecipientInfo>>;\n transfer(input: TransferInput): Promise<VCoinResult<TransferResult>>;\n createCheckout(input: { accessToken: string; packId: string }): Promise<VCoinResult<{ checkoutUrl: string }>>;\n}\n","import { VCoinNetworkError, type VCoinBusinessError, type VCoinResult } from \"./types\";\n\nexport function functionUrl(baseUrl: string, fn: string): string {\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`vCoin baseUrl must be a valid URL (got: ${baseUrl})`);\n }\n const isLocal =\n parsed.protocol === \"http:\" && (parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\");\n if (parsed.protocol !== \"https:\" && !isLocal) {\n throw new Error(`vCoin baseUrl must use https:// (got: ${baseUrl})`);\n }\n return `${baseUrl.replace(/\\/+$/, \"\")}/functions/v1/${fn}`;\n}\n\nexport interface VCoinRawResponse {\n status: number;\n ok: boolean;\n json: Record<string, unknown>;\n}\n\nexport async function vcoinFetch(\n url: string,\n init: { method: \"GET\" | \"POST\"; headers: Record<string, string>; body?: unknown },\n): Promise<VCoinRawResponse> {\n let res: Response;\n try {\n res = await fetch(url, {\n method: init.method,\n headers: { \"Content-Type\": \"application/json\", ...init.headers },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n });\n } catch (err) {\n throw new VCoinNetworkError(\"Network error calling the vCoin API\", undefined, err);\n }\n\n const text = await res.text();\n let json: Record<string, unknown>;\n try {\n json = text.length > 0 ? JSON.parse(text) : {};\n } catch (err) {\n throw new VCoinNetworkError(\"vCoin API returned a non-JSON response\", res.status, err);\n }\n\n if (json === null || typeof json !== \"object\" || Array.isArray(json)) {\n throw new VCoinNetworkError(\"vCoin API returned a non-object JSON response\", res.status);\n }\n\n return { status: res.status, ok: res.ok, json };\n}\n\nexport function genericErrorResult<T>(raw: VCoinRawResponse): VCoinResult<T> {\n const code = typeof raw.json.error === \"string\" ? raw.json.error : undefined;\n if (!code) {\n if (raw.status === 401) {\n return { ok: false, error: \"invalid_token\", detail: undefined };\n }\n throw new VCoinNetworkError(`vCoin API returned ${raw.status} with no recognizable error code`, raw.status);\n }\n const detail = typeof raw.json.detail === \"string\" ? raw.json.detail : undefined;\n return { ok: false, error: code as VCoinBusinessError, detail };\n}\n","import { genericErrorResult, type VCoinRawResponse } from \"./fetcher\";\nimport type {\n BalanceResult,\n BalanceTransaction,\n ConfigResult,\n RecipientInfo,\n RefundResult,\n TransactionResult,\n TransferResult,\n VCoinResult,\n} from \"./types\";\n\nfunction num(v: unknown): number {\n return v as number;\n}\nfunction str(v: unknown): string {\n return v as string;\n}\nfunction strOrNull(v: unknown): string | null {\n return (v as string | null) ?? null;\n}\n\nexport function toTransactionResult(raw: VCoinRawResponse): VCoinResult<TransactionResult> {\n if (raw.ok) {\n if (raw.json.idempotent === true) {\n return { ok: true, data: { idempotent: true } };\n }\n return {\n ok: true,\n data: {\n idempotent: false,\n balance: num(raw.json.balance),\n transactionId: str(raw.json.transaction_id),\n amount: num(raw.json.amount),\n },\n };\n }\n if (raw.json.error === \"insufficient_balance\") {\n return { ok: false, error: \"insufficient_balance\", balance: num(raw.json.balance), required: num(raw.json.required) };\n }\n return genericErrorResult<TransactionResult>(raw);\n}\n\nexport function toRefundResult(raw: VCoinRawResponse): VCoinResult<RefundResult> {\n if (raw.ok) {\n return {\n ok: true,\n data: {\n refundTransactionId: str(raw.json.refund_transaction_id),\n originalTransactionId: str(raw.json.original_transaction_id),\n balance: num(raw.json.balance),\n amount: num(raw.json.amount),\n },\n };\n }\n if (raw.json.error === \"insufficient_balance_for_refund\") {\n return {\n ok: false,\n error: \"insufficient_balance_for_refund\",\n balance: num(raw.json.balance),\n required: num(raw.json.required),\n };\n }\n return genericErrorResult<RefundResult>(raw);\n}\n\nexport function toBalanceResult(raw: VCoinRawResponse): VCoinResult<BalanceResult> {\n if (!raw.ok) return genericErrorResult<BalanceResult>(raw);\n const rawTx = (raw.json.transactions as Record<string, unknown>[] | undefined) ?? [];\n const transactions: BalanceTransaction[] = rawTx.map((t) => ({\n id: str(t.id),\n amount: num(t.amount),\n balanceAfter: num(t.balance_after),\n type: str(t.type),\n description: strOrNull(t.description),\n referenceId: strOrNull(t.reference_id),\n appId: strOrNull(t.app_id),\n createdAt: str(t.created_at),\n }));\n return {\n ok: true,\n data: {\n balance: num(raw.json.balance),\n walletId: strOrNull(raw.json.wallet_id),\n updatedAt: strOrNull(raw.json.updated_at),\n transactions,\n },\n };\n}\n\nexport function toConfigResult(raw: VCoinRawResponse): VCoinResult<ConfigResult> {\n if (!raw.ok) return genericErrorResult<ConfigResult>(raw);\n return {\n ok: true,\n data: { key: str(raw.json.key), value: str(raw.json.value), ratio: num(raw.json.ratio) },\n };\n}\n\nexport function toRecipientInfo(raw: VCoinRawResponse): VCoinResult<RecipientInfo> {\n if (!raw.ok) return genericErrorResult<RecipientInfo>(raw);\n return {\n ok: true,\n data: {\n walletId: strOrNull(raw.json.wallet_id),\n name: str(raw.json.name),\n avatarUrl: strOrNull(raw.json.avatar_url),\n },\n };\n}\n\nexport function toTransferResult(raw: VCoinRawResponse): VCoinResult<TransferResult> {\n // vcoin-transfer's business errors use real HTTP status codes (402 for\n // insufficient_balance, 429 for daily_limit_exceeded) — but we check\n // raw.json.error before trusting raw.ok as defense in depth, since this\n // endpoint's exact status-code contract isn't guaranteed stable.\n if (raw.json.error === \"insufficient_balance\") {\n return { ok: false, error: \"insufficient_balance\", balance: num(raw.json.balance), required: num(raw.json.required) };\n }\n if (raw.json.error === \"daily_limit_exceeded\") {\n return {\n ok: false,\n error: \"daily_limit_exceeded\",\n dailySent: num(raw.json.daily_sent),\n dailyLimit: num(raw.json.daily_limit),\n remaining: num(raw.json.remaining),\n };\n }\n if (!raw.ok || typeof raw.json.error === \"string\") {\n return genericErrorResult<TransferResult>(raw);\n }\n if (raw.json.idempotent === true) {\n return { ok: true, data: { idempotent: true } };\n }\n return {\n ok: true,\n data: {\n idempotent: false,\n senderBalance: num(raw.json.sender_balance),\n recipientWalletId: str(raw.json.recipient_wallet_id),\n txOutId: str(raw.json.tx_out_id),\n txInId: str(raw.json.tx_in_id),\n amount: num(raw.json.amount),\n },\n };\n}\n\nexport function toCheckoutResult(raw: VCoinRawResponse): VCoinResult<{ checkoutUrl: string }> {\n if (!raw.ok) return genericErrorResult<{ checkoutUrl: string }>(raw);\n return { ok: true, data: { checkoutUrl: str(raw.json.checkout_url) } };\n}\n","import { functionUrl, vcoinFetch } from \"./fetcher\";\nimport {\n toBalanceResult,\n toConfigResult,\n toRefundResult,\n toTransactionResult,\n} from \"./mappers\";\nimport type {\n BalanceResult,\n ConfigResult,\n EarnInput,\n RefundInput,\n RefundResult,\n SpendInput,\n TransactionResult,\n VCoinResult,\n VCoinServerClient,\n VCoinServerConfig,\n} from \"./types\";\n\nexport function createVCoinServerClient(config: VCoinServerConfig): VCoinServerClient {\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"createVCoinServerClient must never run in a browser — it holds a secret. Use createVCoinUserClient for browser code.\",\n );\n }\n\n async function earn(input: EarnInput): Promise<VCoinResult<TransactionResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-earn\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: {\n user_id: input.userId,\n amount: input.amount,\n type: input.type,\n description: input.description,\n reference_id: input.referenceId,\n app_id: config.appId,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransactionResult(raw);\n }\n\n async function spend(input: SpendInput): Promise<VCoinResult<TransactionResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-spend\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: {\n user_id: input.userId,\n amount: input.amount,\n type: input.type,\n description: input.description,\n reference_id: input.referenceId,\n app_id: config.appId,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransactionResult(raw);\n }\n\n async function refund(input: RefundInput): Promise<VCoinResult<RefundResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-refund\"), {\n method: \"POST\",\n headers: { \"x-vcoin-secret\": config.clientSecret },\n body: { transaction_id: input.transactionId, reason: input.reason, app_id: config.appId },\n });\n return toRefundResult(raw);\n }\n\n async function balance(input: { userId: string }): Promise<VCoinResult<BalanceResult>> {\n const url = `${functionUrl(config.baseUrl, \"vcoin-balance\")}?user_id=${encodeURIComponent(input.userId)}`;\n const raw = await vcoinFetch(url, {\n method: \"GET\",\n headers: { \"x-vcoin-secret\": config.clientSecret, \"x-app-id\": config.appId },\n });\n return toBalanceResult(raw);\n }\n\n async function getConfig(key?: string): Promise<VCoinResult<ConfigResult>> {\n const qs = key ? `?key=${encodeURIComponent(key)}` : \"\";\n const raw = await vcoinFetch(`${functionUrl(config.baseUrl, \"vcoin-config\")}${qs}`, {\n method: \"GET\",\n headers: {},\n });\n return toConfigResult(raw);\n }\n\n return { earn, spend, refund, balance, getConfig };\n}\n","import { functionUrl, vcoinFetch } from \"./fetcher\";\nimport { toBalanceResult, toCheckoutResult, toRecipientInfo, toTransferResult } from \"./mappers\";\nimport type {\n BalanceResult,\n RecipientInfo,\n TransferInput,\n TransferResult,\n VCoinResult,\n VCoinUserClient,\n VCoinUserConfig,\n} from \"./types\";\n\nexport function createVCoinUserClient(config: VCoinUserConfig): VCoinUserClient {\n async function balance(input: { accessToken: string }): Promise<VCoinResult<BalanceResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-balance\"), {\n method: \"GET\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n });\n return toBalanceResult(raw);\n }\n\n async function verifyRecipient(input: {\n accessToken: string;\n recipient: string;\n }): Promise<VCoinResult<RecipientInfo>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-transfer\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: { action: \"verify\", recipient: input.recipient },\n });\n return toRecipientInfo(raw);\n }\n\n async function transfer(input: TransferInput): Promise<VCoinResult<TransferResult>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-transfer\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: {\n action: \"transfer\",\n recipient: input.recipient,\n amount: input.amount,\n description: input.description,\n idempotency_key: input.idempotencyKey,\n },\n });\n return toTransferResult(raw);\n }\n\n async function createCheckout(input: {\n accessToken: string;\n packId: string;\n }): Promise<VCoinResult<{ checkoutUrl: string }>> {\n const raw = await vcoinFetch(functionUrl(config.baseUrl, \"vcoin-create-checkout\"), {\n method: \"POST\",\n headers: { Authorization: `Bearer ${input.accessToken}` },\n body: { pack_id: input.packId },\n });\n return toCheckoutResult(raw);\n }\n\n return { balance, verifyRecipient, transfer, createCheckout };\n}\n","/**\n * Builds a stable idempotency key from one or more parts (e.g. an action\n * name, a resource id, a user id). Replaces the ad-hoc string\n * concatenation (`upload_${documentId}_${userId}`) that every consuming\n * app currently writes by hand.\n */\nexport function idempotencyKey(...parts: string[]): string {\n if (parts.length === 0) {\n throw new Error(\"idempotencyKey requires at least one part\");\n }\n for (const part of parts) {\n if (part.includes(\"::\")) {\n throw new Error(\n 'idempotencyKey parts must not contain \"::\" (the separator) — this would let two different calls silently collide',\n );\n }\n }\n return parts.join(\"::\");\n}\n","export const VCOIN_CLIENT_VERSION = \"0.1.0\";\n\nexport { createVCoinServerClient } from \"./server\";\nexport { createVCoinUserClient } from \"./user\";\nexport { idempotencyKey } from \"./idempotency\";\n\nexport {\n VCoinNetworkError,\n type VCoinResult,\n type VCoinBusinessError,\n type EarnType,\n type SpendType,\n type EarnInput,\n type SpendInput,\n type TransactionResult,\n type RefundInput,\n type RefundResult,\n type BalanceResult,\n type BalanceTransaction,\n type ConfigResult,\n type RecipientInfo,\n type TransferInput,\n type TransferResult,\n type VCoinServerConfig,\n type VCoinUserConfig,\n type VCoinServerClient,\n type VCoinUserClient,\n} from \"./types\";\n"],"mappings":";AAEO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YAAY,SAA0B,QAA0B,OAAiB;AAC/E,UAAM,OAAO;AADuB;AAA0B;AAE9D,SAAK,OAAO;AAAA,EACd;AACF;;;ACLO,SAAS,YAAY,SAAiB,IAAoB;AAC/D,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,OAAO;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI,MAAM,2CAA2C,OAAO,GAAG;AAAA,EACvE;AACA,QAAM,UACJ,OAAO,aAAa,YAAY,OAAO,aAAa,eAAe,OAAO,aAAa;AACzF,MAAI,OAAO,aAAa,YAAY,CAAC,SAAS;AAC5C,UAAM,IAAI,MAAM,yCAAyC,OAAO,GAAG;AAAA,EACrE;AACA,SAAO,GAAG,QAAQ,QAAQ,QAAQ,EAAE,CAAC,iBAAiB,EAAE;AAC1D;AAQA,eAAsB,WACpB,KACA,MAC2B;AAC3B,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,QAAQ;AAAA,MAC/D,MAAM,KAAK,SAAS,SAAY,KAAK,UAAU,KAAK,IAAI,IAAI;AAAA,IAC9D,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,IAAI,kBAAkB,uCAAuC,QAAW,GAAG;AAAA,EACnF;AAEA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,SAAS,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,EAC/C,SAAS,KAAK;AACZ,UAAM,IAAI,kBAAkB,0CAA0C,IAAI,QAAQ,GAAG;AAAA,EACvF;AAEA,MAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;AACpE,UAAM,IAAI,kBAAkB,iDAAiD,IAAI,MAAM;AAAA,EACzF;AAEA,SAAO,EAAE,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,KAAK;AAChD;AAEO,SAAS,mBAAsB,KAAuC;AAC3E,QAAM,OAAO,OAAO,IAAI,KAAK,UAAU,WAAW,IAAI,KAAK,QAAQ;AACnE,MAAI,CAAC,MAAM;AACT,QAAI,IAAI,WAAW,KAAK;AACtB,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,QAAQ,OAAU;AAAA,IAChE;AACA,UAAM,IAAI,kBAAkB,sBAAsB,IAAI,MAAM,oCAAoC,IAAI,MAAM;AAAA,EAC5G;AACA,QAAM,SAAS,OAAO,IAAI,KAAK,WAAW,WAAW,IAAI,KAAK,SAAS;AACvE,SAAO,EAAE,IAAI,OAAO,OAAO,MAA4B,OAAO;AAChE;;;ACnDA,SAAS,IAAI,GAAoB;AAC/B,SAAO;AACT;AACA,SAAS,IAAI,GAAoB;AAC/B,SAAO;AACT;AACA,SAAS,UAAU,GAA2B;AAC5C,SAAQ,KAAuB;AACjC;AAEO,SAAS,oBAAoB,KAAuD;AACzF,MAAI,IAAI,IAAI;AACV,QAAI,IAAI,KAAK,eAAe,MAAM;AAChC,aAAO,EAAE,IAAI,MAAM,MAAM,EAAE,YAAY,KAAK,EAAE;AAAA,IAChD;AACA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM;AAAA,QACJ,YAAY;AAAA,QACZ,SAAS,IAAI,IAAI,KAAK,OAAO;AAAA,QAC7B,eAAe,IAAI,IAAI,KAAK,cAAc;AAAA,QAC1C,QAAQ,IAAI,IAAI,KAAK,MAAM;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACA,MAAI,IAAI,KAAK,UAAU,wBAAwB;AAC7C,WAAO,EAAE,IAAI,OAAO,OAAO,wBAAwB,SAAS,IAAI,IAAI,KAAK,OAAO,GAAG,UAAU,IAAI,IAAI,KAAK,QAAQ,EAAE;AAAA,EACtH;AACA,SAAO,mBAAsC,GAAG;AAClD;AAEO,SAAS,eAAe,KAAkD;AAC/E,MAAI,IAAI,IAAI;AACV,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM;AAAA,QACJ,qBAAqB,IAAI,IAAI,KAAK,qBAAqB;AAAA,QACvD,uBAAuB,IAAI,IAAI,KAAK,uBAAuB;AAAA,QAC3D,SAAS,IAAI,IAAI,KAAK,OAAO;AAAA,QAC7B,QAAQ,IAAI,IAAI,KAAK,MAAM;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACA,MAAI,IAAI,KAAK,UAAU,mCAAmC;AACxD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,SAAS,IAAI,IAAI,KAAK,OAAO;AAAA,MAC7B,UAAU,IAAI,IAAI,KAAK,QAAQ;AAAA,IACjC;AAAA,EACF;AACA,SAAO,mBAAiC,GAAG;AAC7C;AAEO,SAAS,gBAAgB,KAAmD;AACjF,MAAI,CAAC,IAAI,GAAI,QAAO,mBAAkC,GAAG;AACzD,QAAM,QAAS,IAAI,KAAK,gBAA0D,CAAC;AACnF,QAAM,eAAqC,MAAM,IAAI,CAAC,OAAO;AAAA,IAC3D,IAAI,IAAI,EAAE,EAAE;AAAA,IACZ,QAAQ,IAAI,EAAE,MAAM;AAAA,IACpB,cAAc,IAAI,EAAE,aAAa;AAAA,IACjC,MAAM,IAAI,EAAE,IAAI;AAAA,IAChB,aAAa,UAAU,EAAE,WAAW;AAAA,IACpC,aAAa,UAAU,EAAE,YAAY;AAAA,IACrC,OAAO,UAAU,EAAE,MAAM;AAAA,IACzB,WAAW,IAAI,EAAE,UAAU;AAAA,EAC7B,EAAE;AACF,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,SAAS,IAAI,IAAI,KAAK,OAAO;AAAA,MAC7B,UAAU,UAAU,IAAI,KAAK,SAAS;AAAA,MACtC,WAAW,UAAU,IAAI,KAAK,UAAU;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,eAAe,KAAkD;AAC/E,MAAI,CAAC,IAAI,GAAI,QAAO,mBAAiC,GAAG;AACxD,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM,EAAE,KAAK,IAAI,IAAI,KAAK,GAAG,GAAG,OAAO,IAAI,IAAI,KAAK,KAAK,GAAG,OAAO,IAAI,IAAI,KAAK,KAAK,EAAE;AAAA,EACzF;AACF;AAEO,SAAS,gBAAgB,KAAmD;AACjF,MAAI,CAAC,IAAI,GAAI,QAAO,mBAAkC,GAAG;AACzD,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,UAAU,UAAU,IAAI,KAAK,SAAS;AAAA,MACtC,MAAM,IAAI,IAAI,KAAK,IAAI;AAAA,MACvB,WAAW,UAAU,IAAI,KAAK,UAAU;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,iBAAiB,KAAoD;AAKnF,MAAI,IAAI,KAAK,UAAU,wBAAwB;AAC7C,WAAO,EAAE,IAAI,OAAO,OAAO,wBAAwB,SAAS,IAAI,IAAI,KAAK,OAAO,GAAG,UAAU,IAAI,IAAI,KAAK,QAAQ,EAAE;AAAA,EACtH;AACA,MAAI,IAAI,KAAK,UAAU,wBAAwB;AAC7C,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,WAAW,IAAI,IAAI,KAAK,UAAU;AAAA,MAClC,YAAY,IAAI,IAAI,KAAK,WAAW;AAAA,MACpC,WAAW,IAAI,IAAI,KAAK,SAAS;AAAA,IACnC;AAAA,EACF;AACA,MAAI,CAAC,IAAI,MAAM,OAAO,IAAI,KAAK,UAAU,UAAU;AACjD,WAAO,mBAAmC,GAAG;AAAA,EAC/C;AACA,MAAI,IAAI,KAAK,eAAe,MAAM;AAChC,WAAO,EAAE,IAAI,MAAM,MAAM,EAAE,YAAY,KAAK,EAAE;AAAA,EAChD;AACA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,YAAY;AAAA,MACZ,eAAe,IAAI,IAAI,KAAK,cAAc;AAAA,MAC1C,mBAAmB,IAAI,IAAI,KAAK,mBAAmB;AAAA,MACnD,SAAS,IAAI,IAAI,KAAK,SAAS;AAAA,MAC/B,QAAQ,IAAI,IAAI,KAAK,QAAQ;AAAA,MAC7B,QAAQ,IAAI,IAAI,KAAK,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;AAEO,SAAS,iBAAiB,KAA6D;AAC5F,MAAI,CAAC,IAAI,GAAI,QAAO,mBAA4C,GAAG;AACnE,SAAO,EAAE,IAAI,MAAM,MAAM,EAAE,aAAa,IAAI,IAAI,KAAK,YAAY,EAAE,EAAE;AACvE;;;ACjIO,SAAS,wBAAwB,QAA8C;AACpF,MAAI,OAAO,aAAa,aAAa;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,KAAK,OAA2D;AAC7E,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,YAAY,GAAG;AAAA,MACtE,QAAQ;AAAA,MACR,SAAS,EAAE,kBAAkB,OAAO,aAAa;AAAA,MACjD,MAAM;AAAA,QACJ,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,QACd,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,QACpB,QAAQ,OAAO;AAAA,QACf,iBAAiB,MAAM;AAAA,MACzB;AAAA,IACF,CAAC;AACD,WAAO,oBAAoB,GAAG;AAAA,EAChC;AAEA,iBAAe,MAAM,OAA4D;AAC/E,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,aAAa,GAAG;AAAA,MACvE,QAAQ;AAAA,MACR,SAAS,EAAE,kBAAkB,OAAO,aAAa;AAAA,MACjD,MAAM;AAAA,QACJ,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,QACd,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,QACpB,QAAQ,OAAO;AAAA,QACf,iBAAiB,MAAM;AAAA,MACzB;AAAA,IACF,CAAC;AACD,WAAO,oBAAoB,GAAG;AAAA,EAChC;AAEA,iBAAe,OAAO,OAAwD;AAC5E,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,cAAc,GAAG;AAAA,MACxE,QAAQ;AAAA,MACR,SAAS,EAAE,kBAAkB,OAAO,aAAa;AAAA,MACjD,MAAM,EAAE,gBAAgB,MAAM,eAAe,QAAQ,MAAM,QAAQ,QAAQ,OAAO,MAAM;AAAA,IAC1F,CAAC;AACD,WAAO,eAAe,GAAG;AAAA,EAC3B;AAEA,iBAAe,QAAQ,OAAgE;AACrF,UAAM,MAAM,GAAG,YAAY,OAAO,SAAS,eAAe,CAAC,YAAY,mBAAmB,MAAM,MAAM,CAAC;AACvG,UAAM,MAAM,MAAM,WAAW,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,kBAAkB,OAAO,cAAc,YAAY,OAAO,MAAM;AAAA,IAC7E,CAAC;AACD,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAEA,iBAAe,UAAU,KAAkD;AACzE,UAAM,KAAK,MAAM,QAAQ,mBAAmB,GAAG,CAAC,KAAK;AACrD,UAAM,MAAM,MAAM,WAAW,GAAG,YAAY,OAAO,SAAS,cAAc,CAAC,GAAG,EAAE,IAAI;AAAA,MAClF,QAAQ;AAAA,MACR,SAAS,CAAC;AAAA,IACZ,CAAC;AACD,WAAO,eAAe,GAAG;AAAA,EAC3B;AAEA,SAAO,EAAE,MAAM,OAAO,QAAQ,SAAS,UAAU;AACnD;;;AC7EO,SAAS,sBAAsB,QAA0C;AAC9E,iBAAe,QAAQ,OAAqE;AAC1F,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,eAAe,GAAG;AAAA,MACzE,QAAQ;AAAA,MACR,SAAS,EAAE,eAAe,UAAU,MAAM,WAAW,GAAG;AAAA,IAC1D,CAAC;AACD,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAEA,iBAAe,gBAAgB,OAGS;AACtC,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,gBAAgB,GAAG;AAAA,MAC1E,QAAQ;AAAA,MACR,SAAS,EAAE,eAAe,UAAU,MAAM,WAAW,GAAG;AAAA,MACxD,MAAM,EAAE,QAAQ,UAAU,WAAW,MAAM,UAAU;AAAA,IACvD,CAAC;AACD,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAEA,iBAAe,SAAS,OAA4D;AAClF,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,gBAAgB,GAAG;AAAA,MAC1E,QAAQ;AAAA,MACR,SAAS,EAAE,eAAe,UAAU,MAAM,WAAW,GAAG;AAAA,MACxD,MAAM;AAAA,QACJ,QAAQ;AAAA,QACR,WAAW,MAAM;AAAA,QACjB,QAAQ,MAAM;AAAA,QACd,aAAa,MAAM;AAAA,QACnB,iBAAiB,MAAM;AAAA,MACzB;AAAA,IACF,CAAC;AACD,WAAO,iBAAiB,GAAG;AAAA,EAC7B;AAEA,iBAAe,eAAe,OAGoB;AAChD,UAAM,MAAM,MAAM,WAAW,YAAY,OAAO,SAAS,uBAAuB,GAAG;AAAA,MACjF,QAAQ;AAAA,MACR,SAAS,EAAE,eAAe,UAAU,MAAM,WAAW,GAAG;AAAA,MACxD,MAAM,EAAE,SAAS,MAAM,OAAO;AAAA,IAChC,CAAC;AACD,WAAO,iBAAiB,GAAG;AAAA,EAC7B;AAEA,SAAO,EAAE,SAAS,iBAAiB,UAAU,eAAe;AAC9D;;;ACvDO,SAAS,kBAAkB,OAAyB;AACzD,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AACA,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,SAAS,IAAI,GAAG;AACvB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;;;AClBO,IAAM,uBAAuB;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@versenco/vcoin-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server and user-session TypeScript client for Versenco's vCoin API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Versenco/versenco-shared.git",
|
|
9
|
+
"directory": "packages/vcoin-client"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
18
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
22
|
+
"publishConfig": { "access": "public" },
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.3.0",
|
|
30
|
+
"typescript": "^5.6.0",
|
|
31
|
+
"vitest": "^2.1.0"
|
|
32
|
+
}
|
|
33
|
+
}
|