@xiashe/sdk 0.1.22 → 0.1.24
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/alipayWallet.mjs +178 -9
- package/index.mjs +2 -1
- package/package.json +1 -1
package/alipayWallet.mjs
CHANGED
|
@@ -41,6 +41,8 @@ const SAFE_OFFICIAL_FAILURE_CODES = new Set([
|
|
|
41
41
|
'SELLER_ID_EMPTY',
|
|
42
42
|
'TRADE_STATUS_INVALID'
|
|
43
43
|
]);
|
|
44
|
+
const SAFE_OFFICIAL_FIELD_VALUE = /^[A-Za-z][A-Za-z0-9_.:-]{2,127}$/;
|
|
45
|
+
const SAFE_OFFICIAL_ERROR_MESSAGE = /^(?!.*(?:https?:\/\/|payment[-_ ]?needed|payment[-_ ]?proof|seller_signature|authorization\s*:|runtimeToken|client[_ -]?session|(?:access|refresh)[_-]?token|credential|\b[A-Za-z0-9_-]{80,}\b)).{1,240}$/i;
|
|
44
46
|
|
|
45
47
|
const OFFICIAL_ERROR_DIAGNOSES = {
|
|
46
48
|
PAYMENT_NEEDED_REJECTED: {
|
|
@@ -142,6 +144,52 @@ function safeDiagnostic(value) {
|
|
|
142
144
|
return SENSITIVE_OUTPUT.test(text) ? 'protocol_payload_processed' : 'official_wallet_response_received';
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
function explicitJsonString(output, fields) {
|
|
148
|
+
for (const field of fields) {
|
|
149
|
+
const match = output.match(new RegExp(`"${field}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, 'i'));
|
|
150
|
+
if (!match) continue;
|
|
151
|
+
try {
|
|
152
|
+
const value = JSON.parse(match[1]);
|
|
153
|
+
if (typeof value === 'string') return value;
|
|
154
|
+
} catch {
|
|
155
|
+
// Do not fall back to arbitrary vendor text: it can carry protocol data.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The official client may include Payment-Needed, Payment-Proof, credentials,
|
|
163
|
+
* or mutable diagnostic text. Persist only explicit, bounded support fields.
|
|
164
|
+
*/
|
|
165
|
+
function safeOfficialDiagnostic(stdout, stderr) {
|
|
166
|
+
const output = `${stdout}\n${stderr}`;
|
|
167
|
+
const pick = (fields) => {
|
|
168
|
+
const value = explicitJsonString(output, fields);
|
|
169
|
+
return value && SAFE_OFFICIAL_FIELD_VALUE.test(value) ? value : null;
|
|
170
|
+
};
|
|
171
|
+
const code = pick(['errorCode', 'error_code', 'previous_error', 'code']);
|
|
172
|
+
const message = explicitJsonString(output, ['errorMsg', 'subMsg', 'error_message', 'message']);
|
|
173
|
+
return {
|
|
174
|
+
code,
|
|
175
|
+
subCode: pick(['subCode', 'sub_code']),
|
|
176
|
+
requestId: pick(['requestId', 'request_id']),
|
|
177
|
+
traceId: pick(['traceId', 'trace_id']),
|
|
178
|
+
// For known downstream codes, our normalized recovery text is safer and
|
|
179
|
+
// more useful than arbitrary vendor prose. TRADE_PROGRESS_FAILED is the
|
|
180
|
+
// one generic wallet-creation code where the official short message adds
|
|
181
|
+
// context without exposing protocol material.
|
|
182
|
+
...(code?.toUpperCase() === 'TRADE_PROGRESS_FAILED' && message && SAFE_OFFICIAL_ERROR_MESSAGE.test(message.trim())
|
|
183
|
+
? { message: safeText(message, 240) }
|
|
184
|
+
: {})
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function safeRuntimeVersion(stdout, stderr) {
|
|
189
|
+
const match = `${stdout}\n${stderr}`.match(/\balipay-bot(?:-cli)?\s+v?(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)/i);
|
|
190
|
+
return match?.[1] || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
145
193
|
function normalizeResourceUrl(value) {
|
|
146
194
|
let url;
|
|
147
195
|
try {
|
|
@@ -340,10 +388,10 @@ function walletFailureCode(stdout, stderr, exitCode) {
|
|
|
340
388
|
* credentials, URLs, trade identifiers, or PayGuard internals.
|
|
341
389
|
*/
|
|
342
390
|
function safeOfficialFailureCode(stdout, stderr) {
|
|
343
|
-
const
|
|
344
|
-
const candidates = [
|
|
345
|
-
|
|
346
|
-
|
|
391
|
+
const official = safeOfficialDiagnostic(stdout, stderr);
|
|
392
|
+
const candidates = [official.code, official.subCode]
|
|
393
|
+
.filter(Boolean)
|
|
394
|
+
.map((candidate) => candidate.toUpperCase());
|
|
347
395
|
return candidates.find((candidate) => SAFE_OFFICIAL_FAILURE_CODES.has(candidate)) || null;
|
|
348
396
|
}
|
|
349
397
|
|
|
@@ -358,12 +406,17 @@ function safeMerchantDiagnosticCode(stdout, stderr) {
|
|
|
358
406
|
}
|
|
359
407
|
|
|
360
408
|
function paymentFailureDiagnosis(failureCode, stdout, stderr) {
|
|
409
|
+
const official = safeOfficialDiagnostic(stdout, stderr);
|
|
361
410
|
const officialCode = safeOfficialFailureCode(stdout, stderr);
|
|
362
411
|
const merchantDiagnostic = safeMerchantDiagnosticCode(stdout, stderr);
|
|
363
412
|
const base = {
|
|
364
413
|
code: failureCode,
|
|
365
414
|
stage: 'official_wallet_trade_creation',
|
|
366
|
-
nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。'
|
|
415
|
+
nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。',
|
|
416
|
+
...(official.subCode ? { officialSubCode: official.subCode } : {}),
|
|
417
|
+
...(official.requestId ? { officialRequestId: official.requestId } : {}),
|
|
418
|
+
...(official.traceId ? { officialTraceId: official.traceId } : {}),
|
|
419
|
+
...(official.message ? { officialMessage: official.message } : {})
|
|
367
420
|
};
|
|
368
421
|
if (officialCode && OFFICIAL_ERROR_DIAGNOSES[officialCode]) {
|
|
369
422
|
return {
|
|
@@ -435,6 +488,10 @@ function paymentFailureUserVisible(diagnosis) {
|
|
|
435
488
|
`诊断代码:${diagnosis.code}`,
|
|
436
489
|
`失败阶段:${diagnosis.stage}`,
|
|
437
490
|
...(diagnosis.officialCode ? [`官方返回代码:${diagnosis.officialCode}`] : []),
|
|
491
|
+
...(diagnosis.officialSubCode ? [`官方子码:${diagnosis.officialSubCode}`] : []),
|
|
492
|
+
...(diagnosis.officialMessage ? [`官方消息:${diagnosis.officialMessage}`] : []),
|
|
493
|
+
...(diagnosis.officialRequestId ? [`官方请求标识:${diagnosis.officialRequestId}`] : []),
|
|
494
|
+
...(diagnosis.officialTraceId ? [`官方追踪标识:${diagnosis.officialTraceId}`] : []),
|
|
438
495
|
...(diagnosis.merchantDiagnostic ? [`商家验证诊断:${diagnosis.merchantDiagnostic}`] : []),
|
|
439
496
|
`下一步:${diagnosis.nextStep}`
|
|
440
497
|
];
|
|
@@ -480,7 +537,7 @@ async function fetchPaymentNeeded(resourceUrl, options = {}) {
|
|
|
480
537
|
if (!paymentNeeded) {
|
|
481
538
|
throw new AlipayWalletError('ALIPAY_RESOURCE_PAYMENT_NEEDED_MISSING', '支付资源未返回有效的支付宝付款请求。');
|
|
482
539
|
}
|
|
483
|
-
return { paymentNeeded };
|
|
540
|
+
return { paymentNeeded, responseStatus: response.status };
|
|
484
541
|
} catch (error) {
|
|
485
542
|
if (error instanceof AlipayWalletError) throw error;
|
|
486
543
|
if (error?.name === 'AbortError') {
|
|
@@ -492,6 +549,102 @@ async function fetchPaymentNeeded(resourceUrl, options = {}) {
|
|
|
492
549
|
}
|
|
493
550
|
}
|
|
494
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Validate only the public shape of a 402 bill in memory. This is deliberately
|
|
554
|
+
* not a wallet call and never returns the bill, its signature, or any payment
|
|
555
|
+
* credentials to the caller.
|
|
556
|
+
*/
|
|
557
|
+
function validatePaymentNeededShape(paymentNeeded) {
|
|
558
|
+
let bill;
|
|
559
|
+
try {
|
|
560
|
+
bill = JSON.parse(Buffer.from(String(paymentNeeded), 'base64url').toString('utf8'));
|
|
561
|
+
} catch {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
checks: [{ id: 'payment_needed_encoding', ok: false }]
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const protocol = bill?.protocol && typeof bill.protocol === 'object' ? bill.protocol : {};
|
|
568
|
+
const method = bill?.method && typeof bill.method === 'object' ? bill.method : {};
|
|
569
|
+
const sellerId = safeText(protocol.seller_unique_id, 80);
|
|
570
|
+
const methodSellerId = safeText(method.seller_id, 80);
|
|
571
|
+
const amount = safeText(protocol.amount, 32);
|
|
572
|
+
const payBefore = safeText(protocol.pay_before, 80);
|
|
573
|
+
const checks = [
|
|
574
|
+
{
|
|
575
|
+
id: 'payment_needed_encoding',
|
|
576
|
+
ok: true
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: 'protocol_fields',
|
|
580
|
+
ok: Boolean(
|
|
581
|
+
safeText(protocol.out_trade_no, 200) &&
|
|
582
|
+
/^\d+(?:\.\d{1,2})?$/.test(amount) && Number(amount) > 0 &&
|
|
583
|
+
safeText(protocol.currency, 16).toUpperCase() === 'CNY' &&
|
|
584
|
+
safeText(protocol.resource_id, 1_000) &&
|
|
585
|
+
payBefore && Number.isFinite(Date.parse(payBefore)) &&
|
|
586
|
+
safeText(protocol.seller_signature, 8_000) &&
|
|
587
|
+
safeText(protocol.seller_sign_type, 32).toUpperCase() === 'RSA2' &&
|
|
588
|
+
/^2088\d{8,}$/.test(sellerId)
|
|
589
|
+
)
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: 'merchant_fields',
|
|
593
|
+
ok: Boolean(
|
|
594
|
+
safeText(method.seller_name, 160) &&
|
|
595
|
+
/^2088\d{8,}$/.test(methodSellerId) &&
|
|
596
|
+
safeText(method.seller_app_id, 80) &&
|
|
597
|
+
safeText(method.goods_name, 160) &&
|
|
598
|
+
safeText(method.seller_unique_id_key, 80) === 'seller_id' &&
|
|
599
|
+
safeText(method.service_id, 160)
|
|
600
|
+
)
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: 'seller_identity_consistency',
|
|
604
|
+
ok: Boolean(sellerId && sellerId === methodSellerId)
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
return { ok: checks.every((check) => check.ok), checks };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Read-only seller-resource preflight. It proves that this exact resource
|
|
612
|
+
* produces a structurally valid 402 bill, but never opens the wallet or
|
|
613
|
+
* attempts to create a trade.
|
|
614
|
+
*/
|
|
615
|
+
export async function preflightAlipay402Resource(input, options = {}) {
|
|
616
|
+
const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
|
|
617
|
+
const resource = await fetchPaymentNeeded(resourceUrl, options);
|
|
618
|
+
if (resource.alreadyPaid) {
|
|
619
|
+
return {
|
|
620
|
+
ok: true,
|
|
621
|
+
status: 'resource_already_available',
|
|
622
|
+
provider: 'alipay-agent-payment',
|
|
623
|
+
walletInvoked: false,
|
|
624
|
+
message: '该资源已可访问,无需再次创建付款账单。',
|
|
625
|
+
checks: [{ id: 'resource_access', ok: true }]
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const validation = validatePaymentNeededShape(resource.paymentNeeded);
|
|
629
|
+
return {
|
|
630
|
+
ok: validation.ok,
|
|
631
|
+
status: validation.ok ? 'payment_needed_ready' : 'payment_needed_invalid',
|
|
632
|
+
provider: 'alipay-agent-payment',
|
|
633
|
+
walletInvoked: false,
|
|
634
|
+
resourceStatus: resource.responseStatus,
|
|
635
|
+
message: validation.ok
|
|
636
|
+
? '虾舍已为该资源签发字段结构有效的支付宝 402 账单;尚未调用钱包或创建交易,且未验证支付宝端服务状态、已登记公钥或账单签名。'
|
|
637
|
+
: '虾舍资源返回了 402,但账单结构不符合支付宝 AI收要求;未调用钱包。',
|
|
638
|
+
validationScope: {
|
|
639
|
+
billFields: true,
|
|
640
|
+
sellerSignatureVerifiedByAlipay: false,
|
|
641
|
+
merchantServiceVerifiedByAlipay: false,
|
|
642
|
+
transactionCreationVerified: false
|
|
643
|
+
},
|
|
644
|
+
checks: validation.checks
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
495
648
|
function publicPaymentResult(state, status, diagnostic, options = {}) {
|
|
496
649
|
const commandFailed = options.commandFailed === true;
|
|
497
650
|
const failureCode = options.failureCode || 'official_cli_nonzero_exit';
|
|
@@ -505,6 +658,7 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
|
|
|
505
658
|
walletInvoked: true,
|
|
506
659
|
message: failed ? paymentFailureMessage(failureCode) : paymentMessage(status),
|
|
507
660
|
diagnostic,
|
|
661
|
+
...(options.merchantResource ? { merchantResource: options.merchantResource } : {}),
|
|
508
662
|
...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
|
|
509
663
|
...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
|
|
510
664
|
...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
|
|
@@ -531,11 +685,15 @@ export async function checkAlipayWallet(options = {}) {
|
|
|
531
685
|
/** Checks that the official buyer CLI is present, without querying wallet state. */
|
|
532
686
|
export async function getAlipayWalletBridgeInfo(options = {}) {
|
|
533
687
|
const { stdout, stderr } = await runOfficialCli(['--version'], options);
|
|
688
|
+
const runtimeVersion = safeRuntimeVersion(stdout, stderr);
|
|
534
689
|
return {
|
|
535
690
|
ok: true,
|
|
536
|
-
state: '
|
|
691
|
+
state: 'runtime_available',
|
|
537
692
|
provider: 'alipay-agent-payment',
|
|
538
|
-
message: '
|
|
693
|
+
message: '已找到支付宝官方钱包程序;这只表示程序可执行,尚未检查钱包授权或交易资格。',
|
|
694
|
+
walletStateChecked: false,
|
|
695
|
+
transactionCreationVerified: false,
|
|
696
|
+
...(runtimeVersion ? { runtimeVersion } : {}),
|
|
539
697
|
diagnostic: safeDiagnostic(stdout || stderr)
|
|
540
698
|
};
|
|
541
699
|
}
|
|
@@ -566,6 +724,11 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
566
724
|
return publicPaymentResult(state, 'completed', 'resource_already_available');
|
|
567
725
|
}
|
|
568
726
|
|
|
727
|
+
const billValidation = validatePaymentNeededShape(resource.paymentNeeded);
|
|
728
|
+
if (!billValidation.ok) {
|
|
729
|
+
throw new AlipayWalletError('ALIPAY_PAYMENT_NEEDED_INVALID', '虾舍返回的支付宝付款账单结构无效;未调用钱包或创建交易。');
|
|
730
|
+
}
|
|
731
|
+
|
|
569
732
|
const paths = statePaths(resourceUrl, options);
|
|
570
733
|
await ensurePrivateDirectory(paths.directory);
|
|
571
734
|
await writePrivateFile(paths.paymentNeededPath, resource.paymentNeeded);
|
|
@@ -576,6 +739,10 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
576
739
|
sessionId: randomUUID(),
|
|
577
740
|
paymentNeededPath: paths.paymentNeededPath,
|
|
578
741
|
queryCredential: null,
|
|
742
|
+
merchantResource: {
|
|
743
|
+
paymentNeededReceived: true,
|
|
744
|
+
billShapeValidated: true
|
|
745
|
+
},
|
|
579
746
|
status: 'started',
|
|
580
747
|
createdAt: new Date().toISOString(),
|
|
581
748
|
updatedAt: new Date().toISOString()
|
|
@@ -618,7 +785,8 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
618
785
|
userVisible: ['pending_confirmation', 'completed'].includes(status)
|
|
619
786
|
? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
|
|
620
787
|
: null,
|
|
621
|
-
officialMedia
|
|
788
|
+
officialMedia,
|
|
789
|
+
merchantResource: state.merchantResource
|
|
622
790
|
});
|
|
623
791
|
}
|
|
624
792
|
|
|
@@ -701,6 +869,7 @@ export async function diagnoseAlipay402Resource(input, options = {}) {
|
|
|
701
869
|
walletInvoked: state.status !== 'started',
|
|
702
870
|
queryAvailable: Boolean(state.queryCredential?.value),
|
|
703
871
|
diagnostic: state.lastWalletDiagnostic || 'payment_state_recorded',
|
|
872
|
+
merchantResource: state.merchantResource || null,
|
|
704
873
|
...(state.lastFailureCode ? { failureCode: state.lastFailureCode } : {}),
|
|
705
874
|
...(state.lastDiagnosis ? {
|
|
706
875
|
diagnosis: state.lastDiagnosis,
|
package/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { ConvexHttpClient } from 'convex/browser';
|
|
7
7
|
import { anyApi as api } from 'convex/server';
|
|
8
8
|
|
|
9
|
-
export const VERSION = '0.1.
|
|
9
|
+
export const VERSION = '0.1.24';
|
|
10
10
|
|
|
11
11
|
export const DEFAULTS = {
|
|
12
12
|
cn: {
|
|
@@ -1362,6 +1362,7 @@ export {
|
|
|
1362
1362
|
AlipayWalletError,
|
|
1363
1363
|
checkAlipayWallet,
|
|
1364
1364
|
getAlipayWalletBridgeInfo,
|
|
1365
|
+
preflightAlipay402Resource,
|
|
1365
1366
|
payAlipay402Resource,
|
|
1366
1367
|
queryAlipay402Resource,
|
|
1367
1368
|
diagnoseAlipay402Resource
|