@cardinali-studio/adminkit 2.2.7 → 2.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,7 @@ export declare class D1DeploymentAttemptRepository {
75
75
  fail(input: {
76
76
  attemptId: string;
77
77
  releaseId: string;
78
+ intentKind: DeploymentIntent;
78
79
  tokenChecksum: string;
79
80
  phase: DeploymentPhase;
80
81
  now: number;
@@ -142,12 +142,22 @@ export class D1DeploymentAttemptRepository {
142
142
  async fail(input) {
143
143
  if (isTerminalDeploymentPhase(input.phase))
144
144
  throw new Error("invalid_attempt_transition");
145
- const result = await this.db.prepare(`UPDATE production_deployment_attempts
146
- SET phase = 'failed', active_slot = NULL, error_code = ?, error_stage = ?, safe_message = ?, updated_at = ?, finished_at = ?
147
- WHERE attempt_id = ? AND release_id = ? AND phase = ? AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?`)
148
- .bind(input.code, input.stage, input.message, input.now, input.now, input.attemptId, input.releaseId, input.phase, input.tokenChecksum, input.now).run();
149
- if (result.meta?.changes !== 1)
145
+ const guard = `EXISTS (SELECT 1 FROM production_deployment_attempts WHERE attempt_id = ? AND release_id = ?
146
+ AND intent_kind = ? AND phase = ? AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?)`;
147
+ const results = await this.db.batch([
148
+ this.db.prepare(`UPDATE content_releases SET status = 'failed', active_slot = NULL, finished_at = ?
149
+ WHERE release_id = ? AND status = 'publishing' AND ? = 'content' AND ${guard}`)
150
+ .bind(input.now, input.releaseId, input.intentKind, input.attemptId, input.releaseId, input.intentKind, input.phase, input.tokenChecksum, input.now),
151
+ this.db.prepare(`UPDATE production_deployment_attempts
152
+ SET phase = 'failed', active_slot = NULL, error_code = ?, error_stage = ?, safe_message = ?, updated_at = ?, finished_at = ?
153
+ WHERE attempt_id = ? AND release_id = ? AND intent_kind = ? AND phase = ?
154
+ AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?`)
155
+ .bind(input.code, input.stage, input.message, input.now, input.now, input.attemptId, input.releaseId, input.intentKind, input.phase, input.tokenChecksum, input.now),
156
+ ]);
157
+ const expectedReleaseChanges = input.intentKind === "content" ? 1 : 0;
158
+ if (results[0]?.meta?.changes !== expectedReleaseChanges || results[1]?.meta?.changes !== 1) {
150
159
  throw new Error("attempt_unavailable");
160
+ }
151
161
  }
152
162
  /** Promotes the release and succeeds the exact verified attempt in one D1
153
163
  * batch. Every mutating statement repeats the capability guard, so a stale
@@ -130,7 +130,7 @@ export async function handleInternalContentCi({ request, env }) {
130
130
  if (path === "/internal/deployment/fail" && request.method === "POST") {
131
131
  const detail = failure[input.code];
132
132
  if (!detail) throw new Error("invalid_request");
133
- await attempts.fail({ ...identity, tokenChecksum: checksum, phase: attempt.phase, now, code: input.code, ...detail });
133
+ await attempts.fail({ ...identity, intentKind: attempt.intentKind, tokenChecksum: checksum, phase: attempt.phase, now, code: input.code, ...detail });
134
134
  return response({ ok: true, attempt: { ...identity, phase: "failed" } });
135
135
  }
136
136
  return safeError("invalid_request", "routing", 405);
@@ -158,7 +158,10 @@ export class D1ContentRepository {
158
158
  return "repeated";
159
159
  if ((input.payload === null) === !input.tombstone)
160
160
  throw new Error("invalid_document");
161
- const document = await this.db.prepare("SELECT document_id, draft_revision_id, abandoned_at FROM content_documents WHERE document_id = ?").bind(input.documentId).first();
161
+ /* `kind` and `locale` are part of the immutable identity check below, so
162
+ * they must be projected by the real D1 query. Test doubles used to return
163
+ * extra object properties and accidentally hid their absence in SQL. */
164
+ const document = await this.db.prepare("SELECT document_id, kind, locale, draft_revision_id, abandoned_at FROM content_documents WHERE document_id = ?").bind(input.documentId).first();
162
165
  if (document && (document.kind !== input.kind || document.locale !== input.locale))
163
166
  throw new Error("invalid_document");
164
167
  const expectedDraftRevisionId = input.expectedDraftRevisionId === undefined ? input.baseRevisionId : input.expectedDraftRevisionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "UNLICENSED",