import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { request, invoke, appId, functionsUrl } from '../server/scripts/platform-client.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const OUT = path.join(ROOT, 'work/workshop-auth-recovery-20261108-r1'); const PLAN = path.join(OUT, 'renewal-plan.json'); const RECEIPT = path.join(OUT, 'server-renewal-receipt.json'); const EXPECTED_ID = 'MqKRUv9JQc'; const ORIGINAL_EXPIRY = '2026-11-06T10:42:23.873Z'; const EXPECTED_SCOPES = ['source:write', 'ingest:write', 'data:read', 'report:ingest', 'report:publish', 'noise:write', 'noise:read']; const sha = value => crypto.createHash('sha256').update(value).digest('hex'); const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b); const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); }; const expiryOf = row => typeof row.expiresAt === 'string' ? row.expiresAt : row.expiresAt?.iso; const safeRow = row => ({ objectId: row.objectId, enabled: row.enabled, expiresAt: expiryOf(row), scopes: row.scopes, projectIds: row.projectIds, updatedAt: row.updatedAt }); function readServiceConfig() { const config = {}; for (const line of fs.readFileSync('F:/文档/康师傅数字人受限连接.env', 'utf8').split(/\r?\n/)) { const m = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/); if (m) config[m[1]] = m[2].trim(); } must(config.KSF_SERVICE_TOKEN && config.KSF_FUNCTIONS_URL === functionsUrl && config.KSF_PARSE_APP_ID === appId, 'SERVICE_CONFIG_MISMATCH'); return config; } async function readCredential(tokenHash) { const q = new URLSearchParams({ where: JSON.stringify({ tokenHash }), limit: '2' }); const rows = (await request('GET', '/classes/KsfServiceCredential?' + q)).results; must(rows?.length === 1 && rows[0].objectId === EXPECTED_ID, 'CREDENTIAL_IDENTITY_MISMATCH'); const row = rows[0]; must(row.enabled === true && row.tokenHash === tokenHash, 'CREDENTIAL_NOT_ENABLED'); must(equal(row.scopes, EXPECTED_SCOPES) && equal(row.projectIds, ['*']), 'CREDENTIAL_SCOPE_CHANGED'); return row; } async function run() { must(process.argv.length <= 3 && (!process.argv[2] || process.argv[2] === '--apply'), 'USAGE'); fs.mkdirSync(OUT, { recursive: true }); const config = readServiceConfig(); const tokenHash = sha(config.KSF_SERVICE_TOKEN); const headers = { 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': config.KSF_SERVICE_TOKEN, 'Content-Type': 'application/json' }; const before = await readCredential(tokenHash); const existingReceipt = fs.existsSync(RECEIPT) ? JSON.parse(fs.readFileSync(RECEIPT, 'utf8')) : null; if (existingReceipt?.serverAuthorizationRestored && expiryOf(before) === existingReceipt.after.expiresAt) { console.log(JSON.stringify({ ok: true, alreadyApplied: true, receipt: RECEIPT, expiresAt: expiryOf(before), authorizationRecordWrites: 0 })); return; } must(expiryOf(before) === ORIGINAL_EXPIRY, 'UNKNOWN_CREDENTIAL_EXPIRY'); const pre = await invoke('ksfGetIngestContract', { requestId: 'workshop-auth-before-20261108-r1' }, headers); must(pre.http === 401 && pre.body.result?.errors?.some(e => e.code === 'AUTH_REQUIRED'), 'EXPIRY_FAILURE_NOT_REPRODUCED'); if (process.argv[2] !== '--apply') { const plan = { schema: 'ksf.workshop.authorization-renewal-plan.v1', plannedAt: new Date().toISOString(), dryRun: true, before: safeRow(before), beforeFingerprint: sha(JSON.stringify(before)), newExpiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(), fieldsToChange: ['expiresAt'], ownerIdentityPreserved: true, tokenPreserved: true, scopesPreserved: true, preProbe: { http: pre.http, errorCodes: pre.body.result.errors.map(e => e.code) }, authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0, }; fs.writeFileSync(PLAN, JSON.stringify(plan, null, 2) + '\n'); console.log(JSON.stringify(plan, null, 2)); return; } must(fs.existsSync(PLAN), 'REVIEWABLE_PLAN_REQUIRED'); const plan = JSON.parse(fs.readFileSync(PLAN, 'utf8')); must(plan.beforeFingerprint === sha(JSON.stringify(before)), 'CONCURRENT_CREDENTIAL_CHANGE'); must(Date.now() - Date.parse(plan.plannedAt) < 3600000 && Date.parse(plan.newExpiresAt) > Date.now(), 'RENEWAL_PLAN_EXPIRED'); const receipt = { schema: 'ksf.workshop.authorization-renewal-receipt.v1', startedAt: new Date().toISOString(), before: safeRow(before), requestedExpiresAt: plan.newExpiresAt, mutationAttempted: false, serverAuthorizationRestored: false, agentEncryptedEntryVerified: false, actualBusinessWriteVerified: false, authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0, }; const save = () => fs.writeFileSync(RECEIPT, JSON.stringify(receipt, null, 2) + '\n'); save(); try { receipt.mutationAttempted = true; save(); await request('PUT', '/classes/KsfServiceCredential/' + EXPECTED_ID, { expiresAt: { __type: 'Date', iso: plan.newExpiresAt } }); receipt.authorizationRecordWrites = 1; const after = await readCredential(tokenHash); receipt.after = safeRow(after); must(expiryOf(after) === plan.newExpiresAt, 'EXPIRY_READBACK_MISMATCH'); receipt.ownerIdentityPreserved = after.objectId === before.objectId; receipt.tokenPreserved = after.tokenHash === before.tokenHash; receipt.scopesPreserved = equal(after.scopes, before.scopes) && equal(after.projectIds, before.projectIds); const contract = await invoke('ksfGetIngestContract', { requestId: 'workshop-auth-after-20261108-r1' }, headers); const data = contract.body.result?.data; must(contract.http === 200 && contract.body.result?.ok === true && data?.allowedClasses?.length > 0, 'RESTRICTED_CONTRACT_FAILED'); receipt.contract = { http: contract.http, ok: true, schemaVersion: data.schemaVersion, contractRevision: data.contractRevision, allowedClassCount: data.allowedClasses.length, workshop: data.workshop, reportConfirmation: data.capabilities?.reportConfirmation, reportPublication: data.capabilities?.reportPublication }; const dictionary = await invoke('ksfGetDataDictionary', { summary: true, requestId: 'workshop-auth-schema-20261108-r1' }, headers); must(dictionary.http === 200 && dictionary.body.result?.ok === true, 'RESTRICTED_SCHEMA_FAILED'); receipt.schemaProbe = { http: dictionary.http, ok: true, tableCount: dictionary.body.result.data?.tableCount, fieldCount: dictionary.body.result.data?.fieldCount }; const search = await invoke('ksfSearchRecords', { className: 'KfArtifact', filters: { dataMode: 'manual' }, limit: 1, requestId: 'workshop-auth-read-20261108-r1' }, headers); must(search.http === 200 && search.body.result?.ok === true, 'BUSINESS_READ_FAILED'); const searchData = search.body.result.data; const rows = searchData.records || searchData.results || searchData.items || []; receipt.businessRead = { http: search.http, ok: true, requestedClass: 'KfArtifact', dataMode: 'manual', returnedRows: rows.length }; if (rows[0]?.objectId) { const read = await invoke('ksfGetRecord', { className: 'KfArtifact', objectId: rows[0].objectId, requestId: 'workshop-auth-exact-read-20261108-r1' }, headers); must(read.http === 200 && read.body.result?.ok === true && read.body.result.data?.record?.objectId === rows[0].objectId, 'EXACT_READ_FAILED'); receipt.exactRead = { http: read.http, ok: true, className: 'KfArtifact', objectId: rows[0].objectId }; } receipt.serverAuthorizationRestored = true; receipt.managementMachineRestrictedTokenVerified = true; receipt.finishedAt = new Date().toISOString(); save(); console.log(JSON.stringify(receipt, null, 2)); } catch (error) { receipt.error = { code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'RECOVERY_CHECK_FAILED' }; receipt.stoppedAt = new Date().toISOString(); save(); throw error; } finally { delete config.KSF_SERVICE_TOKEN; } } run().catch(error => { console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'AUTH_RECOVERY_STOPPED', message: '授权修复停止;核对脱敏回执,不输出配置或原始异常。' })); process.exitCode = 1; });