feat(ql3): apply legacy data transformation

This commit is contained in:
whyour
2026-08-21 12:06:41 +08:00
parent 69c322fa8d
commit d4813e48a4
32 changed files with 3877 additions and 64 deletions
@@ -0,0 +1,272 @@
import {
applyReviewedLocalDataDirectoryAdoption,
LocalDataDirectoryAdoptionApplicationConfigurationError,
} from '@qinglong/local-admin/data-directory-adoption';
import { LocalSecretKeyringFileProvider } from '@qinglong/local-secret';
import { openLocalSqliteBootstrapDatabase } from '@qinglong/local-sqlite/bootstrap';
import {
establishAuthenticatedLocalCommand,
type AuthenticatedLocalCommand,
} from '@qinglong/local-owner-console/authenticated-command';
import {
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_VERIFY_OPERATION,
LocalDataDirectoryAdoptionConfigurationError,
type ApplyLocalDataDirectoryAdoptionCommand,
type VerifyLocalDataDirectoryAdoptionApplicationCommand,
type VerifyLocalDataDirectoryAdoptionCommand,
} from '../contract';
import { verifyLocalDataDirectoryAdoption } from '../staging';
import { transformationAuthority } from '../transformation/files';
import {
loadStaticTransformation,
verifyTransformationManifestBinding,
} from '../transformation/manifest';
import {
reclaimCommittedTransformationModel,
verifyCommittedTransformationModel,
} from './cleanup';
export interface LocalDataDirectoryApplicationResult {
readonly schemaVersion: 1;
readonly operation:
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION;
readonly status: 'committed' | 'verified';
readonly evidence: Readonly<{
profile: 'edge' | 'standalone';
databaseStatus: 'inserted' | 'existing';
sourceStageManifestDigest: string;
transformationDigest: string;
modelDigest: string;
publicationDigest: string;
receiptDigest: string;
commitDigest: string;
secretCount: number;
environmentSecretCount: number;
sshSecretCount: number;
committedAtMs: number;
modelReclaimed: true;
plaintextFilesRemoved: true;
physicalErasureGuaranteed: false;
}>;
}
type ApplicationCommand =
| Readonly<ApplyLocalDataDirectoryAdoptionCommand>
| Readonly<VerifyLocalDataDirectoryAdoptionApplicationCommand>;
function sourceVerificationCommand(
command: ApplicationCommand,
): Readonly<VerifyLocalDataDirectoryAdoptionCommand> {
return Object.freeze({
schemaVersion: 1,
operation: LOCAL_DATA_DIRECTORY_ADOPTION_VERIFY_OPERATION,
options: Object.freeze({
deploymentRoot: command.options.deploymentRoot,
dataRoot: command.options.dataRoot,
stagingRoot: command.options.stagingRoot,
profile: command.options.profile,
sqlite: command.options.sqlite,
expectedManifestDigest: command.options.expectedManifestDigest,
}),
});
}
async function loadPreparedModel(
command: ApplicationCommand,
authority: ReturnType<typeof transformationAuthority>,
) {
const before = await verifyLocalDataDirectoryAdoption(
sourceVerificationCommand(command),
);
const loaded = loadStaticTransformation({
authority,
profile: command.options.profile,
projectId: command.options.projectId,
sourceStageManifestDigest: command.options.expectedManifestDigest,
expectedTransformationDigest: command.options.expectedTransformationDigest,
});
if (
loaded.manifest.assessment !== 'ready' ||
loaded.manifest.model.manualCategories !== 0 ||
(loaded.prepared.model.manualReview as { readonly required?: unknown })
.required !== false
) {
throw new LocalDataDirectoryAdoptionApplicationConfigurationError(
'manual review must be resolved in a new transformation before apply',
);
}
const after = await verifyLocalDataDirectoryAdoption(
sourceVerificationCommand(command),
);
if (JSON.stringify(before.evidence) !== JSON.stringify(after.evidence)) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'staged source changed while preparing the application transaction',
);
}
return loaded.prepared;
}
function result(
operation: LocalDataDirectoryApplicationResult['operation'],
status: LocalDataDirectoryApplicationResult['status'],
databaseStatus: 'inserted' | 'existing',
adoption: Awaited<
ReturnType<typeof applyReviewedLocalDataDirectoryAdoption>
>['adoption'],
commit: ReturnType<typeof reclaimCommittedTransformationModel>,
): Readonly<LocalDataDirectoryApplicationResult> {
return Object.freeze({
schemaVersion: 1,
operation,
status,
evidence: Object.freeze({
profile: adoption.profile,
databaseStatus,
sourceStageManifestDigest: adoption.sourceStageManifestDigest,
transformationDigest: adoption.transformationDigest,
modelDigest: adoption.modelDigest,
publicationDigest: adoption.publicationDigest,
receiptDigest: adoption.receiptDigest,
commitDigest: commit.commitDigest,
secretCount: adoption.receipt.secretCount,
environmentSecretCount: adoption.receipt.environmentSecretCount,
sshSecretCount: adoption.receipt.sshSecretCount,
committedAtMs: adoption.committedAtMs,
modelReclaimed: true,
plaintextFilesRemoved: true,
physicalErasureGuaranteed: false,
}),
});
}
async function authenticatedAuthority(command: ApplicationCommand): Promise<
Readonly<{
authenticated: Readonly<AuthenticatedLocalCommand>;
close(): Promise<void>;
}>
> {
const database = await openLocalSqliteBootstrapDatabase({
databasePath: command.options.sqlite.targetPath,
profile: command.options.profile,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
});
try {
const authenticated = await establishAuthenticatedLocalCommand(database, {
deploymentRoot: command.options.deploymentRoot,
databasePath: command.options.sqlite.targetPath,
ownerPepperKeyringDirectory: command.options.ownerPepperKeyringDirectory,
credentialFilePath: command.options.credentialFilePath,
authenticationNamespace: 'local_data_adoption',
});
return Object.freeze({
authenticated,
close: () => database.close(),
});
} catch (error) {
await database.close();
throw error;
}
}
async function apply(
command: ApplicationCommand,
verifyOnly: boolean,
): Promise<Readonly<LocalDataDirectoryApplicationResult>> {
const authority = transformationAuthority(command.options, false);
const manifest = verifyTransformationManifestBinding({
authority,
profile: command.options.profile,
projectId: command.options.projectId,
sourceStageManifestDigest: command.options.expectedManifestDigest,
expectedTransformationDigest: command.options.expectedTransformationDigest,
});
const authentication = await authenticatedAuthority(command);
try {
const applied = await applyReviewedLocalDataDirectoryAdoption({
databasePath: command.options.sqlite.targetPath,
profile: command.options.profile,
projectId: command.options.projectId,
mutationId: command.options.mutationId,
failureAuditEventId: command.options.failureAuditEventId,
requestId: command.options.requestId,
sourceStageManifestDigest: command.options.expectedManifestDigest,
transformationDigest: command.options.expectedTransformationDigest,
modelDigest: manifest.model.digest,
principal: authentication.authenticated.principal,
keyProvider: new LocalSecretKeyringFileProvider(
command.options.secretKeyringPath,
),
observedAtMs: Date.now(),
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
loadPreparedModel: verifyOnly
? () => {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application receipt does not exist',
);
}
: () => loadPreparedModel(command, authority),
confirmAuthenticationAuthority: () =>
authentication.authenticated.confirm(),
async confirmPreparedAuthority() {
await loadPreparedModel(command, authority);
},
});
if (
applied.adoption.modelDigest !== manifest.model.digest ||
applied.adoption.transformationDigest !== manifest.transformationDigest
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'database application does not match the transformation manifest',
);
}
if (verifyOnly) {
const verifyCommand =
command as Readonly<VerifyLocalDataDirectoryAdoptionApplicationCommand>;
const commit = verifyCommittedTransformationModel({
authority,
adoption: applied.adoption,
expectedReceiptDigest: verifyCommand.options.expectedReceiptDigest,
});
return result(
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION,
'verified',
applied.status,
applied.adoption,
commit,
);
}
const commit = reclaimCommittedTransformationModel({
authority,
adoption: applied.adoption,
});
return result(
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION,
'committed',
applied.status,
applied.adoption,
commit,
);
} finally {
await authentication.close();
}
}
export function applyLocalDataDirectoryAdoption(
command: Readonly<ApplyLocalDataDirectoryAdoptionCommand>,
): Promise<Readonly<LocalDataDirectoryApplicationResult>> {
return apply(command, false);
}
export function verifyLocalDataDirectoryAdoptionApplication(
command: Readonly<VerifyLocalDataDirectoryAdoptionApplicationCommand>,
): Promise<Readonly<LocalDataDirectoryApplicationResult>> {
return apply(command, true);
}
@@ -0,0 +1,468 @@
import fs from 'node:fs';
import path from 'node:path';
import {
createLocalDataDirectorySourceNameDigest,
type LocalDataDirectoryAdoptionRecord,
} from '@qinglong/local-sqlite/data-directory-adoption';
import { LocalDataDirectoryAdoptionConfigurationError } from '../contract';
import {
assertPrivateDirectory,
sameStat,
sortedNames,
syncDirectory,
} from '../filesystem';
import { sha256Text } from '../manifest';
import {
readStablePrivateUtf8File,
writePrivateJson,
type TransformationAuthority,
} from '../transformation/files';
import { verifyTransformationModel } from '../transformation/model';
import { verifyTransformationManifestBinding } from '../transformation/manifest';
export const APPLICATION_COMMIT_NAME = 'commit.json';
const COMMIT_INCOMPLETE_NAME = '.commit-incomplete';
const RECLAIMING_MODEL_NAME = '.reclaiming-model';
const MODEL_NAME = 'model';
const MAX_JSON_BYTES = 1024 * 1024;
const ZERO_CHUNK = Buffer.alloc(64 * 1024);
interface LocalDataDirectoryApplicationCommitPayload {
readonly schemaVersion: 1;
readonly kind: 'qinglong3-legacy-data-directory-application';
readonly state: 'committed';
readonly mutationId: string;
readonly profile: 'edge' | 'standalone';
readonly projectIdDigest: string;
readonly sourceStageManifestDigest: string;
readonly transformationDigest: string;
readonly modelDigest: string;
readonly publicationDigest: string;
readonly receiptDigest: string;
readonly secretCount: number;
readonly environmentSecretCount: number;
readonly sshSecretCount: number;
readonly committedAtMs: number;
readonly reclamation: Readonly<{
modelRemoved: true;
plaintextFilesRemoved: true;
physicalErasureGuaranteed: false;
}>;
}
export interface LocalDataDirectoryApplicationCommit
extends LocalDataDirectoryApplicationCommitPayload {
readonly commitDigest: string;
}
function exactKeys(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
return (
actual.length === canonical.length &&
actual.every((key, index) => key === canonical[index])
);
}
function expectedCommit(
adoption: Readonly<LocalDataDirectoryAdoptionRecord>,
): Readonly<LocalDataDirectoryApplicationCommit> {
const payload: LocalDataDirectoryApplicationCommitPayload = {
schemaVersion: 1,
kind: 'qinglong3-legacy-data-directory-application',
state: 'committed',
mutationId: adoption.mutationId,
profile: adoption.profile,
projectIdDigest: sha256Text(adoption.projectId),
sourceStageManifestDigest: adoption.sourceStageManifestDigest,
transformationDigest: adoption.transformationDigest,
modelDigest: adoption.modelDigest,
publicationDigest: adoption.publicationDigest,
receiptDigest: adoption.receiptDigest,
secretCount: adoption.receipt.secretCount,
environmentSecretCount: adoption.receipt.environmentSecretCount,
sshSecretCount: adoption.receipt.sshSecretCount,
committedAtMs: adoption.committedAtMs,
reclamation: Object.freeze({
modelRemoved: true,
plaintextFilesRemoved: true,
physicalErasureGuaranteed: false,
}),
};
return Object.freeze({
...payload,
commitDigest: sha256Text(JSON.stringify(payload)),
});
}
function readJson(filePath: string, uid: number, label: string): unknown {
try {
return JSON.parse(
readStablePrivateUtf8File(filePath, uid, MAX_JSON_BYTES, label),
);
} catch (error) {
if (error instanceof LocalDataDirectoryAdoptionConfigurationError) {
throw error;
}
throw new LocalDataDirectoryAdoptionConfigurationError(
`${label} JSON is invalid`,
error,
);
}
}
function sameJson(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function verifyMarker(
root: string,
uid: number,
adoption: Readonly<LocalDataDirectoryAdoptionRecord>,
): void {
const marker = readJson(
path.join(root, COMMIT_INCOMPLETE_NAME),
uid,
'application recovery marker',
);
if (
!marker ||
typeof marker !== 'object' ||
Array.isArray(marker) ||
!exactKeys(marker, [
'kind',
'mutationId',
'receiptDigest',
'schemaVersion',
'transformationDigest',
])
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application recovery marker shape is invalid',
);
}
const candidate = marker as Record<string, unknown>;
if (
candidate.schemaVersion !== 1 ||
candidate.kind !==
'qinglong3-legacy-data-directory-application-incomplete' ||
candidate.mutationId !== adoption.mutationId ||
candidate.transformationDigest !== adoption.transformationDigest ||
candidate.receiptDigest !== adoption.receiptDigest
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application recovery marker does not match the committed database',
);
}
}
function verifyCommit(
authority: Readonly<TransformationAuthority>,
adoption: Readonly<LocalDataDirectoryAdoptionRecord>,
): Readonly<LocalDataDirectoryApplicationCommit> {
const actual = readJson(
path.join(authority.transformationRoot, APPLICATION_COMMIT_NAME),
authority.uid,
'application commit',
);
const expected = expectedCommit(adoption);
if (!sameJson(actual, expected)) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application commit does not match the durable database receipt',
);
}
return expected;
}
function assertPreparedMatches(
modelRoot: string,
authority: Readonly<TransformationAuthority>,
adoption: Readonly<LocalDataDirectoryAdoptionRecord>,
): void {
const manifest = verifyTransformationManifestBinding({
authority,
profile: adoption.profile,
projectId: adoption.projectId,
sourceStageManifestDigest: adoption.sourceStageManifestDigest,
expectedTransformationDigest: adoption.transformationDigest,
});
const prepared = verifyTransformationModel({
modelRoot,
uid: authority.uid,
projectId: adoption.projectId,
profile: adoption.profile,
expected: manifest.model,
});
if (
manifest.assessment !== 'ready' ||
manifest.model.manualCategories !== 0 ||
!sameJson(prepared.model, adoption.model) ||
prepared.secrets.length !== adoption.secrets.length ||
prepared.secrets.some((secret, index) => {
const stored = adoption.secrets[index];
return (
!stored ||
stored.ordinal !== index + 1 ||
stored.kind !== secret.kind ||
stored.sourceNameDigest !==
createLocalDataDirectorySourceNameDigest(
secret.kind,
secret.sourceName,
) ||
stored.secretName !== secret.targetName ||
stored.valueFile !== secret.valueFile ||
stored.valueDigest !== secret.valueDigest
);
})
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'prepared model does not match the committed database receipt',
);
}
}
function privateEntry(
entryPath: string,
uid: number,
kind: 'file' | 'directory',
): fs.BigIntStats {
const stat = fs.lstatSync(entryPath, { bigint: true });
if (
stat.isSymbolicLink() ||
stat.uid !== BigInt(uid) ||
(stat.mode & 0o777n) !== (kind === 'file' ? 0o600n : 0o700n) ||
(kind === 'file'
? !stat.isFile() || stat.nlink !== 1n
: !stat.isDirectory())
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming model identity or mode is invalid',
);
}
return stat;
}
function overwriteAndUnlink(filePath: string, uid: number): void {
const expected = privateEntry(filePath, uid, 'file');
if (expected.size < 0n || expected.size > BigInt(32 * 1024)) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming Secret file size is invalid',
);
}
const descriptor = fs.openSync(
filePath,
fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0),
);
try {
const opened = fs.fstatSync(descriptor, { bigint: true });
if (!sameStat(expected, opened)) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming Secret identity changed before overwrite',
);
}
let remaining = Number(opened.size);
let offset = 0;
while (remaining > 0) {
const count = Math.min(remaining, ZERO_CHUNK.length);
const written = fs.writeSync(descriptor, ZERO_CHUNK, 0, count, offset);
if (written < 1) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming Secret overwrite made no progress',
);
}
remaining -= written;
offset += written;
}
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
fs.unlinkSync(filePath);
}
function reclaimRenamedModel(
root: string,
uid: number,
adoption: Readonly<LocalDataDirectoryAdoptionRecord>,
): void {
const modelRoot = path.join(root, RECLAIMING_MODEL_NAME);
privateEntry(modelRoot, uid, 'directory');
const allowedRootFiles = new Set([
'config.json',
'keyv.json',
'manual-review.json',
'secret-imports.json',
'ssh.json',
'secret-values',
]);
if (sortedNames(modelRoot).some((name) => !allowedRootFiles.has(name))) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming model contains an unexpected entry',
);
}
const secretRoot = path.join(modelRoot, 'secret-values');
if (fs.existsSync(secretRoot)) {
privateEntry(secretRoot, uid, 'directory');
const expectedFiles = new Set(
adoption.secrets.map(({ valueFile }) => path.basename(valueFile)),
);
const actualFiles = sortedNames(secretRoot);
if (actualFiles.some((name) => !expectedFiles.has(name))) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming Secret directory contains an unexpected entry',
);
}
for (const name of actualFiles) {
overwriteAndUnlink(path.join(secretRoot, name), uid);
}
fs.rmdirSync(secretRoot);
syncDirectory(modelRoot);
}
for (const name of [
'config.json',
'keyv.json',
'ssh.json',
'manual-review.json',
'secret-imports.json',
]) {
const filePath = path.join(modelRoot, name);
if (!fs.existsSync(filePath)) continue;
privateEntry(filePath, uid, 'file');
fs.unlinkSync(filePath);
}
syncDirectory(modelRoot);
if (sortedNames(modelRoot).length !== 0) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'reclaiming model could not be emptied',
);
}
fs.rmdirSync(modelRoot);
syncDirectory(root);
}
export function reclaimCommittedTransformationModel(options: {
readonly authority: Readonly<TransformationAuthority>;
readonly adoption: Readonly<LocalDataDirectoryAdoptionRecord>;
}): Readonly<LocalDataDirectoryApplicationCommit> {
const { authority, adoption } = options;
const root = authority.transformationRoot;
const before = assertPrivateDirectory(
root,
authority.uid,
'transformationRoot',
);
verifyTransformationManifestBinding({
authority,
profile: adoption.profile,
projectId: adoption.projectId,
sourceStageManifestDigest: adoption.sourceStageManifestDigest,
expectedTransformationDigest: adoption.transformationDigest,
});
const initialNames = sortedNames(root);
const allowedNames = new Set([
'manifest.json',
APPLICATION_COMMIT_NAME,
COMMIT_INCOMPLETE_NAME,
MODEL_NAME,
RECLAIMING_MODEL_NAME,
]);
if (
initialNames.some((name) => !allowedNames.has(name)) ||
!initialNames.includes('manifest.json') ||
(initialNames.includes(MODEL_NAME) &&
initialNames.includes(RECLAIMING_MODEL_NAME))
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application recovery root shape is invalid',
);
}
if (!initialNames.includes(COMMIT_INCOMPLETE_NAME)) {
if (
initialNames.includes(APPLICATION_COMMIT_NAME) &&
!initialNames.includes(MODEL_NAME) &&
!initialNames.includes(RECLAIMING_MODEL_NAME)
) {
return verifyCommit(authority, adoption);
}
writePrivateJson(path.join(root, COMMIT_INCOMPLETE_NAME), {
schemaVersion: 1,
kind: 'qinglong3-legacy-data-directory-application-incomplete',
mutationId: adoption.mutationId,
transformationDigest: adoption.transformationDigest,
receiptDigest: adoption.receiptDigest,
});
syncDirectory(root);
}
verifyMarker(root, authority.uid, adoption);
const modelRoot = path.join(root, MODEL_NAME);
const reclaimingRoot = path.join(root, RECLAIMING_MODEL_NAME);
if (fs.existsSync(modelRoot)) {
assertPreparedMatches(modelRoot, authority, adoption);
if (fs.existsSync(reclaimingRoot)) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'multiple application model recovery roots exist',
);
}
fs.renameSync(modelRoot, reclaimingRoot);
syncDirectory(root);
}
if (fs.existsSync(reclaimingRoot)) {
reclaimRenamedModel(root, authority.uid, adoption);
}
const commitPath = path.join(root, APPLICATION_COMMIT_NAME);
if (!fs.existsSync(commitPath)) {
writePrivateJson(commitPath, expectedCommit(adoption));
syncDirectory(root);
}
const commit = verifyCommit(authority, adoption);
fs.unlinkSync(path.join(root, COMMIT_INCOMPLETE_NAME));
syncDirectory(root);
if (
JSON.stringify(sortedNames(root)) !==
JSON.stringify([APPLICATION_COMMIT_NAME, 'manifest.json'].sort())
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'committed transformation root contains unexpected entries',
);
}
const after = fs.lstatSync(root, { bigint: true });
if (
before.dev !== after.dev ||
before.ino !== after.ino ||
before.uid !== after.uid ||
(after.mode & 0o777n) !== 0o700n
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'transformation root identity changed during application cleanup',
);
}
return commit;
}
export function verifyCommittedTransformationModel(options: {
readonly authority: Readonly<TransformationAuthority>;
readonly adoption: Readonly<LocalDataDirectoryAdoptionRecord>;
readonly expectedReceiptDigest: string;
}): Readonly<LocalDataDirectoryApplicationCommit> {
if (
options.adoption.receiptDigest !== options.expectedReceiptDigest ||
JSON.stringify(sortedNames(options.authority.transformationRoot)) !==
JSON.stringify([APPLICATION_COMMIT_NAME, 'manifest.json'].sort())
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'committed transformation evidence is incomplete',
);
}
verifyTransformationManifestBinding({
authority: options.authority,
profile: options.adoption.profile,
projectId: options.adoption.projectId,
sourceStageManifestDigest: options.adoption.sourceStageManifestDigest,
expectedTransformationDigest: options.adoption.transformationDigest,
});
return verifyCommit(options.authority, options.adoption);
}
@@ -1,10 +1,17 @@
import {
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_INSPECT_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_STAGE_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION,
LOCAL_DATA_DIRECTORY_ADOPTION_VERIFY_OPERATION,
normalizeLocalDataDirectoryAdoptionCommand,
} from './contract';
import {
applyLocalDataDirectoryAdoption,
verifyLocalDataDirectoryAdoptionApplication,
type LocalDataDirectoryApplicationResult,
} from './application/application';
import {
inspectLocalDataDirectoryAdoption,
type LocalDataDirectoryAdoptionInspectResult,
@@ -23,7 +30,8 @@ import {
export type LocalDataDirectoryAdoptionProductCommandResult =
| LocalDataDirectoryAdoptionInspectResult
| LocalDataDirectoryAdoptionMutationResult
| LocalDataDirectoryTransformationResult;
| LocalDataDirectoryTransformationResult
| LocalDataDirectoryApplicationResult;
export async function runLocalDataDirectoryAdoptionProductCommand(
value: unknown,
@@ -41,5 +49,13 @@ export async function runLocalDataDirectoryAdoptionProductCommand(
if (command.operation === LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION) {
return transformLocalDataDirectoryAdoption(command);
}
if (command.operation === LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION) {
return applyLocalDataDirectoryAdoption(command);
}
if (
command.operation === LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION
) {
return verifyLocalDataDirectoryAdoptionApplication(command);
}
return verifyLocalDataDirectoryAdoptionTransformation(command);
}
@@ -3,6 +3,9 @@ import path from 'node:path';
const MAX_PATH_BYTES = 4_096;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export const LOCAL_DATA_DIRECTORY_ADOPTION_INSPECT_OPERATION =
'local-data-directory.adoption.inspect' as const;
@@ -14,13 +17,19 @@ export const LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION =
'local-data-directory.adoption.transform' as const;
export const LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION =
'local-data-directory.adoption.transform.verify' as const;
export const LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION =
'local-data-directory.adoption.apply' as const;
export const LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION =
'local-data-directory.adoption.apply.verify' as const;
export type LocalDataDirectoryAdoptionOperation =
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_INSPECT_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_STAGE_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_VERIFY_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION;
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION
| typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION;
export interface InspectLocalDataDirectoryAdoptionCommand {
readonly schemaVersion: 1;
@@ -85,12 +94,40 @@ export interface VerifyLocalDataDirectoryAdoptionTransformationCommand {
};
}
interface LocalDataDirectoryAdoptionApplicationOptions
extends LocalDataDirectoryAdoptionTransformationOptions {
readonly expectedTransformationDigest: string;
readonly ownerPepperKeyringDirectory: string;
readonly credentialFilePath: string;
readonly secretKeyringPath: string;
readonly mutationId: string;
readonly failureAuditEventId: string;
readonly requestId: string;
readonly busyTimeoutMs?: number;
}
export interface ApplyLocalDataDirectoryAdoptionCommand {
readonly schemaVersion: 1;
readonly operation: typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION;
readonly options: LocalDataDirectoryAdoptionApplicationOptions;
}
export interface VerifyLocalDataDirectoryAdoptionApplicationCommand {
readonly schemaVersion: 1;
readonly operation: typeof LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION;
readonly options: LocalDataDirectoryAdoptionApplicationOptions & {
readonly expectedReceiptDigest: string;
};
}
export type LocalDataDirectoryAdoptionCommand =
| InspectLocalDataDirectoryAdoptionCommand
| StageLocalDataDirectoryAdoptionCommand
| VerifyLocalDataDirectoryAdoptionCommand
| TransformLocalDataDirectoryAdoptionCommand
| VerifyLocalDataDirectoryAdoptionTransformationCommand;
| VerifyLocalDataDirectoryAdoptionTransformationCommand
| ApplyLocalDataDirectoryAdoptionCommand
| VerifyLocalDataDirectoryAdoptionApplicationCommand;
export class LocalDataDirectoryAdoptionConfigurationError extends TypeError {
readonly code = 'LOCAL_DATA_DIRECTORY_ADOPTION_CONFIGURATION_INVALID';
@@ -129,7 +166,19 @@ export function isLocalDataDirectoryAdoptionOperation(
value === LOCAL_DATA_DIRECTORY_ADOPTION_STAGE_OPERATION ||
value === LOCAL_DATA_DIRECTORY_ADOPTION_VERIFY_OPERATION ||
value === LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION ||
value === LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION
value === LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION ||
value === LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION ||
value === LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION
);
}
function descendant(root: string, candidate: string): boolean {
const relative = path.relative(root, candidate);
return (
relative.length > 0 &&
relative !== '..' &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative)
);
}
@@ -219,6 +268,10 @@ export function normalizeLocalDataDirectoryAdoptionCommand(
);
}
const options = candidate.options as Record<string, unknown>;
const application =
candidate.operation === LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_OPERATION ||
candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION;
let expectedKeys: readonly string[];
if (candidate.operation === LOCAL_DATA_DIRECTORY_ADOPTION_INSPECT_OPERATION) {
expectedKeys = ['dataRoot', 'profile'];
@@ -245,9 +298,24 @@ export function normalizeLocalDataDirectoryAdoptionCommand(
? []
: ['projectId', 'transformationRoot']),
...(candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION || application
? ['expectedTransformationDigest']
: []),
...(application
? [
'credentialFilePath',
'failureAuditEventId',
'mutationId',
'ownerPepperKeyringDirectory',
'requestId',
'secretKeyringPath',
...(options.busyTimeoutMs === undefined ? [] : ['busyTimeoutMs']),
]
: []),
...(candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION
? ['expectedReceiptDigest']
: []),
];
}
if (
@@ -282,7 +350,8 @@ export function normalizeLocalDataDirectoryAdoptionCommand(
candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_OPERATION ||
candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION ||
application
) {
if (
!normalizedAbsolutePath(options.transformationRoot) ||
@@ -294,8 +363,9 @@ export function normalizeLocalDataDirectoryAdoptionCommand(
);
}
if (
candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION &&
(candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_TRANSFORM_VERIFY_OPERATION ||
application) &&
(typeof options.expectedTransformationDigest !== 'string' ||
!DIGEST_PATTERN.test(options.expectedTransformationDigest))
) {
@@ -303,6 +373,47 @@ export function normalizeLocalDataDirectoryAdoptionCommand(
'transformation digest is invalid',
);
}
if (application) {
for (const key of [
'ownerPepperKeyringDirectory',
'credentialFilePath',
'secretKeyringPath',
]) {
if (
!normalizedAbsolutePath(options[key]) ||
!descendant(
options.deploymentRoot as string,
options[key] as string,
)
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application authority path is invalid',
);
}
}
if (
options.credentialFilePath === options.secretKeyringPath ||
typeof options.mutationId !== 'string' ||
!UUID_V4_PATTERN.test(options.mutationId) ||
typeof options.failureAuditEventId !== 'string' ||
!UUID_V4_PATTERN.test(options.failureAuditEventId) ||
options.failureAuditEventId === options.mutationId ||
typeof options.requestId !== 'string' ||
!REQUEST_ID_PATTERN.test(options.requestId) ||
(options.busyTimeoutMs !== undefined &&
(!Number.isSafeInteger(options.busyTimeoutMs) ||
(options.busyTimeoutMs as number) < 100 ||
(options.busyTimeoutMs as number) > 30_000)) ||
(candidate.operation ===
LOCAL_DATA_DIRECTORY_ADOPTION_APPLY_VERIFY_OPERATION &&
(typeof options.expectedReceiptDigest !== 'string' ||
!DIGEST_PATTERN.test(options.expectedReceiptDigest)))
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'application authority binding is invalid',
);
}
}
}
}
return Object.freeze(value as LocalDataDirectoryAdoptionCommand);
@@ -13,6 +13,7 @@ import {
type LocalDataDirectoryTransformationManifest,
type TransformationModelEvidence,
type TransformationSourceEvidence,
type VerifiedTransformationModel,
} from './model';
export const TRANSFORMATION_MANIFEST_NAME = 'manifest.json';
@@ -210,6 +211,54 @@ export function verifyStaticTransformation(options: {
readonly sourceStageManifestDigest: string;
readonly expectedTransformationDigest: string;
}): Readonly<LocalDataDirectoryTransformationManifest> {
return loadStaticTransformation(options).manifest;
}
export function verifyTransformationManifestBinding(options: {
readonly authority: Readonly<TransformationAuthority>;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly sourceStageManifestDigest: string;
readonly expectedTransformationDigest: string;
}): Readonly<LocalDataDirectoryTransformationManifest> {
const before = assertPrivateDirectory(
options.authority.transformationRoot,
options.authority.uid,
'transformationRoot',
);
const manifest = readManifest(
options.authority.transformationRoot,
options.authority.uid,
);
if (
manifest.transformationDigest !== options.expectedTransformationDigest ||
manifest.profile !== options.profile ||
manifest.projectIdDigest !== sha256Text(options.projectId) ||
manifest.sourceStageManifestDigest !== options.sourceStageManifestDigest ||
manifest.transformationRootPathDigest !==
sha256Text(options.authority.transformationRoot) ||
!sameStat(
before,
fs.lstatSync(options.authority.transformationRoot, { bigint: true }),
)
) {
throw new LocalDataDirectoryAdoptionConfigurationError(
'transformation manifest authority binding is invalid',
);
}
return manifest;
}
export function loadStaticTransformation(options: {
readonly authority: Readonly<TransformationAuthority>;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly sourceStageManifestDigest: string;
readonly expectedTransformationDigest: string;
}): Readonly<{
manifest: Readonly<LocalDataDirectoryTransformationManifest>;
prepared: Readonly<VerifiedTransformationModel>;
}> {
const before = assertPrivateDirectory(
options.authority.transformationRoot,
options.authority.uid,
@@ -239,7 +288,7 @@ export function verifyStaticTransformation(options: {
'transformation manifest authority binding is invalid',
);
}
verifyTransformationModel({
const prepared = verifyTransformationModel({
modelRoot: path.join(options.authority.transformationRoot, 'model'),
uid: options.authority.uid,
projectId: options.projectId,
@@ -256,5 +305,5 @@ export function verifyStaticTransformation(options: {
'transformation root changed during verification',
);
}
return manifest;
return Object.freeze({ manifest, prepared });
}
@@ -58,6 +58,22 @@ interface SecretImportEntry {
readonly valueDigest: string;
}
export interface VerifiedTransformationSecret extends SecretImportEntry {
readonly plaintext: string;
}
export interface VerifiedTransformationModel {
readonly model: Readonly<{
schema: 'qinglong/legacy-data-directory-applied-model@v1';
activation: 'disabled';
config: Readonly<Record<string, unknown>>;
keyv: Readonly<Record<string, unknown>>;
ssh: Readonly<Record<string, unknown>>;
manualReview: Readonly<Record<string, unknown>>;
}>;
readonly secrets: readonly Readonly<VerifiedTransformationSecret>[];
}
function exactKeys(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
@@ -252,7 +268,7 @@ export function verifyTransformationModel(options: {
readonly projectId: string;
readonly profile: 'edge' | 'standalone';
readonly expected: Readonly<TransformationModelEvidence>;
}): void {
}): Readonly<VerifiedTransformationModel> {
if (
JSON.stringify(sortedNames(options.modelRoot)) !==
JSON.stringify(
@@ -270,17 +286,17 @@ export function verifyTransformationModel(options: {
'transformation model root contains unexpected entries',
);
}
assertSchemaFile(
const config = assertSchemaFile(
path.join(options.modelRoot, 'config.json'),
options.uid,
'qinglong/legacy-config-transformation@v1',
);
assertSchemaFile(
const keyv = assertSchemaFile(
path.join(options.modelRoot, 'keyv.json'),
options.uid,
'qinglong/legacy-keyv-transformation@v1',
);
assertSchemaFile(
const ssh = assertSchemaFile(
path.join(options.modelRoot, 'ssh.json'),
options.uid,
'qinglong/legacy-ssh-transformation@v1',
@@ -329,6 +345,7 @@ export function verifyTransformationModel(options: {
}
const expectedFiles: string[] = [];
const targets = new Set<string>();
const secrets: VerifiedTransformationSecret[] = [];
let environmentSecrets = 0;
let sshSecrets = 0;
for (const value of candidate.imports) {
@@ -353,7 +370,13 @@ export function verifyTransformationModel(options: {
if (
(entry.kind !== 'environment' && entry.kind !== 'ssh_private_key') ||
typeof entry.sourceName !== 'string' ||
entry.sourceName.length < 1 ||
Buffer.byteLength(entry.sourceName, 'utf8') > 255 ||
/[\u0000-\u001f\u007f]/.test(entry.sourceName) ||
typeof entry.targetName !== 'string' ||
entry.targetName.length < 1 ||
Buffer.byteLength(entry.targetName, 'utf8') > 128 ||
/[\u0000-\u001f\u007f]/.test(entry.targetName) ||
entry.expectedCurrentVersion !== 0 ||
typeof entry.valueFile !== 'string' ||
!SECRET_FILE_PATTERN.test(entry.valueFile) ||
@@ -396,6 +419,17 @@ export function verifyTransformationModel(options: {
}
if (entry.kind === 'environment') environmentSecrets += 1;
else sshSecrets += 1;
secrets.push(
Object.freeze({
kind: entry.kind,
sourceName: entry.sourceName,
targetName: entry.targetName,
expectedCurrentVersion: 0,
valueFile: entry.valueFile,
valueDigest: entry.valueDigest,
plaintext: secretValue.value,
}) as Readonly<VerifiedTransformationSecret>,
);
}
expectedFiles.sort();
if (
@@ -427,4 +461,15 @@ export function verifyTransformationModel(options: {
'transformation model no longer matches the manifest',
);
}
return Object.freeze({
model: Object.freeze({
schema: 'qinglong/legacy-data-directory-applied-model@v1' as const,
activation: 'disabled' as const,
config: Object.freeze(config),
keyv: Object.freeze(keyv),
ssh: Object.freeze(ssh),
manualReview: Object.freeze(manual),
}),
secrets: Object.freeze(secrets),
});
}