package-workshop-auth-recovery.mjs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import { fileURLToPath } from 'node:url';
  5. import { request, invoke, appId, functionsUrl } from '../server/scripts/platform-client.mjs';
  6. import { sealCredential, sha256 } from './lib/workshop-credential-envelope.mjs';
  7. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  8. const INPUT = path.join(ROOT, 'docs/workshop-auth-recovery-20261108/receipts/r1-return');
  9. const OUTPUT = path.join(ROOT, 'artifacts/ksf-workshop-auth-envelope-20261108-r2/package');
  10. const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); };
  11. const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
  12. async function main() {
  13. must(process.argv.length === 2, 'USAGE');
  14. must(!fs.existsSync(OUTPUT), 'OUTPUT_EXISTS_KEEP_FROZEN_PACKAGE');
  15. const validation = json(path.join(INPUT, 'validation.json'));
  16. must(validation.ok && validation.originalFilesVerified === 5
  17. && validation.archiveSha256 === '4a55e70cab2a7a25be5131ea9a7a3da3bab2f06ad9aaaf07cd670121a4f48782', 'RETURN_NOT_VERIFIED');
  18. const original = path.join(INPUT, 'files');
  19. const incoming = json(path.join(INPUT, 'manifest.json'));
  20. for (const file of incoming.files) {
  21. must(!path.isAbsolute(file.path) && !file.path.split(/[\\/]/).includes('..'), 'BAD_RETURN_PATH');
  22. const bytes = fs.readFileSync(path.join(original, file.path));
  23. must(bytes.length === file.bytes && sha256(bytes) === file.sha256, 'RETURN_CHANGED');
  24. }
  25. const meta = json(path.join(original, 'public-recipient/recipient.json'));
  26. const receipt = json(path.join(original, 'agent-auth-receipt.json'));
  27. must(receipt.contract?.code === 'CREDENTIAL_EXPIRED_OR_MISMATCH'
  28. && receipt.skillStateUnchanged && receipt.businessRecordWrites === 0, 'UNEXPECTED_RETURN');
  29. must(meta.host === '043bb9231c09' && meta.publicKeySha256 === '1dbefc093985a077fdd7f7433bb024e93e69bf69e89ae8f7e25f749819025bae'
  30. && meta.keyDirectory === '/opt/data/ksf-key-receive-20261031-6R6BjG'
  31. && meta.privateKeyExported === false && meta.tokenExported === false, 'RECIPIENT_CHANGED');
  32. const pem = fs.readFileSync(path.join(original, 'public-recipient/recipient-public.pem'));
  33. const key = crypto.createPublicKey(pem);
  34. must(sha256(pem) === meta.exportedPublicPemSha256
  35. && sha256(key.export({ type: 'spki', format: 'der' })) === meta.publicKeySha256, 'PUBLIC_KEY_MISMATCH');
  36. const decryptor = fs.readFileSync(path.join(original, 'public-recipient/decrypt-module.mjs'));
  37. must(sha256(decryptor) === meta.decryptModuleSha256, 'DECRYPTOR_CHANGED');
  38. const wrapper = fs.readFileSync(path.join(ROOT, 'skills/ksf-workshop-runtime/scripts/ksf-secure.mjs'));
  39. must(sha256(wrapper) === receipt.wrapperSha256, 'WRAPPER_CHANGED');
  40. // Read the existing restricted configuration only into this process. Never package it.
  41. const env = {};
  42. for (const line of fs.readFileSync('F:/文档/康师傅数字人受限连接.env', 'utf8').split(/\r?\n/)) {
  43. const match = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/);
  44. if (match) env[match[1]] = match[2].trim();
  45. }
  46. try {
  47. must(env.KSF_SERVICE_TOKEN && env.KSF_FUNCTIONS_URL === functionsUrl && env.KSF_PARSE_APP_ID === appId, 'CONFIG_MISMATCH');
  48. const current = await request('GET', '/classes/KsfServiceCredential/MqKRUv9JQc');
  49. const prior = json(path.join(ROOT, 'work/workshop-auth-recovery-20261108-r1/server-renewal-receipt.json'));
  50. const expiresAt = typeof current.expiresAt === 'string' ? current.expiresAt : current.expiresAt?.iso;
  51. must(current.objectId === prior.after.objectId && current.tokenHash === sha256(env.KSF_SERVICE_TOKEN)
  52. && current.enabled === true && expiresAt === prior.after.expiresAt
  53. && JSON.stringify(current.scopes) === JSON.stringify(prior.after.scopes)
  54. && JSON.stringify(current.projectIds) === JSON.stringify(prior.after.projectIds), 'SERVER_CREDENTIAL_CHANGED');
  55. const headers = { 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': env.KSF_SERVICE_TOKEN, 'Content-Type': 'application/json' };
  56. const contract = await invoke('ksfGetIngestContract', { requestId: 'auth-envelope-20261108-r2-contract' }, headers);
  57. must(contract.http === 200 && contract.body.result?.ok === true
  58. && contract.body.result.data?.allowedClasses?.length > 0, 'RESTRICTED_CONTRACT_FAILED');
  59. const schema = await invoke('ksfGetDataDictionary', { summary: true, requestId: 'auth-envelope-20261108-r2-schema' }, headers);
  60. const dictionary = schema.body.result?.data;
  61. must(schema.http === 200 && schema.body.result?.ok === true && dictionary?.readOnly === true
  62. && dictionary.databaseWrites === 0 && dictionary.rowValuesReturned === false, 'RESTRICTED_SCHEMA_FAILED');
  63. const envelope = sealCredential({ publicKey: pem, host: meta.host, directory: meta.keyDirectory,
  64. credentialId: current.objectId, expiresAt, env });
  65. const envelopeBytes = Buffer.from(JSON.stringify(envelope, null, 2) + '\n');
  66. const release = {
  67. schema: 'ksf.workshop.encrypted-authorization-release.v1', preparedAt: new Date().toISOString(),
  68. host: meta.host, containerNameForOperator: 'agent-node-u17879018108', uid: 10000,
  69. skillDirectory: '/opt/data/skills/ksf-workshop-runtime', keyDirectory: meta.keyDirectory,
  70. publicKeySha256: meta.publicKeySha256, decryptModule: meta.decryptModule, decryptModuleSha256: meta.decryptModuleSha256,
  71. wrapperSha256: receipt.wrapperSha256, credentialId: current.objectId, expiresAt,
  72. expectedBindingSha256: meta.bindingSha256, oldEnvelopeFile: meta.envelopeFile, oldEnvelopeSha256: meta.envelopeSha256,
  73. installDirectory: '/opt/data/ksf-workshop-auth-envelope-20261108-r2',
  74. installEnvelope: '/opt/data/ksf-workshop-auth-envelope-20261108-r2/credential-envelope.json',
  75. envelopeSha256: sha256(envelopeBytes), originalReturnArchiveSha256: validation.archiveSha256,
  76. serverAuthorizationRestored: true, agentEncryptedEntryVerified: false, actualBusinessWriteVerified: false,
  77. formalReportSubmitted: false, serverAuthorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0,
  78. };
  79. const recheck = { checkedAt: new Date().toISOString(), readOnly: true,
  80. serverAuthorizationRestored: true, credentialId: current.objectId, expiresAt,
  81. originalTokenIdentityAndScopesPreserved: true, serverAuthorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0,
  82. contract: { http: contract.http, ok: true, allowedClassCount: contract.body.result.data.allowedClasses.length },
  83. schemaProbe: { http: schema.http, ok: true, readOnly: true, databaseWrites: 0, rowValuesReturned: false,
  84. tableCount: dictionary.tableCount, fieldCount: dictionary.fieldCount } };
  85. fs.mkdirSync(OUTPUT, { recursive: true });
  86. const write = (name, bytes) => {
  87. must(!Buffer.from(bytes).includes(Buffer.from(env.KSF_SERVICE_TOKEN)), 'PLAINTEXT_CREDENTIAL_IN_PACKAGE');
  88. const file = path.join(OUTPUT, name); fs.mkdirSync(path.dirname(file), { recursive: true });
  89. fs.writeFileSync(file, bytes, { flag: 'wx' });
  90. };
  91. write('credential-envelope.json', envelopeBytes);
  92. write('RELEASE.json', JSON.stringify(release, null, 2) + '\n');
  93. write('server-recheck.json', JSON.stringify(recheck, null, 2) + '\n');
  94. write('return-validation.json', JSON.stringify(validation, null, 2) + '\n');
  95. write('restore-workshop-credential-binding.mjs', fs.readFileSync(path.join(ROOT, 'scripts/restore-workshop-credential-binding.mjs')));
  96. write('lib/workshop-credential-envelope.mjs', fs.readFileSync(path.join(ROOT, 'scripts/lib/workshop-credential-envelope.mjs')));
  97. write('public-recipient/decrypt-module.mjs', decryptor);
  98. write('public-recipient/recipient.json', fs.readFileSync(path.join(original, 'public-recipient/recipient.json')));
  99. write('public-recipient/recipient-public.pem', pem);
  100. write('tests/credential-envelope.test.mjs', fs.readFileSync(path.join(ROOT, 'test/workshop-credential-envelope.test.mjs'), 'utf8')
  101. .replace("'../scripts/lib/workshop-credential-envelope.mjs'", "'../lib/workshop-credential-envelope.mjs'")
  102. .replace("'../scripts/restore-workshop-credential-binding.mjs'", "'../restore-workshop-credential-binding.mjs'")
  103. .replace("'../docs/workshop-auth-recovery-20261108/receipts/r1-return/files/public-recipient/decrypt-module.mjs'", "'../public-recipient/decrypt-module.mjs'"));
  104. write('运维001-原密钥重封安装复验-r2.md', fs.readFileSync(path.join(ROOT, 'docs/workshop-auth-recovery-20261108/运维001-原密钥重封安装复验-r2.md')));
  105. console.log(JSON.stringify({ prepared: true, directory: OUTPUT, expiresAt, serverReadOnlyRechecked: true,
  106. plaintextCredentialPackaged: false, agentEncryptedEntryVerified: false, remoteInstallationPending: true }));
  107. } finally { delete env.KSF_SERVICE_TOKEN; }
  108. }
  109. main().catch(error => {
  110. console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'PACKAGE_PREPARATION_STOPPED',
  111. message: '准备停止;不输出凭据、配置、原始服务器报文或异常。' }));
  112. process.exitCode = 1;
  113. });