@cardinali-studio/adminkit 2.2.8 → 2.2.10

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;
@@ -88,6 +89,7 @@ export declare class D1DeploymentAttemptRepository {
88
89
  finalize(input: {
89
90
  attemptId: string;
90
91
  releaseId: string;
92
+ intentKind: DeploymentIntent;
91
93
  tokenChecksum: string;
92
94
  now: number;
93
95
  }): Promise<void>;
@@ -142,17 +142,40 @@ 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
154
164
  * claim cannot publish content even if a prior read was valid. */
155
165
  async finalize(input) {
166
+ if (input.intentKind === "code") {
167
+ const result = await this.db.prepare(`UPDATE production_deployment_attempts
168
+ SET phase = 'succeeded', active_slot = NULL, updated_at = ?, finished_at = ?
169
+ WHERE attempt_id = ? AND release_id = ? AND intent_kind = 'code' AND phase = 'verifying_canonical'
170
+ AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?
171
+ AND EXISTS (SELECT 1 FROM content_site_state state JOIN content_releases release
172
+ ON release.release_id = state.current_release_id
173
+ WHERE state.singleton = 1 AND state.current_release_id = ? AND release.status = 'published')`)
174
+ .bind(input.now, input.now, input.attemptId, input.releaseId, input.tokenChecksum, input.now, input.releaseId).run();
175
+ if (result.meta?.changes !== 1)
176
+ throw new Error("attempt_unavailable");
177
+ return;
178
+ }
156
179
  const guard = `EXISTS (SELECT 1 FROM production_deployment_attempts WHERE attempt_id = ? AND release_id = ?
157
180
  AND phase = 'verifying_canonical' AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?)`;
158
181
  const statements = [
@@ -4,11 +4,12 @@ import {
4
4
  D1DeploymentAttemptRepository, claimTokenChecksum, constantTimeSecretEqual,
5
5
  isTerminalDeploymentPhase,
6
6
  } from "../../deployment.js";
7
- import { releaseExportFromStored } from "../../release.js";
7
+ import { deploymentFingerprint, releaseExportFromStored } from "../../release.js";
8
8
 
9
9
  const MAX_BODY_BYTES = 8_000;
10
10
  const claimLifetimeMs = 20 * 60 * 1_000;
11
11
  const id = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12
+ const sha = /^[0-9a-f]{40}$/i;
12
13
  const phases = new Set(["validating", "materializing", "building", "deploying", "verifying_deployment_url", "verifying_canonical"]);
13
14
  const failure = {
14
15
  materialization_failed: { stage: "materializing", message: "La generazione del sito non è riuscita." },
@@ -94,6 +95,27 @@ export async function handleInternalContentCi({ request, env }) {
94
95
  const now = Date.now();
95
96
  const { attempts, content } = repositories(env);
96
97
 
98
+ if (path === "/internal/deployment/register-code" && request.method === "POST") {
99
+ const input = await body(request);
100
+ if (!sha.test(input.codeSha || "")) throw new Error("invalid_request");
101
+ const state = await env.CONTENT_DB.prepare("SELECT current_release_id AS currentReleaseId FROM content_site_state WHERE singleton = 1")
102
+ .first();
103
+ if (!state?.currentReleaseId) throw new Error("content_unavailable");
104
+ const snapshot = await content.getReleaseExport(state.currentReleaseId);
105
+ if (!snapshot || snapshot.status !== "published") throw new Error("content_unavailable");
106
+ const fingerprint = await deploymentFingerprint({
107
+ codeSha: input.codeSha, manifestChecksum: snapshot.manifestChecksum,
108
+ schemaPackageVersion: snapshot.schemaPackageVersion, materializerVersion: snapshot.materializerVersion,
109
+ });
110
+ const attemptId = crypto.randomUUID();
111
+ await attempts.register({
112
+ attemptId, intentKind: "code", requestedReleaseId: snapshot.releaseId, requestedCodeSha: input.codeSha,
113
+ releaseId: snapshot.releaseId, codeSha: input.codeSha, expectedFingerprint: fingerprint.fingerprint, now,
114
+ });
115
+ return response({ ok: true, attemptId, releaseId: snapshot.releaseId, codeSha: input.codeSha,
116
+ expectedFingerprint: fingerprint.fingerprint });
117
+ }
118
+
97
119
  if (path === "/internal/deployment/claim" && request.method === "POST") {
98
120
  const identity = requiredIdentity(await body(request));
99
121
  const attempt = await attempts.get(identity.attemptId);
@@ -124,13 +146,13 @@ export async function handleInternalContentCi({ request, env }) {
124
146
  return response({ ok: true, attempt: { ...identity, phase: input.phase } });
125
147
  }
126
148
  if (path === "/internal/deployment/finalize" && request.method === "POST") {
127
- await attempts.finalize({ ...identity, tokenChecksum: checksum, now });
149
+ await attempts.finalize({ ...identity, intentKind: attempt.intentKind, tokenChecksum: checksum, now });
128
150
  return response({ ok: true, attempt: { ...identity, phase: "succeeded" } });
129
151
  }
130
152
  if (path === "/internal/deployment/fail" && request.method === "POST") {
131
153
  const detail = failure[input.code];
132
154
  if (!detail) throw new Error("invalid_request");
133
- await attempts.fail({ ...identity, tokenChecksum: checksum, phase: attempt.phase, now, code: input.code, ...detail });
155
+ await attempts.fail({ ...identity, intentKind: attempt.intentKind, tokenChecksum: checksum, phase: attempt.phase, now, code: input.code, ...detail });
134
156
  return response({ ok: true, attempt: { ...identity, phase: "failed" } });
135
157
  }
136
158
  return safeError("invalid_request", "routing", 405);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.8",
3
+ "version": "2.2.10",
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",