feat(api): advance capability and secure operations slices

This commit is contained in:
chick
2026-07-17 11:57:20 +08:00
parent 8018b36adf
commit 1cc4a995ee
19 changed files with 2102 additions and 22 deletions
@@ -29,11 +29,19 @@ export class DeleteInstanceOperationError extends Error {
interface PreparationRow {
operation_id: string;
risk_level: string;
status: string;
target_instance_id: string;
target_revision: number;
target_origin: string;
method: string;
path: string;
canonical_query: string;
body_digest: string;
content_type: string;
parameter_schema_id: string;
parameters_digest: string;
nonce: string;
token_digest: string;
expires_at: string;
}
@@ -128,8 +136,10 @@ export class DeleteInstanceOperation {
this.db
.prepare(
`INSERT INTO operation_preparations
(id,operation_id,risk_level,status,target_instance_id,target_revision,parameter_schema_id,parameters_digest,token_digest,requested_by,request_id,expires_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
(id,operation_id,risk_level,status,target_instance_id,target_revision,target_origin,method,path,
canonical_query,body_digest,content_type,parameter_schema_id,parameters_digest,nonce,token_digest,
requested_by,request_id,expires_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
)
.run(
id,
@@ -138,8 +148,15 @@ export class DeleteInstanceOperation {
'prepared',
target.instanceId,
target.revision,
current.origin,
'DELETE',
`/api/v1/instances/${target.instanceId}`,
'',
digest(''),
'',
DELETE_INSTANCE_PARAMETER_SCHEMA_ID,
PARAMETERS_DIGEST,
`delete-${id}`,
digest(token),
ACTOR,
requestId,
@@ -169,8 +186,9 @@ export class DeleteInstanceOperation {
this.db.transaction(() => {
const row = this.db
.prepare(
`SELECT operation_id,status,target_instance_id,target_revision,parameter_schema_id,
parameters_digest,token_digest,expires_at
`SELECT operation_id,risk_level,status,target_instance_id,target_revision,target_origin,
method,path,canonical_query,body_digest,content_type,parameter_schema_id,
parameters_digest,nonce,token_digest,expires_at
FROM operation_preparations WHERE id=?`,
)
.get(input.preparationId) as PreparationRow | undefined;
@@ -180,15 +198,44 @@ export class DeleteInstanceOperation {
'Confirmation could not be accepted',
);
const secretValid =
row.expires_at > now &&
equalDigest(row.token_digest, digest(input.confirmationToken)) &&
const confirmationValid =
row.expires_at > now && equalDigest(row.token_digest, digest(input.confirmationToken));
if (!confirmationValid) return;
const expectedPath = `/api/v1/instances/${input.instanceId}`;
const bindingValid =
row.operation_id === OPERATION_ID &&
row.risk_level === 'R3' &&
row.target_instance_id === input.instanceId &&
row.target_revision === input.revision &&
row.method === 'DELETE' &&
row.path === expectedPath &&
row.canonical_query === '' &&
row.body_digest === digest('') &&
row.content_type === '' &&
row.parameter_schema_id === DELETE_INSTANCE_PARAMETER_SCHEMA_ID &&
row.parameters_digest === PARAMETERS_DIGEST;
if (!secretValid) return;
row.parameters_digest === PARAMETERS_DIGEST &&
row.nonce === `delete-${input.preparationId}`;
if (!bindingValid) {
this.db
.prepare(
"UPDATE operation_preparations SET status='invalidated',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
return;
}
const current = this.db
.prepare('SELECT base_url,config_revision FROM instances WHERE id=?')
.get(input.instanceId) as { base_url: string; config_revision: number } | undefined;
if (current && current.base_url !== row.target_origin) {
this.db
.prepare(
"UPDATE operation_preparations SET status='invalidated',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
return;
}
// A correctly authenticated confirmation is one-shot, including stale/missing target results.
this.db
@@ -196,9 +243,6 @@ export class DeleteInstanceOperation {
"UPDATE operation_preparations SET status='consumed',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
const current = this.db
.prepare('SELECT config_revision FROM instances WHERE id=?')
.get(input.instanceId) as { config_revision: number } | undefined;
if (!current) {
targetError = 'NOT_FOUND';
return;