@tokenbuddy/contracts 1.0.0 → 1.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/crypto.d.ts +50 -4
- package/dist/src/crypto.js +26 -4
- package/dist/src/errors.d.ts +42 -0
- package/dist/src/errors.js +30 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/upstream-balance-probe.d.ts +41 -0
- package/dist/src/upstream-balance-probe.js +384 -0
- package/package.json +6 -2
- package/dist/src/crypto.d.ts.map +0 -1
- package/dist/src/crypto.js.map +0 -1
- package/dist/src/errors.d.ts.map +0 -1
- package/dist/src/errors.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/src/crypto.ts +0 -120
- package/src/errors.ts +0 -173
- package/src/index.ts +0 -2
- package/tests/crypto.test.ts +0 -91
- package/tests/errors.test.ts +0 -34
- package/tsconfig.json +0 -8
package/dist/src/crypto.d.ts
CHANGED
|
@@ -9,37 +9,83 @@ export declare function encryptSm4EcbPkcs7Base64(plaintext: string, sm4KeyBase64
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function decryptSm4EcbPkcs7Base64(encryptedBase64: string, sm4KeyBase64: string): string;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 计算资源 slug 的 MD5 摘要(hex 编码)。
|
|
13
|
+
* 用作 Clawtip 支付的 indicator 字段,seller 端用于校验资源一致性。
|
|
14
|
+
*
|
|
15
|
+
* @param slug 资源 slug
|
|
16
|
+
* @returns 小写十六进制的 MD5 摘要
|
|
13
17
|
*/
|
|
14
18
|
export declare function computeIndicator(slug: string): string;
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
20
|
+
* 生成 32 字符小写十六进制字符串作为订单号。
|
|
21
|
+
* 实现细节:UUID v4 去除中横线,保证全局唯一且 URL/日志友好。
|
|
22
|
+
*
|
|
23
|
+
* @returns 32 字符小写 hex 字符串
|
|
17
24
|
*/
|
|
18
25
|
export declare function generateOrderNo(): string;
|
|
19
26
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
27
|
+
* 加密 Clawtip 订单数据。
|
|
28
|
+
* 明文格式:`{"orderNo":..., "amount":..., "payTo":...}`,使用 SM4-ECB-PKCS7 加密后 base64 编码。
|
|
29
|
+
*
|
|
30
|
+
* @param orderNo 订单号
|
|
31
|
+
* @param amount 订单金额(分)
|
|
32
|
+
* @param payTo 收款方标识
|
|
33
|
+
* @param sm4KeyBase64 SM4 密钥的 base64 编码(必须解码为 16 字节)
|
|
34
|
+
* @returns base64 编码的密文
|
|
22
35
|
*/
|
|
23
36
|
export declare function encryptClawtipOrderData(orderNo: string, amount: number, payTo: string, sm4KeyBase64: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Clawtip 支付下单所需的完整参数。
|
|
39
|
+
* 由 `buildClawtipPaymentParams` 在 buyer 端组装后下发到 seller。
|
|
40
|
+
*/
|
|
24
41
|
export interface ClawtipPaymentParams {
|
|
42
|
+
/** 32 字符小写十六进制的订单号(UUID v4 去横线),全局唯一标识一次下单 */
|
|
25
43
|
orderNo: string;
|
|
44
|
+
/** 订单金额,单位为分(人民币 1 元 = 100 分) */
|
|
26
45
|
amountFen: number;
|
|
46
|
+
/** 收款方标识,seller 校验该字段决定打款目标 */
|
|
27
47
|
payTo: string;
|
|
48
|
+
/** SM4-ECB-PKCS7 加密的 `{orderNo, amount, payTo}` JSON,base64 编码 */
|
|
28
49
|
encryptedData: string;
|
|
50
|
+
/** 资源 slug 的 MD5 摘要,用于 seller 校验资源一致性 */
|
|
29
51
|
indicator: string;
|
|
52
|
+
/** 资源 slug(人类可读 ID),与 indicator 配对使用 */
|
|
30
53
|
slug: string;
|
|
54
|
+
/** Clawtip 平台的技能 ID */
|
|
31
55
|
skillId: string;
|
|
56
|
+
/** 支付场景的人类可读描述,展示给支付方 */
|
|
32
57
|
description: string;
|
|
58
|
+
/** 资源下载/访问 URL,可用于支付完成后的资源定位 */
|
|
33
59
|
resourceUrl: string;
|
|
34
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* 构造 `ClawtipPaymentParams` 所需的原始输入。
|
|
63
|
+
* 调用方只需要提供业务字段;`orderNo` 和 `encryptedData` 由 builder 自动生成。
|
|
64
|
+
*/
|
|
35
65
|
export interface ClawtipPaymentParamsInput {
|
|
66
|
+
/** 收款方标识 */
|
|
36
67
|
payTo: string;
|
|
68
|
+
/** SM4 密钥的 base64 编码(解密后必须为 16 字节) */
|
|
37
69
|
sm4KeyBase64: string;
|
|
70
|
+
/** 资源 slug,会同时参与 indicator 计算 */
|
|
38
71
|
skillSlug: string;
|
|
72
|
+
/** Clawtip 平台的技能 ID */
|
|
39
73
|
skillId: string;
|
|
74
|
+
/** 支付场景描述 */
|
|
40
75
|
description: string;
|
|
76
|
+
/** 资源 URL */
|
|
41
77
|
resourceUrl: string;
|
|
78
|
+
/** 订单金额(分),必须为正整数 */
|
|
42
79
|
amountFen: number;
|
|
43
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* 构造 `ClawtipPaymentParams` 完整参数。
|
|
83
|
+
* 负责:生成 `orderNo`、对 `(orderNo, amount, payTo)` 做 SM4 加密、计算 `indicator`。
|
|
84
|
+
* `amountFen` 必须为正整数,否则抛错。
|
|
85
|
+
*
|
|
86
|
+
* @param input 业务字段输入
|
|
87
|
+
* @returns 包含加密数据的完整支付参数
|
|
88
|
+
* @throws Error 当 `amountFen` 不是正整数时
|
|
89
|
+
*/
|
|
44
90
|
export declare function buildClawtipPaymentParams(input: ClawtipPaymentParamsInput): ClawtipPaymentParams;
|
|
45
91
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/src/crypto.js
CHANGED
|
@@ -86,20 +86,33 @@ function decryptSm4EcbPkcs7Base64(encryptedBase64, sm4KeyBase64) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* 计算资源 slug 的 MD5 摘要(hex 编码)。
|
|
90
|
+
* 用作 Clawtip 支付的 indicator 字段,seller 端用于校验资源一致性。
|
|
91
|
+
*
|
|
92
|
+
* @param slug 资源 slug
|
|
93
|
+
* @returns 小写十六进制的 MD5 摘要
|
|
90
94
|
*/
|
|
91
95
|
function computeIndicator(slug) {
|
|
92
96
|
return crypto.createHash("md5").update(slug, "utf8").digest("hex");
|
|
93
97
|
}
|
|
94
98
|
/**
|
|
95
|
-
*
|
|
99
|
+
* 生成 32 字符小写十六进制字符串作为订单号。
|
|
100
|
+
* 实现细节:UUID v4 去除中横线,保证全局唯一且 URL/日志友好。
|
|
101
|
+
*
|
|
102
|
+
* @returns 32 字符小写 hex 字符串
|
|
96
103
|
*/
|
|
97
104
|
function generateOrderNo() {
|
|
98
105
|
return (0, uuid_1.v4)().replace(/-/g, "");
|
|
99
106
|
}
|
|
100
107
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
108
|
+
* 加密 Clawtip 订单数据。
|
|
109
|
+
* 明文格式:`{"orderNo":..., "amount":..., "payTo":...}`,使用 SM4-ECB-PKCS7 加密后 base64 编码。
|
|
110
|
+
*
|
|
111
|
+
* @param orderNo 订单号
|
|
112
|
+
* @param amount 订单金额(分)
|
|
113
|
+
* @param payTo 收款方标识
|
|
114
|
+
* @param sm4KeyBase64 SM4 密钥的 base64 编码(必须解码为 16 字节)
|
|
115
|
+
* @returns base64 编码的密文
|
|
103
116
|
*/
|
|
104
117
|
function encryptClawtipOrderData(orderNo, amount, payTo, sm4KeyBase64) {
|
|
105
118
|
const plaintext = JSON.stringify({
|
|
@@ -109,6 +122,15 @@ function encryptClawtipOrderData(orderNo, amount, payTo, sm4KeyBase64) {
|
|
|
109
122
|
});
|
|
110
123
|
return encryptSm4EcbPkcs7Base64(plaintext, sm4KeyBase64);
|
|
111
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* 构造 `ClawtipPaymentParams` 完整参数。
|
|
127
|
+
* 负责:生成 `orderNo`、对 `(orderNo, amount, payTo)` 做 SM4 加密、计算 `indicator`。
|
|
128
|
+
* `amountFen` 必须为正整数,否则抛错。
|
|
129
|
+
*
|
|
130
|
+
* @param input 业务字段输入
|
|
131
|
+
* @returns 包含加密数据的完整支付参数
|
|
132
|
+
* @throws Error 当 `amountFen` 不是正整数时
|
|
133
|
+
*/
|
|
112
134
|
function buildClawtipPaymentParams(input) {
|
|
113
135
|
if (!Number.isInteger(input.amountFen) || input.amountFen < 1) {
|
|
114
136
|
throw new Error("amount_fen must be greater than zero");
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -30,18 +30,60 @@ export declare enum ErrorCode {
|
|
|
30
30
|
UpstreamTimeout = "upstream_timeout",
|
|
31
31
|
ContractVersionDrift = "contract_version_drift"
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* 错误码到重试策略分类的映射。
|
|
35
|
+
* buyer / seller 路由层根据该结果决定是否自动重试、是否触发 fail-closed。
|
|
36
|
+
*
|
|
37
|
+
* @param code 业务错误码
|
|
38
|
+
* @returns 重试分类:`RetryableWithBackoff`(建议退避重试)、`FailClosed`(关闭交易、人工介入)、`NotRetryable`(直接报错给用户)
|
|
39
|
+
*/
|
|
33
40
|
export declare function getRetryClass(code: ErrorCode): RetryClass;
|
|
41
|
+
/**
|
|
42
|
+
* 错误码到 HTTP 状态码的映射。
|
|
43
|
+
* 当协议层把业务错误透传为 HTTP 响应(如 buyer → wallet-bootstrap 健康检查)时使用。
|
|
44
|
+
*
|
|
45
|
+
* @param code 业务错误码
|
|
46
|
+
* @returns 对应的 HTTP 状态码(4xx/5xx)
|
|
47
|
+
*/
|
|
34
48
|
export declare function getHttpStatus(code: ErrorCode): number;
|
|
49
|
+
/**
|
|
50
|
+
* 错误码到默认人类可读消息的映射。
|
|
51
|
+
* 用于 `createErrorEnvelope` 未传 `message` 参数时的兜底文案。
|
|
52
|
+
*
|
|
53
|
+
* @param code 业务错误码
|
|
54
|
+
* @returns 默认错误消息
|
|
55
|
+
*/
|
|
35
56
|
export declare function getDefaultMessage(code: ErrorCode): string;
|
|
57
|
+
/**
|
|
58
|
+
* 标准错误体的字段定义。HTTP 响应或 JSON-RPC error 对象均使用该结构。
|
|
59
|
+
*/
|
|
36
60
|
export interface ErrorBody {
|
|
61
|
+
/** 机器可读的错误码,用于客户端做错误分支处理 */
|
|
37
62
|
code: ErrorCode;
|
|
63
|
+
/** 错误重试分类(fail_closed / retryable_with_backoff / not_retryable) */
|
|
38
64
|
retryClass: RetryClass;
|
|
65
|
+
/** 人类可读的错误描述 */
|
|
39
66
|
message: string;
|
|
67
|
+
/** 推荐的 HTTP 状态码,HTTP 透传场景下使用 */
|
|
40
68
|
httpStatus: number;
|
|
69
|
+
/** 退避建议秒数(仅 429/重试类错误附带) */
|
|
41
70
|
retryAfterSeconds?: number;
|
|
42
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 错误响应的标准外壳。所有非 2xx 响应都以 `{ error: {...} }` 形式返回。
|
|
74
|
+
*/
|
|
43
75
|
export interface ErrorEnvelope {
|
|
76
|
+
/** 嵌套的错误体 */
|
|
44
77
|
error: ErrorBody;
|
|
45
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 构造标准错误响应外壳。
|
|
81
|
+
* 自动填充 `retryClass`、`httpStatus`、默认 `message`,调用方只需提供 `code` 和可选的覆盖字段。
|
|
82
|
+
*
|
|
83
|
+
* @param code 业务错误码,决定 retryClass / httpStatus / 默认 message
|
|
84
|
+
* @param message 自定义消息;省略时使用 `getDefaultMessage(code)` 兜底文案
|
|
85
|
+
* @param retryAfterSeconds 退避秒数,仅在需要时填入(如 429)
|
|
86
|
+
* @returns 完整的 `ErrorEnvelope` 对象
|
|
87
|
+
*/
|
|
46
88
|
export declare function createErrorEnvelope(code: ErrorCode, message?: string, retryAfterSeconds?: number): ErrorEnvelope;
|
|
47
89
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/src/errors.js
CHANGED
|
@@ -39,6 +39,13 @@ var ErrorCode;
|
|
|
39
39
|
ErrorCode["UpstreamTimeout"] = "upstream_timeout";
|
|
40
40
|
ErrorCode["ContractVersionDrift"] = "contract_version_drift";
|
|
41
41
|
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
42
|
+
/**
|
|
43
|
+
* 错误码到重试策略分类的映射。
|
|
44
|
+
* buyer / seller 路由层根据该结果决定是否自动重试、是否触发 fail-closed。
|
|
45
|
+
*
|
|
46
|
+
* @param code 业务错误码
|
|
47
|
+
* @returns 重试分类:`RetryableWithBackoff`(建议退避重试)、`FailClosed`(关闭交易、人工介入)、`NotRetryable`(直接报错给用户)
|
|
48
|
+
*/
|
|
42
49
|
function getRetryClass(code) {
|
|
43
50
|
switch (code) {
|
|
44
51
|
case ErrorCode.BusyCapacity:
|
|
@@ -52,6 +59,13 @@ function getRetryClass(code) {
|
|
|
52
59
|
return RetryClass.NotRetryable;
|
|
53
60
|
}
|
|
54
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 错误码到 HTTP 状态码的映射。
|
|
64
|
+
* 当协议层把业务错误透传为 HTTP 响应(如 buyer → wallet-bootstrap 健康检查)时使用。
|
|
65
|
+
*
|
|
66
|
+
* @param code 业务错误码
|
|
67
|
+
* @returns 对应的 HTTP 状态码(4xx/5xx)
|
|
68
|
+
*/
|
|
55
69
|
function getHttpStatus(code) {
|
|
56
70
|
switch (code) {
|
|
57
71
|
case ErrorCode.BusyCapacity:
|
|
@@ -92,6 +106,13 @@ function getHttpStatus(code) {
|
|
|
92
106
|
return 500;
|
|
93
107
|
}
|
|
94
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* 错误码到默认人类可读消息的映射。
|
|
111
|
+
* 用于 `createErrorEnvelope` 未传 `message` 参数时的兜底文案。
|
|
112
|
+
*
|
|
113
|
+
* @param code 业务错误码
|
|
114
|
+
* @returns 默认错误消息
|
|
115
|
+
*/
|
|
95
116
|
function getDefaultMessage(code) {
|
|
96
117
|
switch (code) {
|
|
97
118
|
case ErrorCode.BusyCapacity:
|
|
@@ -148,6 +169,15 @@ function getDefaultMessage(code) {
|
|
|
148
169
|
return "An unknown error occurred.";
|
|
149
170
|
}
|
|
150
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* 构造标准错误响应外壳。
|
|
174
|
+
* 自动填充 `retryClass`、`httpStatus`、默认 `message`,调用方只需提供 `code` 和可选的覆盖字段。
|
|
175
|
+
*
|
|
176
|
+
* @param code 业务错误码,决定 retryClass / httpStatus / 默认 message
|
|
177
|
+
* @param message 自定义消息;省略时使用 `getDefaultMessage(code)` 兜底文案
|
|
178
|
+
* @param retryAfterSeconds 退避秒数,仅在需要时填入(如 429)
|
|
179
|
+
* @returns 完整的 `ErrorEnvelope` 对象
|
|
180
|
+
*/
|
|
151
181
|
function createErrorEnvelope(code, message, retryAfterSeconds) {
|
|
152
182
|
return {
|
|
153
183
|
error: {
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -16,4 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./errors.js"), exports);
|
|
18
18
|
__exportStar(require("./crypto.js"), exports);
|
|
19
|
+
__exportStar(require("./upstream-balance-probe.js"), exports);
|
|
19
20
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type BalanceCurrency = "USD" | "CNY" | null;
|
|
2
|
+
export type BalanceSource = "deepseek" | "stepfun" | "siliconflow" | "openrouter" | "novita" | "newapi_generic" | "usage_generic" | "unknown";
|
|
3
|
+
export type BalanceProbeTemplate = "auto" | "deepseek" | "stepfun" | "siliconflow" | "openrouter" | "novita" | "newapi_generic" | "usage_generic" | "none";
|
|
4
|
+
export interface BalanceSnapshot {
|
|
5
|
+
rawAmount: number | null;
|
|
6
|
+
amountUsdMicros: number | null;
|
|
7
|
+
currency: BalanceCurrency;
|
|
8
|
+
source: BalanceSource;
|
|
9
|
+
fetchedAt: number;
|
|
10
|
+
error?: {
|
|
11
|
+
httpStatus: number;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface BalanceProbeConfig {
|
|
16
|
+
upstreamUrl?: string;
|
|
17
|
+
upstreamBalanceUrl?: string;
|
|
18
|
+
upstreamApiKey?: string;
|
|
19
|
+
upstreamUserId?: string;
|
|
20
|
+
upstreamBalanceProbe?: {
|
|
21
|
+
template?: BalanceProbeTemplate;
|
|
22
|
+
url?: string;
|
|
23
|
+
userId?: string;
|
|
24
|
+
rechargeUrl?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface BalanceProbeOptions {
|
|
28
|
+
fetch?: typeof fetch;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
cnyUsdRate?: number;
|
|
32
|
+
cache?: BalanceProbeCache;
|
|
33
|
+
}
|
|
34
|
+
export declare class BalanceProbeCache {
|
|
35
|
+
private readonly entries;
|
|
36
|
+
get(key: string, now: number): BalanceSnapshot | undefined;
|
|
37
|
+
setFailure(key: string, snapshot: BalanceSnapshot, now: number): void;
|
|
38
|
+
}
|
|
39
|
+
export declare const defaultBalanceProbeCache: BalanceProbeCache;
|
|
40
|
+
export declare function probeUpstreamBalance(config: BalanceProbeConfig, options?: BalanceProbeOptions): Promise<BalanceSnapshot>;
|
|
41
|
+
//# sourceMappingURL=upstream-balance-probe.d.ts.map
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultBalanceProbeCache = exports.BalanceProbeCache = void 0;
|
|
4
|
+
exports.probeUpstreamBalance = probeUpstreamBalance;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 7000;
|
|
6
|
+
const FAILURE_CACHE_TTL_MS = 30000;
|
|
7
|
+
const DEFAULT_CNY_USD_RATE = 0.14;
|
|
8
|
+
class BalanceProbeCache {
|
|
9
|
+
entries = new Map();
|
|
10
|
+
get(key, now) {
|
|
11
|
+
const entry = this.entries.get(key);
|
|
12
|
+
if (!entry || entry.expiresAt <= now) {
|
|
13
|
+
if (entry) {
|
|
14
|
+
this.entries.delete(key);
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return entry.snapshot;
|
|
19
|
+
}
|
|
20
|
+
setFailure(key, snapshot, now) {
|
|
21
|
+
this.entries.set(key, {
|
|
22
|
+
snapshot,
|
|
23
|
+
expiresAt: now + FAILURE_CACHE_TTL_MS
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.BalanceProbeCache = BalanceProbeCache;
|
|
28
|
+
exports.defaultBalanceProbeCache = new BalanceProbeCache();
|
|
29
|
+
async function probeUpstreamBalance(config, options = {}) {
|
|
30
|
+
const now = options.now || Date.now;
|
|
31
|
+
const fetchedAt = now();
|
|
32
|
+
const key = cacheKey(config);
|
|
33
|
+
const cache = options.cache || exports.defaultBalanceProbeCache;
|
|
34
|
+
const cached = cache.get(key, fetchedAt);
|
|
35
|
+
if (cached) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
const plan = requestPlan(config);
|
|
39
|
+
if (!plan) {
|
|
40
|
+
const snapshot = failure("unknown", fetchedAt, 0, "unsupported upstream - configure a balance parser");
|
|
41
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
42
|
+
return snapshot;
|
|
43
|
+
}
|
|
44
|
+
if (!config.upstreamApiKey) {
|
|
45
|
+
const snapshot = failure(plan.source, fetchedAt, 0, "missing upstreamApiKey for balance probe");
|
|
46
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
47
|
+
return snapshot;
|
|
48
|
+
}
|
|
49
|
+
const upstreamUserId = stringValue(config.upstreamBalanceProbe?.userId) || stringValue(config.upstreamUserId);
|
|
50
|
+
if (plan.requiresUserId && !upstreamUserId) {
|
|
51
|
+
const snapshot = failure(plan.source, fetchedAt, 0, "missing upstreamUserId for newapi upstream");
|
|
52
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
53
|
+
return snapshot;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetchWithTimeout(plan.url, {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: requestHeaders(config.upstreamApiKey, plan.requiresUserId ? upstreamUserId : undefined)
|
|
59
|
+
}, options.fetch || fetch, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const snapshot = failure(plan.source, fetchedAt, response.status, httpErrorMessage(response.status));
|
|
62
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
63
|
+
return snapshot;
|
|
64
|
+
}
|
|
65
|
+
const payload = await response.json();
|
|
66
|
+
const snapshot = parseBalancePayload(payload, plan, fetchedAt, cnyUsdRate(options.cnyUsdRate));
|
|
67
|
+
if (snapshot.error) {
|
|
68
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
69
|
+
}
|
|
70
|
+
return snapshot;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const snapshot = failure(plan.source, fetchedAt, 0, `network: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
cache.setFailure(key, snapshot, fetchedAt);
|
|
75
|
+
return snapshot;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function requestPlan(config) {
|
|
79
|
+
const template = balanceProbeTemplateValue(config.upstreamBalanceProbe?.template);
|
|
80
|
+
if (template === "none") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const balanceUrl = stringValue(config.upstreamBalanceProbe?.url) || stringValue(config.upstreamBalanceUrl);
|
|
84
|
+
const upstreamUrl = stringValue(config.upstreamUrl);
|
|
85
|
+
if (template && template !== "auto") {
|
|
86
|
+
return explicitRequestPlan(template, balanceUrl, upstreamUrl);
|
|
87
|
+
}
|
|
88
|
+
const host = hostName(balanceUrl || upstreamUrl);
|
|
89
|
+
if (host === "api.deepseek.com") {
|
|
90
|
+
return { source: "deepseek", url: "https://api.deepseek.com/user/balance", currency: "CNY", requiresUserId: false };
|
|
91
|
+
}
|
|
92
|
+
if (host === "api.stepfun.ai" || host === "api.stepfun.com") {
|
|
93
|
+
return { source: "stepfun", url: "https://api.stepfun.com/v1/accounts", currency: "CNY", requiresUserId: false };
|
|
94
|
+
}
|
|
95
|
+
if (host === "api.siliconflow.cn" || host === "api.siliconflow.com") {
|
|
96
|
+
const currency = host.endsWith(".cn") ? "CNY" : "USD";
|
|
97
|
+
const url = host.endsWith(".cn") ? "https://api.siliconflow.cn/v1/user/info" : "https://api.siliconflow.com/v1/user/info";
|
|
98
|
+
return { source: "siliconflow", url, currency, requiresUserId: false };
|
|
99
|
+
}
|
|
100
|
+
if (host === "openrouter.ai") {
|
|
101
|
+
return { source: "openrouter", url: "https://openrouter.ai/api/v1/credits", currency: "USD", requiresUserId: false };
|
|
102
|
+
}
|
|
103
|
+
if (host === "api.novita.ai") {
|
|
104
|
+
return { source: "novita", url: "https://api.novita.ai/v3/user/balance", currency: "USD", requiresUserId: false };
|
|
105
|
+
}
|
|
106
|
+
if (balanceUrl && isUsageEndpoint(balanceUrl)) {
|
|
107
|
+
return { source: "usage_generic", url: balanceUrl, currency: "USD", requiresUserId: false };
|
|
108
|
+
}
|
|
109
|
+
if (balanceUrl) {
|
|
110
|
+
return { source: "newapi_generic", url: balanceUrl, currency: "USD", requiresUserId: true };
|
|
111
|
+
}
|
|
112
|
+
const genericUsageUrl = upstreamUrl ? usageUrl(upstreamUrl) : undefined;
|
|
113
|
+
if (genericUsageUrl) {
|
|
114
|
+
return { source: "usage_generic", url: genericUsageUrl, currency: "USD", requiresUserId: false };
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function explicitRequestPlan(template, balanceUrl, upstreamUrl) {
|
|
119
|
+
if (template === "deepseek") {
|
|
120
|
+
return { source: "deepseek", url: balanceUrl || "https://api.deepseek.com/user/balance", currency: "CNY", requiresUserId: false };
|
|
121
|
+
}
|
|
122
|
+
if (template === "stepfun") {
|
|
123
|
+
return { source: "stepfun", url: balanceUrl || "https://api.stepfun.com/v1/accounts", currency: "CNY", requiresUserId: false };
|
|
124
|
+
}
|
|
125
|
+
if (template === "siliconflow") {
|
|
126
|
+
const host = hostName(balanceUrl || upstreamUrl);
|
|
127
|
+
const currency = host.endsWith(".com") ? "USD" : "CNY";
|
|
128
|
+
const url = balanceUrl || (currency === "USD" ? "https://api.siliconflow.com/v1/user/info" : "https://api.siliconflow.cn/v1/user/info");
|
|
129
|
+
return { source: "siliconflow", url, currency, requiresUserId: false };
|
|
130
|
+
}
|
|
131
|
+
if (template === "openrouter") {
|
|
132
|
+
return { source: "openrouter", url: balanceUrl || "https://openrouter.ai/api/v1/credits", currency: "USD", requiresUserId: false };
|
|
133
|
+
}
|
|
134
|
+
if (template === "novita") {
|
|
135
|
+
return { source: "novita", url: balanceUrl || "https://api.novita.ai/v3/user/balance", currency: "USD", requiresUserId: false };
|
|
136
|
+
}
|
|
137
|
+
if (template === "newapi_generic") {
|
|
138
|
+
return balanceUrl ? { source: "newapi_generic", url: balanceUrl, currency: "USD", requiresUserId: true } : undefined;
|
|
139
|
+
}
|
|
140
|
+
if (template === "usage_generic") {
|
|
141
|
+
const url = balanceUrl || (upstreamUrl ? usageUrl(upstreamUrl) : undefined);
|
|
142
|
+
return url ? { source: "usage_generic", url, currency: "USD", requiresUserId: false } : undefined;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function requestHeaders(upstreamApiKey, upstreamUserId) {
|
|
147
|
+
const headers = {
|
|
148
|
+
"Authorization": `Bearer ${upstreamApiKey}`
|
|
149
|
+
};
|
|
150
|
+
if (upstreamUserId) {
|
|
151
|
+
headers["New-Api-User"] = upstreamUserId;
|
|
152
|
+
}
|
|
153
|
+
return headers;
|
|
154
|
+
}
|
|
155
|
+
async function fetchWithTimeout(url, init, fetchFn, timeoutMs) {
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
158
|
+
try {
|
|
159
|
+
return await fetchFn(url, { ...init, signal: controller.signal });
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function parseBalancePayload(payload, plan, fetchedAt, cnyRate) {
|
|
166
|
+
const object = objectValue(payload);
|
|
167
|
+
if (!object) {
|
|
168
|
+
return failure(plan.source, fetchedAt, 200, "balance response must be an object");
|
|
169
|
+
}
|
|
170
|
+
if (plan.source === "deepseek") {
|
|
171
|
+
return parseDeepSeek(object, fetchedAt, cnyRate);
|
|
172
|
+
}
|
|
173
|
+
if (plan.source === "stepfun") {
|
|
174
|
+
return amountSnapshot(numberFrom(object.balance), "CNY", "stepfun", fetchedAt, cnyRate);
|
|
175
|
+
}
|
|
176
|
+
if (plan.source === "siliconflow") {
|
|
177
|
+
return parseSiliconFlow(object, plan.currency, fetchedAt, cnyRate);
|
|
178
|
+
}
|
|
179
|
+
if (plan.source === "openrouter") {
|
|
180
|
+
return parseOpenRouter(object, fetchedAt, cnyRate);
|
|
181
|
+
}
|
|
182
|
+
if (plan.source === "novita") {
|
|
183
|
+
const availableBalance = numberFrom(object.availableBalance);
|
|
184
|
+
return amountSnapshot(availableBalance === null ? null : availableBalance / 10000, "USD", "novita", fetchedAt, cnyRate);
|
|
185
|
+
}
|
|
186
|
+
if (plan.source === "newapi_generic") {
|
|
187
|
+
return parseNewApi(object, fetchedAt, cnyRate);
|
|
188
|
+
}
|
|
189
|
+
if (plan.source === "usage_generic") {
|
|
190
|
+
return parseUsageGeneric(object, fetchedAt, cnyRate);
|
|
191
|
+
}
|
|
192
|
+
return failure("unknown", fetchedAt, 0, "unsupported upstream - configure a balance parser");
|
|
193
|
+
}
|
|
194
|
+
function parseDeepSeek(payload, fetchedAt, cnyRate) {
|
|
195
|
+
const infos = Array.isArray(payload.balance_infos) ? payload.balance_infos : [];
|
|
196
|
+
const first = objectValue(infos[0]);
|
|
197
|
+
const currency = currencyValue(first?.currency) || "CNY";
|
|
198
|
+
const rawAmount = numberFrom(first?.total_balance);
|
|
199
|
+
if (payload.is_available === false) {
|
|
200
|
+
return {
|
|
201
|
+
...amountSnapshot(rawAmount, currency, "deepseek", fetchedAt, cnyRate),
|
|
202
|
+
error: { httpStatus: 200, message: "Insufficient balance" }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!first) {
|
|
206
|
+
return amountSnapshot(null, currency, "deepseek", fetchedAt, cnyRate);
|
|
207
|
+
}
|
|
208
|
+
if (rawAmount === null) {
|
|
209
|
+
return failure("deepseek", fetchedAt, 200, "missing field: balance_infos[].total_balance", currency);
|
|
210
|
+
}
|
|
211
|
+
return amountSnapshot(rawAmount, currency, "deepseek", fetchedAt, cnyRate);
|
|
212
|
+
}
|
|
213
|
+
function parseSiliconFlow(payload, currency, fetchedAt, cnyRate) {
|
|
214
|
+
const code = numberFrom(payload.code);
|
|
215
|
+
if (code !== null && code !== 20000) {
|
|
216
|
+
return failure("siliconflow", fetchedAt, 200, `upstream code: ${code}`, currency);
|
|
217
|
+
}
|
|
218
|
+
const data = objectValue(payload.data);
|
|
219
|
+
if (!data) {
|
|
220
|
+
return failure("siliconflow", fetchedAt, 200, "missing field: data", currency);
|
|
221
|
+
}
|
|
222
|
+
const snapshot = amountSnapshot(numberFrom(data.totalBalance), currency, "siliconflow", fetchedAt, cnyRate);
|
|
223
|
+
const status = stringValue(data.status);
|
|
224
|
+
if (status && status !== "ok") {
|
|
225
|
+
return {
|
|
226
|
+
...snapshot,
|
|
227
|
+
error: { httpStatus: 200, message: `upstream status: ${status}` }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return snapshot;
|
|
231
|
+
}
|
|
232
|
+
function parseOpenRouter(payload, fetchedAt, cnyRate) {
|
|
233
|
+
const data = objectValue(payload.data) || payload;
|
|
234
|
+
const totalCredits = numberFrom(data.total_credits);
|
|
235
|
+
const totalUsage = numberFrom(data.total_usage);
|
|
236
|
+
if (totalCredits === null || totalUsage === null) {
|
|
237
|
+
return failure("openrouter", fetchedAt, 200, "missing field: data.total_credits or data.total_usage", "USD");
|
|
238
|
+
}
|
|
239
|
+
return amountSnapshot(Math.max(0, totalCredits - totalUsage), "USD", "openrouter", fetchedAt, cnyRate);
|
|
240
|
+
}
|
|
241
|
+
function parseNewApi(payload, fetchedAt, cnyRate) {
|
|
242
|
+
const data = objectValue(payload.data);
|
|
243
|
+
if (!data) {
|
|
244
|
+
return failure("newapi_generic", fetchedAt, 200, "missing field: data", "USD");
|
|
245
|
+
}
|
|
246
|
+
const quota = numberFrom(data.quota);
|
|
247
|
+
const usedQuota = numberFrom(data.used_quota);
|
|
248
|
+
if (quota === null || usedQuota === null) {
|
|
249
|
+
return failure("newapi_generic", fetchedAt, 200, "missing field: data.quota or data.used_quota", "USD");
|
|
250
|
+
}
|
|
251
|
+
return amountSnapshot((quota - usedQuota) / 500000, "USD", "newapi_generic", fetchedAt, cnyRate);
|
|
252
|
+
}
|
|
253
|
+
function parseUsageGeneric(payload, fetchedAt, cnyRate) {
|
|
254
|
+
const quota = objectValue(payload.quota);
|
|
255
|
+
const rawAmount = numberFrom(payload.remaining) ?? numberFrom(quota?.remaining) ?? numberFrom(payload.balance);
|
|
256
|
+
const currency = currencyValue(payload.unit) || currencyValue(quota?.unit) || "USD";
|
|
257
|
+
if (rawAmount === null) {
|
|
258
|
+
return failure("usage_generic", fetchedAt, 200, "missing field: remaining, quota.remaining, or balance", currency);
|
|
259
|
+
}
|
|
260
|
+
const snapshot = amountSnapshot(rawAmount, currency, "usage_generic", fetchedAt, cnyRate);
|
|
261
|
+
const isValid = booleanValue(payload.is_active) ?? booleanValue(payload.isValid) ?? true;
|
|
262
|
+
if (!isValid) {
|
|
263
|
+
return {
|
|
264
|
+
...snapshot,
|
|
265
|
+
error: { httpStatus: 200, message: "upstream key is not active" }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return snapshot;
|
|
269
|
+
}
|
|
270
|
+
function amountSnapshot(rawAmount, currency, source, fetchedAt, cnyRate) {
|
|
271
|
+
return {
|
|
272
|
+
rawAmount,
|
|
273
|
+
amountUsdMicros: rawAmount === null ? null : Math.round(rawAmount * (currency === "CNY" ? cnyRate : 1) * 1000000),
|
|
274
|
+
currency,
|
|
275
|
+
source,
|
|
276
|
+
fetchedAt
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function failure(source, fetchedAt, httpStatus, message, currency = null) {
|
|
280
|
+
return {
|
|
281
|
+
rawAmount: null,
|
|
282
|
+
amountUsdMicros: null,
|
|
283
|
+
currency,
|
|
284
|
+
source,
|
|
285
|
+
fetchedAt,
|
|
286
|
+
error: { httpStatus, message }
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function httpErrorMessage(status) {
|
|
290
|
+
if (status === 401) {
|
|
291
|
+
return "unauthorized: check upstreamApiKey";
|
|
292
|
+
}
|
|
293
|
+
if (status === 403) {
|
|
294
|
+
return "forbidden: upstream rejected the credentials";
|
|
295
|
+
}
|
|
296
|
+
if (status === 429) {
|
|
297
|
+
return "rate limited";
|
|
298
|
+
}
|
|
299
|
+
if (status >= 500) {
|
|
300
|
+
return "upstream 5xx";
|
|
301
|
+
}
|
|
302
|
+
return `upstream http ${status}`;
|
|
303
|
+
}
|
|
304
|
+
function cnyUsdRate(value) {
|
|
305
|
+
if (value !== undefined && Number.isFinite(value) && value > 0) {
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
const envValue = Number(process.env.TB_CNY_USD_RATE);
|
|
309
|
+
return Number.isFinite(envValue) && envValue > 0 ? envValue : DEFAULT_CNY_USD_RATE;
|
|
310
|
+
}
|
|
311
|
+
function cacheKey(config) {
|
|
312
|
+
return [
|
|
313
|
+
stringValue(config.upstreamBalanceProbe?.template),
|
|
314
|
+
stringValue(config.upstreamBalanceProbe?.url),
|
|
315
|
+
stringValue(config.upstreamBalanceProbe?.userId),
|
|
316
|
+
stringValue(config.upstreamBalanceUrl),
|
|
317
|
+
stringValue(config.upstreamUrl),
|
|
318
|
+
stringValue(config.upstreamApiKey),
|
|
319
|
+
stringValue(config.upstreamUserId)
|
|
320
|
+
].join("|");
|
|
321
|
+
}
|
|
322
|
+
function balanceProbeTemplateValue(value) {
|
|
323
|
+
return value === "auto" ||
|
|
324
|
+
value === "deepseek" ||
|
|
325
|
+
value === "stepfun" ||
|
|
326
|
+
value === "siliconflow" ||
|
|
327
|
+
value === "openrouter" ||
|
|
328
|
+
value === "novita" ||
|
|
329
|
+
value === "newapi_generic" ||
|
|
330
|
+
value === "usage_generic" ||
|
|
331
|
+
value === "none"
|
|
332
|
+
? value
|
|
333
|
+
: undefined;
|
|
334
|
+
}
|
|
335
|
+
function hostName(value) {
|
|
336
|
+
if (!value) {
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function usageUrl(value) {
|
|
347
|
+
try {
|
|
348
|
+
const url = new URL(value);
|
|
349
|
+
url.pathname = "/v1/usage";
|
|
350
|
+
url.search = "";
|
|
351
|
+
url.hash = "";
|
|
352
|
+
return url.toString();
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function isUsageEndpoint(value) {
|
|
359
|
+
try {
|
|
360
|
+
return new URL(value).pathname.replace(/\/+$/, "") === "/v1/usage";
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function objectValue(value) {
|
|
367
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
368
|
+
? value
|
|
369
|
+
: undefined;
|
|
370
|
+
}
|
|
371
|
+
function stringValue(value) {
|
|
372
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
373
|
+
}
|
|
374
|
+
function numberFrom(value) {
|
|
375
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
376
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
377
|
+
}
|
|
378
|
+
function booleanValue(value) {
|
|
379
|
+
return typeof value === "boolean" ? value : undefined;
|
|
380
|
+
}
|
|
381
|
+
function currencyValue(value) {
|
|
382
|
+
return value === "USD" || value === "CNY" ? value : undefined;
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=upstream-balance-probe.js.map
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenbuddy/contracts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "Shared contract models and crypto functions",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
7
7
|
"private": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/src/**/*.js",
|
|
10
|
+
"dist/src/**/*.d.ts"
|
|
11
|
+
],
|
|
8
12
|
"scripts": {
|
|
9
13
|
"build": "tsc"
|
|
10
14
|
},
|
|
@@ -14,4 +18,4 @@
|
|
|
14
18
|
"devDependencies": {
|
|
15
19
|
"@types/uuid": "^9.0.8"
|
|
16
20
|
}
|
|
17
|
-
}
|
|
21
|
+
}
|
package/dist/src/crypto.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWxF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAU9F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GACnB,MAAM,CAOR;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,yBAAyB,GAAG,oBAAoB,CAiBhG"}
|
package/dist/src/crypto.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/src/errors.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,UAAU,gBAAgB;IAC1B,oBAAoB,2BAA2B;IAC/C,YAAY,kBAAkB;CAC/B;AAED,oBAAY,SAAS;IACnB,YAAY,kBAAkB;IAC9B,eAAe,qBAAqB;IACpC,0BAA0B,iCAAiC;IAC3D,qBAAqB,6BAA6B;IAClD,4BAA4B,mCAAmC;IAC/D,gBAAgB,sBAAsB;IACtC,cAAc,oBAAoB;IAClC,qBAAqB,4BAA4B;IACjD,sBAAsB,6BAA6B;IACnD,wBAAwB,+BAA+B;IACvD,oBAAoB,2BAA2B;IAC/C,iBAAiB,uBAAuB;IACxC,qBAAqB,6BAA6B;IAClD,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,qBAAqB,4BAA4B;IACjD,mBAAmB,yBAAyB;IAC5C,gBAAgB,uBAAuB;IACvC,eAAe,sBAAsB;IACrC,iBAAiB,wBAAwB;IACzC,qBAAqB,4BAA4B;IACjD,oBAAoB,2BAA2B;IAC/C,gBAAgB,uBAAuB;IACvC,eAAe,qBAAqB;IACpC,oBAAoB,2BAA2B;CAChD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAYzD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAuCrD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAuDzD;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,SAAS,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,iBAAiB,CAAC,EAAE,MAAM,GACzB,aAAa,CAUf"}
|
package/dist/src/errors.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|
package/dist/src/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8CAA4B;AAC5B,8CAA4B"}
|
package/src/crypto.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Decode a base64 SM4 key to a 16-byte Buffer.
|
|
6
|
-
* Throws an error if the decoded key is not exactly 16 bytes.
|
|
7
|
-
*/
|
|
8
|
-
function decodeSm4Key(sm4KeyBase64: string): Buffer {
|
|
9
|
-
const key = Buffer.from(sm4KeyBase64, "base64");
|
|
10
|
-
if (key.length !== 16) {
|
|
11
|
-
throw new Error(`SM4 key must decode to 16 bytes, got ${key.length}`);
|
|
12
|
-
}
|
|
13
|
-
return key;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Encrypt a plaintext string using SM4-ECB with PKCS7 padding.
|
|
18
|
-
* Returns the ciphertext encoded as a base64 string.
|
|
19
|
-
*/
|
|
20
|
-
export function encryptSm4EcbPkcs7Base64(plaintext: string, sm4KeyBase64: string): string {
|
|
21
|
-
try {
|
|
22
|
-
const key = decodeSm4Key(sm4KeyBase64);
|
|
23
|
-
// Node.js crypto uses 'sm4-ecb' which defaults to PKCS7 padding for ECB
|
|
24
|
-
const cipher = crypto.createCipheriv("sm4-ecb", key, null);
|
|
25
|
-
let encrypted = cipher.update(plaintext, "utf8", "base64");
|
|
26
|
-
encrypted += cipher.final("base64");
|
|
27
|
-
return encrypted;
|
|
28
|
-
} catch (error: any) {
|
|
29
|
-
throw new Error(`SM4 encryption failed: ${error.message}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Decrypt a base64 ciphertext string using SM4-ECB with PKCS7 padding.
|
|
35
|
-
* Returns the decrypted plaintext string.
|
|
36
|
-
*/
|
|
37
|
-
export function decryptSm4EcbPkcs7Base64(encryptedBase64: string, sm4KeyBase64: string): string {
|
|
38
|
-
try {
|
|
39
|
-
const key = decodeSm4Key(sm4KeyBase64);
|
|
40
|
-
const decipher = crypto.createDecipheriv("sm4-ecb", key, null);
|
|
41
|
-
let decrypted = decipher.update(encryptedBase64, "base64", "utf8");
|
|
42
|
-
decrypted += decipher.final("utf8");
|
|
43
|
-
return decrypted;
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
throw new Error(`SM4 decryption failed: ${error.message}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Compute md5 hex digest of a string (indicator).
|
|
51
|
-
*/
|
|
52
|
-
export function computeIndicator(slug: string): string {
|
|
53
|
-
return crypto.createHash("md5").update(slug, "utf8").digest("hex");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Generate a 32-character lowercase hex string as orderNo (UUID v4 without dashes).
|
|
58
|
-
*/
|
|
59
|
-
export function generateOrderNo(): string {
|
|
60
|
-
return uuidv4().replace(/-/g, "");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Encrypt Clawtip order data: {"orderNo":..., "amount":..., "payTo":...}
|
|
65
|
-
* Returns the base64 encrypted data.
|
|
66
|
-
*/
|
|
67
|
-
export function encryptClawtipOrderData(
|
|
68
|
-
orderNo: string,
|
|
69
|
-
amount: number,
|
|
70
|
-
payTo: string,
|
|
71
|
-
sm4KeyBase64: string
|
|
72
|
-
): string {
|
|
73
|
-
const plaintext = JSON.stringify({
|
|
74
|
-
orderNo,
|
|
75
|
-
amount,
|
|
76
|
-
payTo
|
|
77
|
-
});
|
|
78
|
-
return encryptSm4EcbPkcs7Base64(plaintext, sm4KeyBase64);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ClawtipPaymentParams {
|
|
82
|
-
orderNo: string;
|
|
83
|
-
amountFen: number;
|
|
84
|
-
payTo: string;
|
|
85
|
-
encryptedData: string;
|
|
86
|
-
indicator: string;
|
|
87
|
-
slug: string;
|
|
88
|
-
skillId: string;
|
|
89
|
-
description: string;
|
|
90
|
-
resourceUrl: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ClawtipPaymentParamsInput {
|
|
94
|
-
payTo: string;
|
|
95
|
-
sm4KeyBase64: string;
|
|
96
|
-
skillSlug: string;
|
|
97
|
-
skillId: string;
|
|
98
|
-
description: string;
|
|
99
|
-
resourceUrl: string;
|
|
100
|
-
amountFen: number;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function buildClawtipPaymentParams(input: ClawtipPaymentParamsInput): ClawtipPaymentParams {
|
|
104
|
-
if (!Number.isInteger(input.amountFen) || input.amountFen < 1) {
|
|
105
|
-
throw new Error("amount_fen must be greater than zero");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const orderNo = generateOrderNo();
|
|
109
|
-
return {
|
|
110
|
-
orderNo,
|
|
111
|
-
amountFen: input.amountFen,
|
|
112
|
-
payTo: input.payTo,
|
|
113
|
-
encryptedData: encryptClawtipOrderData(orderNo, input.amountFen, input.payTo, input.sm4KeyBase64),
|
|
114
|
-
indicator: computeIndicator(input.skillSlug),
|
|
115
|
-
slug: input.skillSlug,
|
|
116
|
-
skillId: input.skillId,
|
|
117
|
-
description: input.description,
|
|
118
|
-
resourceUrl: input.resourceUrl
|
|
119
|
-
};
|
|
120
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
export enum RetryClass {
|
|
2
|
-
FailClosed = "fail_closed",
|
|
3
|
-
RetryableWithBackoff = "retryable_with_backoff",
|
|
4
|
-
NotRetryable = "not_retryable"
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export enum ErrorCode {
|
|
8
|
-
BusyCapacity = "busy_capacity",
|
|
9
|
-
PaymentRequired = "payment_required",
|
|
10
|
-
PaymentAccountInsufficient = "payment_account_insufficient",
|
|
11
|
-
PaymentMethodNotReady = "payment_method_not_ready",
|
|
12
|
-
PaymentAuthorizationRequired = "payment_authorization_required",
|
|
13
|
-
PaymentCancelled = "payment_cancelled",
|
|
14
|
-
PaymentTimeout = "payment_timeout",
|
|
15
|
-
PaymentProviderFailed = "payment_provider_failed",
|
|
16
|
-
InvalidPurchaseRequest = "invalid_purchase_request",
|
|
17
|
-
ClawtipCredentialInvalid = "clawtip_credential_invalid",
|
|
18
|
-
ClawtipOrderMismatch = "clawtip_order_mismatch",
|
|
19
|
-
InsufficientFunds = "insufficient_funds",
|
|
20
|
-
InvalidOrExpiredToken = "invalid_or_expired_token",
|
|
21
|
-
TokenClassRevoked = "token_class_revoked",
|
|
22
|
-
RequestIdRequired = "request_id_required",
|
|
23
|
-
StreamingNotSupported = "streaming_not_supported",
|
|
24
|
-
IdempotencyConflict = "idempotency_conflict",
|
|
25
|
-
PurchaseNotFound = "purchase_not_found",
|
|
26
|
-
RequestNotFound = "request_not_found",
|
|
27
|
-
ModelNotAvailable = "model_not_available",
|
|
28
|
-
UpstreamNotConfigured = "upstream_not_configured",
|
|
29
|
-
ProtocolNotAvailable = "protocol_not_available",
|
|
30
|
-
LedgerFailClosed = "ledger_fail_closed",
|
|
31
|
-
UpstreamTimeout = "upstream_timeout",
|
|
32
|
-
ContractVersionDrift = "contract_version_drift"
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function getRetryClass(code: ErrorCode): RetryClass {
|
|
36
|
-
switch (code) {
|
|
37
|
-
case ErrorCode.BusyCapacity:
|
|
38
|
-
case ErrorCode.UpstreamTimeout:
|
|
39
|
-
return RetryClass.RetryableWithBackoff;
|
|
40
|
-
case ErrorCode.LedgerFailClosed:
|
|
41
|
-
case ErrorCode.ClawtipCredentialInvalid:
|
|
42
|
-
case ErrorCode.ClawtipOrderMismatch:
|
|
43
|
-
return RetryClass.FailClosed;
|
|
44
|
-
default:
|
|
45
|
-
return RetryClass.NotRetryable;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function getHttpStatus(code: ErrorCode): number {
|
|
50
|
-
switch (code) {
|
|
51
|
-
case ErrorCode.BusyCapacity:
|
|
52
|
-
return 429;
|
|
53
|
-
case ErrorCode.PaymentRequired:
|
|
54
|
-
case ErrorCode.PaymentAccountInsufficient:
|
|
55
|
-
case ErrorCode.PaymentMethodNotReady:
|
|
56
|
-
case ErrorCode.PaymentAuthorizationRequired:
|
|
57
|
-
case ErrorCode.PaymentCancelled:
|
|
58
|
-
case ErrorCode.PaymentTimeout:
|
|
59
|
-
case ErrorCode.PaymentProviderFailed:
|
|
60
|
-
case ErrorCode.ClawtipCredentialInvalid:
|
|
61
|
-
case ErrorCode.ClawtipOrderMismatch:
|
|
62
|
-
case ErrorCode.InsufficientFunds:
|
|
63
|
-
return 402;
|
|
64
|
-
case ErrorCode.InvalidPurchaseRequest:
|
|
65
|
-
return 400;
|
|
66
|
-
case ErrorCode.InvalidOrExpiredToken:
|
|
67
|
-
case ErrorCode.TokenClassRevoked:
|
|
68
|
-
return 401;
|
|
69
|
-
case ErrorCode.RequestIdRequired:
|
|
70
|
-
case ErrorCode.StreamingNotSupported:
|
|
71
|
-
case ErrorCode.ModelNotAvailable:
|
|
72
|
-
case ErrorCode.UpstreamNotConfigured:
|
|
73
|
-
case ErrorCode.ProtocolNotAvailable:
|
|
74
|
-
case ErrorCode.ContractVersionDrift:
|
|
75
|
-
return 400;
|
|
76
|
-
case ErrorCode.IdempotencyConflict:
|
|
77
|
-
return 409;
|
|
78
|
-
case ErrorCode.PurchaseNotFound:
|
|
79
|
-
case ErrorCode.RequestNotFound:
|
|
80
|
-
return 404;
|
|
81
|
-
case ErrorCode.LedgerFailClosed:
|
|
82
|
-
return 500;
|
|
83
|
-
case ErrorCode.UpstreamTimeout:
|
|
84
|
-
return 504;
|
|
85
|
-
default:
|
|
86
|
-
return 500;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function getDefaultMessage(code: ErrorCode): string {
|
|
91
|
-
switch (code) {
|
|
92
|
-
case ErrorCode.BusyCapacity:
|
|
93
|
-
return "Seller capacity is currently exhausted.";
|
|
94
|
-
case ErrorCode.PaymentRequired:
|
|
95
|
-
return "Additional credit is required before this operation can proceed.";
|
|
96
|
-
case ErrorCode.PaymentAccountInsufficient:
|
|
97
|
-
return "Payment account balance is insufficient. Add funds to the selected payment method and retry.";
|
|
98
|
-
case ErrorCode.PaymentMethodNotReady:
|
|
99
|
-
return "Selected payment method is not ready. Configure or enable a payment method and retry.";
|
|
100
|
-
case ErrorCode.PaymentAuthorizationRequired:
|
|
101
|
-
return "Selected payment method requires authorization before payment can proceed.";
|
|
102
|
-
case ErrorCode.PaymentCancelled:
|
|
103
|
-
return "Payment was cancelled before completion.";
|
|
104
|
-
case ErrorCode.PaymentTimeout:
|
|
105
|
-
return "Payment did not complete before the timeout.";
|
|
106
|
-
case ErrorCode.PaymentProviderFailed:
|
|
107
|
-
return "Selected payment provider could not complete the payment.";
|
|
108
|
-
case ErrorCode.ClawtipCredentialInvalid:
|
|
109
|
-
return "Clawtip payment credential is invalid.";
|
|
110
|
-
case ErrorCode.InvalidPurchaseRequest:
|
|
111
|
-
return "Purchase request is invalid.";
|
|
112
|
-
case ErrorCode.ClawtipOrderMismatch:
|
|
113
|
-
return "Clawtip payment does not match the expected order.";
|
|
114
|
-
case ErrorCode.InsufficientFunds:
|
|
115
|
-
return "Purchase balance is exhausted.";
|
|
116
|
-
case ErrorCode.InvalidOrExpiredToken:
|
|
117
|
-
return "Bearer token is invalid or expired.";
|
|
118
|
-
case ErrorCode.TokenClassRevoked:
|
|
119
|
-
return "Token class has been revoked by seller operator.";
|
|
120
|
-
case ErrorCode.RequestIdRequired:
|
|
121
|
-
return "Chat requests must include requestId.";
|
|
122
|
-
case ErrorCode.StreamingNotSupported:
|
|
123
|
-
return "Streaming responses are not supported in P0.";
|
|
124
|
-
case ErrorCode.IdempotencyConflict:
|
|
125
|
-
return "Idempotency key was reused with a different request payload.";
|
|
126
|
-
case ErrorCode.PurchaseNotFound:
|
|
127
|
-
return "Purchase could not be found.";
|
|
128
|
-
case ErrorCode.RequestNotFound:
|
|
129
|
-
return "Request could not be found.";
|
|
130
|
-
case ErrorCode.ModelNotAvailable:
|
|
131
|
-
return "Requested model is not available on this seller.";
|
|
132
|
-
case ErrorCode.UpstreamNotConfigured:
|
|
133
|
-
return "Seller upstream is not configured. Ask the seller operator to configure upstreams.";
|
|
134
|
-
case ErrorCode.ProtocolNotAvailable:
|
|
135
|
-
return "Requested protocol is not available for this seller upstream.";
|
|
136
|
-
case ErrorCode.LedgerFailClosed:
|
|
137
|
-
return "Ledger entered fail-closed mode.";
|
|
138
|
-
case ErrorCode.UpstreamTimeout:
|
|
139
|
-
return "Upstream model request timed out.";
|
|
140
|
-
case ErrorCode.ContractVersionDrift:
|
|
141
|
-
return "Contract version drift detected.";
|
|
142
|
-
default:
|
|
143
|
-
return "An unknown error occurred.";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export interface ErrorBody {
|
|
148
|
-
code: ErrorCode;
|
|
149
|
-
retryClass: RetryClass;
|
|
150
|
-
message: string;
|
|
151
|
-
httpStatus: number;
|
|
152
|
-
retryAfterSeconds?: number;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface ErrorEnvelope {
|
|
156
|
-
error: ErrorBody;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function createErrorEnvelope(
|
|
160
|
-
code: ErrorCode,
|
|
161
|
-
message?: string,
|
|
162
|
-
retryAfterSeconds?: number
|
|
163
|
-
): ErrorEnvelope {
|
|
164
|
-
return {
|
|
165
|
-
error: {
|
|
166
|
-
code,
|
|
167
|
-
retryClass: getRetryClass(code),
|
|
168
|
-
message: message || getDefaultMessage(code),
|
|
169
|
-
httpStatus: getHttpStatus(code),
|
|
170
|
-
...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {})
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
package/src/index.ts
DELETED
package/tests/crypto.test.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
encryptSm4EcbPkcs7Base64,
|
|
3
|
-
decryptSm4EcbPkcs7Base64,
|
|
4
|
-
computeIndicator,
|
|
5
|
-
generateOrderNo,
|
|
6
|
-
encryptClawtipOrderData,
|
|
7
|
-
buildClawtipPaymentParams
|
|
8
|
-
} from "../src/crypto.js";
|
|
9
|
-
|
|
10
|
-
// base64("0123456789ABCDEF") — exactly 16 bytes
|
|
11
|
-
const KEY = "MDEyMzQ1Njc4OUFCQ0RFRg==";
|
|
12
|
-
|
|
13
|
-
describe("SM4 Cryptography and Helpers Tests", () => {
|
|
14
|
-
test("sm4 ecb pkcs7 roundtrip", () => {
|
|
15
|
-
const plaintext = '{"orderNo":"abc123","amount":100,"payTo":"payto-test"}';
|
|
16
|
-
const encrypted = encryptSm4EcbPkcs7Base64(plaintext, KEY);
|
|
17
|
-
const decrypted = decryptSm4EcbPkcs7Base64(encrypted, KEY);
|
|
18
|
-
|
|
19
|
-
expect(encrypted).not.toBe(plaintext);
|
|
20
|
-
expect(decrypted).toBe(plaintext);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("sm4 invalid base64 key throws error", () => {
|
|
24
|
-
expect(() => {
|
|
25
|
-
encryptSm4EcbPkcs7Base64("hello", "!!!not-base64!!!");
|
|
26
|
-
}).toThrow();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("sm4 short key throws error", () => {
|
|
30
|
-
// base64("short") = 8 bytes
|
|
31
|
-
expect(() => {
|
|
32
|
-
encryptSm4EcbPkcs7Base64("hello", "c2hvcnQ=");
|
|
33
|
-
}).toThrow("16 bytes");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("compute_indicator is md5 hex of slug", () => {
|
|
37
|
-
// echo -n "test" | md5sum => 098f6bcd4621d373cade4e832627b4f6
|
|
38
|
-
expect(computeIndicator("test")).toBe("098f6bcd4621d373cade4e832627b4f6");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("generate_order_no is 32 lowercase hex chars", () => {
|
|
42
|
-
const no = generateOrderNo();
|
|
43
|
-
expect(no.length).toBe(32);
|
|
44
|
-
expect(/^[0-9a-f]{32}$/.test(no)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("encryptClawtipOrderData produces valid json after decrypt", () => {
|
|
48
|
-
const enc = encryptClawtipOrderData("ord001", 500, "pay-to-addr", KEY);
|
|
49
|
-
const dec = decryptSm4EcbPkcs7Base64(enc, KEY);
|
|
50
|
-
const val = JSON.parse(dec);
|
|
51
|
-
expect(val.orderNo).toBe("ord001");
|
|
52
|
-
expect(val.amount).toBe(500);
|
|
53
|
-
expect(val.payTo).toBe("pay-to-addr");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("buildClawtipPaymentParams produces the shared wallet bootstrap shape", () => {
|
|
57
|
-
const params = buildClawtipPaymentParams({
|
|
58
|
-
payTo: "pay-to-addr",
|
|
59
|
-
sm4KeyBase64: KEY,
|
|
60
|
-
skillSlug: "tb-wallet-bootstrap",
|
|
61
|
-
skillId: "si-tb-wallet-bootstrap",
|
|
62
|
-
description: "Activate",
|
|
63
|
-
resourceUrl: "https://example.com",
|
|
64
|
-
amountFen: 1
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(params.amountFen).toBe(1);
|
|
68
|
-
expect(params.payTo).toBe("pay-to-addr");
|
|
69
|
-
expect(params.slug).toBe("tb-wallet-bootstrap");
|
|
70
|
-
expect(params.skillId).toBe("si-tb-wallet-bootstrap");
|
|
71
|
-
expect(params.orderNo).toMatch(/^[0-9a-f]{32}$/);
|
|
72
|
-
expect(params.indicator).toBe(computeIndicator("tb-wallet-bootstrap"));
|
|
73
|
-
|
|
74
|
-
const decoded = JSON.parse(decryptSm4EcbPkcs7Base64(params.encryptedData, KEY));
|
|
75
|
-
expect(decoded.orderNo).toBe(params.orderNo);
|
|
76
|
-
expect(decoded.amount).toBe(1);
|
|
77
|
-
expect(decoded.payTo).toBe("pay-to-addr");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("buildClawtipPaymentParams rejects non-positive amountFen", () => {
|
|
81
|
-
expect(() => buildClawtipPaymentParams({
|
|
82
|
-
payTo: "pay-to-addr",
|
|
83
|
-
sm4KeyBase64: KEY,
|
|
84
|
-
skillSlug: "tb-wallet-bootstrap",
|
|
85
|
-
skillId: "si-tb-wallet-bootstrap",
|
|
86
|
-
description: "Activate",
|
|
87
|
-
resourceUrl: "https://example.com",
|
|
88
|
-
amountFen: 0
|
|
89
|
-
})).toThrow("amount_fen");
|
|
90
|
-
});
|
|
91
|
-
});
|
package/tests/errors.test.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ErrorCode,
|
|
3
|
-
RetryClass,
|
|
4
|
-
getRetryClass,
|
|
5
|
-
getHttpStatus,
|
|
6
|
-
getDefaultMessage,
|
|
7
|
-
createErrorEnvelope
|
|
8
|
-
} from "../src/errors.js";
|
|
9
|
-
|
|
10
|
-
describe("ErrorCode and RetryClass Mapping Tests", () => {
|
|
11
|
-
test("retry class maps correctly", () => {
|
|
12
|
-
expect(getRetryClass(ErrorCode.BusyCapacity)).toBe(RetryClass.RetryableWithBackoff);
|
|
13
|
-
expect(getRetryClass(ErrorCode.UpstreamTimeout)).toBe(RetryClass.RetryableWithBackoff);
|
|
14
|
-
expect(getRetryClass(ErrorCode.LedgerFailClosed)).toBe(RetryClass.FailClosed);
|
|
15
|
-
expect(getRetryClass(ErrorCode.ClawtipCredentialInvalid)).toBe(RetryClass.FailClosed);
|
|
16
|
-
expect(getRetryClass(ErrorCode.PaymentRequired)).toBe(RetryClass.NotRetryable);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("http status maps correctly", () => {
|
|
20
|
-
expect(getHttpStatus(ErrorCode.BusyCapacity)).toBe(429);
|
|
21
|
-
expect(getHttpStatus(ErrorCode.PaymentRequired)).toBe(402);
|
|
22
|
-
expect(getHttpStatus(ErrorCode.InvalidOrExpiredToken)).toBe(401);
|
|
23
|
-
expect(getHttpStatus(ErrorCode.LedgerFailClosed)).toBe(500);
|
|
24
|
-
expect(getHttpStatus(ErrorCode.UpstreamTimeout)).toBe(504);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("createErrorEnvelope produces standard wire structure", () => {
|
|
28
|
-
const envelope = createErrorEnvelope(ErrorCode.BusyCapacity);
|
|
29
|
-
expect(envelope.error.code).toBe(ErrorCode.BusyCapacity);
|
|
30
|
-
expect(envelope.error.retryClass).toBe(RetryClass.RetryableWithBackoff);
|
|
31
|
-
expect(envelope.error.httpStatus).toBe(429);
|
|
32
|
-
expect(envelope.error.message).toContain("capacity");
|
|
33
|
-
});
|
|
34
|
-
});
|