@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 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 output = `${stdout}\n${stderr}`;
344
- const candidates = [
345
- ...output.matchAll(/(?:"(?:errorCode|code|sub_code|previous_error)"|\b(?:errorCode|code|sub_code|previous_error)\b)\s*[:=]\s*["']?([A-Z][A-Z0-9_]{2,80})/ig)
346
- ].map((match) => match[1].toUpperCase());
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: 'available',
691
+ state: 'runtime_available',
537
692
  provider: 'alipay-agent-payment',
538
- message: '支付宝官方付款桥已可用;付款时将由支付宝 App 请求确认。',
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.22';
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/sdk",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {