@cardinali-studio/adminkit 2.2.9 → 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.
@@ -89,6 +89,7 @@ export declare class D1DeploymentAttemptRepository {
89
89
  finalize(input: {
90
90
  attemptId: string;
91
91
  releaseId: string;
92
+ intentKind: DeploymentIntent;
92
93
  tokenChecksum: string;
93
94
  now: number;
94
95
  }): Promise<void>;
@@ -163,6 +163,19 @@ export class D1DeploymentAttemptRepository {
163
163
  * batch. Every mutating statement repeats the capability guard, so a stale
164
164
  * claim cannot publish content even if a prior read was valid. */
165
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
+ }
166
179
  const guard = `EXISTS (SELECT 1 FROM production_deployment_attempts WHERE attempt_id = ? AND release_id = ?
167
180
  AND phase = 'verifying_canonical' AND ci_claim_token_checksum = ? AND ci_claim_expires_at > ?)`;
168
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,7 +146,7 @@ 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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.9",
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",