@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.
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +28 -5
- package/dist/functions/api/content-ci.js +25 -3
- package/package.json +1 -1
package/dist/deployment.d.ts
CHANGED
|
@@ -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>;
|
package/dist/deployment.js
CHANGED
|
@@ -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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.
|
|
149
|
-
|
|
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.
|
|
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",
|