mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+317
@@ -0,0 +1,317 @@
|
||||
import {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import {
|
||||
openLocalOwnerDeliveryAcknowledgementGc,
|
||||
type CompactLocalOwnerDeliveryAcknowledgementRequest,
|
||||
type OpenLocalOwnerDeliveryAcknowledgementGcOptions,
|
||||
} from '../security-maintenance/acknowledgementGc';
|
||||
import {
|
||||
openLocalOwnerPepperMaterialGc,
|
||||
type CollectLocalOwnerPepperMaterialRequest,
|
||||
type OpenLocalOwnerPepperMaterialGcOptions,
|
||||
} from '../security-maintenance/pepperGc';
|
||||
import type {
|
||||
LocalOwnerPromptOutputGcAuthority,
|
||||
OpenLocalOwnerPromptOutputGcOptions,
|
||||
} from '../prompt-output-maintenance/promptOutputGc';
|
||||
import type {
|
||||
LocalOwnerPromptOutputKeyRetirementAuthority,
|
||||
OpenLocalOwnerPromptOutputKeyRetirementOptions,
|
||||
RetireLocalOwnerPromptOutputKeyRequest,
|
||||
} from '../prompt-output-maintenance/promptOutputKeyRetirement';
|
||||
|
||||
export interface LocalOwnerAcknowledgementGcCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.delivery-acknowledgement.compact';
|
||||
readonly options: OpenLocalOwnerDeliveryAcknowledgementGcOptions;
|
||||
readonly request: CompactLocalOwnerDeliveryAcknowledgementRequest;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPepperMaterialGcCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.pepper-material.collect';
|
||||
readonly options: OpenLocalOwnerPepperMaterialGcOptions;
|
||||
readonly request: CollectLocalOwnerPepperMaterialRequest;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputGcCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.prompt-output.collect';
|
||||
readonly options: OpenLocalOwnerPromptOutputGcOptions;
|
||||
readonly request: Readonly<Record<string, never>>;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputKeyRetirementCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.prompt-output-key.retire';
|
||||
readonly options: OpenLocalOwnerPromptOutputKeyRetirementOptions;
|
||||
readonly request: RetireLocalOwnerPromptOutputKeyRequest;
|
||||
}
|
||||
|
||||
export type LocalOwnerGcCommand =
|
||||
| LocalOwnerAcknowledgementGcCommand
|
||||
| LocalOwnerPepperMaterialGcCommand
|
||||
| LocalOwnerPromptOutputGcCommand
|
||||
| LocalOwnerPromptOutputKeyRetirementCommand;
|
||||
|
||||
export interface LocalOwnerAcknowledgementGcCommandResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.delivery-acknowledgement.compact';
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly gcMutationId: string;
|
||||
readonly acknowledgementMutationId: string;
|
||||
readonly acknowledgementKind: 'credential' | 'challenge';
|
||||
readonly retentionEligibleAtMs: number;
|
||||
readonly compactedAtMs: number;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPepperMaterialGcCommandResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.pepper-material.collect';
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly prepareMutationId: string;
|
||||
readonly completeMutationId: string;
|
||||
readonly pepperKeyId: string;
|
||||
readonly state: 'completed';
|
||||
readonly completedAtMs: number;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputGcCommandResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.prompt-output.collect';
|
||||
readonly scanned: number;
|
||||
readonly tombstoned: number;
|
||||
readonly skipped: number;
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputKeyRetirementCommandResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.prompt-output-key.retire';
|
||||
readonly status: 'completed' | 'existing';
|
||||
readonly keyId: string;
|
||||
readonly retirementId: string;
|
||||
readonly preparationDigest: string;
|
||||
readonly completionDigest: string;
|
||||
readonly completedAtMs: number;
|
||||
}
|
||||
|
||||
export type LocalOwnerGcCommandResult =
|
||||
| LocalOwnerAcknowledgementGcCommandResult
|
||||
| LocalOwnerPepperMaterialGcCommandResult
|
||||
| LocalOwnerPromptOutputGcCommandResult
|
||||
| LocalOwnerPromptOutputKeyRetirementCommandResult;
|
||||
|
||||
export interface LocalOwnerGcCommandRunnerDependencies {
|
||||
readonly openAcknowledgementGc: typeof openLocalOwnerDeliveryAcknowledgementGc;
|
||||
readonly openPepperMaterialGc: typeof openLocalOwnerPepperMaterialGc;
|
||||
readonly openPromptOutputGc: (
|
||||
options: OpenLocalOwnerPromptOutputGcOptions,
|
||||
) => Promise<LocalOwnerPromptOutputGcAuthority>;
|
||||
readonly openPromptOutputKeyRetirement: (
|
||||
options: OpenLocalOwnerPromptOutputKeyRetirementOptions,
|
||||
) => Promise<LocalOwnerPromptOutputKeyRetirementAuthority>;
|
||||
}
|
||||
|
||||
export interface LocalOwnerGcCommandRunner {
|
||||
run(commandFilePath: string): Promise<Readonly<LocalOwnerGcCommandResult>>;
|
||||
}
|
||||
|
||||
export class LocalOwnerGcCliConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_OWNER_GC_CLI_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(`Local Owner GC CLI configuration is invalid: ${message}`);
|
||||
this.name = 'LocalOwnerGcCliConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
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 normalizeCommand(value: unknown): Readonly<LocalOwnerGcCommand> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, ['schemaVersion', 'operation', 'options', 'request'])
|
||||
) {
|
||||
throw new LocalOwnerGcCliConfigurationError('command shape is invalid');
|
||||
}
|
||||
const candidate = value as Record<string, unknown>;
|
||||
if (
|
||||
candidate.schemaVersion !== 1 ||
|
||||
(candidate.operation !== 'owner.delivery-acknowledgement.compact' &&
|
||||
candidate.operation !== 'owner.pepper-material.collect' &&
|
||||
candidate.operation !== 'owner.prompt-output.collect' &&
|
||||
candidate.operation !== 'owner.prompt-output-key.retire') ||
|
||||
!candidate.options ||
|
||||
typeof candidate.options !== 'object' ||
|
||||
Array.isArray(candidate.options) ||
|
||||
!candidate.request ||
|
||||
typeof candidate.request !== 'object' ||
|
||||
Array.isArray(candidate.request)
|
||||
) {
|
||||
throw new LocalOwnerGcCliConfigurationError('command value is invalid');
|
||||
}
|
||||
return Object.freeze(value as LocalOwnerGcCommand);
|
||||
}
|
||||
|
||||
function readCommandFile(candidatePath: string): Readonly<LocalOwnerGcCommand> {
|
||||
try {
|
||||
return normalizeCommand(readPrivateLocalCommandFile(candidatePath));
|
||||
} catch (error) {
|
||||
if (error instanceof LocalOwnerGcCliConfigurationError) throw error;
|
||||
if (error instanceof PrivateLocalCommandFileError) {
|
||||
throw new LocalOwnerGcCliConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
throw new LocalOwnerGcCliConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function dependencies(
|
||||
value: LocalOwnerGcCommandRunnerDependencies,
|
||||
): Readonly<LocalOwnerGcCommandRunnerDependencies> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, [
|
||||
'openAcknowledgementGc',
|
||||
'openPepperMaterialGc',
|
||||
'openPromptOutputGc',
|
||||
'openPromptOutputKeyRetirement',
|
||||
]) ||
|
||||
typeof value.openAcknowledgementGc !== 'function' ||
|
||||
typeof value.openPepperMaterialGc !== 'function' ||
|
||||
typeof value.openPromptOutputGc !== 'function' ||
|
||||
typeof value.openPromptOutputKeyRetirement !== 'function'
|
||||
) {
|
||||
throw new LocalOwnerGcCliConfigurationError(
|
||||
'runner dependencies are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
export function createLocalOwnerGcCommandRunner(
|
||||
candidateDependencies: LocalOwnerGcCommandRunnerDependencies = {
|
||||
openAcknowledgementGc: openLocalOwnerDeliveryAcknowledgementGc,
|
||||
openPepperMaterialGc: openLocalOwnerPepperMaterialGc,
|
||||
async openPromptOutputGc(options) {
|
||||
const { openLocalOwnerPromptOutputGc } = await import(
|
||||
'../prompt-output-maintenance/promptOutputGc.js'
|
||||
);
|
||||
return openLocalOwnerPromptOutputGc(options);
|
||||
},
|
||||
async openPromptOutputKeyRetirement(options) {
|
||||
const { openLocalOwnerPromptOutputKeyRetirement } = await import(
|
||||
'../prompt-output-maintenance/promptOutputKeyRetirement.js'
|
||||
);
|
||||
return openLocalOwnerPromptOutputKeyRetirement(options);
|
||||
},
|
||||
},
|
||||
): LocalOwnerGcCommandRunner {
|
||||
const adapters = dependencies(candidateDependencies);
|
||||
return Object.freeze({
|
||||
async run(commandFilePath: string) {
|
||||
const command = readCommandFile(commandFilePath);
|
||||
if (command.operation === 'owner.delivery-acknowledgement.compact') {
|
||||
const authority = await adapters.openAcknowledgementGc(command.options);
|
||||
try {
|
||||
const result = await authority.compact(command.request);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
gcMutationId: result.record.mutationId,
|
||||
acknowledgementMutationId: result.record.acknowledgementMutationId,
|
||||
acknowledgementKind: result.record.acknowledgementKind,
|
||||
retentionEligibleAtMs: result.record.retentionEligibleAtMs,
|
||||
compactedAtMs: result.record.compactedAtMs,
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
}
|
||||
if (command.operation === 'owner.prompt-output.collect') {
|
||||
if (Object.keys(command.request).length !== 0) {
|
||||
throw new LocalOwnerGcCliConfigurationError(
|
||||
'Prompt output collection request must be empty',
|
||||
);
|
||||
}
|
||||
const authority = await adapters.openPromptOutputGc(command.options);
|
||||
try {
|
||||
const result = await authority.collect();
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
...result,
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
}
|
||||
if (command.operation === 'owner.prompt-output-key.retire') {
|
||||
const authority = await adapters.openPromptOutputKeyRetirement(
|
||||
command.options,
|
||||
);
|
||||
try {
|
||||
const result = await authority.retire(command.request);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
...result,
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
}
|
||||
const authority = await adapters.openPepperMaterialGc(command.options);
|
||||
try {
|
||||
const result = await authority.collect(command.request);
|
||||
if (
|
||||
result.record.state !== 'completed' ||
|
||||
!result.record.completeMutationId ||
|
||||
result.record.completedAtMs === undefined
|
||||
) {
|
||||
throw new LocalOwnerGcCliConfigurationError(
|
||||
'pepper material collection did not complete',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
prepareMutationId: result.record.prepareMutationId,
|
||||
completeMutationId: result.record.completeMutationId,
|
||||
pepperKeyId: result.record.pepperKeyId,
|
||||
state: result.record.state,
|
||||
completedAtMs: result.record.completedAtMs,
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function runLocalOwnerGcCommandFile(
|
||||
commandFilePath: string,
|
||||
): Promise<Readonly<LocalOwnerGcCommandResult>> {
|
||||
return createLocalOwnerGcCommandRunner().run(commandFilePath);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalOwnerGcCommandFile } from './application-command/localOwnerMaintenanceCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-owner-gc run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
if (argv.length !== 3 || argv[0] !== 'run' || argv[1] !== '--command-file') {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_OWNER_GC_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await runLocalOwnerGcCommandFile(argv[2]!);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
readonly message?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_OWNER_GC_CLI_FAILED',
|
||||
name: typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
message:
|
||||
typeof candidate.message === 'string'
|
||||
? candidate.message
|
||||
: 'Local Owner GC command failed',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,69 @@
|
||||
import { assertLocalModelInvocationFeatureReady } from '@qinglong/ai/model-invocation-migration';
|
||||
import { LocalPluginPackagePromptOutputGarbageCollector } from '@qinglong/ai/local-plugin-package-prompt-output-retention-storage';
|
||||
import {
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver,
|
||||
type PluginPackagePromptOutputRetentionPolicyCatalog,
|
||||
} from '@qinglong/ai/plugin-package-prompt-output-retention';
|
||||
import {
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
|
||||
export interface OpenLocalOwnerPromptOutputGcOptions
|
||||
extends LocalSqliteDatabaseOptions {
|
||||
readonly retentionPolicyCatalog: PluginPackagePromptOutputRetentionPolicyCatalog;
|
||||
readonly limit?: number;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputGcAuthority {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
collect(): Promise<Readonly<{
|
||||
scanned: number;
|
||||
tombstoned: number;
|
||||
skipped: number;
|
||||
hasMore: boolean;
|
||||
}>>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function openLocalOwnerPromptOutputGc(
|
||||
options: OpenLocalOwnerPromptOutputGcOptions,
|
||||
): Promise<LocalOwnerPromptOutputGcAuthority> {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new TypeError('Local Prompt output GC options are invalid');
|
||||
}
|
||||
const policies =
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver(
|
||||
options.retentionPolicyCatalog,
|
||||
);
|
||||
const database = await openLocalSqliteOptionalFeatureRuntimeDatabase({
|
||||
databasePath: options.databasePath,
|
||||
profile: options.profile,
|
||||
...(options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: options.busyTimeoutMs }),
|
||||
});
|
||||
try {
|
||||
assertLocalModelInvocationFeatureReady(database.authority.client);
|
||||
const collector = new LocalPluginPackagePromptOutputGarbageCollector({
|
||||
authority: database.authority,
|
||||
policies,
|
||||
...(options.limit === undefined ? {} : { limit: options.limit }),
|
||||
});
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: database.profile,
|
||||
collect() {
|
||||
return collector.collect();
|
||||
},
|
||||
close() {
|
||||
closePromise ??= database.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
await database.close();
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { assertLocalModelInvocationFeatureReady } from '@qinglong/ai/model-invocation-migration';
|
||||
import { PluginPackagePromptOutputFileKeyring } from '@qinglong/ai/plugin-package-prompt-output-file-keyring';
|
||||
import { PluginPackagePromptOutputKeyRetirementCoordinator } from '@qinglong/ai/plugin-package-prompt-output-key-retirement';
|
||||
import { LocalPluginPackagePromptOutputKeyRetirementRepository } from '@qinglong/ai/local-plugin-package-prompt-output-key-retirement-storage';
|
||||
import {
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
|
||||
export interface OpenLocalOwnerPromptOutputKeyRetirementOptions
|
||||
extends LocalSqliteDatabaseOptions {
|
||||
readonly keyringPath: string;
|
||||
}
|
||||
|
||||
export interface RetireLocalOwnerPromptOutputKeyRequest {
|
||||
readonly keyId: string;
|
||||
readonly retirementId: string;
|
||||
readonly requestId: string;
|
||||
readonly mutationId: string;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPromptOutputKeyRetirementAuthority {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
retire(request: RetireLocalOwnerPromptOutputKeyRequest): Promise<Readonly<{
|
||||
status: 'completed' | 'existing';
|
||||
keyId: string;
|
||||
retirementId: string;
|
||||
preparationDigest: string;
|
||||
completionDigest: string;
|
||||
completedAtMs: number;
|
||||
}>>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function openLocalOwnerPromptOutputKeyRetirement(
|
||||
options: OpenLocalOwnerPromptOutputKeyRetirementOptions,
|
||||
): Promise<LocalOwnerPromptOutputKeyRetirementAuthority> {
|
||||
if (
|
||||
!options ||
|
||||
typeof options !== 'object' ||
|
||||
Array.isArray(options) ||
|
||||
typeof options.keyringPath !== 'string'
|
||||
) {
|
||||
throw new TypeError('Local Prompt output key retirement options are invalid');
|
||||
}
|
||||
const database = await openLocalSqliteOptionalFeatureRuntimeDatabase({
|
||||
databasePath: options.databasePath,
|
||||
profile: options.profile,
|
||||
...(options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: options.busyTimeoutMs }),
|
||||
});
|
||||
try {
|
||||
assertLocalModelInvocationFeatureReady(database.authority.client);
|
||||
const keys = new PluginPackagePromptOutputFileKeyring(options.keyringPath);
|
||||
const repository =
|
||||
new LocalPluginPackagePromptOutputKeyRetirementRepository({
|
||||
authority: database.authority,
|
||||
});
|
||||
const coordinator = new PluginPackagePromptOutputKeyRetirementCoordinator({
|
||||
repository,
|
||||
materials: keys,
|
||||
});
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: database.profile,
|
||||
async retire(request: RetireLocalOwnerPromptOutputKeyRequest) {
|
||||
const result = await coordinator.retire(request);
|
||||
return Object.freeze({
|
||||
status: result.status,
|
||||
keyId: result.preparation.keyId,
|
||||
retirementId: result.preparation.retirementId,
|
||||
preparationDigest: result.preparation.preparationDigest,
|
||||
completionDigest: result.completion.completionDigest,
|
||||
completedAtMs: result.completion.completedAtMs,
|
||||
});
|
||||
},
|
||||
close() {
|
||||
closePromise ??= database.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
await database.close();
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { FileLocalOwnerBootstrapSecretDelivery } from '@qinglong/local-owner-console/secret-delivery';
|
||||
import {
|
||||
openLocalSqliteAcknowledgementGcDatabase,
|
||||
type LocalSqliteAcknowledgementGcDatabase,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/acknowledgement-gc';
|
||||
import {
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest,
|
||||
type LocalOwnerDeliveryAcknowledgementGcRecord,
|
||||
type LocalOwnerDeliveryAcknowledgementGcRepository,
|
||||
type LocalOwnerDeliveryAcknowledgementGcRetentionPolicy,
|
||||
} from '@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc';
|
||||
|
||||
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}$/;
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
|
||||
export interface CompactLocalOwnerDeliveryAcknowledgementRequest {
|
||||
readonly mutationId: string;
|
||||
readonly requestId: string;
|
||||
readonly acknowledgementMutationId: string;
|
||||
readonly expectedKind: 'credential' | 'challenge';
|
||||
readonly expectedDeliveryDigest: string;
|
||||
}
|
||||
|
||||
export interface CompactLocalOwnerDeliveryAcknowledgementResult {
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly record: Readonly<LocalOwnerDeliveryAcknowledgementGcRecord>;
|
||||
}
|
||||
|
||||
export interface LocalOwnerDeliveryAcknowledgementGcService {
|
||||
compact(
|
||||
request: CompactLocalOwnerDeliveryAcknowledgementRequest,
|
||||
): Promise<Readonly<CompactLocalOwnerDeliveryAcknowledgementResult>>;
|
||||
}
|
||||
|
||||
export interface CreateLocalOwnerDeliveryAcknowledgementGcServiceOptions {
|
||||
readonly secretDeliveryDirectory: string;
|
||||
readonly retentionPolicy: LocalOwnerDeliveryAcknowledgementGcRetentionPolicy;
|
||||
}
|
||||
|
||||
export interface OpenLocalOwnerDeliveryAcknowledgementGcOptions
|
||||
extends LocalSqliteDatabaseOptions,
|
||||
CreateLocalOwnerDeliveryAcknowledgementGcServiceOptions {}
|
||||
|
||||
export interface LocalOwnerDeliveryAcknowledgementGcAuthority
|
||||
extends LocalOwnerDeliveryAcknowledgementGcService {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class LocalOwnerDeliveryAcknowledgementGcConfigurationError extends TypeError {
|
||||
readonly code =
|
||||
'LOCAL_OWNER_DELIVERY_ACKNOWLEDGEMENT_GC_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Local Owner delivery acknowledgement GC configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'LocalOwnerDeliveryAcknowledgementGcConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
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 request(
|
||||
value: CompactLocalOwnerDeliveryAcknowledgementRequest,
|
||||
): Readonly<CompactLocalOwnerDeliveryAcknowledgementRequest> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, [
|
||||
'mutationId',
|
||||
'requestId',
|
||||
'acknowledgementMutationId',
|
||||
'expectedKind',
|
||||
'expectedDeliveryDigest',
|
||||
]) ||
|
||||
!UUID_V4_PATTERN.test(value.mutationId) ||
|
||||
!UUID_V4_PATTERN.test(value.acknowledgementMutationId) ||
|
||||
value.mutationId === value.acknowledgementMutationId ||
|
||||
!REQUEST_ID_PATTERN.test(value.requestId) ||
|
||||
(value.expectedKind !== 'credential' &&
|
||||
value.expectedKind !== 'challenge') ||
|
||||
!DIGEST_PATTERN.test(value.expectedDeliveryDigest)
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'request shape is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
function options(
|
||||
value: CreateLocalOwnerDeliveryAcknowledgementGcServiceOptions,
|
||||
): Readonly<CreateLocalOwnerDeliveryAcknowledgementGcServiceOptions> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, ['secretDeliveryDirectory', 'retentionPolicy'])
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'options shape is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest(
|
||||
value.retentionPolicy,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'retentionPolicy is invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
secretDeliveryDirectory: value.secretDeliveryDirectory,
|
||||
retentionPolicy: Object.freeze({ ...value.retentionPolicy }),
|
||||
});
|
||||
}
|
||||
|
||||
function sameExisting(
|
||||
record: Readonly<LocalOwnerDeliveryAcknowledgementGcRecord>,
|
||||
value: Readonly<CompactLocalOwnerDeliveryAcknowledgementRequest>,
|
||||
retentionPolicy: LocalOwnerDeliveryAcknowledgementGcRetentionPolicy,
|
||||
): boolean {
|
||||
return (
|
||||
record.mutationId === value.mutationId &&
|
||||
record.requestId === value.requestId &&
|
||||
record.acknowledgementMutationId === value.acknowledgementMutationId &&
|
||||
record.acknowledgementKind === value.expectedKind &&
|
||||
record.deliveryDigest === value.expectedDeliveryDigest &&
|
||||
record.retentionPolicyDigest ===
|
||||
localOwnerDeliveryAcknowledgementGcRetentionPolicyDigest(retentionPolicy)
|
||||
);
|
||||
}
|
||||
|
||||
function audit(eventId: string, requestId: string, occurredAtMs: number) {
|
||||
return Object.freeze({
|
||||
eventId,
|
||||
requestId,
|
||||
operationId: 'owner.delivery_acknowledgement.gc',
|
||||
projectId: null,
|
||||
subject: Object.freeze({
|
||||
type: 'system' as const,
|
||||
id: 'owner-acknowledgement-gc',
|
||||
}),
|
||||
authenticationId: 'local-owner-console',
|
||||
outcome: 'allowed' as const,
|
||||
reasons: Object.freeze(['delivery_acknowledgement_gc']),
|
||||
fence: null,
|
||||
occurredAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function createLocalOwnerDeliveryAcknowledgementGcService(
|
||||
repository: LocalOwnerDeliveryAcknowledgementGcRepository,
|
||||
candidateOptions: CreateLocalOwnerDeliveryAcknowledgementGcServiceOptions,
|
||||
): LocalOwnerDeliveryAcknowledgementGcService {
|
||||
if (
|
||||
!repository ||
|
||||
typeof repository.resolveByAcknowledgement !== 'function' ||
|
||||
typeof repository.compact !== 'function'
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'repository boundary is invalid',
|
||||
);
|
||||
}
|
||||
const settings = options(candidateOptions);
|
||||
const delivery = new FileLocalOwnerBootstrapSecretDelivery(
|
||||
settings.secretDeliveryDirectory,
|
||||
);
|
||||
return Object.freeze({
|
||||
async compact(candidate: CompactLocalOwnerDeliveryAcknowledgementRequest) {
|
||||
const command = request(candidate);
|
||||
const existing = await repository.resolveByAcknowledgement(
|
||||
command.acknowledgementMutationId,
|
||||
);
|
||||
if (existing) {
|
||||
if (!sameExisting(existing, command, settings.retentionPolicy)) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'request conflicts with the durable GC record',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ status: 'existing' as const, record: existing });
|
||||
}
|
||||
const evidence = delivery.inspectBridgeClear(
|
||||
command.expectedKind,
|
||||
command.acknowledgementMutationId,
|
||||
);
|
||||
return repository.compact({
|
||||
...command,
|
||||
bridgeClearEvidence: evidence,
|
||||
retentionPolicy: settings.retentionPolicy,
|
||||
compactedAtMs: evidence.inspectedAtMs,
|
||||
audit: audit(
|
||||
command.mutationId,
|
||||
command.requestId,
|
||||
evidence.inspectedAtMs,
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function openLocalOwnerDeliveryAcknowledgementGc(
|
||||
candidate: OpenLocalOwnerDeliveryAcknowledgementGcOptions,
|
||||
): Promise<LocalOwnerDeliveryAcknowledgementGcAuthority> {
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
Array.isArray(candidate) ||
|
||||
!exactKeys(candidate, [
|
||||
'databasePath',
|
||||
'profile',
|
||||
'secretDeliveryDirectory',
|
||||
'retentionPolicy',
|
||||
...(candidate.busyTimeoutMs === undefined ? [] : ['busyTimeoutMs']),
|
||||
])
|
||||
) {
|
||||
throw new LocalOwnerDeliveryAcknowledgementGcConfigurationError(
|
||||
'open options are invalid',
|
||||
);
|
||||
}
|
||||
const settings = options({
|
||||
secretDeliveryDirectory: candidate.secretDeliveryDirectory,
|
||||
retentionPolicy: candidate.retentionPolicy,
|
||||
});
|
||||
const databaseOptions: LocalSqliteDatabaseOptions = {
|
||||
databasePath: candidate.databasePath,
|
||||
profile: candidate.profile,
|
||||
...(candidate.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: candidate.busyTimeoutMs }),
|
||||
};
|
||||
let database: LocalSqliteAcknowledgementGcDatabase | null = null;
|
||||
try {
|
||||
database = await openLocalSqliteAcknowledgementGcDatabase(databaseOptions);
|
||||
const service = createLocalOwnerDeliveryAcknowledgementGcService(
|
||||
database.acknowledgementGc,
|
||||
settings,
|
||||
);
|
||||
const owned = database;
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: owned.profile,
|
||||
compact(request: CompactLocalOwnerDeliveryAcknowledgementRequest) {
|
||||
return service.compact(request);
|
||||
},
|
||||
close() {
|
||||
return (closePromise ??= owned.close());
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await database?.close().catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
LocalOwnerPepperKeyringFileProvider,
|
||||
type LocalOwnerPepperKeyMaterial,
|
||||
} from '@qinglong/local-owner-console/pepper-custody';
|
||||
import {
|
||||
destroyLocalOwnerPepperKey,
|
||||
type DestroyLocalOwnerPepperKeyResult,
|
||||
} from '@qinglong/local-owner-console/pepper-custody/destructive';
|
||||
import {
|
||||
openLocalSqlitePepperGcDatabase,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqlitePepperGcDatabase,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/pepper-gc';
|
||||
import { assertApiCredentialPepperKeyId } from '@qinglong/runtime-core/api-credential';
|
||||
import {
|
||||
type LocalOwnerPepperActivationRecord,
|
||||
type LocalOwnerPepperKeyRecord,
|
||||
type LocalOwnerPepperReferenceRepository,
|
||||
} from '@qinglong/runtime-core/local-owner-pepper';
|
||||
import {
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest,
|
||||
type LocalOwnerPepperMaterialGcRecord,
|
||||
type LocalOwnerPepperMaterialGcRepository,
|
||||
type LocalOwnerPepperMaterialGcRetentionPolicy,
|
||||
} from '@qinglong/runtime-core/local-owner-pepper-material-gc';
|
||||
|
||||
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}$/;
|
||||
|
||||
type LocalOwnerPepperGcCatalog = Pick<
|
||||
LocalOwnerPepperReferenceRepository,
|
||||
'resolveKey' | 'resolveActive'
|
||||
>;
|
||||
|
||||
export interface CollectLocalOwnerPepperMaterialRequest {
|
||||
readonly prepareMutationId: string;
|
||||
readonly prepareRequestId: string;
|
||||
readonly completeMutationId: string;
|
||||
readonly completeRequestId: string;
|
||||
readonly pepperKeyId: string;
|
||||
}
|
||||
|
||||
export interface CollectLocalOwnerPepperMaterialResult {
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly record: Readonly<LocalOwnerPepperMaterialGcRecord>;
|
||||
readonly runtimeMaterial: Readonly<DestroyLocalOwnerPepperKeyResult>;
|
||||
readonly backupMaterial: Readonly<DestroyLocalOwnerPepperKeyResult>;
|
||||
}
|
||||
|
||||
export interface LocalOwnerPepperMaterialGcService {
|
||||
collect(
|
||||
request: CollectLocalOwnerPepperMaterialRequest,
|
||||
): Promise<Readonly<CollectLocalOwnerPepperMaterialResult>>;
|
||||
}
|
||||
|
||||
export interface CreateLocalOwnerPepperMaterialGcServiceOptions {
|
||||
readonly keyringDirectory: string;
|
||||
readonly backupDirectory: string;
|
||||
readonly retentionPolicy: LocalOwnerPepperMaterialGcRetentionPolicy;
|
||||
}
|
||||
|
||||
export interface OpenLocalOwnerPepperMaterialGcOptions
|
||||
extends LocalSqliteDatabaseOptions,
|
||||
CreateLocalOwnerPepperMaterialGcServiceOptions {}
|
||||
|
||||
export interface LocalOwnerPepperMaterialGcAuthority
|
||||
extends LocalOwnerPepperMaterialGcService {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class LocalOwnerPepperMaterialGcConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_OWNER_PEPPER_MATERIAL_GC_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Local Owner pepper material GC configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'LocalOwnerPepperMaterialGcConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalOwnerPepperMaterialGcMaterialUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_OWNER_PEPPER_MATERIAL_GC_MATERIAL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
'Local Owner pepper material or its independent backup is unavailable',
|
||||
);
|
||||
this.name = 'LocalOwnerPepperMaterialGcMaterialUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
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 request(
|
||||
value: CollectLocalOwnerPepperMaterialRequest,
|
||||
): Readonly<CollectLocalOwnerPepperMaterialRequest> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, [
|
||||
'prepareMutationId',
|
||||
'prepareRequestId',
|
||||
'completeMutationId',
|
||||
'completeRequestId',
|
||||
'pepperKeyId',
|
||||
]) ||
|
||||
!UUID_V4_PATTERN.test(value.prepareMutationId) ||
|
||||
!UUID_V4_PATTERN.test(value.completeMutationId) ||
|
||||
value.prepareMutationId === value.completeMutationId ||
|
||||
!REQUEST_ID_PATTERN.test(value.prepareRequestId) ||
|
||||
!REQUEST_ID_PATTERN.test(value.completeRequestId)
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'request shape is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
assertApiCredentialPepperKeyId(value.pepperKeyId);
|
||||
} catch {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'pepperKeyId is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
function currentTime(): number {
|
||||
const value = Date.now();
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'trusted clock is invalid',
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function options(
|
||||
value: CreateLocalOwnerPepperMaterialGcServiceOptions,
|
||||
): Readonly<CreateLocalOwnerPepperMaterialGcServiceOptions> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
!exactKeys(value, [
|
||||
'keyringDirectory',
|
||||
'backupDirectory',
|
||||
'retentionPolicy',
|
||||
]) ||
|
||||
typeof value.keyringDirectory !== 'string' ||
|
||||
typeof value.backupDirectory !== 'string' ||
|
||||
!path.isAbsolute(value.keyringDirectory) ||
|
||||
!path.isAbsolute(value.backupDirectory) ||
|
||||
path.normalize(value.keyringDirectory) !== value.keyringDirectory ||
|
||||
path.normalize(value.backupDirectory) !== value.backupDirectory ||
|
||||
value.keyringDirectory === value.backupDirectory
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'options shape is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest(value.retentionPolicy);
|
||||
} catch (error) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'retentionPolicy is invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
keyringDirectory: value.keyringDirectory,
|
||||
backupDirectory: value.backupDirectory,
|
||||
retentionPolicy: Object.freeze({ ...value.retentionPolicy }),
|
||||
});
|
||||
}
|
||||
|
||||
function audit(
|
||||
eventId: string,
|
||||
requestId: string,
|
||||
operation: 'prepare' | 'complete',
|
||||
occurredAtMs: number,
|
||||
) {
|
||||
return Object.freeze({
|
||||
eventId,
|
||||
requestId,
|
||||
operationId: `owner.pepper.material_gc.${operation}`,
|
||||
projectId: null,
|
||||
subject: Object.freeze({ type: 'system' as const, id: 'owner-pepper-gc' }),
|
||||
authenticationId: 'local-owner-console',
|
||||
outcome: 'allowed' as const,
|
||||
reasons: Object.freeze(['pepper_material_gc']),
|
||||
fence: null,
|
||||
occurredAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
function materialMatches(
|
||||
material: Readonly<LocalOwnerPepperKeyMaterial> | null,
|
||||
expectedDigest: string | undefined,
|
||||
): material is Readonly<LocalOwnerPepperKeyMaterial> {
|
||||
return (
|
||||
!!material && !!expectedDigest && material.summary.digest === expectedDigest
|
||||
);
|
||||
}
|
||||
|
||||
async function verifiedActive(
|
||||
catalog: LocalOwnerPepperGcCatalog,
|
||||
runtime: LocalOwnerPepperKeyringFileProvider,
|
||||
backup: LocalOwnerPepperKeyringFileProvider,
|
||||
): Promise<Readonly<LocalOwnerPepperActivationRecord>> {
|
||||
const active = await catalog.resolveActive();
|
||||
if (!active) {
|
||||
throw new LocalOwnerPepperMaterialGcMaterialUnavailableError();
|
||||
}
|
||||
const key = await catalog.resolveKey(active.activePepperKeyId);
|
||||
if (
|
||||
!key ||
|
||||
key.state !== 'active' ||
|
||||
key.materialDigest !== active.materialDigest ||
|
||||
key.backupDigest !== active.backupDigest ||
|
||||
!materialMatches(
|
||||
runtime.resolve(active.activePepperKeyId),
|
||||
active.materialDigest,
|
||||
) ||
|
||||
!materialMatches(
|
||||
backup.resolve(active.activePepperKeyId),
|
||||
active.backupDigest,
|
||||
)
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcMaterialUnavailableError();
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
function verifiedRetired(
|
||||
key: Readonly<LocalOwnerPepperKeyRecord> | null,
|
||||
runtime: LocalOwnerPepperKeyringFileProvider,
|
||||
backup: LocalOwnerPepperKeyringFileProvider,
|
||||
pepperKeyId: string,
|
||||
): Readonly<LocalOwnerPepperKeyRecord> {
|
||||
if (
|
||||
!key ||
|
||||
key.state !== 'retired' ||
|
||||
!key.materialDigest ||
|
||||
!key.backupDigest ||
|
||||
!materialMatches(runtime.resolve(pepperKeyId), key.materialDigest) ||
|
||||
!materialMatches(backup.resolve(pepperKeyId), key.backupDigest)
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcMaterialUnavailableError();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function combinedProof(
|
||||
runtime: Readonly<DestroyLocalOwnerPepperKeyResult>,
|
||||
backup: Readonly<DestroyLocalOwnerPepperKeyResult>,
|
||||
): string {
|
||||
return createHash('sha256')
|
||||
.update('qinglong.local-owner-pepper-material-gc-completion.v1\0', 'utf8')
|
||||
.update(runtime.destructionProofDigest, 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(backup.destructionProofDigest, 'utf8')
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function createLocalOwnerPepperMaterialGcService(
|
||||
repository: LocalOwnerPepperMaterialGcRepository,
|
||||
catalog: LocalOwnerPepperGcCatalog,
|
||||
candidateOptions: CreateLocalOwnerPepperMaterialGcServiceOptions,
|
||||
): LocalOwnerPepperMaterialGcService {
|
||||
if (
|
||||
!repository ||
|
||||
typeof repository.resolve !== 'function' ||
|
||||
typeof repository.prepare !== 'function' ||
|
||||
typeof repository.complete !== 'function' ||
|
||||
!catalog ||
|
||||
typeof catalog.resolveKey !== 'function' ||
|
||||
typeof catalog.resolveActive !== 'function'
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'repository boundary is invalid',
|
||||
);
|
||||
}
|
||||
const settings = options(candidateOptions);
|
||||
return Object.freeze({
|
||||
async collect(candidate: CollectLocalOwnerPepperMaterialRequest) {
|
||||
const command = request(candidate);
|
||||
const runtimeProvider = new LocalOwnerPepperKeyringFileProvider(
|
||||
settings.keyringDirectory,
|
||||
);
|
||||
const backupProvider = new LocalOwnerPepperKeyringFileProvider(
|
||||
settings.backupDirectory,
|
||||
);
|
||||
let record = await repository.resolve(command.prepareMutationId);
|
||||
if (!record) {
|
||||
const target = verifiedRetired(
|
||||
await catalog.resolveKey(command.pepperKeyId),
|
||||
runtimeProvider,
|
||||
backupProvider,
|
||||
command.pepperKeyId,
|
||||
);
|
||||
const active = await verifiedActive(
|
||||
catalog,
|
||||
runtimeProvider,
|
||||
backupProvider,
|
||||
);
|
||||
const preparedAtMs = currentTime();
|
||||
record = (
|
||||
await repository.prepare({
|
||||
mutationId: command.prepareMutationId,
|
||||
requestId: command.prepareRequestId,
|
||||
pepperKeyId: command.pepperKeyId,
|
||||
expectedMaterialDigest: target.materialDigest!,
|
||||
expectedBackupMaterialDigest: target.backupDigest!,
|
||||
expectedActivePepperKeyId: active.activePepperKeyId,
|
||||
expectedActiveGeneration: active.generation,
|
||||
expectedActiveMaterialDigest: active.materialDigest,
|
||||
retentionPolicy: settings.retentionPolicy,
|
||||
preparedAtMs,
|
||||
audit: audit(
|
||||
command.prepareMutationId,
|
||||
command.prepareRequestId,
|
||||
'prepare',
|
||||
preparedAtMs,
|
||||
),
|
||||
})
|
||||
).record;
|
||||
}
|
||||
if (
|
||||
record.prepareMutationId !== command.prepareMutationId ||
|
||||
record.prepareRequestId !== command.prepareRequestId ||
|
||||
record.pepperKeyId !== command.pepperKeyId ||
|
||||
record.retentionPolicyDigest !==
|
||||
localOwnerPepperMaterialGcRetentionPolicyDigest(
|
||||
settings.retentionPolicy,
|
||||
)
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'request conflicts with the durable GC record',
|
||||
);
|
||||
}
|
||||
await verifiedActive(catalog, runtimeProvider, backupProvider);
|
||||
const runtimeMaterial = destroyLocalOwnerPepperKey({
|
||||
keyringDirectory: settings.keyringDirectory,
|
||||
pepperKeyId: record.pepperKeyId,
|
||||
materialRole: 'runtime',
|
||||
expectedMaterialDigest: record.materialDigest,
|
||||
prepareMutationId: record.prepareMutationId,
|
||||
});
|
||||
const backupMaterial = destroyLocalOwnerPepperKey({
|
||||
keyringDirectory: settings.backupDirectory,
|
||||
pepperKeyId: record.pepperKeyId,
|
||||
materialRole: 'backup',
|
||||
expectedMaterialDigest: record.backupMaterialDigest,
|
||||
prepareMutationId: record.prepareMutationId,
|
||||
});
|
||||
const completedAtMs =
|
||||
record.completedAtMs ?? Math.max(currentTime(), record.preparedAtMs);
|
||||
const completion = await repository.complete({
|
||||
prepareMutationId: record.prepareMutationId,
|
||||
mutationId: command.completeMutationId,
|
||||
requestId: command.completeRequestId,
|
||||
destructionProofDigest: combinedProof(runtimeMaterial, backupMaterial),
|
||||
completedAtMs,
|
||||
audit: audit(
|
||||
command.completeMutationId,
|
||||
command.completeRequestId,
|
||||
'complete',
|
||||
completedAtMs,
|
||||
),
|
||||
});
|
||||
return Object.freeze({
|
||||
status: completion.status,
|
||||
record: completion.record,
|
||||
runtimeMaterial,
|
||||
backupMaterial,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function openLocalOwnerPepperMaterialGc(
|
||||
candidate: OpenLocalOwnerPepperMaterialGcOptions,
|
||||
): Promise<LocalOwnerPepperMaterialGcAuthority> {
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
Array.isArray(candidate) ||
|
||||
!exactKeys(candidate, [
|
||||
'databasePath',
|
||||
'profile',
|
||||
'keyringDirectory',
|
||||
'backupDirectory',
|
||||
'retentionPolicy',
|
||||
...(candidate.busyTimeoutMs === undefined ? [] : ['busyTimeoutMs']),
|
||||
])
|
||||
) {
|
||||
throw new LocalOwnerPepperMaterialGcConfigurationError(
|
||||
'open options are invalid',
|
||||
);
|
||||
}
|
||||
const settings = options({
|
||||
keyringDirectory: candidate.keyringDirectory,
|
||||
backupDirectory: candidate.backupDirectory,
|
||||
retentionPolicy: candidate.retentionPolicy,
|
||||
});
|
||||
const databaseOptions: LocalSqliteDatabaseOptions = {
|
||||
databasePath: candidate.databasePath,
|
||||
profile: candidate.profile,
|
||||
...(candidate.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: candidate.busyTimeoutMs }),
|
||||
};
|
||||
let database: LocalSqlitePepperGcDatabase | null = null;
|
||||
try {
|
||||
database = await openLocalSqlitePepperGcDatabase(databaseOptions);
|
||||
const service = createLocalOwnerPepperMaterialGcService(
|
||||
database.materialGc,
|
||||
database.ownerPepper,
|
||||
settings,
|
||||
);
|
||||
const owned = database;
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: owned.profile,
|
||||
collect(request: CollectLocalOwnerPepperMaterialRequest) {
|
||||
return service.collect(request);
|
||||
},
|
||||
close() {
|
||||
return (closePromise ??= owned.close());
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await database?.close().catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user