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
+12 -4
View File
@@ -84,7 +84,10 @@
"dist/plugin-package/pluginPackageResourceMaterialization.d.ts"
],
"plugin-package-secret-binding": [
"dist/plugin-package/pluginPackageSecretBinding.d.ts"
"dist/plugin-package/secret-binding/binding.d.ts"
],
"plugin-package-secret-binding-plan": [
"dist/plugin-package/secret-binding/plan.d.ts"
],
"plugin-package-task-reconciliation": [
"dist/plugin-package/pluginPackageTaskReconciliation.d.ts"
@@ -375,9 +378,14 @@
"default": "./dist/plugin-package/pluginPackageResourceMaterialization.js"
},
"./plugin-package-secret-binding": {
"types": "./dist/plugin-package/pluginPackageSecretBinding.d.ts",
"require": "./dist/plugin-package/pluginPackageSecretBinding.js",
"default": "./dist/plugin-package/pluginPackageSecretBinding.js"
"types": "./dist/plugin-package/secret-binding/binding.d.ts",
"require": "./dist/plugin-package/secret-binding/binding.js",
"default": "./dist/plugin-package/secret-binding/binding.js"
},
"./plugin-package-secret-binding-plan": {
"types": "./dist/plugin-package/secret-binding/plan.d.ts",
"require": "./dist/plugin-package/secret-binding/plan.js",
"default": "./dist/plugin-package/secret-binding/plan.js"
},
"./plugin-package-task-reconciliation": {
"types": "./dist/plugin-package/pluginPackageTaskReconciliation.d.ts",
@@ -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,
});
}
@@ -25,7 +25,7 @@ const {
const { createSecretRef } = require('../dist/secret/secretReference');
const {
createPluginPackageSecretBinding,
} = require('../dist/plugin-package/pluginPackageSecretBinding');
} = require('../dist/plugin-package/secret-binding/binding');
const {
InvalidPluginPackageResourceMaterializationError,
MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES,
@@ -7,7 +7,7 @@ const {
assertPluginPackageSecretBindingMatches,
createPluginPackageSecretBinding,
normalizePluginPackageSecretBinding,
} = require('../dist/plugin-package/pluginPackageSecretBinding');
} = require('../dist/plugin-package/secret-binding/binding');
const {
createPluginPackageResourceGeneration,
} = require('../dist/plugin-package/pluginPackageResourceGeneration');
@@ -0,0 +1,144 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createPluginPackageSecretBindingFromPlan,
createPluginPackageSecretBindingPlan,
normalizePluginPackageSecretBindingPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-plan');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const manifest = {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.0.0',
description: 'Secret binding plan fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '32Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [
{ name: 'OPTIONAL_TOKEN', required: false },
{ name: 'TOKEN', required: true },
],
tools: ['secret.use'],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const generation = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: 'a'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
contents: manifest.spec.contents,
});
function plan() {
return createPluginPackageSecretBindingPlan({
generation,
manifest,
assignments: [
{ name: 'OPTIONAL_TOKEN', secretRef: null },
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'runtime-token',
version: 2,
}),
},
],
plannedAtMs: 100,
});
}
test('creates one canonical content-free Secret binding plan', () => {
const value = plan();
assert.match(value.planDigest, /^[0-9a-f]{64}$/);
assert.equal(value.plannedAtMs, 100);
assert.deepEqual(normalizePluginPackageSecretBindingPlan(value), value);
assert.equal(JSON.stringify(value).includes('secret-value'), false);
assert.equal(Object.isFrozen(value), true);
});
test('rejects target, entry, time and digest drift', () => {
const value = plan();
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({ ...value, plannedAtMs: -1 }),
/plannedAtMs is invalid/,
);
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({
...value,
target: { ...value.target, projectId: 'project-2' },
}),
/crosses Project boundary|plan digest/,
);
assert.throws(
() => normalizePluginPackageSecretBindingPlan({ ...value, extra: true }),
/shape is invalid/,
);
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({
...value,
planDigest: 'c'.repeat(64),
}),
/plan digest does not match/,
);
});
test('creates Local and Cluster authority bindings from the same plan', () => {
const value = plan();
const local = createPluginPackageSecretBindingFromPlan(
value,
'local-owner-confirmation',
110,
);
const cluster = createPluginPackageSecretBindingFromPlan(
value,
'approved-action-execution',
120,
);
assert.equal(local.authority.evidenceDigest, value.planDigest);
assert.equal(cluster.authority.evidenceDigest, value.planDigest);
assert.equal(local.boundAtMs, 110);
assert.equal(cluster.boundAtMs, 120);
assert.notEqual(local.bindingDigest, cluster.bindingDigest);
});
test('exports the plan contract only through its explicit subpath', () => {
assert.equal(
require('../dist').createPluginPackageSecretBindingPlan,
undefined,
);
assert.equal(
require('@qinglong/runtime-core/plugin-package-secret-binding-plan')
.createPluginPackageSecretBindingPlan,
createPluginPackageSecretBindingPlan,
);
});