const assert = require('node:assert/strict'); const { DatabaseSync } = require('node:sqlite'); const { test } = require('node:test'); const { PluginPackageSecretBindingConflictError, PluginPackageSecretBindingUnavailableError, createPluginPackageSecretBinding, } = require('@qinglong/runtime-core/plugin-package-secret-binding'); const { createPluginPackageResourceGeneration, } = require('@qinglong/runtime-core/plugin-package-resource-generation'); const { createSecretRef } = require('@qinglong/runtime-core/secret-reference'); const { createPluginPackageSecretBindingTransitionPlan, } = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan'); const { createPluginPackageSecretBindingTransitionReceipt, } = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt'); const { LocalSqlitePluginPackageSecretBindingRepository, } = require('../dist/plugin-package/secret-binding/repository'); const { LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository, } = require('../dist/plugin-package/secret-binding/transitionReceiptRepository'); const { migrateLocalSqliteDatabase } = require('../dist/migration/migration'); const LOCK_DIGEST = 'a'.repeat(64); const MANIFEST = { apiVersion: 'qinglong.io/v1alpha1', kind: 'Package', metadata: { name: 'example-monitor', displayName: 'Example Monitor', version: '1.0.0', description: 'Secret binding repository fixture', license: 'Apache-2.0', }, spec: { compatibility: { qinglong: '>=3.0.0-0 <4.0.0', architectures: ['arm64'], deploymentProfiles: ['edge'], }, runtimes: [], resources: { memory: { recommended: '32Mi' }, disk: { install: '4Mi', working: '8Mi' }, }, permissions: { network: { allowedHosts: [] }, secrets: [{ name: 'TOKEN', required: true }], tools: [], }, contents: { tasks: [], workflows: [], prompts: [], tools: [] }, }, }; function fixture(boundAtMs = 100) { const generation = createPluginPackageResourceGeneration({ installationId: 'install-1', projectId: 'project-1', packageName: 'example-monitor', lockDigest: LOCK_DIGEST, generation: 1, previousActiveLockDigest: null, contentDigest: 'b'.repeat(64), contents: MANIFEST.spec.contents, }); const binding = createPluginPackageSecretBinding({ generation, manifest: MANIFEST, assignments: [ { name: 'TOKEN', secretRef: createSecretRef({ projectId: 'project-1', name: 'runtime-token', version: 2, }), }, ], authority: { kind: 'local-owner-confirmation', evidenceDigest: 'c'.repeat(64), }, boundAtMs, }); return { binding, generation }; } async function harness(state = 'active') { const client = new DatabaseSync(':memory:'); client.exec('PRAGMA foreign_keys = ON'); await migrateLocalSqliteDatabase(client); const isActive = state === 'active'; const isStaged = state === 'staged'; const source = fixture(); const previousLockDigest = 'f'.repeat(64); const generation = isStaged ? createPluginPackageResourceGeneration({ installationId: 'install-2', projectId: 'project-1', packageName: 'example-monitor', lockDigest: LOCK_DIGEST, generation: 2, previousActiveLockDigest: previousLockDigest, contentDigest: 'b'.repeat(64), contents: MANIFEST.spec.contents, }) : source.generation; const binding = isStaged ? createPluginPackageSecretBinding({ generation, manifest: MANIFEST, assignments: source.binding.entries.map(({ name, secretRef }) => ({ name, secretRef, })), authority: source.binding.authority, boundAtMs: source.binding.boundAtMs, }) : source.binding; client .prepare( `INSERT INTO "QingLong3Projects" (id, name, slug, status, version, created_at_ms, updated_at_ms) VALUES ('project-1', 'Project 1', 'project-1', 'active', 1, 1, 1)`, ) .run(); const recordDigest = 'd'.repeat(64); const lockJson = JSON.stringify({ lockDigest: LOCK_DIGEST, projectId: 'project-1', packageName: 'example-monitor', manifestDigest: binding.target.manifestDigest, }); const recordJson = JSON.stringify({ installationId: isStaged ? 'install-2' : 'install-1', projectId: 'project-1', packageName: 'example-monitor', lockDigest: LOCK_DIGEST, state, version: 1, recordDigest, }); client .prepare( `INSERT INTO "QingLong3PluginPackageInstalls" ( installation_id, project_id, package_name, package_version, operation, lock_digest, target_generation, previous_active_lock_digest, active_lock_digest, state, version, last_mutation_id, last_mutation_digest, lock_json, record_json, record_digest, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, '1.0.0', 'install', ?, ?, ?, ?, ?, 1, 'mutation-1', ?, ?, ?, ?, 1, 1)`, ) .run( isStaged ? 'install-2' : 'install-1', 'project-1', 'example-monitor', LOCK_DIGEST, generation.generation, isStaged ? previousLockDigest : null, isActive ? LOCK_DIGEST : isStaged ? previousLockDigest : null, state, 'e'.repeat(64), lockJson, recordJson, recordDigest, ); client .prepare( `INSERT INTO "QingLong3PluginPackageInstallHeads" (project_id, package_name, installation_id) VALUES ('project-1', 'example-monitor', ?)`, ) .run(isStaged ? 'install-2' : 'install-1'); if (isStaged) { const previousRecordDigest = '9'.repeat(64); const previousLockJson = JSON.stringify({ lockDigest: previousLockDigest, projectId: 'project-1', packageName: 'example-monitor', manifestDigest: '8'.repeat(64), }); const previousRecordJson = JSON.stringify({ installationId: 'install-1', projectId: 'project-1', packageName: 'example-monitor', lockDigest: previousLockDigest, state: 'active', version: 1, recordDigest: previousRecordDigest, }); client .prepare( `INSERT INTO "QingLong3PluginPackageInstalls" ( installation_id, project_id, package_name, package_version, operation, lock_digest, target_generation, previous_active_lock_digest, active_lock_digest, state, version, last_mutation_id, last_mutation_digest, lock_json, record_json, record_digest, created_at_ms, updated_at_ms ) VALUES ('install-1', 'project-1', 'example-monitor', '0.9.0', 'install', ?, 1, NULL, ?, 'active', 1, 'mutation-previous', ?, ?, ?, ?, 0, 0)`, ) .run( previousLockDigest, previousLockDigest, '7'.repeat(64), previousLockJson, previousRecordJson, previousRecordDigest, ); } return { client, binding, generation, repository: new LocalSqlitePluginPackageSecretBindingRepository(client), }; } test('publishes and exact-replays one active generation binding', async (t) => { const value = await harness(); t.after(() => value.client.close()); const created = await value.repository.publish(value.binding); assert.equal(created.status, 'created'); assert.deepEqual(created.binding, value.binding); const replay = await value.repository.publish(value.binding); assert.equal(replay.status, 'existing'); assert.deepEqual( await value.repository.find(value.generation.generationDigest), value.binding, ); }); test('rejects inactive targets and conflicting content', async (t) => { const inactive = await harness('failed'); t.after(() => inactive.client.close()); await assert.rejects( inactive.repository.publish(inactive.binding), PluginPackageSecretBindingConflictError, ); const active = await harness(); t.after(() => active.client.close()); await active.repository.publish(active.binding); await assert.rejects( active.repository.publish(fixture(101).binding), PluginPackageSecretBindingConflictError, ); }); test('publishes a reviewed current staged generation but rejects post-stage states', async (t) => { const staged = await harness('staged'); t.after(() => staged.client.close()); assert.equal( (await staged.repository.publish(staged.binding)).status, 'created', ); for (const state of ['queued', 'activating']) { const rejected = await harness(state); t.after(() => rejected.client.close()); await assert.rejects( rejected.repository.publish(rejected.binding), PluginPackageSecretBindingConflictError, ); } }); test('persists one exact staged transition receipt and rejects durable drift', async (t) => { const value = await harness('staged'); t.after(() => value.client.close()); const previousGeneration = createPluginPackageResourceGeneration({ installationId: 'install-1', projectId: 'project-1', packageName: 'example-monitor', lockDigest: 'f'.repeat(64), generation: 1, previousActiveLockDigest: null, contentDigest: '9'.repeat(64), contents: MANIFEST.spec.contents, }); const previousBinding = createPluginPackageSecretBinding({ generation: previousGeneration, manifest: MANIFEST, assignments: value.binding.entries.map(({ name, secretRef }) => ({ name, secretRef, })), authority: value.binding.authority, boundAtMs: 90, }); value.client .prepare( `UPDATE "QingLong3PluginPackageInstalls" SET lock_json = json_set(lock_json, '$.manifestDigest', ?) WHERE installation_id = 'install-1'`, ) .run(previousBinding.target.manifestDigest); value.client .prepare( `UPDATE "QingLong3PluginPackageInstalls" SET lock_json = json_set(lock_json, '$.manifestDigest', ?) WHERE installation_id = 'install-2'`, ) .run(value.binding.target.manifestDigest); value.client .prepare( `UPDATE "QingLong3PluginPackageInstallHeads" SET installation_id = 'install-1' WHERE project_id = 'project-1' AND package_name = 'example-monitor'`, ) .run(); await value.repository.publish(previousBinding); value.client .prepare( `UPDATE "QingLong3PluginPackageInstallHeads" SET installation_id = 'install-2' WHERE project_id = 'project-1' AND package_name = 'example-monitor'`, ) .run(); await value.repository.publish(value.binding); const plan = createPluginPackageSecretBindingTransitionPlan({ previousTarget: previousBinding.target, previousBinding, previousAttemptGeneration: 1, nextGeneration: value.generation, nextManifest: MANIFEST, assignments: value.binding.entries.map(({ name, secretRef }) => ({ name, secretRef, })), plannedAtMs: 95, }); const receipt = createPluginPackageSecretBindingTransitionReceipt({ transitionPlan: plan, authority: value.binding.authority, binding: value.binding, committedAtMs: value.binding.boundAtMs, }); const receipts = new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository( value.client, ); assert.equal(receipts.publishInTransaction(receipt).status, 'created'); assert.equal(receipts.publishInTransaction(receipt).status, 'existing'); assert.deepEqual( await receipts.find(value.generation.generationDigest), receipt, ); value.client.exec('PRAGMA ignore_check_constraints = ON'); value.client .prepare( `UPDATE "QingLong3PluginPackageSecretBindingTransitionReceipts" SET receipt_json = json_set(receipt_json, '$.committedAtMs', 999) WHERE generation_digest = ?`, ) .run(value.generation.generationDigest); await assert.rejects( receipts.find(value.generation.generationDigest), PluginPackageSecretBindingUnavailableError, ); }); test('fails closed when durable binding JSON is changed in place', async (t) => { const value = await harness(); t.after(() => value.client.close()); await value.repository.publish(value.binding); value.client.exec('PRAGMA ignore_check_constraints = ON'); value.client .prepare( `UPDATE "QingLong3PluginPackageSecretBindings" SET binding_json = json_set(binding_json, '$.boundAtMs', 999) WHERE generation_digest = ?`, ) .run(value.generation.generationDigest); await assert.rejects( value.repository.find(value.generation.generationDigest), PluginPackageSecretBindingUnavailableError, ); }); test('publishes storage only through the explicit subpath', () => { assert.equal( require('@qinglong/local-sqlite/plugin-package-secret-binding') .LocalSqlitePluginPackageSecretBindingRepository, LocalSqlitePluginPackageSecretBindingRepository, ); assert.equal( require('../dist').LocalSqlitePluginPackageSecretBindingRepository, undefined, ); });