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-longterm-authorization-20261108-r3'); const EXPECTED_ID = 'MqKRUv9JQc'; const BEFORE_EXPIRY = '2026-11-15T01:48:23.854Z'; const LONGTERM_EXPIRY = '9999-12-31T00:00:00.000Z'; const SCOPES = ['source:write', 'ingest:write', 'data:read', 'report:ingest', 'report:publish', 'noise:write', 'noise:read']; const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); const must = (value, code) => { if (!value) throw Object.assign(new Error(code), { code }); }; const expiry = row => row.expiresAt?.iso || row.expiresAt; const safe = row => ({ objectId: row.objectId, enabled: row.enabled, expiresAt: expiry(row), scopes: row.scopes, projectIds: row.projectIds, updatedAt: row.updatedAt }); async function main() { const apply = process.argv[2] === '--apply'; must(process.argv.length <= 3 && (!process.argv[2] || apply), 'USAGE'); const env = {}; 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) env[m[1]] = m[2].trim(); } must(env.KSF_SERVICE_TOKEN && env.KSF_FUNCTIONS_URL === functionsUrl && env.KSF_PARSE_APP_ID === appId, 'SERVICE_CONFIG_MISMATCH'); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': env.KSF_SERVICE_TOKEN }; const where = { tokenHash: sha(env.KSF_SERVICE_TOKEN) }; const getRow = async () => { const rows = (await request('GET', '/classes/KsfServiceCredential?' + new URLSearchParams({ where: JSON.stringify(where), limit: '2' }))).results; must(rows?.length === 1 && rows[0].objectId === EXPECTED_ID && rows[0].enabled === true, 'CREDENTIAL_IDENTITY_MISMATCH'); must(JSON.stringify(rows[0].scopes) === JSON.stringify(SCOPES) && JSON.stringify(rows[0].projectIds) === '["*"]', 'CREDENTIAL_SCOPE_CHANGED'); return rows[0]; }; const before = await getRow(); fs.mkdirSync(OUT, { recursive: true }); const receiptFile = path.join(OUT, 'server-longterm-receipt.json'); const planFile = path.join(OUT, 'longterm-plan.json'); if (expiry(before) === LONGTERM_EXPIRY) { must(fs.existsSync(receiptFile), 'LONGTERM_STATE_REQUIRES_PRIOR_RECEIPT'); const probe = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-recheck-20261108-r3' }, headers); must(probe.http === 200 && probe.body.result?.ok === true, 'LONGTERM_AUTH_RECHECK_FAILED'); console.log(JSON.stringify({ ok: true, alreadyApplied: true, expiresAt: expiry(before), authorizationRecordWrites: 0 })); return; } must(expiry(before) === BEFORE_EXPIRY, 'UNKNOWN_AUTHORIZATION_VERSION'); const pre = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-before-20261108-r3' }, headers); must(pre.http === 200 && pre.body.result?.ok === true, 'EXISTING_AUTHORIZATION_NOT_WORKING'); if (!apply) { const plan = { schema: 'ksf.workshop.longterm-authorization-plan.v1', plannedAt: new Date().toISOString(), before: safe(before), beforeFingerprint: sha(JSON.stringify(before)), expiresAt: LONGTERM_EXPIRY, authorizationMode: 'long-term-with-explicit-expiry', fieldsToChange: ['expiresAt'], userRequest: '不能永久吗', task: 'Replace weekly renewal with long-lived, revocable authorization', serviceIdentityPreserved: true, scopesPreserved: true, tokenPreserved: true, revocable: true, authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0 }; fs.writeFileSync(planFile, JSON.stringify(plan, null, 2) + '\n'); console.log(JSON.stringify(plan, null, 2)); return; } must(fs.existsSync(planFile), 'LONGTERM_PLAN_REQUIRED'); const plan = JSON.parse(fs.readFileSync(planFile)); must(plan.beforeFingerprint === sha(JSON.stringify(before)) && Date.now() - Date.parse(plan.plannedAt) < 3600000, 'STALE_OR_CONCURRENT_PLAN'); const receipt = { schema: 'ksf.workshop.longterm-authorization-receipt.v1', startedAt: new Date().toISOString(), before: safe(before), requestedExpiresAt: LONGTERM_EXPIRY, authorizationMode: 'long-term-with-explicit-expiry', revocable: true, authorizationRecordWrites: 0, businessRecordWrites: 0, paidCalls: 0, serverLongtermAuthorizationVerified: false, agentLongtermEnvelopeVerified: false, originalAgentAuthorizationStillWorksUntil: BEFORE_EXPIRY }; const save = () => fs.writeFileSync(receiptFile, JSON.stringify(receipt, null, 2) + '\n'); save(); try { receipt.mutationAttempted = true; save(); await request('PUT', '/classes/KsfServiceCredential/' + EXPECTED_ID, { expiresAt: { __type: 'Date', iso: LONGTERM_EXPIRY } }); receipt.authorizationRecordWrites = 1; const after = await getRow(); must(expiry(after) === LONGTERM_EXPIRY && after.tokenHash === before.tokenHash, 'LONGTERM_READBACK_MISMATCH'); receipt.after = safe(after); receipt.serviceIdentityPreserved = after.objectId === before.objectId; receipt.tokenPreserved = true; receipt.scopesPreserved = true; const post = await invoke('ksfGetIngestContract', { requestId: 'longterm-auth-after-20261108-r3' }, headers); must(post.http === 200 && post.body.result?.ok === true, 'LONGTERM_AUTH_PROBE_FAILED'); receipt.contract = { http: post.http, ok: true, allowedClassCount: post.body.result.data?.allowedClasses?.length }; receipt.serverLongtermAuthorizationVerified = 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 : 'LONGTERM_SWITCH_CHECK_FAILED' }; save(); throw error; } finally { delete env.KSF_SERVICE_TOKEN; } } main().catch(error => { console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'LONGTERM_AUTHORIZATION_STOPPED', message: '长期授权调整停止;保留原状态,不输出凭据或原始异常。' })); process.exitCode = 1; });