@alipay/alipay-aipay 1.7.3 → 1.7.5

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.
Files changed (53) hide show
  1. package/dist/commands/install.js +24 -17
  2. package/package.json +3 -3
  3. package/skill/SKILL.md +24 -2
  4. package/skill/VERSION +1 -1
  5. package/skill/references/normal/json/customer-messages.json +1 -1
  6. package/skill/references/normal/scripts/channel_offer.mjs +227 -0
  7. package/skill/references/normal/scripts/runtime.mjs +3 -1
  8. package/skill-pre/SKILL.md +23 -3
  9. package/skill-pre/VERSION +1 -1
  10. package/skill-pre/references/integration/flow.md +43 -25
  11. package/skill-pre/references/integration/modules/checklist.md +4 -4
  12. package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +33 -33
  13. package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +3 -3
  14. package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +12 -2
  15. package/skill-pre/references/normal/alipay-cli-env.md +3 -1
  16. package/skill-pre/references/normal/conversation-telemetry.md +2 -2
  17. package/skill-pre/references/normal/full-process-routing.md +2 -2
  18. package/skill-pre/references/normal/json/policy-rules.json +1 -1
  19. package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +9 -0
  20. package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +10 -1
  21. package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +4 -1
  22. package/skill-pre/references/normal/scripts/common.sh +11 -1
  23. package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +4 -1
  24. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +17 -2
  25. package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +4 -1
  26. package/skill-pre/references/normal/scripts/open_official_url.sh +10 -2
  27. package/skill-pre/references/normal/scripts/python_script_runner.mjs +9 -4
  28. package/skill-pre/references/normal/scripts/render_customer_message.mjs +4 -2
  29. package/skill-pre/references/normal/scripts/runtime.mjs +50 -6
  30. package/skill-pre/references/normal/scripts/telemetry.mjs +9 -0
  31. package/skill-pre/references/normal/scripts/windows/acl-native.mjs +3 -0
  32. package/skill-pre/references/normal/scripts/windows/acl.mjs +41 -0
  33. package/skill-pre/references/normal/scripts/windows/bin/alipay-cli +4 -0
  34. package/skill-pre/references/normal/scripts/windows/bootstrap-node.ps1 +140 -0
  35. package/skill-pre/references/normal/scripts/windows/cli-install.mjs +51 -0
  36. package/skill-pre/references/normal/scripts/windows/cli-process.mjs +11 -0
  37. package/skill-pre/references/normal/scripts/windows/execution.mjs +51 -0
  38. package/skill-pre/references/normal/scripts/windows/git-discovery.mjs +42 -0
  39. package/skill-pre/references/normal/scripts/windows/policy.json +13 -0
  40. package/skill-pre/references/normal/scripts/windows/preflight.mjs +182 -0
  41. package/skill-pre/references/normal/scripts/windows/prepare.mjs +89 -0
  42. package/skill-pre/references/normal/scripts/windows/probe.sh +14 -0
  43. package/skill-pre/references/normal/scripts/windows/python-execution.mjs +38 -0
  44. package/skill-pre/references/normal/scripts/windows/python-runtime.mjs +86 -0
  45. package/skill-pre/references/normal/scripts/windows/routing.mjs +83 -0
  46. package/skill-pre/references/normal/scripts/windows/shell_helpers.sh +83 -0
  47. package/skill-pre/references/normal/scripts/windows/winget.mjs +13 -0
  48. package/skill-pre/references/normal/windows-environment.md +82 -0
  49. package/skill-pre/references/onboarding/flow.md +4 -1
  50. package/skill-pre/references/onboarding/modules/scripts/app.sh +1 -0
  51. package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +10 -6
  52. package/skill-pre/references/onboarding/modules/scripts/auth.sh +1 -0
  53. package/skill-pre/references/runtime-entry.md +13 -4
@@ -6,6 +6,12 @@ KIND="${1:-}"
6
6
  IFS= read -r URL || URL=""
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  POLICY_FILE="${SCRIPT_DIR}/../json/policy-rules.json"
9
+ case "$(uname -s)" in
10
+ MINGW*|MSYS*)
11
+ source "$SCRIPT_DIR/windows/shell_helpers.sh" || exit 78
12
+ POLICY_FILE=$(cygpath -m "$POLICY_FILE") || exit 78
13
+ ;;
14
+ esac
9
15
 
10
16
  case "$KIND" in authorization|public-key-confirmation) ;; *) echo "OPEN_FAILED"; exit 1 ;; esac
11
17
  command -v node >/dev/null 2>&1 || { echo "OPEN_FAILED"; exit 1; }
