feat(ql3): add owner-confirmed package secret binding

This commit is contained in:
whyour
2026-08-13 11:28:06 +08:00
parent d72631a2b0
commit 8ba27dc148
21 changed files with 1503 additions and 40 deletions
@@ -27,7 +27,7 @@ import {
assertPluginPackageSecretBindingMatches,
type PluginPackageSecretBinding,
type PluginPackageSecretBindingRepository,
} from './pluginPackageSecretBinding';
} from './secret-binding/binding';
import {
normalizeTaskDefinitionLabels,
normalizeTaskDefinitionSpec,
@@ -9,7 +9,7 @@ import {
type PluginPackageResourceLockSource,
} from './pluginPackageResourceMaterialization';
import type { PluginPackageResourceGenerationSource } from './pluginPackageResourceGeneration';
import type { PluginPackageSecretBindingRepository } from './pluginPackageSecretBinding';
import type { PluginPackageSecretBindingRepository } from './secret-binding/binding';
import {
InvalidPluginPackageTaskReconciliationError,
PluginPackageTaskReconciliationConflictError,
@@ -5,13 +5,13 @@ import {
normalizePluginPackageManifest,
type PluginPackageManifest,
type PluginPackageSecretRequirement,
} from './pluginPackage';
import { pluginPackageManifestDigest } from './installation/pluginPackageInstall';
} from '../pluginPackage';
import { pluginPackageManifestDigest } from '../installation/pluginPackageInstall';
import {
normalizePluginPackageResourceGeneration,
type PluginPackageResourceGeneration,
} from './pluginPackageResourceGeneration';
import { parseSecretRef } from '../secret/secretReference';
} from '../pluginPackageResourceGeneration';
import { parseSecretRef } from '../../secret/secretReference';
export const PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA =
'qinglong/plugin-package-secret-binding@v1' as const;
@@ -67,6 +67,13 @@ export interface CreatePluginPackageSecretBindingInput {
readonly boundAtMs: number;
}
export interface CreatePluginPackageSecretBindingFromEntriesInput {
readonly target: Readonly<PluginPackageSecretBindingTarget>;
readonly entries: readonly Readonly<PluginPackageSecretBindingEntry>[];
readonly authority: Readonly<PluginPackageSecretBindingAuthority>;
readonly boundAtMs: number;
}
export interface PluginPackageSecretBindingRepository {
find(
generationDigest: string,
@@ -408,19 +415,24 @@ export function createPluginPackageSecretBinding(
return Object.freeze({ ...unsigned, bindingDigest: bindingDigest(unsigned) });
}
export function normalizePluginPackageSecretBinding(
value: unknown,
export function createPluginPackageSecretBindingFromEntries(
input: CreatePluginPackageSecretBindingFromEntriesInput,
): Readonly<PluginPackageSecretBinding> {
const binding = dataRecord(value, 'binding');
exactKeys(
binding,
['authority', 'bindingDigest', 'boundAtMs', 'entries', 'schema', 'target'],
'binding',
);
if (binding.schema !== PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA) {
return invalid('schema is unsupported');
}
const targetValue = dataRecord(binding.target, 'target');
const target = normalizeTarget(input.target);
const entries = normalizeEntries(input.entries, target.projectId);
const authority = normalizeAuthority(input.authority);
const boundAtMs = timestamp(input.boundAtMs);
const unsigned = unsignedBinding(target, entries, authority, boundAtMs);
return normalizePluginPackageSecretBinding({
...unsigned,
bindingDigest: bindingDigest(unsigned),
});
}
function normalizeTarget(
value: unknown,
): Readonly<PluginPackageSecretBindingTarget> {
const targetValue = dataRecord(value, 'target');
exactKeys(
targetValue,
[
@@ -434,7 +446,7 @@ export function normalizePluginPackageSecretBinding(
],
'target',
);
const target = Object.freeze({
return Object.freeze({
installationId: identifier(targetValue.installationId, 'installation ID'),
projectId: identifier(targetValue.projectId, 'Project ID'),
packageName: packageName(targetValue.packageName),
@@ -443,6 +455,21 @@ export function normalizePluginPackageSecretBinding(
generationDigest: digest(targetValue.generationDigest, 'generation digest'),
manifestDigest: digest(targetValue.manifestDigest, 'Manifest digest'),
});
}
export function normalizePluginPackageSecretBinding(
value: unknown,
): Readonly<PluginPackageSecretBinding> {
const binding = dataRecord(value, 'binding');
exactKeys(
binding,
['authority', 'bindingDigest', 'boundAtMs', 'entries', 'schema', 'target'],
'binding',
);
if (binding.schema !== PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA) {
return invalid('schema is unsupported');
}
const target = normalizeTarget(binding.target);
const entries = normalizeEntries(binding.entries, target.projectId);
const authority = normalizeAuthority(binding.authority);
const boundAtMs = timestamp(binding.boundAtMs);
@@ -0,0 +1,168 @@
import { createHash } from 'node:crypto';
import {
createPluginPackageSecretBinding,
createPluginPackageSecretBindingFromEntries,
type PluginPackageSecretBinding,
type PluginPackageSecretBindingAssignment,
type PluginPackageSecretBindingEntry,
type PluginPackageSecretBindingTarget,
} from './binding';
import type { PluginPackageManifest } from '../pluginPackage';
import type { PluginPackageResourceGeneration } from '../pluginPackageResourceGeneration';
export const PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA =
'qinglong/plugin-package-secret-binding-plan@v1' as const;
export const MAX_PLUGIN_PACKAGE_SECRET_BINDING_PLAN_JSON_BYTES = 64 * 1024;
export interface PluginPackageSecretBindingPlan {
readonly schema: typeof PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA;
readonly target: Readonly<PluginPackageSecretBindingTarget>;
readonly entries: readonly Readonly<PluginPackageSecretBindingEntry>[];
readonly plannedAtMs: number;
readonly planDigest: string;
}
export interface CreatePluginPackageSecretBindingPlanInput {
readonly generation: Readonly<PluginPackageResourceGeneration>;
readonly manifest: Readonly<PluginPackageManifest>;
readonly assignments: readonly Readonly<PluginPackageSecretBindingAssignment>[];
readonly plannedAtMs: number;
}
const DIGEST = /^[0-9a-f]{64}$/;
const PLACEHOLDER_EVIDENCE_DIGEST = '0'.repeat(64);
const PLAN_DIGEST_DOMAIN = Buffer.from(
'qinglong/plugin-package-secret-binding-plan-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new TypeError(
`Plugin Package Secret binding plan is invalid: ${message}`,
);
}
function exactKeys(value: object, expected: readonly string[]): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
invalid('shape is invalid');
}
}
function unsignedPlan(
binding: Pick<PluginPackageSecretBinding, 'target' | 'entries'>,
plannedAtMs: number,
): Omit<PluginPackageSecretBindingPlan, 'planDigest'> {
if (!Number.isSafeInteger(plannedAtMs) || plannedAtMs < 0) {
return invalid('plannedAtMs is invalid');
}
return Object.freeze({
schema: PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA,
target: binding.target,
entries: binding.entries,
plannedAtMs,
});
}
function planDigest(
value: Omit<PluginPackageSecretBindingPlan, 'planDigest'>,
): string {
return createHash('sha256')
.update(PLAN_DIGEST_DOMAIN)
.update(JSON.stringify(value), 'utf8')
.digest('hex');
}
function withDigest(
value: Omit<PluginPackageSecretBindingPlan, 'planDigest'>,
): Readonly<PluginPackageSecretBindingPlan> {
const normalized = Object.freeze({
...value,
planDigest: planDigest(value),
});
if (
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
MAX_PLUGIN_PACKAGE_SECRET_BINDING_PLAN_JSON_BYTES
) {
return invalid('durable JSON byte budget exceeded');
}
return normalized;
}
export function createPluginPackageSecretBindingPlan(
input: CreatePluginPackageSecretBindingPlanInput,
): Readonly<PluginPackageSecretBindingPlan> {
const draft = createPluginPackageSecretBinding({
generation: input.generation,
manifest: input.manifest,
assignments: input.assignments,
authority: Object.freeze({
kind: 'local-owner-confirmation',
evidenceDigest: PLACEHOLDER_EVIDENCE_DIGEST,
}),
boundAtMs: 0,
});
return withDigest(unsignedPlan(draft, input.plannedAtMs));
}
export function normalizePluginPackageSecretBindingPlan(
value: unknown,
): Readonly<PluginPackageSecretBindingPlan> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return invalid('value must be an object');
}
exactKeys(value, [
'entries',
'planDigest',
'plannedAtMs',
'schema',
'target',
]);
const candidate = value as PluginPackageSecretBindingPlan;
if (candidate.schema !== PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA) {
return invalid('schema is unsupported');
}
if (
typeof candidate.planDigest !== 'string' ||
!DIGEST.test(candidate.planDigest)
) {
return invalid('plan digest is invalid');
}
const normalizedBinding = createPluginPackageSecretBindingFromEntries({
target: candidate.target,
entries: candidate.entries,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: PLACEHOLDER_EVIDENCE_DIGEST,
},
boundAtMs: 0,
});
const unsigned = unsignedPlan(normalizedBinding, candidate.plannedAtMs);
const normalized = withDigest(unsigned);
if (normalized.planDigest !== candidate.planDigest) {
return invalid('plan digest does not match content');
}
return normalized;
}
export function createPluginPackageSecretBindingFromPlan(
planValue: unknown,
authorityKind: PluginPackageSecretBinding['authority']['kind'],
boundAtMs: number,
): Readonly<PluginPackageSecretBinding> {
const plan = normalizePluginPackageSecretBindingPlan(planValue);
return createPluginPackageSecretBindingFromEntries({
target: plan.target,
entries: plan.entries,
authority: {
kind: authorityKind,
evidenceDigest: plan.planDigest,
},
boundAtMs,
});
}