set-workshop-longterm-authorization.mjs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  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-longterm-authorization-20261108-r3');
  8. const EXPECTED_ID = 'MqKRUv9JQc';
  9. const BEFORE_EXPIRY = '2026-11-15T01:48:23.854Z';
  10. const LONGTERM_EXPIRY = '9999-12-31T00:00:00.000Z';
  11. const SCOPES = ['source:write', 'ingest:write', 'data:read', 'report:ingest', 'report:publish', 'noise:write', 'noise:read'];
  12. const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
  13. const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); };
  14. const expiry = row => row.expiresAt?.iso || row.expiresAt;
  15. const safe = row => ({ objectId: row.objectId, enabled: row.enabled, expiresAt: expiry(row), scopes: row.scopes, projectIds: row.projectIds, updatedAt: row.updatedAt });
  16. async function main() {
  17. const apply = process.argv[2] === '--apply';
  18. must(process.argv.length <= 3 && (!process.argv[2] || apply), 'USAGE');
  19. const env = {};
  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) env[m[1]] = m[2].trim();
  23. }
  24. must(env.KSF_SERVICE_TOKEN && env.KSF_FUNCTIONS_URL === functionsUrl && env.KSF_PARSE_APP_ID === appId, 'SERVICE_CONFIG_MISMATCH');
  25. const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': env.KSF_SERVICE_TOKEN };
  26. const where = { tokenHash: sha(env.KSF_SERVICE_TOKEN) };
  27. const getRow = async () => {
  28. const rows = (await request('GET', '/classes/KsfServiceCredential?' + new URLSearchParams({ where: JSON.stringify(where), limit: '2' }))).results;
  29. must(rows?.length === 1 && rows[0].objectId === EXPECTED_ID && rows[0].enabled === true, 'CREDENTIAL_IDENTITY_MISMATCH');
  30. must(JSON.stringify(rows[0].scopes) === JSON.stringify(SCOPES) && JSON.stringify(rows[0].projectIds) === '["*"]', 'CREDENTIAL_SCOPE_CHANGED');
  31. return rows[0];
  32. };
  33. const before = await getRow();
  34. fs.mkdirSync(OUT, { recursive: true });
  35. const receiptFile = path.join(OUT, 'server-longterm-receipt.json');
  36. const planFile = path.join(OUT, 'longterm-plan.json');
  37. if (expiry(before) === LONGTERM_EXPIRY) {
  38. must(fs.existsSync(receiptFile), 'LONGTERM_STATE_REQUIRES_PRIOR_RECEIPT');
  39. const probe = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-recheck-20261108-r3' }, headers);
  40. must(probe.http === 200 && probe.body.result?.ok === true, 'LONGTERM_AUTH_RECHECK_FAILED');
  41. console.log(JSON.stringify({ ok: true, alreadyApplied: true, expiresAt: expiry(before), authorizationRecordWrites: 0 }));
  42. return;
  43. }
  44. must(expiry(before) === BEFORE_EXPIRY, 'UNKNOWN_AUTHORIZATION_VERSION');
  45. const pre = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-before-20261108-r3' }, headers);
  46. must(pre.http === 200 && pre.body.result?.ok === true, 'EXISTING_AUTHORIZATION_NOT_WORKING');
  47. if (!apply) {
  48. const plan = { schema: 'ksf.workshop.longterm-authorization-plan.v1', plannedAt: new Date().toISOString(),
  49. before: safe(before), beforeFingerprint: sha(JSON.stringify(before)), expiresAt: LONGTERM_EXPIRY,
  50. authorizationMode: 'long-term-with-explicit-expiry', fieldsToChange: ['expiresAt'],
  51. userRequest: '不能永久吗', task: 'Replace weekly renewal with long-lived, revocable authorization',
  52. serviceIdentityPreserved: true, scopesPreserved: true, tokenPreserved: true, revocable: true,
  53. authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0 };
  54. fs.writeFileSync(planFile, JSON.stringify(plan, null, 2) + '\n');
  55. console.log(JSON.stringify(plan, null, 2)); return;
  56. }
  57. must(fs.existsSync(planFile), 'LONGTERM_PLAN_REQUIRED');
  58. const plan = JSON.parse(fs.readFileSync(planFile));
  59. must(plan.beforeFingerprint === sha(JSON.stringify(before)) && Date.now() - Date.parse(plan.plannedAt) < 3600000, 'STALE_OR_CONCURRENT_PLAN');
  60. const receipt = { schema: 'ksf.workshop.longterm-authorization-receipt.v1', startedAt: new Date().toISOString(), before: safe(before),
  61. requestedExpiresAt: LONGTERM_EXPIRY, authorizationMode: 'long-term-with-explicit-expiry', revocable: true,
  62. authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0,
  63. serverLongtermAuthorizationVerified: false, agentLongtermEnvelopeVerified: false,
  64. originalAgentAuthorizationStillWorksUntil: BEFORE_EXPIRY };
  65. const save = () => fs.writeFileSync(receiptFile, JSON.stringify(receipt, null, 2) + '\n');
  66. save();
  67. try {
  68. receipt.mutationAttempted = true; save();
  69. await request('PUT', '/classes/KsfServiceCredential/' + EXPECTED_ID, { expiresAt: { __type: 'Date', iso: LONGTERM_EXPIRY } });
  70. receipt.authorizationRecordWrites = 1;
  71. const after = await getRow();
  72. must(expiry(after) === LONGTERM_EXPIRY && after.tokenHash === before.tokenHash, 'LONGTERM_READBACK_MISMATCH');
  73. receipt.after = safe(after); receipt.serviceIdentityPreserved = after.objectId === before.objectId; receipt.tokenPreserved = true; receipt.scopesPreserved = true;
  74. const post = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-after-20261108-r3' }, headers);
  75. must(post.http === 200 && post.body.result?.ok === true, 'LONGTERM_AUTH_PROBE_FAILED');
  76. receipt.contract = { http: post.http, ok: true, allowedClassCount: post.body.result.data?.allowedClasses?.length };
  77. receipt.serverLongtermAuthorizationVerified = true; receipt.finishedAt = new Date().toISOString(); save();
  78. console.log(JSON.stringify(receipt, null, 2));
  79. } catch (error) {
  80. receipt.error = { code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'LONGTERM_SWITCH_CHECK_FAILED' }; save(); throw error;
  81. } finally { delete env.KSF_SERVICE_TOKEN; }
  82. }
  83. main().catch(error => { console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'LONGTERM_AUTHORIZATION_STOPPED',
  84. message: '长期授权调整停止;保留原状态,不输出凭据或原始异常。' })); process.exitCode = 1; });