feat(local): publish secret config application atomically

This commit is contained in:
whyour
2026-08-24 00:11:48 +08:00
parent e261d4130d
commit f822e92b02
25 changed files with 2149 additions and 58 deletions
+5
View File
@@ -90,6 +90,11 @@
"require": "./dist/adoption/data-directory/applicationCommit.js",
"default": "./dist/adoption/data-directory/applicationCommit.js"
},
"./secret-config-application": {
"types": "./dist/adoption/secret-config/secretConfigApplicationDatabase.d.ts",
"require": "./dist/adoption/secret-config/secretConfigApplicationDatabase.js",
"default": "./dist/adoption/secret-config/secretConfigApplicationDatabase.js"
},
"./plugin-package-install": {
"types": "./dist/plugin-package/pluginPackageInstallRepository.d.ts",
"require": "./dist/plugin-package/pluginPackageInstallRepository.js",
@@ -112,6 +112,8 @@ import { local0099LegacyDataDirectoryAdoptionsMigration } from '../migrations/00
import { local0100CapabilityV50Migration } from '../migrations/0100-capability-v50';
import { local0101LegacyAdoptionProvenanceMigration } from '../migrations/0101-legacy-adoption-provenance';
import { local0102CapabilityV51Migration } from '../migrations/0102-capability-v51';
import { local0103SecretConfigApplicationsMigration } from '../migrations/0103-secret-config-applications';
import { local0104CapabilityV52Migration } from '../migrations/0104-capability-v52';
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
import {
LOCAL_SQLITE_MIGRATION_STREAM_ID,
@@ -236,6 +238,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
local0100CapabilityV50Migration,
local0101LegacyAdoptionProvenanceMigration,
local0102CapabilityV51Migration,
local0103SecretConfigApplicationsMigration,
local0104CapabilityV52Migration,
]),
});
@@ -522,5 +522,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
checksum:
'539f9d60b41b7cfebb203888804329241c178b1ee0105d14949750856260ac1f',
}),
Object.freeze({
id: '0103-secret-config-applications',
checksum:
'd94aea3a7fc7bdd72cb4f0a6ae4807e12b17d90cd16e242f7d813dfb67a1630e',
}),
Object.freeze({
id: '0104-capability-v52',
checksum:
'6eb1dab3a49d0075a1e5a18aa90df4e8013af2c006dd2e97a78e8a644438e94d',
}),
]),
});
@@ -0,0 +1,254 @@
import { defineLocalSqliteMigration } from './sqlMigration';
export const local0103SecretConfigApplicationsMigration =
defineLocalSqliteMigration({
id: '0103-secret-config-applications',
statements: [
`
CREATE TABLE "QingLong3SecretConfigApplications" (
"mutation_id" TEXT PRIMARY KEY NOT NULL,
"project_id" TEXT NOT NULL,
"profile" TEXT NOT NULL,
"secret_config_plan_digest" TEXT NOT NULL,
"decision_digest" TEXT NOT NULL,
"candidate_set_digest" TEXT NOT NULL,
"automation_adoption_set_digest" TEXT NOT NULL,
"active_binding_count" INTEGER NOT NULL,
"disabled_preservation_count" INTEGER NOT NULL,
"task_count" INTEGER NOT NULL,
"trigger_count" INTEGER NOT NULL,
"publication_digest" TEXT NOT NULL,
"audit_event_id" TEXT NOT NULL,
"applied_at_ms" INTEGER NOT NULL,
"receipt_digest" TEXT NOT NULL,
"receipt_json" TEXT NOT NULL,
CONSTRAINT ql3_secret_config_application_project_fk
FOREIGN KEY ("project_id") REFERENCES "QingLong3Projects" ("id")
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_application_audit_fk
FOREIGN KEY ("audit_event_id")
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_application_identity_check CHECK (
length("mutation_id") = 36 AND
replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND
length("project_id") BETWEEN 1 AND 128 AND
"profile" IN ('edge', 'standalone') AND
"active_binding_count" BETWEEN 0 AND 256 AND
"disabled_preservation_count" BETWEEN 0 AND 512 AND
"task_count" BETWEEN 0 AND 100000 AND
"trigger_count" BETWEEN 0 AND 500000 AND
"applied_at_ms" >= 0
),
CONSTRAINT ql3_secret_config_application_digest_check CHECK (
length("secret_config_plan_digest") = 64 AND
"secret_config_plan_digest" NOT GLOB '*[^0-9a-f]*' AND
length("decision_digest") = 64 AND
"decision_digest" NOT GLOB '*[^0-9a-f]*' AND
length("candidate_set_digest") = 64 AND
"candidate_set_digest" NOT GLOB '*[^0-9a-f]*' AND
length("automation_adoption_set_digest") = 64 AND
"automation_adoption_set_digest" NOT GLOB '*[^0-9a-f]*' AND
length("publication_digest") = 64 AND
"publication_digest" NOT GLOB '*[^0-9a-f]*' AND
length("receipt_digest") = 64 AND
"receipt_digest" NOT GLOB '*[^0-9a-f]*'
)
)
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_applications_plan_uidx"
ON "QingLong3SecretConfigApplications" ("secret_config_plan_digest")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_applications_decision_uidx"
ON "QingLong3SecretConfigApplications" ("decision_digest")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_applications_receipt_uidx"
ON "QingLong3SecretConfigApplications" ("receipt_digest")
`,
`
CREATE INDEX "ql3_secret_config_applications_project_time_idx"
ON "QingLong3SecretConfigApplications" ("project_id", "applied_at_ms")
`,
`
CREATE TABLE "QingLong3SecretConfigApplicationSecrets" (
"application_mutation_id" TEXT NOT NULL,
"ordinal" INTEGER NOT NULL,
"project_id" TEXT NOT NULL,
"disposition" TEXT NOT NULL,
"candidate_digest" TEXT NOT NULL,
"source_set_digest" TEXT NOT NULL,
"environment_name" TEXT,
"secret_name" TEXT NOT NULL,
"secret_version" INTEGER NOT NULL,
"secret_mutation_id" TEXT NOT NULL,
"secret_ref" TEXT NOT NULL,
"item_digest" TEXT NOT NULL,
PRIMARY KEY ("application_mutation_id", "ordinal"),
CONSTRAINT ql3_secret_config_secret_parent_fk
FOREIGN KEY ("application_mutation_id")
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
ON DELETE RESTRICT ON UPDATE RESTRICT
DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT ql3_secret_config_secret_envelope_fk
FOREIGN KEY ("project_id", "secret_name", "secret_version")
REFERENCES "QingLong3LocalSecretEnvelopes" (
"project_id", "secret_name", "version"
) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_secret_identity_check CHECK (
"ordinal" BETWEEN 1 AND 768 AND
"disposition" IN ('active_binding', 'disabled_preservation') AND
(("disposition" = 'active_binding' AND "environment_name" IS NOT NULL) OR
("disposition" = 'disabled_preservation' AND "environment_name" IS NULL)) AND
"secret_version" = 1 AND
length("secret_mutation_id") = 36 AND
replace("secret_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
),
CONSTRAINT ql3_secret_config_secret_digest_check CHECK (
length("candidate_digest") = 64 AND
"candidate_digest" NOT GLOB '*[^0-9a-f]*' AND
length("source_set_digest") = 64 AND
"source_set_digest" NOT GLOB '*[^0-9a-f]*' AND
length("item_digest") = 64 AND
"item_digest" NOT GLOB '*[^0-9a-f]*'
)
)
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_secrets_mutation_uidx"
ON "QingLong3SecretConfigApplicationSecrets" ("secret_mutation_id")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_secrets_candidate_uidx"
ON "QingLong3SecretConfigApplicationSecrets" ("candidate_digest")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_secrets_target_uidx"
ON "QingLong3SecretConfigApplicationSecrets" ("project_id", "secret_name")
`,
`
CREATE TABLE "QingLong3SecretConfigApplicationTasks" (
"application_mutation_id" TEXT NOT NULL,
"ordinal" INTEGER NOT NULL,
"project_id" TEXT NOT NULL,
"adoption_mutation_id" TEXT NOT NULL,
"adoption_row_ordinal" INTEGER NOT NULL,
"task_id" TEXT NOT NULL,
"previous_revision" INTEGER NOT NULL,
"previous_content_digest" TEXT NOT NULL,
"task_revision" INTEGER NOT NULL,
"task_mutation_id" TEXT NOT NULL,
"task_content_digest" TEXT NOT NULL,
"item_digest" TEXT NOT NULL,
PRIMARY KEY ("application_mutation_id", "ordinal"),
CONSTRAINT ql3_secret_config_task_parent_fk
FOREIGN KEY ("application_mutation_id")
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
ON DELETE RESTRICT ON UPDATE RESTRICT
DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT ql3_secret_config_task_adoption_fk
FOREIGN KEY (
"adoption_mutation_id", "adoption_row_ordinal", "project_id",
"task_id", "previous_revision"
) REFERENCES "QingLong3LegacyAdoptionTasks" (
"adoption_mutation_id", "row_ordinal", "project_id", "task_id",
"task_revision"
) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_task_revision_fk
FOREIGN KEY ("project_id", "task_id", "task_revision")
REFERENCES "QingLong3TaskDefinitionRevisions" (
"project_id", "task_id", "revision"
) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_task_identity_check CHECK (
"ordinal" BETWEEN 1 AND 100000 AND
"adoption_row_ordinal" BETWEEN 1 AND 100000 AND
"previous_revision" = 1 AND
"task_revision" = 2 AND
length("task_mutation_id") = 36 AND
replace("task_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
),
CONSTRAINT ql3_secret_config_task_digest_check CHECK (
length("previous_content_digest") = 64 AND
"previous_content_digest" NOT GLOB '*[^0-9a-f]*' AND
length("task_content_digest") = 64 AND
"task_content_digest" NOT GLOB '*[^0-9a-f]*' AND
length("item_digest") = 64 AND
"item_digest" NOT GLOB '*[^0-9a-f]*'
)
)
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_tasks_identity_uidx"
ON "QingLong3SecretConfigApplicationTasks" ("project_id", "task_id")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_tasks_mutation_uidx"
ON "QingLong3SecretConfigApplicationTasks" ("task_mutation_id")
`,
`
CREATE TABLE "QingLong3SecretConfigApplicationTriggers" (
"application_mutation_id" TEXT NOT NULL,
"ordinal" INTEGER NOT NULL,
"project_id" TEXT NOT NULL,
"adoption_mutation_id" TEXT NOT NULL,
"adoption_row_ordinal" INTEGER NOT NULL,
"adoption_trigger_ordinal" INTEGER NOT NULL,
"task_id" TEXT NOT NULL,
"task_revision" INTEGER NOT NULL,
"trigger_id" TEXT NOT NULL,
"previous_revision" INTEGER NOT NULL,
"previous_content_digest" TEXT NOT NULL,
"trigger_revision" INTEGER NOT NULL,
"trigger_mutation_id" TEXT NOT NULL,
"trigger_content_digest" TEXT NOT NULL,
"item_digest" TEXT NOT NULL,
PRIMARY KEY ("application_mutation_id", "ordinal"),
CONSTRAINT ql3_secret_config_trigger_parent_fk
FOREIGN KEY ("application_mutation_id")
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
ON DELETE RESTRICT ON UPDATE RESTRICT
DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT ql3_secret_config_trigger_adoption_fk
FOREIGN KEY (
"adoption_mutation_id", "adoption_row_ordinal",
"adoption_trigger_ordinal"
) REFERENCES "QingLong3LegacyAdoptionTriggers" (
"adoption_mutation_id", "row_ordinal", "trigger_ordinal"
) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_trigger_revision_fk
FOREIGN KEY ("project_id", "trigger_id", "trigger_revision")
REFERENCES "QingLong3TriggerRevisions" (
"project_id", "trigger_id", "revision"
) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_secret_config_trigger_identity_check CHECK (
"ordinal" BETWEEN 1 AND 500000 AND
"adoption_row_ordinal" BETWEEN 1 AND 100000 AND
"adoption_trigger_ordinal" BETWEEN 1 AND 500000 AND
"task_revision" = 2 AND
"previous_revision" = 1 AND
"trigger_revision" = 2 AND
length("trigger_mutation_id") = 36 AND
replace("trigger_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
),
CONSTRAINT ql3_secret_config_trigger_digest_check CHECK (
length("previous_content_digest") = 64 AND
"previous_content_digest" NOT GLOB '*[^0-9a-f]*' AND
length("trigger_content_digest") = 64 AND
"trigger_content_digest" NOT GLOB '*[^0-9a-f]*' AND
length("item_digest") = 64 AND
"item_digest" NOT GLOB '*[^0-9a-f]*'
)
)
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_triggers_identity_uidx"
ON "QingLong3SecretConfigApplicationTriggers" ("project_id", "trigger_id")
`,
`
CREATE UNIQUE INDEX "ql3_secret_config_triggers_mutation_uidx"
ON "QingLong3SecretConfigApplicationTriggers" ("trigger_mutation_id")
`,
],
});
@@ -0,0 +1,14 @@
import { CAPABILITIES_V51 } from './0102-capability-v51';
import { defineLocalSqliteMigration } from './sqlMigration';
export const CAPABILITIES_V52 = CAPABILITIES_V51.replace(
'"legacy_adoption_provenance":1,',
'"legacy_adoption_provenance":1,"secret_config_application":1,',
);
export const local0104CapabilityV52Migration = defineLocalSqliteMigration({
id: '0104-capability-v52',
statements: [
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 52, migration_id = '0103-secret-config-applications', capabilities = '${CAPABILITIES_V52}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 51 AND migration_id = '0101-legacy-adoption-provenance' AND capabilities = '${CAPABILITIES_V51}'`,
],
});
@@ -12,7 +12,7 @@ import {
} from '../run/stepRunSchemaContract';
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
export const LOCAL_SQLITE_CONTRACT_VERSION = 51;
export const LOCAL_SQLITE_CONTRACT_VERSION = 52;
const LEGACY_DATA_DIRECTORY_ADOPTION_TRIGGERS = Object.freeze([
Object.freeze({
@@ -1439,6 +1439,96 @@ const REQUIRED_SCHEMA = Object.freeze({
'ql3_legacy_adoption_triggers_item_uidx',
]),
}),
QingLong3SecretConfigApplications: Object.freeze({
columns: Object.freeze([
'mutation_id',
'project_id',
'profile',
'secret_config_plan_digest',
'decision_digest',
'candidate_set_digest',
'automation_adoption_set_digest',
'active_binding_count',
'disabled_preservation_count',
'task_count',
'trigger_count',
'publication_digest',
'audit_event_id',
'applied_at_ms',
'receipt_digest',
'receipt_json',
]),
indexes: Object.freeze([
'ql3_secret_config_applications_plan_uidx',
'ql3_secret_config_applications_decision_uidx',
'ql3_secret_config_applications_receipt_uidx',
'ql3_secret_config_applications_project_time_idx',
]),
}),
QingLong3SecretConfigApplicationSecrets: Object.freeze({
columns: Object.freeze([
'application_mutation_id',
'ordinal',
'project_id',
'disposition',
'candidate_digest',
'source_set_digest',
'environment_name',
'secret_name',
'secret_version',
'secret_mutation_id',
'secret_ref',
'item_digest',
]),
indexes: Object.freeze([
'ql3_secret_config_secrets_mutation_uidx',
'ql3_secret_config_secrets_candidate_uidx',
'ql3_secret_config_secrets_target_uidx',
]),
}),
QingLong3SecretConfigApplicationTasks: Object.freeze({
columns: Object.freeze([
'application_mutation_id',
'ordinal',
'project_id',
'adoption_mutation_id',
'adoption_row_ordinal',
'task_id',
'previous_revision',
'previous_content_digest',
'task_revision',
'task_mutation_id',
'task_content_digest',
'item_digest',
]),
indexes: Object.freeze([
'ql3_secret_config_tasks_identity_uidx',
'ql3_secret_config_tasks_mutation_uidx',
]),
}),
QingLong3SecretConfigApplicationTriggers: Object.freeze({
columns: Object.freeze([
'application_mutation_id',
'ordinal',
'project_id',
'adoption_mutation_id',
'adoption_row_ordinal',
'adoption_trigger_ordinal',
'task_id',
'task_revision',
'trigger_id',
'previous_revision',
'previous_content_digest',
'trigger_revision',
'trigger_mutation_id',
'trigger_content_digest',
'item_digest',
]),
indexes: Object.freeze([
'ql3_secret_config_triggers_identity_uidx',
'ql3_secret_config_triggers_mutation_uidx',
]),
}),
QingLong3LegacyDataDirectoryAdoptions: Object.freeze({
columns: Object.freeze([
'mutation_id',
@@ -2699,10 +2789,10 @@ export async function auditLocalSqliteReadiness(
!capability ||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
capability.migration_id !== '0101-legacy-adoption-provenance' ||
capability.migration_id !== '0103-secret-config-applications' ||
typeof capability.capabilities !== 'string' ||
capability.capabilities !==
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,"secret_config_application":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
typeof capability.updated_at_ms !== 'number' ||
!Number.isSafeInteger(capability.updated_at_ms) ||
capability.updated_at_ms < 0
@@ -3472,6 +3472,259 @@ export const legacyAdoptionTriggers = sqliteTable(
],
);
export const secretConfigApplications = sqliteTable(
'QingLong3SecretConfigApplications',
{
mutationId: text('mutation_id').primaryKey(),
projectId: text('project_id')
.notNull()
.references(() => localProjects.id, {
onDelete: 'restrict',
onUpdate: 'restrict',
}),
profile: text('profile').notNull(),
secretConfigPlanDigest: text('secret_config_plan_digest').notNull(),
decisionDigest: text('decision_digest').notNull(),
candidateSetDigest: text('candidate_set_digest').notNull(),
automationAdoptionSetDigest: text(
'automation_adoption_set_digest',
).notNull(),
activeBindingCount: integer('active_binding_count').notNull(),
disabledPreservationCount: integer('disabled_preservation_count').notNull(),
taskCount: integer('task_count').notNull(),
triggerCount: integer('trigger_count').notNull(),
publicationDigest: text('publication_digest').notNull(),
auditEventId: text('audit_event_id')
.notNull()
.references(() => localSecurityAuditEvents.eventId, {
onDelete: 'restrict',
onUpdate: 'restrict',
}),
appliedAtMs: integer('applied_at_ms').notNull(),
receiptDigest: text('receipt_digest').notNull(),
receiptJson: text('receipt_json', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull(),
},
(table) => [
check(
'ql3_secret_config_application_identity_check',
sql`length(${table.mutationId}) = 36 and replace(${table.mutationId}, '-', '') not glob '*[^0-9a-f]*' and length(${table.projectId}) between 1 and 128 and ${table.profile} in ('edge', 'standalone') and ${table.activeBindingCount} between 0 and 256 and ${table.disabledPreservationCount} between 0 and 512 and ${table.taskCount} between 0 and 100000 and ${table.triggerCount} between 0 and 500000 and ${table.appliedAtMs} >= 0`,
),
check(
'ql3_secret_config_application_digest_check',
sql`length(${table.secretConfigPlanDigest}) = 64 and ${table.secretConfigPlanDigest} not glob '*[^0-9a-f]*' and length(${table.decisionDigest}) = 64 and ${table.decisionDigest} not glob '*[^0-9a-f]*' and length(${table.candidateSetDigest}) = 64 and ${table.candidateSetDigest} not glob '*[^0-9a-f]*' and length(${table.automationAdoptionSetDigest}) = 64 and ${table.automationAdoptionSetDigest} not glob '*[^0-9a-f]*' and length(${table.publicationDigest}) = 64 and ${table.publicationDigest} not glob '*[^0-9a-f]*' and length(${table.receiptDigest}) = 64 and ${table.receiptDigest} not glob '*[^0-9a-f]*'`,
),
uniqueIndex('ql3_secret_config_applications_plan_uidx').on(
table.secretConfigPlanDigest,
),
uniqueIndex('ql3_secret_config_applications_decision_uidx').on(
table.decisionDigest,
),
uniqueIndex('ql3_secret_config_applications_receipt_uidx').on(
table.receiptDigest,
),
index('ql3_secret_config_applications_project_time_idx').on(
table.projectId,
table.appliedAtMs,
),
],
);
export const secretConfigApplicationSecrets = sqliteTable(
'QingLong3SecretConfigApplicationSecrets',
{
applicationMutationId: text('application_mutation_id').notNull(),
ordinal: integer('ordinal').notNull(),
projectId: text('project_id').notNull(),
disposition: text('disposition').notNull(),
candidateDigest: text('candidate_digest').notNull(),
sourceSetDigest: text('source_set_digest').notNull(),
environmentName: text('environment_name'),
secretName: text('secret_name').notNull(),
secretVersion: integer('secret_version').notNull(),
secretMutationId: text('secret_mutation_id').notNull(),
secretRef: text('secret_ref').notNull(),
itemDigest: text('item_digest').notNull(),
},
(table) => [
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
foreignKey({
columns: [table.applicationMutationId],
foreignColumns: [secretConfigApplications.mutationId],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
columns: [table.projectId, table.secretName, table.secretVersion],
foreignColumns: [
localSecretEnvelopes.projectId,
localSecretEnvelopes.name,
localSecretEnvelopes.version,
],
})
.onDelete('restrict')
.onUpdate('restrict'),
check(
'ql3_secret_config_secret_identity_check',
sql`${table.ordinal} between 1 and 768 and ${table.disposition} in ('active_binding', 'disabled_preservation') and ((${table.disposition} = 'active_binding' and ${table.environmentName} is not null) or (${table.disposition} = 'disabled_preservation' and ${table.environmentName} is null)) and ${table.secretVersion} = 1 and length(${table.secretMutationId}) = 36 and replace(${table.secretMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
),
check(
'ql3_secret_config_secret_digest_check',
sql`length(${table.candidateDigest}) = 64 and ${table.candidateDigest} not glob '*[^0-9a-f]*' and length(${table.sourceSetDigest}) = 64 and ${table.sourceSetDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
),
uniqueIndex('ql3_secret_config_secrets_mutation_uidx').on(
table.secretMutationId,
),
uniqueIndex('ql3_secret_config_secrets_candidate_uidx').on(
table.candidateDigest,
),
uniqueIndex('ql3_secret_config_secrets_target_uidx').on(
table.projectId,
table.secretName,
),
],
);
export const secretConfigApplicationTasks = sqliteTable(
'QingLong3SecretConfigApplicationTasks',
{
applicationMutationId: text('application_mutation_id').notNull(),
ordinal: integer('ordinal').notNull(),
projectId: text('project_id').notNull(),
adoptionMutationId: text('adoption_mutation_id').notNull(),
adoptionRowOrdinal: integer('adoption_row_ordinal').notNull(),
taskId: text('task_id').notNull(),
previousRevision: integer('previous_revision').notNull(),
previousContentDigest: text('previous_content_digest').notNull(),
taskRevision: integer('task_revision').notNull(),
taskMutationId: text('task_mutation_id').notNull(),
taskContentDigest: text('task_content_digest').notNull(),
itemDigest: text('item_digest').notNull(),
},
(table) => [
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
foreignKey({
columns: [table.applicationMutationId],
foreignColumns: [secretConfigApplications.mutationId],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
columns: [
table.adoptionMutationId,
table.adoptionRowOrdinal,
table.projectId,
table.taskId,
table.previousRevision,
],
foreignColumns: [
legacyAdoptionTasks.adoptionMutationId,
legacyAdoptionTasks.rowOrdinal,
legacyAdoptionTasks.projectId,
legacyAdoptionTasks.taskId,
legacyAdoptionTasks.taskRevision,
],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
columns: [table.projectId, table.taskId, table.taskRevision],
foreignColumns: [
taskDefinitionRevisions.projectId,
taskDefinitionRevisions.taskId,
taskDefinitionRevisions.revision,
],
})
.onDelete('restrict')
.onUpdate('restrict'),
check(
'ql3_secret_config_task_identity_check',
sql`${table.ordinal} between 1 and 100000 and ${table.adoptionRowOrdinal} between 1 and 100000 and ${table.previousRevision} = 1 and ${table.taskRevision} = 2 and length(${table.taskMutationId}) = 36 and replace(${table.taskMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
),
check(
'ql3_secret_config_task_digest_check',
sql`length(${table.previousContentDigest}) = 64 and ${table.previousContentDigest} not glob '*[^0-9a-f]*' and length(${table.taskContentDigest}) = 64 and ${table.taskContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
),
uniqueIndex('ql3_secret_config_tasks_identity_uidx').on(
table.projectId,
table.taskId,
),
uniqueIndex('ql3_secret_config_tasks_mutation_uidx').on(
table.taskMutationId,
),
],
);
export const secretConfigApplicationTriggers = sqliteTable(
'QingLong3SecretConfigApplicationTriggers',
{
applicationMutationId: text('application_mutation_id').notNull(),
ordinal: integer('ordinal').notNull(),
projectId: text('project_id').notNull(),
adoptionMutationId: text('adoption_mutation_id').notNull(),
adoptionRowOrdinal: integer('adoption_row_ordinal').notNull(),
adoptionTriggerOrdinal: integer('adoption_trigger_ordinal').notNull(),
taskId: text('task_id').notNull(),
taskRevision: integer('task_revision').notNull(),
triggerId: text('trigger_id').notNull(),
previousRevision: integer('previous_revision').notNull(),
previousContentDigest: text('previous_content_digest').notNull(),
triggerRevision: integer('trigger_revision').notNull(),
triggerMutationId: text('trigger_mutation_id').notNull(),
triggerContentDigest: text('trigger_content_digest').notNull(),
itemDigest: text('item_digest').notNull(),
},
(table) => [
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
foreignKey({
columns: [table.applicationMutationId],
foreignColumns: [secretConfigApplications.mutationId],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
columns: [
table.adoptionMutationId,
table.adoptionRowOrdinal,
table.adoptionTriggerOrdinal,
],
foreignColumns: [
legacyAdoptionTriggers.adoptionMutationId,
legacyAdoptionTriggers.rowOrdinal,
legacyAdoptionTriggers.triggerOrdinal,
],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
columns: [table.projectId, table.triggerId, table.triggerRevision],
foreignColumns: [
triggerRevisions.projectId,
triggerRevisions.triggerId,
triggerRevisions.revision,
],
})
.onDelete('restrict')
.onUpdate('restrict'),
check(
'ql3_secret_config_trigger_identity_check',
sql`${table.ordinal} between 1 and 500000 and ${table.adoptionRowOrdinal} between 1 and 100000 and ${table.adoptionTriggerOrdinal} between 1 and 500000 and ${table.taskRevision} = 2 and ${table.previousRevision} = 1 and ${table.triggerRevision} = 2 and length(${table.triggerMutationId}) = 36 and replace(${table.triggerMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
),
check(
'ql3_secret_config_trigger_digest_check',
sql`length(${table.previousContentDigest}) = 64 and ${table.previousContentDigest} not glob '*[^0-9a-f]*' and length(${table.triggerContentDigest}) = 64 and ${table.triggerContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
),
uniqueIndex('ql3_secret_config_triggers_identity_uidx').on(
table.projectId,
table.triggerId,
),
uniqueIndex('ql3_secret_config_triggers_mutation_uidx').on(
table.triggerMutationId,
),
],
);
export const legacyDataDirectoryAdoptions = sqliteTable(
'QingLong3LegacyDataDirectoryAdoptions',
{
@@ -5326,6 +5579,10 @@ export const localSqliteSchema = Object.freeze({
legacyAdoptions,
legacyAdoptionTasks,
legacyAdoptionTriggers,
secretConfigApplications,
secretConfigApplicationSecrets,
secretConfigApplicationTasks,
secretConfigApplicationTriggers,
legacyDataDirectoryAdoptions,
legacyDataDirectoryAdoptionSecrets,
localIdentitySubjects,
@@ -25,7 +25,7 @@ test('authentication projection opens the target read-only without journal or fi
try {
assert.equal(database.profile, 'edge');
assert.equal(database.readiness.contractName, 'local-control-core');
assert.equal(database.readiness.contractVersion, 51);
assert.equal(database.readiness.contractVersion, 52);
assert.equal(await database.apiCredentials.resolve('absent'), null);
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
} finally {
@@ -152,9 +152,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
'0100-capability-v50',
'0101-legacy-adoption-provenance',
'0102-capability-v51',
'0103-secret-config-applications',
'0104-capability-v52',
]);
assert.equal(migrated.readiness.contractName, 'local-control-core');
assert.equal(migrated.readiness.contractVersion, 51);
assert.equal(migrated.readiness.contractVersion, 52);
assert.equal(migrated.readiness.journalMode, 'delete');
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
@@ -600,8 +602,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
.get(),
},
{
contract_version: 51,
migration_id: '0101-legacy-adoption-provenance',
contract_version: 52,
migration_id: '0103-secret-config-applications',
},
);
} finally {
@@ -788,19 +790,19 @@ test('excludes reviewed optional feature tables while preserving unknown table d
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
client.exec(
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
);
client.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
const unknownClient = new DatabaseSync(databasePath);
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
unknownClient.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 86);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 90);
const triggerClient = new DatabaseSync(databasePath);
triggerClient.exec(`
@@ -156,7 +156,7 @@ test('atomically admits one generation-bound Workflow Run and exactly replays it
},
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
@@ -288,7 +288,7 @@ test('exactly replays immutable admission after the Workflow StepRun advances',
},
{ status: 'running', version: 5, eventSequence: 5 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('fails closed before writing when the exact installation is not active', async (t) => {
@@ -231,7 +231,7 @@ test('atomically admits the exact reconciled local Task revision and replays it'
stepAttemptCount: 0,
},
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
});
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
@@ -40,9 +40,9 @@ test('creates and exactly replays a reviewed rollout backup', async (t) => {
await migrateLocalSqlitePath(state);
const prepared = await createLocalSqliteRolloutBackup(state);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.contractVersion, 51);
assert.equal(prepared.writeContractVersion, 51);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 51);
assert.equal(prepared.contractVersion, 52);
assert.equal(prepared.writeContractVersion, 52);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 52);
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
assert.equal(prepared.bytes > 0, true);
assert.equal(prepared.pageCount > 0, true);
@@ -0,0 +1,401 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const {
openLocalSqliteAdoptionDatabase,
} = require('@qinglong/local-sqlite/adoption');
const {
LocalSecretConfigApplicationConflictError,
openLocalSqliteSecretConfigApplicationDatabase,
} = require('@qinglong/local-sqlite/secret-config-application');
const SUBJECT = Object.freeze({ type: 'user', id: 'local-owner' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
const ADOPTION_MUTATION = '12345678-1234-4123-8123-123456789abc';
const APPLICATION_MUTATION = '87654321-4321-4123-8123-cba987654321';
const SECRET_MUTATION = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
function digest(character) {
return character.repeat(64);
}
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-secret-config-application-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
async function preparedDatabase(t) {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state", "role",
"mutation_id", "changed_by_type", "changed_by_id", "created_at_ms"
) VALUES ('default', 'user', 'local-owner', 1, 'active', 'owner',
'secret-config-owner-binding', 'user', 'local-owner', 1)`,
)
.run();
client.close();
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
await adoption.publisher.publish({
mutationId: ADOPTION_MUTATION,
decisionId: '019f7200-0000-7000-8000-000000000001',
projectId: 'default',
profile: 'edge',
planDigest: digest('1'),
inventoryDigest: digest('2'),
decisionDigest: digest('3'),
receiptDigest: digest('4'),
authorizationFileDigest: digest('5'),
rowCount: 1,
skippedCount: 0,
subject: SUBJECT,
fence: FENCE,
audit: {
eventId: ADOPTION_MUTATION,
requestId: 'automation-adoption',
operationId: 'task.adopt',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 100,
},
candidates: [
{
rowOrdinal: 1,
sourceDigest: digest('6'),
task: {
taskId: 'legacy-cron:1',
name: 'Legacy Task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: ['legacy'] },
},
},
labels: { source: 'legacy-adoption' },
enabled: true,
},
triggers: [
{
triggerId: 'legacy-cron:1:cron:1',
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '0 0 * * *',
timezone: 'UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
},
],
},
],
confirmExternalAuthority() {},
createdAtMs: 100,
});
await adoption.close();
return databasePath;
}
function applicationCommand(confirmExternalAuthority = () => {}) {
return {
mutationId: APPLICATION_MUTATION,
projectId: 'default',
profile: 'edge',
secretConfigPlanDigest: digest('7'),
decisionDigest: digest('8'),
candidateSetDigest: digest('9'),
automationAdoptionSetDigest: digest('a'),
subject: SUBJECT,
fence: FENCE,
audit: {
eventId: APPLICATION_MUTATION,
requestId: 'secret-config-application',
operationId: 'secret-config.apply',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 200,
},
secrets: [
{
ordinal: 1,
disposition: 'active_binding',
candidateDigest: digest('b'),
sourceSetDigest: digest('c'),
environmentName: 'LEGACY_TOKEN',
envelope: {
projectId: 'default',
name: 'legacy-db-env-bbbbbbbbbbbbbbbb',
version: 1,
mutationId: SECRET_MUTATION,
keyId: 'active-key',
algorithm: 'aes-256-gcm',
nonce: Buffer.alloc(12, 1).toString('base64url'),
ciphertext: Buffer.from('ciphertext').toString('base64url'),
authTag: Buffer.alloc(16, 2).toString('base64url'),
createdAtMs: 200,
},
audit: {
eventId: SECRET_MUTATION,
requestId: 'secret-config-application',
operationId: 'secret.create',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:review',
outcome: 'allowed',
reasons: ['project_role_allowed'],
fence: FENCE,
occurredAtMs: 200,
},
},
],
appliedAtMs: 200,
confirmExternalAuthority,
};
}
test('atomically binds Secret, Task, dispatch, Trigger, schedule and replay ledger', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
const command = applicationCommand();
const inserted = await database.publisher.publish(command);
assert.equal(inserted.status, 'inserted');
assert.equal(inserted.application.receipt.activeBindingCount, 1);
assert.equal(inserted.application.receipt.taskCount, 1);
assert.equal(inserted.application.receipt.triggerCount, 1);
assert.equal((await database.publisher.publish(command)).status, 'existing');
await database.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3Triggers" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT "trigger_revision" AS revision FROM "QingLong3LocalTriggerSchedules" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.get().revision,
2,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
)
.get().count,
1,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalTaskExecutionRevisions" WHERE "task_id" = 'legacy-cron:1' AND "task_revision" LIKE 'qltd:v1:2:%'`,
)
.get().count,
1,
);
client.close();
});
test('rolls every database mutation back when the commit authority changes', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
let checks = 0;
await assert.rejects(
database.publisher.publish(
applicationCommand(() => {
checks += 1;
if (checks === 2) throw new Error('instance head drifted');
}),
),
/unavailable/,
);
await database.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
)
.get().count,
0,
);
assert.equal(
client
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
)
.get().count,
0,
);
client.close();
});
test('fails closed on occupied Secret without advancing Task or Trigger heads', async (t) => {
const databasePath = await preparedDatabase(t);
const client = new DatabaseSync(databasePath);
client
.prepare(
`INSERT INTO "QingLong3LocalSecretEnvelopes" ("project_id", "secret_name", "version", "mutation_id", "key_id", "algorithm", "nonce", "ciphertext", "auth_tag", "created_at_ms") VALUES ('default', 'legacy-db-env-bbbbbbbbbbbbbbbb', 1, '11111111-2222-4333-8444-555555555555', 'other-key', 'aes-256-gcm', ?, ?, ?, 150)`,
)
.run(Buffer.alloc(12), Buffer.from('occupied'), Buffer.alloc(16));
client.close();
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
database.publisher.publish(applicationCommand()),
LocalSecretConfigApplicationConflictError,
);
await database.close();
const inspected = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
inspected
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
inspected
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
)
.get().count,
0,
);
inspected.close();
});
test('rejects incomplete Trigger provenance and rolls the streamed transaction back', async (t) => {
const databasePath = await preparedDatabase(t);
const client = new DatabaseSync(databasePath);
client.prepare(`DELETE FROM "QingLong3LegacyAdoptionTriggers"`).run();
client.close();
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
database.publisher.publish(applicationCommand()),
LocalSecretConfigApplicationConflictError,
);
await database.close();
const inspected = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
inspected
.prepare(
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
)
.get().revision,
1,
);
assert.equal(
inspected
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplicationTasks"`,
)
.get().count,
0,
);
inspected.close();
});
test('rejects replay after the durable Trigger schedule drifts', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
const command = applicationCommand();
await database.publisher.publish(command);
await database.close();
const client = new DatabaseSync(databasePath);
client
.prepare(
`UPDATE "QingLong3LocalTriggerSchedules" SET "trigger_revision" = 1 WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
)
.run();
client.close();
const reopened = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
reopened.publisher.publish(command),
LocalSecretConfigApplicationConflictError,
);
await reopened.close();
});
test('rejects empty applications and Secret timestamps outside the application instant', async (t) => {
const databasePath = await preparedDatabase(t);
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath,
profile: 'edge',
});
assert.throws(
() => database.publisher.publish({ ...applicationCommand(), secrets: [] }),
LocalSecretConfigApplicationConflictError,
);
const command = applicationCommand();
command.secrets[0].envelope.createdAtMs = command.appliedAtMs - 1;
assert.throws(
() => database.publisher.publish(command),
LocalSecretConfigApplicationConflictError,
);
await database.close();
});