renew-workshop-authorization.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  7. const OUT = path.join(ROOT, 'work/workshop-auth-recovery-20261108-r1');
  8. const PLAN = path.join(OUT, 'renewal-plan.json');
  9. const RECEIPT = path.join(OUT, 'server-renewal-receipt.json');
  10. const EXPECTED_ID = 'MqKRUv9JQc';
  11. const ORIGINAL_EXPIRY = '2026-11-06T10:42:23.873Z';
  12. const EXPECTED_SCOPES = ['source:write', 'ingest:write', 'data:read', 'report:ingest', 'report:publish', 'noise:write', 'noise:read'];
  13. const sha = value => crypto.createHash('sha256').update(value).digest('hex');
  14. const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
  15. const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); };
  16. const expiryOf = row => typeof row.expiresAt === 'string' ? row.expiresAt : row.expiresAt?.iso;
  17. const safeRow = row => ({ objectId: row.objectId, enabled: row.enabled, expiresAt: expiryOf(row), scopes: row.scopes, projectIds: row.projectIds, updatedAt: row.updatedAt });
  18. function readServiceConfig() {
  19. const config = {};
  20. for (const line of fs.readFileSync('F:/文档/康师傅数字人受限连接.env', 'utf8').split(/\r?\n/)) {
  21. const m = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/);
  22. if (m) config[m[1]] = m[2].trim();
  23. }
  24. must(config.KSF_SERVICE_TOKEN && config.KSF_FUNCTIONS_URL === functionsUrl && config.KSF_PARSE_APP_ID === appId, 'SERVICE_CONFIG_MISMATCH');
  25. return config;
  26. }
  27. async function readCredential(tokenHash) {
  28. const q = new URLSearchParams({ where: JSON.stringify({ tokenHash }), limit: '2' });
  29. const rows = (await request('GET', '/classes/KsfServiceCredential?' + q)).results;
  30. must(rows?.length === 1 && rows[0].objectId === EXPECTED_ID, 'CREDENTIAL_IDENTITY_MISMATCH');
  31. const row = rows[0];
  32. must(row.enabled === true && row.tokenHash === tokenHash, 'CREDENTIAL_NOT_ENABLED');
  33. must(equal(row.scopes, EXPECTED_SCOPES) && equal(row.projectIds, ['*']), 'CREDENTIAL_SCOPE_CHANGED');
  34. return row;
  35. }
  36. async function run() {
  37. must(process.argv.length <= 3 && (!process.argv[2] || process.argv[2] === '--apply'), 'USAGE');
  38. fs.mkdirSync(OUT, { recursive: true });
  39. const config = readServiceConfig();
  40. const tokenHash = sha(config.KSF_SERVICE_TOKEN);
  41. const headers = { 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': config.KSF_SERVICE_TOKEN, 'Content-Type': 'application/json' };
  42. const before = await readCredential(tokenHash);
  43. const existingReceipt = fs.existsSync(RECEIPT) ? JSON.parse(fs.readFileSync(RECEIPT, 'utf8')) : null;
  44. if (existingReceipt?.serverAuthorizationRestored && expiryOf(before) === existingReceipt.after.expiresAt) {
  45. console.log(JSON.stringify({ ok: true, alreadyApplied: true, receipt: RECEIPT, expiresAt: expiryOf(before), authorizationRecordWrites: 0 }));
  46. return;
  47. }
  48. must(expiryOf(before) === ORIGINAL_EXPIRY, 'UNKNOWN_CREDENTIAL_EXPIRY');
  49. const pre = await invoke('ksfGetIngestContract', { requestId: 'workshop-auth-before-20261108-r1' }, headers);
  50. must(pre.http === 401 && pre.body.result?.errors?.some(e => e.code === 'AUTH_REQUIRED'), 'EXPIRY_FAILURE_NOT_REPRODUCED');
  51. if (process.argv[2] !== '--apply') {
  52. const plan = {
  53. schema: 'ksf.workshop.authorization-renewal-plan.v1', plannedAt: new Date().toISOString(), dryRun: true,
  54. before: safeRow(before), beforeFingerprint: sha(JSON.stringify(before)),
  55. newExpiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(),
  56. fieldsToChange: ['expiresAt'], ownerIdentityPreserved: true, tokenPreserved: true, scopesPreserved: true,
  57. preProbe: { http: pre.http, errorCodes: pre.body.result.errors.map(e => e.code) },
  58. authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0,
  59. };
  60. fs.writeFileSync(PLAN, JSON.stringify(plan, null, 2) + '\n');
  61. console.log(JSON.stringify(plan, null, 2));
  62. return;
  63. }
  64. must(fs.existsSync(PLAN), 'REVIEWABLE_PLAN_REQUIRED');
  65. const plan = JSON.parse(fs.readFileSync(PLAN, 'utf8'));
  66. must(plan.beforeFingerprint === sha(JSON.stringify(before)), 'CONCURRENT_CREDENTIAL_CHANGE');
  67. must(Date.now() - Date.parse(plan.plannedAt) < 3600000 && Date.parse(plan.newExpiresAt) > Date.now(), 'RENEWAL_PLAN_EXPIRED');
  68. const receipt = {
  69. schema: 'ksf.workshop.authorization-renewal-receipt.v1', startedAt: new Date().toISOString(),
  70. before: safeRow(before), requestedExpiresAt: plan.newExpiresAt, mutationAttempted: false,
  71. serverAuthorizationRestored: false, agentEncryptedEntryVerified: false, actualBusinessWriteVerified: false,
  72. authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0,
  73. };
  74. const save = () => fs.writeFileSync(RECEIPT, JSON.stringify(receipt, null, 2) + '\n');
  75. save();
  76. try {
  77. receipt.mutationAttempted = true;
  78. save();
  79. await request('PUT', '/classes/KsfServiceCredential/' + EXPECTED_ID, { expiresAt: { __type: 'Date', iso: plan.newExpiresAt } });
  80. receipt.authorizationRecordWrites = 1;
  81. const after = await readCredential(tokenHash);
  82. receipt.after = safeRow(after);
  83. must(expiryOf(after) === plan.newExpiresAt, 'EXPIRY_READBACK_MISMATCH');
  84. receipt.ownerIdentityPreserved = after.objectId === before.objectId;
  85. receipt.tokenPreserved = after.tokenHash === before.tokenHash;
  86. receipt.scopesPreserved = equal(after.scopes, before.scopes) && equal(after.projectIds, before.projectIds);
  87. const contract = await invoke('ksfGetIngestContract', { requestId: 'workshop-auth-after-20261108-r1' }, headers);
  88. const data = contract.body.result?.data;
  89. must(contract.http === 200 && contract.body.result?.ok === true && data?.allowedClasses?.length > 0, 'RESTRICTED_CONTRACT_FAILED');
  90. receipt.contract = { http: contract.http, ok: true, schemaVersion: data.schemaVersion, contractRevision: data.contractRevision,
  91. allowedClassCount: data.allowedClasses.length, workshop: data.workshop,
  92. reportConfirmation: data.capabilities?.reportConfirmation, reportPublication: data.capabilities?.reportPublication };
  93. const dictionary = await invoke('ksfGetDataDictionary', { summary: true, requestId: 'workshop-auth-schema-20261108-r1' }, headers);
  94. must(dictionary.http === 200 && dictionary.body.result?.ok === true, 'RESTRICTED_SCHEMA_FAILED');
  95. receipt.schemaProbe = { http: dictionary.http, ok: true, tableCount: dictionary.body.result.data?.tableCount, fieldCount: dictionary.body.result.data?.fieldCount };
  96. const search = await invoke('ksfSearchRecords', { className: 'KfArtifact', filters: { dataMode: 'manual' }, limit: 1, requestId: 'workshop-auth-read-20261108-r1' }, headers);
  97. must(search.http === 200 && search.body.result?.ok === true, 'BUSINESS_READ_FAILED');
  98. const searchData = search.body.result.data;
  99. const rows = searchData.records || searchData.results || searchData.items || [];
  100. receipt.businessRead = { http: search.http, ok: true, requestedClass: 'KfArtifact', dataMode: 'manual', returnedRows: rows.length };
  101. if (rows[0]?.objectId) {
  102. const read = await invoke('ksfGetRecord', { className: 'KfArtifact', objectId: rows[0].objectId, requestId: 'workshop-auth-exact-read-20261108-r1' }, headers);
  103. must(read.http === 200 && read.body.result?.ok === true && read.body.result.data?.record?.objectId === rows[0].objectId, 'EXACT_READ_FAILED');
  104. receipt.exactRead = { http: read.http, ok: true, className: 'KfArtifact', objectId: rows[0].objectId };
  105. }
  106. receipt.serverAuthorizationRestored = true;
  107. receipt.managementMachineRestrictedTokenVerified = true;
  108. receipt.finishedAt = new Date().toISOString();
  109. save();
  110. console.log(JSON.stringify(receipt, null, 2));
  111. } catch (error) {
  112. receipt.error = { code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'RECOVERY_CHECK_FAILED' };
  113. receipt.stoppedAt = new Date().toISOString();
  114. save();
  115. throw error;
  116. } finally {
  117. delete config.KSF_SERVICE_TOKEN;
  118. }
  119. }
  120. run().catch(error => {
  121. console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'AUTH_RECOVERY_STOPPED', message: '授权修复停止;核对脱敏回执,不输出配置或原始异常。' }));
  122. process.exitCode = 1;
  123. });