@@ -18,8 +24,10 @@ const policy = JSON.parse(fs.readFileSync(policyFile, "utf8"));
18
24
  if (/[\u0000-\u0020\u007f`()\[\]<>|]/.test(raw)) process.exit(1);
19
25
  let url;
20
26
  try { url = new URL(raw); } catch { process.exit(1); }
21
- const allowed = policy.temporaryUrlAllowlist[kind === "authorization" ? "authorization" : "publicKeyConfirmation"];
22
- if (!allowed || url.protocol !== "https:" || url.hostname !== allowed.host || url.pathname !== allowed.path || url.username || url.password || url.port || url.hash) process.exit(1);
27
+ const entries = policy.temporaryUrlAllowlist[kind === "authorization" ? "authorization" : "publicKeyConfirmation"];
28
+ const allowed = (Array.isArray(entries) ? entries : [entries]).find((entry) =>
29
+ entry && url.hostname === entry.host && url.pathname === entry.path);
30
+ if (!allowed || url.protocol !== "https:" || url.username || url.password || url.port || url.hash) process.exit(1);
23
31
  if (kind === "authorization") {
24
32
  const required = allowed.requiredQueryParams;
25
33
  const optional = allowed.optionalQueryParams;
@@ -7,6 +7,8 @@ import process from 'node:process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { isWindowsPlatform } from './platform_compat.mjs';
10
+ import { runWindowsPython } from './windows/python-execution.mjs';
11
+ import { findWindowsPython } from './windows/python-runtime.mjs';
10
12
 
11
13
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12
14
  const allowedScript = path.resolve(scriptDir, '../../integration/modules/scripts/local_402_sandbox_pay.py');
@@ -17,6 +19,7 @@ function commandRuns(command, args) {
17
19
  }
18
20
 
19
21
  function resolvePython() {
22
+ if (isWindowsPlatform()) return findWindowsPython();
20
23
  const candidates = isWindowsPlatform()
21
24
  ? [
22
25
  { command: 'py', prefix: ['-3'] },
@@ -40,10 +43,12 @@ export function runPythonScriptCli(argv = process.argv) {
40
43
 
41
44
  const python = resolvePython();
42
45
  if (!python) throw new Error('未找到可用的 Python 3 运行时');
43
- const result = spawnSync(python.command, [...python.prefix, resolvedScript, ...argv.slice(3)], {
44
- stdio: 'inherit',
45
- shell: false
46
- });
46
+ const result = isWindowsPlatform()
47
+ ? runWindowsPython(python, resolvedScript, argv.slice(3))
48
+ : spawnSync(python.command, [...python.prefix, resolvedScript, ...argv.slice(3)], {
49
+ stdio: 'inherit',
50
+ shell: false
51
+ });
47
52
  if (result.error) throw result.error;
48
53
  process.exitCode = Number.isInteger(result.status) ? result.status : 1;
49
54
  }
@@ -343,8 +343,10 @@ function validateUrl(value, policy, urlKind, expected = {}) {
343
343
  } catch {
344
344
  throw new Error('officialTemporaryUrl 不是合法 URL');
345
345
  }
346
- const allowed = policy.temporaryUrlAllowlist[urlKind];
347
- if (!allowed || parsed.protocol !== 'https:' || parsed.hostname !== allowed.host || parsed.pathname !== allowed.path || parsed.username || parsed.password || parsed.port || parsed.hash) {
346
+ const entries = policy.temporaryUrlAllowlist[urlKind];
347
+ const allowed = (Array.isArray(entries) ? entries : [entries]).find((entry) =>
348
+ entry && parsed.hostname === entry.host && parsed.pathname === entry.path);
349
+ if (!allowed || parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.port || parsed.hash) {
348
350
  throw new Error(`officialTemporaryUrl 不符合 ${urlKind} allowlist`);
349
351
  }
350
352
  if (urlKind === 'authorization') {
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'node:child_process';
3
+ import { spawnSync as nativeSpawn } from 'node:child_process';
4
+ const { spawnSync } = process.platform === 'win32'
5
+ ? await import('./windows/execution.mjs')
6
+ : { spawnSync: nativeSpawn };
4
7
  import path from 'node:path';
5
8
  import process from 'node:process';
6
9
  import { fileURLToPath } from 'node:url';
@@ -8,6 +11,16 @@ import { fileURLToPath } from 'node:url';
8
11
  import { applyTelemetrySession, currentRunContext, startTelemetryRun } from './telemetry.mjs';
9
12
  import { applyAgentPlatform } from './agent_platform.mjs';
10
13
 
14
+ const windowsRouting = process.platform === 'win32' ? await import('./windows/routing.mjs') : null;
15
+
16
+ // A user-local Node bootstrap may not be on the host's PATH. Its bundled npm/npx
17
+ // and child Node commands must use the same verified runtime for this process.
18
+ if (process.platform === 'win32') {
19
+ const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === 'path');
20
+ for (const key of Object.keys(process.env)) if (key.toLowerCase() === 'path') delete process.env[key];
21
+ process.env.PATH = [path.dirname(process.execPath), entry?.[1] || ''].join(path.delimiter);
22
+ }
23
+
11
24
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12
25
  const referencesDir = path.resolve(scriptDir, '../..');
13
26
  const flowDirs = Object.freeze({
@@ -41,6 +54,7 @@ const nodeEntrypoints = Object.freeze({
41
54
  });
42
55
 
43
56
  export const runtimeDomains = Object.freeze([
57
+ ...(process.platform === 'win32' ? ['windows-entry'] : []),
44
58
  'self-update',
45
59
  'integration-bootstrap',
46
60
  'onboarding-bootstrap',
@@ -56,12 +70,18 @@ function cliArgv(args) {
56
70
  }
57
71
 
58
72
  function runShell(script, cwd, args) {
73
+ const captureWindowsSandbox = process.platform === 'win32' && path.basename(script) === 'sandbox_config.sh';
59
74
  const result = spawnSync('bash', [script, ...args], {
60
75
  cwd,
61
76
  env: process.env,
62
- stdio: 'inherit',
77
+ stdio: captureWindowsSandbox ? 'pipe' : 'inherit',
78
+ ...(captureWindowsSandbox ? {encoding: 'utf8'} : {}),
63
79
  shell: false
64
80
  });
81
+ if (captureWindowsSandbox) {
82
+ if (result.stdout) process.stdout.write(result.stdout);
83
+ if (result.stderr) process.stderr.write(result.stderr);
84
+ }
65
85
  if (result.error) throw result.error;
66
86
  process.exitCode = Number.isInteger(result.status) ? result.status : 1;
67
87
  }
@@ -186,9 +206,9 @@ async function runOnboardingBootstrap(args) {
186
206
  await startTelemetryRun(rootIntent, productType);
187
207
  const { runOnboardingMessageCli } = await import('./onboarding_message_runner.mjs');
188
208
  const platformEntryArgs = ['platform-entry'];
189
- await runOnboardingMessageCli(cliArgv(platformEntryArgs));
209
+ const route = await runOnboardingMessageCli(cliArgv(platformEntryArgs));
190
210
  const { isWindowsPlatform } = await import('./platform_compat.mjs');
191
- if (isWindowsPlatform()) return;
211
+ if (isWindowsPlatform() && route !== 'READY') return;
192
212
  const { runFlowCli } = await import('./flow_loader.mjs');
193
213
  runFlowCli(cliArgv(['onboarding', 'entry']));
194
214
  }
@@ -203,6 +223,18 @@ async function runOnboardingExecuteReady(args) {
203
223
  }
204
224
 
205
225
  async function dispatch(domain, args) {
226
+ if (windowsRouting) {
227
+ if (domain === 'windows-entry') return windowsRouting.runWindowsEntry(args);
228
+ if (['sandbox', 'mcc', 'auth', 'screenshots', 'special-license', 'sign', 'service', 'app',
229
+ 'onboarding-discovery', 'onboarding-recovery', 'onboarding-execute-ready'].includes(domain)) {
230
+ windowsRouting.requireBusinessEnvironment();
231
+ }
232
+ if (domain === 'env' && ['prepare', 'prepare-jq', 'retry'].includes(args[0])) {
233
+ const { runEnvironment } = await import('./windows/prepare.mjs');
234
+ process.exitCode = runEnvironment(args);
235
+ return;
236
+ }
237
+ }
206
238
  if (domain === 'integration-bootstrap') return runIntegrationBootstrap(args);
207
239
  if (domain === 'onboarding-bootstrap') return runOnboardingBootstrap(args);
208
240
  if (domain === 'onboarding-execute-ready') return runOnboardingExecuteReady(args);
@@ -223,6 +255,11 @@ async function dispatch(domain, args) {
223
255
  return;
224
256
  }
225
257
  if (domain === 'env') {
258
+ if (process.platform === 'win32' && ['check-python', 'prepare-python'].includes(args[0])) {
259
+ const { runPythonEnvironment } = await import('./windows/python-runtime.mjs');
260
+ process.exitCode = await runPythonEnvironment(args);
261
+ return;
262
+ }
226
263
  if (args[0] === 'refresh-alipay-cli') {
227
264
  if (args.length !== 1) throw new Error('用法: runtime.mjs env refresh-alipay-cli');
228
265
  const { runAlipayCliRefreshCli } = await import('./alipay_cli_refresh.mjs');
@@ -259,9 +296,16 @@ async function dispatch(domain, args) {
259
296
  export async function runRuntimeCli(argv = process.argv) {
260
297
  const domain = argv[2] || '';
261
298
  try {
262
- await dispatch(domain, applyTelemetryContext(domain, applyTelemetrySession(applyAgentPlatform(argv.slice(3)))));
299
+ const received = applyTelemetrySession(applyAgentPlatform(argv.slice(3)));
300
+ const routed = windowsRouting ? windowsRouting.consumeRoute(received) : {args: received};
301
+ if (routed.route) process.env.ALIPAY_AIPAY_WINDOWS_ROUTE = routed.route;
302
+ await dispatch(domain, applyTelemetryContext(domain, routed.args));
263
303
  } catch (error) {
264
- runtimeError(domain, error);
304
+ if (windowsRouting && error.reason) {
305
+ const { report } = await import('./windows/preflight.mjs');
306
+ report(error);
307
+ process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE\n');
308
+ } else runtimeError(domain, error);
265
309
  process.exitCode = domain === 'self-update' ? 0 : 1;
266
310
  }
267
311
  }
@@ -1,3 +1,7 @@
1
+
2
+ const windowsProtect = process.platform === 'win32'
3
+ ? (await import('./windows/acl.mjs')).protect
4
+ : null;
1
5
  import crypto from 'node:crypto';
2
6
  import fs from 'node:fs';
3
7
  import https from 'node:https';
@@ -364,6 +368,11 @@ export function telemetryStoragePaths(env = process.env) {
364
368
  }
365
369
 
366
370
  function ensurePrivateDir(dir) {
371
+ if (windowsProtect) {
372
+ fs.mkdirSync(dir, { recursive: true });
373
+ windowsProtect(dir);
374
+ return;
375
+ }
367
376
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
368
377
  try {
369
378
  fs.chmodSync(dir, 0o700);
@@ -0,0 +1,3 @@
1
+ import {protect} from './acl.mjs';
2
+ try { if(process.argv.length!==3)throw new Error('ACL target required');protect(process.argv[2]); }
3
+ catch(e){process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_ACL_ERROR='+(e.reason||'ACL_FAILED')+'\n');process.exitCode=78;}
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawnSync} from 'node:child_process';
4
+
5
+ export function fail(reason, dependency = '') { throw Object.assign(new Error(reason), {reason, dependency}); }
6
+ export function run(command,args,options={}) {
7
+ return spawnSync(command,args,{encoding:'utf8',windowsHide:true,shell:false,timeout:15000,maxBuffer:4*1024*1024,...options});
8
+ }
9
+ export function powershell() {
10
+ const root=process.env.SystemRoot || 'C:\\Windows';
11
+ const command=path.join(root,'System32/WindowsPowerShell/v1.0/powershell.exe');
12
+ if(!fs.existsSync(command)) fail('DEPENDENCY_MISSING','PowerShell');
13
+ return command;
14
+ }
15
+ export function psEnv(env=process.env) {
16
+ return Object.fromEntries(Object.entries(env).filter(([k])=>k.toLowerCase()!=='psmodulepath'));
17
+ }
18
+ export function protect(target) {
19
+ if(fs.lstatSync(target).isSymbolicLink()||path.resolve(target)===path.parse(target).root)fail('ACL_FAILED','unsafe target');
20
+ const system=path.join(process.env.SystemRoot||'C:/Windows','System32');
21
+ const who=run(path.join(system,'whoami.exe'),['/user','/fo','csv','/nh']);
22
+ const sid=who.stdout?.match(/S-1-5-[0-9-]+/)?.[0];if(who.status!==0||!sid)fail('ACL_FAILED');
23
+ const env={...psEnv(),ALIPAY_WINDOWS_ACL_TARGET:path.resolve(target)};
24
+ // Native icacls changes permissions; PowerShell only reads ACL metadata.
25
+ // No .ps1 execution, ExecutionPolicy override, elevation, or policy mutation.
26
+ const inspect=()=>{
27
+ const r=run(powershell(),['-NoProfile','-NonInteractive','-Command',
28
+ '$ErrorActionPreference="Stop"; $p=$env:ALIPAY_WINDOWS_ACL_TARGET; $d=New-Object IO.DriveInfo([IO.Path]::GetPathRoot($p)); if($d.DriveFormat -ne "NTFS"){exit 2}; $a=Get-Acl -LiteralPath $p; @{protected=$a.AreAccessRulesProtected;rules=@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | ForEach-Object { @{sid=$_.IdentityReference.Value;type=[string]$_.AccessControlType;rights=[int]$_.FileSystemRights} })} | ConvertTo-Json -Depth 4 -Compress'],{env});
29
+ if(r.status!==0)fail('ACL_FAILED','NTFS');try{return JSON.parse(r.stdout);}catch{fail('ACL_FAILED');}
30
+ };
31
+ const allowed=[sid,'S-1-5-18','S-1-5-32-544'];inspect();
32
+ const inherited=fs.statSync(target).isDirectory()?'(OI)(CI)F':'F';
33
+ const args=[target,'/inheritance:r','/grant:r',...allowed.map(s=>'*'+s+':'+inherited)];
34
+ if(run(path.join(system,'icacls.exe'),args).status!==0)fail('ACL_FAILED','grant');
35
+ const remove=[...new Set(inspect().rules.filter(r=>!allowed.includes(r.sid)||r.type!=='Allow').map(r=>r.sid))];
36
+ for(const s of remove)if(run(path.join(system,'icacls.exe'),[target,'/remove','*'+s]).status!==0)fail('ACL_FAILED','remove');
37
+ // Reapply after removing any explicit deny ACE for an allowed identity.
38
+ if(remove.length&&run(path.join(system,'icacls.exe'),args).status!==0)fail('ACL_FAILED','regrant');
39
+ const after=inspect();
40
+ if(!after.protected||after.rules.some(r=>!allowed.includes(r.sid)||r.type!=='Allow')||!after.rules.some(r=>r.sid===sid&&(r.rights&2032127)===2032127))fail('ACL_FAILED','readback');
41
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Real CLI forwarding entry for original `env ... alipay-cli` call sites.
3
+ set -eu
4
+ exec "$ALIPAY_WINDOWS_NODE" "$ALIPAY_WINDOWS_CLI_RUNNER" "$@"
@@ -0,0 +1,140 @@
1
+ param([ValidateSet('check', 'prepare', 'repair')][string]$Mode = 'check')
2
+
3
+ # The only pre-Node entry. No runtime/business dispatch, telemetry, elevation,
4
+ # ExecutionPolicy override, or persistent PATH change belongs in this script.
5
+ function Invoke-NodeProbe([string]$Executable) {
6
+ if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) { return $null }
7
+ $process = New-Object System.Diagnostics.Process
8
+ try {
9
+ $process.StartInfo.FileName = $Executable
10
+ $process.StartInfo.Arguments = '-p "JSON.stringify({version:process.versions.node,platform:process.platform,arch:process.arch,executable:process.execPath})"'
11
+ $process.StartInfo.UseShellExecute = $false
12
+ $process.StartInfo.CreateNoWindow = $true
13
+ $process.StartInfo.RedirectStandardOutput = $true
14
+ $process.StartInfo.RedirectStandardError = $true
15
+ if (-not $process.Start()) { return $null }
16
+ $stdout = $process.StandardOutput.ReadToEndAsync()
17
+ $stderr = $process.StandardError.ReadToEndAsync()
18
+ if (-not $process.WaitForExit(5000)) { $process.Kill(); return $null }
19
+ if ($process.ExitCode -ne 0) { return $null }
20
+ $value = $stdout.Result | ConvertFrom-Json -ErrorAction Stop
21
+ if ($value.platform -ne 'win32' -or $value.arch -ne 'x64' -or
22
+ $value.version -notmatch '^([0-9]+)\.[0-9]+\.[0-9]+$' -or [int]$Matches[1] -lt 18 -or
23
+ -not [IO.Path]::IsPathRooted($value.executable)) { return $null }
24
+ return $value
25
+ } catch { return $null } finally { $process.Dispose() }
26
+ }
27
+
28
+ function Find-LocalNode([string]$Root) {
29
+ $candidates = @()
30
+ $candidates += @(Get-Command node.exe -CommandType Application -All -ErrorAction SilentlyContinue | ForEach-Object { $_.Source })
31
+ foreach ($directory in @($env:ProgramFiles, $env:LOCALAPPDATA)) {
32
+ if ($directory) {
33
+ $candidates += (Join-Path $directory 'nodejs/node.exe')
34
+ $candidates += (Join-Path $directory 'Programs/nodejs/node.exe')
35
+ }
36
+ }
37
+ if ($env:NVM_SYMLINK) { $candidates += (Join-Path $env:NVM_SYMLINK 'node.exe') }
38
+ if (Test-Path -LiteralPath $Root -PathType Container) {
39
+ $candidates += @(Get-ChildItem -LiteralPath $Root -Directory -Filter 'node-*' -ErrorAction SilentlyContinue |
40
+ ForEach-Object { Get-ChildItem -LiteralPath $_.FullName -Directory -Filter 'node-v*-win-x64' -ErrorAction SilentlyContinue } |
41
+ ForEach-Object { Join-Path $_.FullName 'node.exe' })
42
+ }
43
+ foreach ($candidate in @($candidates | Select-Object -Unique)) {
44
+ $found = Invoke-NodeProbe $candidate
45
+ if ($null -ne $found) { return $found }
46
+ }
47
+ return $null
48
+ }
49
+
50
+ function Receive-NodeFile([string]$Uri, [string]$Destination) {
51
+ # URLs are constructed only from the official index and a validated version.
52
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
53
+ $ProgressPreference = 'SilentlyContinue'
54
+ try { Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Destination -TimeoutSec 60 -ErrorAction Stop | Out-Null }
55
+ catch { throw 'DOWNLOAD_FAILED' }
56
+ }
57
+
58
+ function Install-UserNode([string]$Root) {
59
+ $staging = Join-Path $Root ('node-' + [Guid]::NewGuid().ToString('N'))
60
+ New-Item -ItemType Directory -Path $staging -ErrorAction Stop | Out-Null
61
+ $complete = $false
62
+ try {
63
+ $indexFile = Join-Path $staging 'index.json'
64
+ Receive-NodeFile 'https://nodejs.org/dist/index.json' $indexFile
65
+ $releases = @(Get-Content -LiteralPath $indexFile -Raw | ConvertFrom-Json)
66
+ $release = $releases | Where-Object {
67
+ $_.lts -is [string] -and $_.lts.Length -gt 0 -and $_.version -match '^v[0-9]+\.[0-9]+\.[0-9]+$' -and
68
+ [version]$_.version.Substring(1) -ge [version]'18.0.0' -and $_.files -contains 'win-x64-zip'
69
+ } | Sort-Object { [version]$_.version.Substring(1) } -Descending | Select-Object -First 1
70
+ if ($null -eq $release) { throw 'NO_LTS_ASSET' }
71
+ $name = 'node-' + $release.version + '-win-x64'
72
+ $zipName = $name + '.zip'
73
+ $base = 'https://nodejs.org/dist/' + $release.version + '/'
74
+ $checksums = Join-Path $staging 'SHASUMS256.txt'
75
+ Receive-NodeFile ($base + 'SHASUMS256.txt') $checksums
76
+ $pattern = '^([0-9a-fA-F]{64})\s+' + [regex]::Escape($zipName) + '$'
77
+ $lines = @(Get-Content -LiteralPath $checksums | Where-Object { $_ -match $pattern })
78
+ if ($lines.Count -ne 1 -or $lines[0] -notmatch $pattern) { throw 'INTEGRITY_FAILED' }
79
+ $expected = $Matches[1]
80
+ $archive = Join-Path $staging $zipName
81
+ Receive-NodeFile ($base + $zipName) $archive
82
+ if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ine $expected) { throw 'INTEGRITY_FAILED' }
83
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
84
+ [IO.Compression.ZipFile]::ExtractToDirectory($archive, $staging)
85
+ $node = Invoke-NodeProbe (Join-Path $staging ($name + '/node.exe'))
86
+ if ($null -eq $node -or $node.version -ne $release.version.Substring(1)) { throw 'VERIFY_FAILED' }
87
+ Remove-Item -LiteralPath $indexFile, $checksums, $archive -Force
88
+ $complete = $true
89
+ return $node
90
+ } finally {
91
+ if (-not $complete) { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue }
92
+ }
93
+ }
94
+
95
+ function Invoke-NodeBootstrap([string]$Action, [string]$Root) {
96
+ $found = Find-LocalNode $Root
97
+ if ($null -ne $found) { return @{status='READY'; nodePath=$found.executable; version=$found.version} }
98
+ if ($Action -eq 'check') { return @{status='NEEDS_SETUP'; reason='NODE_MISSING_OR_UNSUPPORTED'} }
99
+ New-Item -ItemType Directory -Path $Root -Force -ErrorAction Stop | Out-Null
100
+ if ((Get-Item -LiteralPath $Root).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'UNSAFE_DIRECTORY' }
101
+ $lock = $null
102
+ try {
103
+ try { $lock = [IO.File]::Open((Join-Path $Root 'node.lock'), 'OpenOrCreate', 'ReadWrite', 'None') }
104
+ catch { throw 'ENVIRONMENT_BUSY' }
105
+ $stateFile = Join-Path $Root 'node-environment.json'
106
+ $state = @{prepareAttempts=0; repairAttempts=0}
107
+ if (Test-Path -LiteralPath $stateFile) {
108
+ try {
109
+ $loaded = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json
110
+ foreach ($key in @('prepareAttempts', 'repairAttempts')) {
111
+ if ($loaded.$key -isnot [int] -and $loaded.$key -isnot [long]) { throw 'invalid' }
112
+ if ($loaded.$key -lt 0) { throw 'invalid' }
113
+ $state[$key] = $loaded.$key
114
+ }
115
+ } catch { throw 'ENVIRONMENT_STATE_INVALID' }
116
+ }
117
+ if (($Action -eq 'repair' -and $state.repairAttempts -ge 1) -or
118
+ ($Action -eq 'prepare' -and ($state.prepareAttempts -ge 1 -or $state.repairAttempts -ge 1))) { throw 'RETRY_BUDGET_EXHAUSTED' }
119
+ $state.prepareAttempts++
120
+ if ($Action -eq 'repair') { $state.repairAttempts++ }
121
+ [IO.File]::WriteAllText($stateFile, ($state | ConvertTo-Json -Compress))
122
+ $found = Install-UserNode $Root
123
+ return @{status='READY'; nodePath=$found.executable; version=$found.version}
124
+ } finally { if ($null -ne $lock) { $lock.Dispose() } }
125
+ }
126
+
127
+ if ($MyInvocation.InvocationName -ne '.') {
128
+ $ErrorActionPreference = 'Stop'
129
+ try {
130
+ $architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
131
+ if ($env:OS -ne 'Windows_NT' -or $architecture -ne 'AMD64' -or -not $env:LOCALAPPDATA) { throw 'UNSUPPORTED_PLATFORM' }
132
+ $result = Invoke-NodeBootstrap $Mode (Join-Path $env:LOCALAPPDATA 'alipay-aipay-pre/bootstrap')
133
+ } catch {
134
+ $reason = [string]$_.Exception.Message
135
+ if ($reason -notmatch '^(DOWNLOAD_FAILED|NO_LTS_ASSET|INTEGRITY_FAILED|VERIFY_FAILED|UNSAFE_DIRECTORY|ENVIRONMENT_BUSY|ENVIRONMENT_STATE_INVALID|RETRY_BUDGET_EXHAUSTED|UNSUPPORTED_PLATFORM)$') { $reason = 'SETUP_FAILED' }
136
+ $result = @{status='BLOCKED'; reason=$reason}
137
+ }
138
+ [Console]::Error.WriteLine('ALIPAY_AIPAY_INTERNAL:NODE_BOOTSTRAP=' + ($result | ConvertTo-Json -Compress))
139
+ if ($result.status -ne 'READY') { exit 78 }
140
+ }
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import {spawnSync} from 'node:child_process';
4
+
5
+ export const installCommand = String.raw`
6
+ $ErrorActionPreference = 'Stop'
7
+ $env:ALIPAY_CLI_BIN = "$HOME\.local\bin"
8
+ $env:ALIPAY_CLI_SKIP_VERIFY = 'true'
9
+ try {
10
+ Invoke-RestMethod -UseBasicParsing -Uri 'https://opengw.alipay.com/alipaycli/install_ps1' -TimeoutSec 20 -ErrorAction Stop | Invoke-Expression
11
+ if ($global:LASTEXITCODE -ne 0) { exit 1 }
12
+ } catch {
13
+ $code = $_.Exception.Data['InstallerExitCode']
14
+ if ($code -in @(3, 4, 5)) { exit ([int]$code) }
15
+ if ($_.Exception -is [System.Net.WebException]) { exit 3 }
16
+ if ($_.Exception -is [System.UnauthorizedAccessException]) { exit 4 }
17
+ exit 1
18
+ }
19
+ `;
20
+
21
+ export function refreshWindowsCli({env = process.env, home = os.homedir(), run = spawnSync} = {}) {
22
+ const childEnv = {...env};
23
+ // Keep business context and installer overrides out of the official install.
24
+ for (const key of Object.keys(childEnv)) {
25
+ if (['PLATFORM', 'PLATFORM_ID', 'PRODUCT', 'ALIPAYCLI_BIZ_INFO', 'PSMODULEPATH',
26
+ 'ALIPAY_CLI_BIN', 'ALIPAY_CLI_SKIP_VERIFY', 'ALIPAY_CLI_VERSION',
27
+ 'ALIPAY_CLI_CHANNEL', 'ALIPAY_CLI_MANIFEST_URL',
28
+ 'ALIPAY_CLI_AFTS_MANIFEST_BASE'].includes(key.toUpperCase())) delete childEnv[key];
29
+ }
30
+ const command = path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
31
+ const options = {env: childEnv, encoding: 'utf8', shell: false, windowsHide: true,
32
+ maxBuffer: 30 * 1024 * 1024, timeout: 60000};
33
+ try {
34
+ const result = run(command, ['-NoProfile', '-NonInteractive', '-Command', installCommand], options);
35
+ const output = `${result.stdout || ''}\n${result.stderr || ''}\n${result.error?.message || ''}`;
36
+ if (result.error || result.status !== 0) {
37
+ if (result.status === 3) return 'RETRY_WITH_NETWORK';
38
+ if (result.status === 4) return 'RETRY_WITH_LOCAL_FS_PERMISSION';
39
+ if (result.status === 5) return 'VERIFY_FAILED';
40
+ if (result.error?.code === 'ETIMEDOUT' ||
41
+ /could not resolve|failed to connect|connection (?:refused|reset)|timed? out|timeout|network|socket|tls|ssl|ENOTFOUND|ECONNREFUSED|EAI_AGAIN/iu.test(output)) return 'RETRY_WITH_NETWORK';
42
+ if (/permission denied|operation not permitted|EACCES|EPERM|access denied/iu.test(output)) return 'RETRY_WITH_LOCAL_FS_PERMISSION';
43
+ return 'FAILED';
44
+ }
45
+ const verify = run(path.join(home, '.local', 'bin', 'alipay-cli.exe'), ['version'],
46
+ {...options, stdio: 'ignore', timeout: 5000});
47
+ return !verify.error && verify.status === 0 ? 'READY' : 'VERIFY_FAILED';
48
+ } catch {
49
+ return 'FAILED';
50
+ }
51
+ }
@@ -0,0 +1,11 @@
1
+ import {spawnSync} from 'node:child_process';
2
+ import {ensureReady} from './preflight.mjs';
3
+ try {
4
+ const {cli,env}=ensureReady();
5
+ const args=process.argv.slice(2);
6
+ if(args[0]==='file'&&args[1]==='upload'&&/^\/[a-zA-Z]\//.test(args[2]||''))args[2]=args[2][1]+':'+args[2].slice(2);
7
+ if(args.reduce((n,a)=>n+a.length+3,0)>28000)throw new Error('COMMAND_LINE_TOO_LONG');
8
+ const r=spawnSync(cli,args,{env,stdio:'inherit',windowsHide:true,shell:false,timeout:60000});
9
+ if(r.error)process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_CLI_ERROR='+r.error.code+'; result may be unknown; do not replay writes\n');
10
+ process.exitCode=r.status??124;
11
+ }catch(e){process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_CLI_ERROR='+e.message+'\n');process.exitCode=78;}
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import {spawn as nativeSpawn,spawnSync as nativeSpawnSync} from 'node:child_process';
3
+ import {ensureReady,report,fail} from './preflight.mjs';
4
+
5
+ export function posix(p) {
6
+ const value=p.replaceAll('\\','/');
7
+ if(/^[a-zA-Z]:\//.test(value)) return '/'+value[0].toLowerCase()+value.slice(2);
8
+ if(value.startsWith('//')) fail('UNC_PATH_NOT_IMPLEMENTED');
9
+ return value;
10
+ }
11
+ export function shellArguments(script,args) {
12
+ const a=[...args],base=path.win32.basename(script);
13
+ const positions=base==='sandbox_config.sh'?[a[0]==='summary'?2:1]:
14
+ base==='upload_screenshots.sh'?[2,3,4]:
15
+ base==='project_inspector.sh'?(a[0]==='prepare-new'?[1]:a[0]==='scan'?[2]:[]):[];
16
+ for(const i of positions) if(a[i])a[i]=posix(a[i]);
17
+ const flags=new Set(['--project-path','--config-path','--file','--home-img','--shop-img','--pay-img','--key-file']);
18
+ for(let i=0;i<a.length-1;i++)if(flags.has(a[i]))a[i+1]=posix(a[++i]);
19
+ return [posix(script),...a];
20
+ }
21
+ function invocation(command,args,options) {
22
+ if(command!=='bash') return {command,args,options};
23
+ const c=ensureReady();
24
+ // The caller's business environment wins except adapter-owned execution values.
25
+ const env={...c.env,...(options.env||{})};
26
+ for(const k of ['PATH','TEMP','TMP','TMPDIR','NODE_BINARY','MSYS_NO_PATHCONV','MSYS2_ARG_CONV_EXCL'])env[k]=c.env[k];
27
+ for(const k of Object.keys(c.env))if(k.startsWith('ALIPAY_WINDOWS_'))env[k]=c.env[k];
28
+ for(const k of Object.keys(env))if(k.toLowerCase()==='path'&&k!=='PATH')delete env[k];
29
+ delete env.BASH_ENV;delete env.ENV;
30
+ const mapped=shellArguments(args[0],args.slice(1));
31
+ if(mapped.reduce((n,s)=>n+String(s).length+3,0)>28000)fail('COMMAND_LINE_TOO_LONG');
32
+ return {command:c.bash,args:['--noprofile','--norc',...mapped],options:{...options,env,shell:false,windowsHide:true,timeout:options.timeout||180000,maxBuffer:options.maxBuffer||16*1024*1024}};
33
+ }
34
+ export function nativeSandboxMarkers(stderr) {
35
+ return String(stderr || '').replace(/^(ALIPAY_AIPAY_INTERNAL:SANDBOX_(?:CONFIG|PENDING)_PATH=)\/([a-zA-Z])\/(.*)$/gm,
36
+ (_, prefix, drive, rest) => `${prefix}${drive.toUpperCase()}:/${rest}`);
37
+ }
38
+
39
+ export function spawnSync(command,args,options={}) {
40
+ const i=invocation(command,args,options);
41
+ const r=nativeSpawnSync(i.command,i.args,i.options);
42
+ if(command === 'bash' && path.win32.basename(args[0]) === 'sandbox_config.sh' && typeof r.stderr === 'string') {
43
+ r.stderr = nativeSandboxMarkers(r.stderr);
44
+ }
45
+ if(r.error) { report({reason:r.error.code==='ETIMEDOUT'?'EXECUTION_TIMEOUT':'EXECUTION_FAILED'}); }
46
+ return r;
47
+ }
48
+ export function spawn(command,args,options={}) {
49
+ const i=invocation(command,args,options);
50
+ return nativeSpawn(i.command,i.args,i.options);
51
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {run, fail} from './acl.mjs';
4
+
5
+ export function gitCandidates(env = process.env) {
6
+ if (env.ALIPAY_GIT_BASH) return [env.ALIPAY_GIT_BASH];
7
+ const candidates = [
8
+ path.win32.join(env.ProgramFiles || 'C:/Program Files', 'Git/bin/bash.exe'),
9
+ ...(env.LOCALAPPDATA ? [path.win32.join(env.LOCALAPPDATA, 'Programs/Git/bin/bash.exe')] : [])
10
+ ];
11
+ const search = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] || '';
12
+ for (const directory of search.split(';').filter(p => path.win32.isAbsolute(p))) {
13
+ candidates.push(path.win32.join(directory, 'bash.exe'));
14
+ if (['cmd', 'bin'].includes(path.win32.basename(directory).toLowerCase())) {
15
+ candidates.push(path.win32.join(directory, '../bin/bash.exe'));
16
+ }
17
+ }
18
+ return [...new Set(candidates.map(p => path.win32.normalize(p)))];
19
+ }
20
+
21
+ export function findGitBash({env = process.env, exists = fs.existsSync, execute = run} = {}) {
22
+ const cleanEnv = {...env};
23
+ delete cleanEnv.BASH_ENV; delete cleanEnv.ENV; delete cleanEnv.SHELLOPTS;
24
+ for (const candidate of gitCandidates(env)) {
25
+ if (!path.win32.isAbsolute(candidate) || !exists(candidate)) continue;
26
+ // Require a Git for Windows layout before running bash. System32/WSL bash
27
+ // and unrelated MSYS installations are not candidates for this adapter.
28
+ const directory = path.win32.dirname(candidate);
29
+ const git = path.win32.basename(path.win32.dirname(directory)).toLowerCase() === 'usr'
30
+ ? path.win32.resolve(directory, '../..') : path.win32.resolve(directory, '..');
31
+ const bash = path.win32.join(git, 'bin/bash.exe');
32
+ const gitExe = path.win32.join(git, 'cmd/git.exe');
33
+ if (![bash, gitExe, path.win32.join(git, 'usr/bin/cygpath.exe')].every(exists)) continue;
34
+ const version = execute(gitExe, ['--version'], {env: cleanEnv, timeout: 5000});
35
+ if (version.status !== 0 || !/^git version \d[^\r\n]*\.windows\.\d+/m.test(version.stdout || '')) continue;
36
+ const probe = execute(bash, ['--noprofile', '--norc', '-c',
37
+ 'case "$(uname -s)" in MINGW*|MSYS*) exit 0;; *) exit 1;; esac'], {env: cleanEnv, timeout: 5000});
38
+ if (probe.status === 0) return {bash, git};
39
+ }
40
+ if (env.ALIPAY_GIT_BASH) fail('NOT_GIT_BASH', 'ALIPAY_GIT_BASH');
41
+ return null;
42
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "validationCoverage": [],
4
+ "osVersionAllowlistRequired": false,
5
+ "automaticSetupRetries": 0,
6
+ "explicitRepairRetries": 1,
7
+ "nodeMinimumMajor": 18,
8
+ "jq": {
9
+ "version": "1.7.1",
10
+ "sha256": "7451fbbf37feffb9bf262bd97c54f0da558c63f0748e64152dd87b0a07b6d6ab",
11
+ "url": "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-windows-amd64.exe"
12
+ }
13
+ }