@cancia/astro 0.2.0 → 0.3.0
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/chunk-52URFK5Y.js +580 -0
- package/dist/chunk-AE4SIY24.js +63 -0
- package/dist/chunk-AIPRCBJM.js +258 -0
- package/dist/{chunk-ST44VULL.js → chunk-L2VKQJPY.js} +9 -4
- package/dist/chunk-PIDFNJME.js +19 -0
- package/dist/{chunk-337LJIKX.js → chunk-UR5WC3RA.js} +1 -1
- package/dist/endpoints/publish.js +23 -17
- package/dist/git-backed-DFAB0tzf.d.ts +98 -0
- package/dist/github-client-BAZ1pW24.d.ts +48 -0
- package/dist/index.d.ts +57 -1
- package/dist/index.js +112 -176
- package/dist/loader/index.js +2 -2
- package/dist/runtime.d.ts +63 -1
- package/dist/runtime.js +7 -1
- package/dist/storage/index.d.ts +13 -129
- package/dist/storage/index.js +15 -5
- package/package.json +1 -1
- package/dist/chunk-DGCGIEFD.js +0 -14
- package/dist/chunk-SXKZ2WUL.js +0 -305
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGitBackedAdapter,
|
|
3
|
+
createSqliteAdapterV2
|
|
4
|
+
} from "./chunk-52URFK5Y.js";
|
|
5
|
+
import {
|
|
6
|
+
createJsonFileAdapter,
|
|
7
|
+
createJsonFileAdapterV2
|
|
8
|
+
} from "./chunk-L2VKQJPY.js";
|
|
9
|
+
import {
|
|
10
|
+
detectImageType,
|
|
11
|
+
isValidSite
|
|
12
|
+
} from "./chunk-5IPHDIC6.js";
|
|
13
|
+
|
|
14
|
+
// src/routes/upload.ts
|
|
15
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
function makeLocalUploadHandler(opts) {
|
|
19
|
+
const uploadDir = opts.uploadDir ?? join(process.cwd(), "public/uploads");
|
|
20
|
+
const publicUrlBase = opts.publicUrlBase ?? "/uploads";
|
|
21
|
+
const maxBytes = (opts.maxMB ?? 10) * 1024 * 1024;
|
|
22
|
+
return async (file, site, detected) => {
|
|
23
|
+
const dest = join(uploadDir, site);
|
|
24
|
+
await mkdir(dest, { recursive: true });
|
|
25
|
+
const name = `${randomUUID()}.${detected.ext}`;
|
|
26
|
+
await writeFile(join(dest, name), Buffer.from(await file.arrayBuffer()));
|
|
27
|
+
return `${publicUrlBase}/${site}/${name}`;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeUploadRoute(uploadHandler, secret, maxMB = 10) {
|
|
31
|
+
return async ({ request }) => {
|
|
32
|
+
if (secret) {
|
|
33
|
+
const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
|
|
34
|
+
if (token !== secret) {
|
|
35
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const form = await request.formData().catch(() => null);
|
|
39
|
+
const file = form?.get("file");
|
|
40
|
+
const site = form?.get("site");
|
|
41
|
+
if (!(file instanceof File)) {
|
|
42
|
+
return new Response(JSON.stringify({ error: "Missing file field" }), { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
if (typeof site !== "string" || !isValidSite(site)) {
|
|
45
|
+
return new Response(
|
|
46
|
+
JSON.stringify({ error: "Invalid site (allowed: a-z, 0-9, and . _ -; must start alphanumeric)" }),
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (file.size > maxMB * 1024 * 1024) {
|
|
51
|
+
return new Response(JSON.stringify({ error: `File too large (max ${maxMB}MB)` }), { status: 413 });
|
|
52
|
+
}
|
|
53
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
54
|
+
const detected = detectImageType(buf);
|
|
55
|
+
if (!detected) {
|
|
56
|
+
return new Response(JSON.stringify({ error: "File type not allowed" }), { status: 415 });
|
|
57
|
+
}
|
|
58
|
+
const url = await uploadHandler(file, site, detected);
|
|
59
|
+
return new Response(JSON.stringify({ url }), {
|
|
60
|
+
headers: { "Content-Type": "application/json" }
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/routes/upload-r2.ts
|
|
66
|
+
import { createHmac, createHash } from "crypto";
|
|
67
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
68
|
+
function sha256hex(data) {
|
|
69
|
+
return createHash("sha256").update(data).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
function hmacSha256(key, data) {
|
|
72
|
+
return createHmac("sha256", key).update(data).digest();
|
|
73
|
+
}
|
|
74
|
+
function getSigningKey(secretKey, date, region, service) {
|
|
75
|
+
const kDate = hmacSha256(Buffer.from(`AWS4${secretKey}`, "utf8"), date);
|
|
76
|
+
const kRegion = hmacSha256(kDate, region);
|
|
77
|
+
const kService = hmacSha256(kRegion, service);
|
|
78
|
+
const kSigning = hmacSha256(kService, "aws4_request");
|
|
79
|
+
return kSigning;
|
|
80
|
+
}
|
|
81
|
+
async function signedPutRequest(opts) {
|
|
82
|
+
const { endpoint, bucket, key, body, contentType, accessKeyId, secretAccessKey } = opts;
|
|
83
|
+
const region = "auto";
|
|
84
|
+
const service = "s3";
|
|
85
|
+
const now = /* @__PURE__ */ new Date();
|
|
86
|
+
const isoDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "").slice(0, 15) + "Z";
|
|
87
|
+
const shortDate = isoDate.slice(0, 8);
|
|
88
|
+
const url = `${endpoint}/${bucket}/${key}`;
|
|
89
|
+
const host = new URL(endpoint).host;
|
|
90
|
+
const payloadHash = sha256hex(body);
|
|
91
|
+
const headers = {
|
|
92
|
+
"content-type": contentType,
|
|
93
|
+
"host": host,
|
|
94
|
+
"x-amz-content-sha256": payloadHash,
|
|
95
|
+
"x-amz-date": isoDate
|
|
96
|
+
};
|
|
97
|
+
const signedHeaders = Object.keys(headers).sort().join(";");
|
|
98
|
+
const canonicalHeaders = Object.keys(headers).sort().map((k) => `${k}:${headers[k]}
|
|
99
|
+
`).join("");
|
|
100
|
+
const canonicalRequest = [
|
|
101
|
+
"PUT",
|
|
102
|
+
`/${bucket}/${key}`,
|
|
103
|
+
"",
|
|
104
|
+
canonicalHeaders,
|
|
105
|
+
signedHeaders,
|
|
106
|
+
payloadHash
|
|
107
|
+
].join("\n");
|
|
108
|
+
const credentialScope = `${shortDate}/${region}/${service}/aws4_request`;
|
|
109
|
+
const stringToSign = [
|
|
110
|
+
"AWS4-HMAC-SHA256",
|
|
111
|
+
isoDate,
|
|
112
|
+
credentialScope,
|
|
113
|
+
sha256hex(canonicalRequest)
|
|
114
|
+
].join("\n");
|
|
115
|
+
const signingKey = getSigningKey(secretAccessKey, shortDate, region, service);
|
|
116
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
117
|
+
const authHeader = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
118
|
+
let res;
|
|
119
|
+
try {
|
|
120
|
+
res = await fetch(url, {
|
|
121
|
+
method: "PUT",
|
|
122
|
+
headers: { ...headers, Authorization: authHeader },
|
|
123
|
+
body: new Uint8Array(body)
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new Error(`R2 upload: network error reaching ${url} \u2014 ${err}`);
|
|
127
|
+
}
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const text = await res.text().catch(() => res.statusText);
|
|
130
|
+
throw new Error(`R2 upload failed (${res.status}): ${text}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function makeR2UploadHandler(opts) {
|
|
134
|
+
const {
|
|
135
|
+
accountId,
|
|
136
|
+
bucket,
|
|
137
|
+
accessKeyId,
|
|
138
|
+
secretAccessKey,
|
|
139
|
+
publicUrl,
|
|
140
|
+
prefix = "uploads"
|
|
141
|
+
} = opts;
|
|
142
|
+
const endpoint = `https://${accountId}.r2.cloudflarestorage.com`;
|
|
143
|
+
return async (file, _site, detected) => {
|
|
144
|
+
const key = `${prefix}/${randomUUID2()}.${detected.ext}`;
|
|
145
|
+
const body = Buffer.from(await file.arrayBuffer());
|
|
146
|
+
await signedPutRequest({
|
|
147
|
+
endpoint,
|
|
148
|
+
bucket,
|
|
149
|
+
key,
|
|
150
|
+
body,
|
|
151
|
+
contentType: file.type,
|
|
152
|
+
accessKeyId,
|
|
153
|
+
secretAccessKey
|
|
154
|
+
});
|
|
155
|
+
return `${publicUrl.replace(/\/$/, "")}/${key}`;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/runtime.ts
|
|
160
|
+
import { isAbsolute, join as join2 } from "path";
|
|
161
|
+
var _runtime = null;
|
|
162
|
+
var _bakedConfig = null;
|
|
163
|
+
function setBakedConfig(config) {
|
|
164
|
+
_bakedConfig = config;
|
|
165
|
+
}
|
|
166
|
+
function setCanciaRuntime(runtime) {
|
|
167
|
+
_runtime = runtime;
|
|
168
|
+
}
|
|
169
|
+
function buildRuntimeFromBaked(baked) {
|
|
170
|
+
const projectRoot = baked.projectRoot || process.cwd();
|
|
171
|
+
const token = process.env.CANCIA_TOKEN?.trim() || "";
|
|
172
|
+
const secret = baked.public ? void 0 : token || void 0;
|
|
173
|
+
const deployHook = process.env.CANCIA_DEPLOY_HOOK || void 0;
|
|
174
|
+
const deployHookToken = process.env.CANCIA_DEPLOY_HOOK_TOKEN || void 0;
|
|
175
|
+
const storageV2 = buildStorageV2(baked, projectRoot);
|
|
176
|
+
const storage = storageV2 ? storageV2.kv : lazyJsonFileAdapter(projectRoot);
|
|
177
|
+
const uploadHandler = buildUploadHandler(baked, projectRoot);
|
|
178
|
+
return {
|
|
179
|
+
storage,
|
|
180
|
+
storageV2,
|
|
181
|
+
projectRoot,
|
|
182
|
+
schemasPath: baked.schemasPath,
|
|
183
|
+
defaultLocale: baked.defaultLocale,
|
|
184
|
+
locales: baked.locales,
|
|
185
|
+
secret,
|
|
186
|
+
uploadHandler,
|
|
187
|
+
deployHook,
|
|
188
|
+
deployHookToken,
|
|
189
|
+
deployHookMethod: baked.deployHookMethod,
|
|
190
|
+
deployHookHeaders: baked.deployHookHeaders,
|
|
191
|
+
maxUploadMB: baked.maxUploadMB
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function lazyJsonFileAdapter(projectRoot) {
|
|
195
|
+
return createJsonFileAdapter(projectRoot + "/cancia-content.json");
|
|
196
|
+
}
|
|
197
|
+
function resolveDbPath(dbPath, projectRoot) {
|
|
198
|
+
return isAbsolute(dbPath) ? dbPath : join2(projectRoot, dbPath);
|
|
199
|
+
}
|
|
200
|
+
function buildStorageV2(baked, projectRoot) {
|
|
201
|
+
const desc = baked.storage;
|
|
202
|
+
if (!desc) return void 0;
|
|
203
|
+
if (desc.kind === "git-backed") {
|
|
204
|
+
const local = createJsonFileAdapterV2({ projectRoot });
|
|
205
|
+
return createGitBackedAdapter({
|
|
206
|
+
local,
|
|
207
|
+
repo: desc.repo,
|
|
208
|
+
branch: desc.branch,
|
|
209
|
+
token: process.env.CANCIA_GITHUB_TOKEN,
|
|
210
|
+
committer: desc.committer,
|
|
211
|
+
projectRoot,
|
|
212
|
+
debounceMs: desc.debounceMs,
|
|
213
|
+
commitMessage: desc.commitMessage
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (desc.kind === "sqlite-v2") {
|
|
217
|
+
const dbPath = desc.dbPath ? resolveDbPath(desc.dbPath, projectRoot) : `${projectRoot}/cancia.db`;
|
|
218
|
+
return createSqliteAdapterV2({ dbPath });
|
|
219
|
+
}
|
|
220
|
+
return createJsonFileAdapterV2({ projectRoot });
|
|
221
|
+
}
|
|
222
|
+
function buildUploadHandler(baked, projectRoot) {
|
|
223
|
+
if (baked.r2) {
|
|
224
|
+
const env = process.env;
|
|
225
|
+
const resolved = {
|
|
226
|
+
accountId: baked.r2.accountId ?? env.R2_ACCOUNT_ID ?? "",
|
|
227
|
+
bucket: baked.r2.bucket ?? env.R2_BUCKET_NAME ?? env.R2_BUCKET ?? "",
|
|
228
|
+
accessKeyId: env.R2_ACCESS_KEY_ID ?? "",
|
|
229
|
+
secretAccessKey: env.R2_SECRET_ACCESS_KEY ?? "",
|
|
230
|
+
publicUrl: baked.r2.publicUrl ?? env.R2_PUBLIC_URL ?? "",
|
|
231
|
+
prefix: baked.r2.prefix
|
|
232
|
+
};
|
|
233
|
+
return makeR2UploadHandler(resolved);
|
|
234
|
+
}
|
|
235
|
+
return makeLocalUploadHandler({
|
|
236
|
+
uploadDir: projectRoot + "/public/uploads",
|
|
237
|
+
maxMB: baked.maxUploadMB
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function getCanciaRuntime() {
|
|
241
|
+
if (_runtime) return _runtime;
|
|
242
|
+
if (_bakedConfig) {
|
|
243
|
+
_runtime = buildRuntimeFromBaked(_bakedConfig);
|
|
244
|
+
return _runtime;
|
|
245
|
+
}
|
|
246
|
+
throw new Error(
|
|
247
|
+
"[cancia] Runtime not initialised and no baked config available. This is a bug \u2014 the virtual:cancia/runtime module should have called setBakedConfig() at import time."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
makeLocalUploadHandler,
|
|
253
|
+
makeUploadRoute,
|
|
254
|
+
makeR2UploadHandler,
|
|
255
|
+
setBakedConfig,
|
|
256
|
+
setCanciaRuntime,
|
|
257
|
+
getCanciaRuntime
|
|
258
|
+
};
|
|
@@ -48,10 +48,8 @@ function createJsonFileAdapter(filePath) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// src/storage/
|
|
52
|
-
import {
|
|
53
|
-
import { dirname as dirname2, join } from "path";
|
|
54
|
-
import { createHash, randomUUID } from "crypto";
|
|
51
|
+
// src/storage/rev.ts
|
|
52
|
+
import { createHash } from "crypto";
|
|
55
53
|
function canonicalize(value) {
|
|
56
54
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
57
55
|
if (Array.isArray(value)) {
|
|
@@ -64,6 +62,11 @@ function canonicalize(value) {
|
|
|
64
62
|
function hashRev(value) {
|
|
65
63
|
return createHash("sha256").update(canonicalize(value)).digest("hex").slice(0, 16);
|
|
66
64
|
}
|
|
65
|
+
|
|
66
|
+
// src/storage/json-file-v2.ts
|
|
67
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmSync } from "fs";
|
|
68
|
+
import { dirname as dirname2, join } from "path";
|
|
69
|
+
import { randomUUID } from "crypto";
|
|
67
70
|
function readJsonFile(filePath, fallback) {
|
|
68
71
|
if (!existsSync2(filePath)) return fallback;
|
|
69
72
|
try {
|
|
@@ -315,5 +318,7 @@ function createJsonFileAdapterV2(opts = {}) {
|
|
|
315
318
|
|
|
316
319
|
export {
|
|
317
320
|
createJsonFileAdapter,
|
|
321
|
+
canonicalize,
|
|
322
|
+
hashRev,
|
|
318
323
|
createJsonFileAdapterV2
|
|
319
324
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/publish-hook.ts
|
|
2
|
+
async function firePublish(hook, opts = {}) {
|
|
3
|
+
if (!hook) return { ok: false, kind: "no-hook" };
|
|
4
|
+
const headers = { ...opts.headers ?? {} };
|
|
5
|
+
if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
|
|
6
|
+
const init = { method: opts.method ?? "POST" };
|
|
7
|
+
if (Object.keys(headers).length > 0) init.headers = headers;
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(hook, init);
|
|
10
|
+
if (!res.ok) return { ok: false, kind: "bad-response", status: res.status };
|
|
11
|
+
return { ok: true };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false, kind: "unreachable" };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
firePublish
|
|
19
|
+
};
|
|
@@ -1,33 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
firePublish
|
|
3
|
+
} from "../chunk-PIDFNJME.js";
|
|
4
|
+
|
|
1
5
|
// src/endpoints/publish.ts
|
|
2
6
|
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
|
3
7
|
async function POST({ request }) {
|
|
4
|
-
const { deployHook, secret } = getCanciaRuntime();
|
|
8
|
+
const { deployHook, deployHookToken, deployHookMethod, deployHookHeaders, secret } = getCanciaRuntime();
|
|
5
9
|
if (secret) {
|
|
6
10
|
const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
|
|
7
11
|
if (token !== secret)
|
|
8
12
|
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
|
9
13
|
}
|
|
10
14
|
const hook = deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
|
|
11
|
-
|
|
15
|
+
const result = await firePublish(hook, {
|
|
16
|
+
token: deployHookToken,
|
|
17
|
+
method: deployHookMethod,
|
|
18
|
+
headers: deployHookHeaders
|
|
19
|
+
});
|
|
20
|
+
if (result.ok)
|
|
21
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
22
|
+
headers: { "Content-Type": "application/json" }
|
|
23
|
+
});
|
|
24
|
+
if (result.kind === "no-hook")
|
|
12
25
|
return new Response(
|
|
13
26
|
JSON.stringify({ error: "No deploy hook configured. Set CANCIA_DEPLOY_HOOK." }),
|
|
14
27
|
{ status: 503 }
|
|
15
28
|
);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
headers: { "Content-Type": "application/json" }
|
|
25
|
-
});
|
|
26
|
-
} catch {
|
|
27
|
-
return new Response(JSON.stringify({ error: "Failed to reach deploy hook" }), {
|
|
28
|
-
status: 502
|
|
29
|
-
});
|
|
30
|
-
}
|
|
29
|
+
if (result.kind === "bad-response")
|
|
30
|
+
return new Response(
|
|
31
|
+
JSON.stringify({ error: `Deploy hook responded with ${result.status}` }),
|
|
32
|
+
{ status: 502 }
|
|
33
|
+
);
|
|
34
|
+
return new Response(JSON.stringify({ error: "Failed to reach deploy hook" }), {
|
|
35
|
+
status: 502
|
|
36
|
+
});
|
|
31
37
|
}
|
|
32
38
|
export {
|
|
33
39
|
POST
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { C as CanciaStorage, a as CanciaStorageV2 } from './types-BMlLS-OS.js';
|
|
2
|
+
import { G as GitHubCommitter, a as GitHubClient, F as FetchLike } from './github-client-BAZ1pW24.js';
|
|
3
|
+
|
|
4
|
+
declare function createJsonFileAdapter(filePath?: string): CanciaStorage;
|
|
5
|
+
|
|
6
|
+
interface JsonFileV2Options {
|
|
7
|
+
/** Project root. Defaults to process.cwd(). */
|
|
8
|
+
projectRoot?: string;
|
|
9
|
+
/** Override the KV file path. Defaults to <root>/cancia-content.json. */
|
|
10
|
+
kvPath?: string;
|
|
11
|
+
/** Override the pages file path. Defaults to <root>/.cancia/pages.json. */
|
|
12
|
+
pagesPath?: string;
|
|
13
|
+
/** Override the lists directory. Defaults to <root>/.cancia/lists. */
|
|
14
|
+
listsDir?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function createJsonFileAdapterV2(opts?: JsonFileV2Options): CanciaStorageV2;
|
|
17
|
+
|
|
18
|
+
declare function createSQLiteAdapter(dbPath?: string): CanciaStorage;
|
|
19
|
+
|
|
20
|
+
interface SqliteV2Options {
|
|
21
|
+
/** Absolute path to the .db file. Defaults to <projectRoot>/cancia.db. */
|
|
22
|
+
dbPath?: string;
|
|
23
|
+
/** Project root. Defaults to process.cwd(). Used to derive dbPath. */
|
|
24
|
+
projectRoot?: string;
|
|
25
|
+
}
|
|
26
|
+
declare function createSqliteAdapterV2(opts?: SqliteV2Options): CanciaStorageV2;
|
|
27
|
+
/**
|
|
28
|
+
* Close and forget the cached connection for a db path (or all connections
|
|
29
|
+
* when no path is given). The store interface has no lifecycle hook, so this
|
|
30
|
+
* is the seam for graceful shutdown and for tests that need to release the
|
|
31
|
+
* file handle before deleting the .db (Windows keeps WAL files locked while
|
|
32
|
+
* the connection is open). No-op if the path was never opened.
|
|
33
|
+
*/
|
|
34
|
+
declare function closeSqliteAdapterV2(dbPath?: string): void;
|
|
35
|
+
|
|
36
|
+
interface GitBackedContentPaths {
|
|
37
|
+
/** KV file path. Default <projectRoot>/cancia-content.json. */
|
|
38
|
+
kvPath?: string;
|
|
39
|
+
/** Pages file path. Default <projectRoot>/.cancia/pages.json. */
|
|
40
|
+
pagesPath?: string;
|
|
41
|
+
/** Lists directory. Default <projectRoot>/.cancia/lists. */
|
|
42
|
+
listsDir?: string;
|
|
43
|
+
}
|
|
44
|
+
interface GitBackedOptions {
|
|
45
|
+
/** The wrapped local adapter — the on-disk source of truth. */
|
|
46
|
+
local: CanciaStorageV2;
|
|
47
|
+
/** "owner/name" of the GitHub repo whose builds carry the content. */
|
|
48
|
+
repo: string;
|
|
49
|
+
/** Branch to commit onto. Default "main". */
|
|
50
|
+
branch?: string;
|
|
51
|
+
/**
|
|
52
|
+
* GitHub PAT. Reads CANCIA_GITHUB_TOKEN if omitted. When absent entirely the
|
|
53
|
+
* adapter runs in local-only mode (disk writes only; no commits) + warns once.
|
|
54
|
+
*/
|
|
55
|
+
token?: string;
|
|
56
|
+
/** Optional committer identity for commits. */
|
|
57
|
+
committer?: GitHubCommitter;
|
|
58
|
+
/**
|
|
59
|
+
* Project root the local adapter writes under — needed to turn absolute
|
|
60
|
+
* on-disk paths into repo-relative commit paths. Default process.cwd().
|
|
61
|
+
*/
|
|
62
|
+
projectRoot?: string;
|
|
63
|
+
/** Override where the local adapter's content lives (must match `local`). */
|
|
64
|
+
contentPaths?: GitBackedContentPaths;
|
|
65
|
+
/** Quiet window (ms) before a flush fires. Default 3000. */
|
|
66
|
+
debounceMs?: number;
|
|
67
|
+
/** Commit message for content updates. */
|
|
68
|
+
commitMessage?: string;
|
|
69
|
+
/** Injected GitHub client (tests pass a mock). Overrides token/fetch. */
|
|
70
|
+
client?: GitHubClient;
|
|
71
|
+
/** Injected fetch, forwarded to the default GitHub client. */
|
|
72
|
+
fetch?: FetchLike;
|
|
73
|
+
/** API base override, forwarded to the default GitHub client (tests). */
|
|
74
|
+
apiBase?: string;
|
|
75
|
+
/** Warn sink (tests capture). Default console.warn. */
|
|
76
|
+
warn?: (msg: string) => void;
|
|
77
|
+
/** Error sink for push failures (tests capture). Default console.error. */
|
|
78
|
+
onError?: (msg: string, err: unknown) => void;
|
|
79
|
+
}
|
|
80
|
+
/** The extra control surface the git adapter adds on top of CanciaStorageV2. */
|
|
81
|
+
interface GitBackedControls {
|
|
82
|
+
/**
|
|
83
|
+
* Force any pending dirty files to commit now, bypassing the debounce.
|
|
84
|
+
* Resolves once the flush completes (or rejects if the push failed — the
|
|
85
|
+
* files stay dirty for the next flush). For tests + graceful shutdown.
|
|
86
|
+
*/
|
|
87
|
+
flush(): Promise<void>;
|
|
88
|
+
/** True if git commits are active (token present). */
|
|
89
|
+
readonly gitEnabled: boolean;
|
|
90
|
+
/** Snapshot of currently-dirty repo-relative paths (for tests/inspection). */
|
|
91
|
+
pendingPaths(): string[];
|
|
92
|
+
}
|
|
93
|
+
type GitBackedStorage = CanciaStorageV2 & {
|
|
94
|
+
git: GitBackedControls;
|
|
95
|
+
};
|
|
96
|
+
declare function createGitBackedAdapter(opts: GitBackedOptions): GitBackedStorage;
|
|
97
|
+
|
|
98
|
+
export { type GitBackedContentPaths as G, type SqliteV2Options as S, type GitBackedControls as a, type GitBackedOptions as b, type GitBackedStorage as c, closeSqliteAdapterV2 as d, createGitBackedAdapter as e, createJsonFileAdapter as f, createJsonFileAdapterV2 as g, createSQLiteAdapter as h, createSqliteAdapterV2 as i };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Minimal fetch signature — matches the global `fetch` we depend on. */
|
|
2
|
+
type FetchLike = (input: string, init?: {
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: string;
|
|
6
|
+
}) => Promise<{
|
|
7
|
+
ok: boolean;
|
|
8
|
+
status: number;
|
|
9
|
+
json(): Promise<unknown>;
|
|
10
|
+
text(): Promise<string>;
|
|
11
|
+
}>;
|
|
12
|
+
interface GitHubCommitter {
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
}
|
|
16
|
+
interface GitHubClientOptions {
|
|
17
|
+
/** "owner/name" */
|
|
18
|
+
repo: string;
|
|
19
|
+
/** Branch to commit onto, e.g. "main". */
|
|
20
|
+
branch: string;
|
|
21
|
+
/** Fine-grained PAT with contents:write on the one repo. */
|
|
22
|
+
token: string;
|
|
23
|
+
/** Optional committer identity. GitHub uses the token's user if omitted. */
|
|
24
|
+
committer?: GitHubCommitter;
|
|
25
|
+
/** Injectable fetch (defaults to global fetch) — tests mock this. */
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
/** API base — defaults to https://api.github.com. Overridable for tests. */
|
|
28
|
+
apiBase?: string;
|
|
29
|
+
}
|
|
30
|
+
interface CommitFile {
|
|
31
|
+
/** Repo-relative path, forward slashes, no leading slash. */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Raw file content (UTF-8 text). Encoded to base64 before PUT. */
|
|
34
|
+
content: string;
|
|
35
|
+
}
|
|
36
|
+
interface GitHubClient {
|
|
37
|
+
/** Current blob sha for `path`, or null if the file doesn't exist yet. */
|
|
38
|
+
getFileSha(path: string): Promise<string | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Commit each file to the branch. Updates pass the current sha; creates omit
|
|
41
|
+
* it. Resolves once every PUT succeeds; rejects (without partial silence) if
|
|
42
|
+
* any PUT fails so the caller can keep the batch dirty and retry.
|
|
43
|
+
*/
|
|
44
|
+
commitFiles(files: CommitFile[], message: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
declare function createGitHubClient(opts: GitHubClientOptions): GitHubClient;
|
|
47
|
+
|
|
48
|
+
export { type CommitFile as C, type FetchLike as F, type GitHubCommitter as G, type GitHubClient as a, type GitHubClientOptions as b, createGitHubClient as c };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { C as CanciaStorage, a as CanciaStorageV2 } from './types-BMlLS-OS.js';
|
|
|
3
3
|
export { b as CanciaKVStore, c as CanciaListStore, d as CanciaPageStore, L as ListEntry, P as PageMeta, e as PageRecord, f as PageSEO, R as Rev, g as RevConflictError } from './types-BMlLS-OS.js';
|
|
4
4
|
import { U as UploadHandler } from './upload-DwCGjXbz.js';
|
|
5
5
|
export { m as makeLocalUploadHandler } from './upload-DwCGjXbz.js';
|
|
6
|
+
import { G as GitHubCommitter } from './github-client-BAZ1pW24.js';
|
|
6
7
|
export { CanciaLoaderOptions, canciaLoader } from './loader/index.js';
|
|
7
8
|
export { FieldDescription, FieldMeta, FieldMetaBase, FieldWidget, ListDescription, ListSchema, SchemasModule, defineField, defineList, describeList } from './schema/index.js';
|
|
8
|
-
export { GitBackedContentPaths, GitBackedControls, GitBackedOptions, GitBackedStorage, createGitBackedAdapter, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter } from './
|
|
9
|
+
export { G as GitBackedContentPaths, a as GitBackedControls, b as GitBackedOptions, c as GitBackedStorage, S as SqliteV2Options, d as closeSqliteAdapterV2, e as createGitBackedAdapter, f as createJsonFileAdapter, g as createJsonFileAdapterV2, h as createSQLiteAdapter, i as createSqliteAdapterV2 } from './git-backed-DFAB0tzf.js';
|
|
9
10
|
export { z } from 'zod';
|
|
10
11
|
import 'astro/loaders';
|
|
11
12
|
import './portable-text-BikSqS9T.js';
|
|
@@ -47,6 +48,17 @@ interface CanciaIntegrationOptions {
|
|
|
47
48
|
accentColor?: string;
|
|
48
49
|
/** Deploy hook URL. Reads CANCIA_DEPLOY_HOOK env var if not set. */
|
|
49
50
|
deployHook?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Auth token for the deploy hook (e.g. Coolify's "auth required" hook).
|
|
53
|
+
* When set, the publish POST sends `Authorization: Bearer <token>`.
|
|
54
|
+
* SECRET — prefer the CANCIA_DEPLOY_HOOK_TOKEN env var (read at runtime,
|
|
55
|
+
* never baked into the build). This option is a dev/testing convenience.
|
|
56
|
+
*/
|
|
57
|
+
deployHookToken?: string;
|
|
58
|
+
/** Optional HTTP method for the deploy hook. Default: "POST". */
|
|
59
|
+
deployHookMethod?: string;
|
|
60
|
+
/** Optional extra headers merged into the deploy-hook request. */
|
|
61
|
+
deployHookHeaders?: Record<string, string>;
|
|
50
62
|
/**
|
|
51
63
|
* Custom storage adapter.
|
|
52
64
|
* Default: SQLite (cancia.db in project root) — works anywhere with Node.
|
|
@@ -89,6 +101,50 @@ interface CanciaIntegrationOptions {
|
|
|
89
101
|
* (named or default) — a record of list name → defineList() output.
|
|
90
102
|
*/
|
|
91
103
|
schemasPath?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Declarative v2 storage backend — bakes SERIALISABLY into the runtime
|
|
106
|
+
* descriptor so a fresh production SSR server reconstructs the same adapter
|
|
107
|
+
* from config + `process.cwd()` (no live object crosses the build→deploy
|
|
108
|
+
* boundary). Prefer this over a live `storageV2` object for production.
|
|
109
|
+
*
|
|
110
|
+
* - `{ kind: "sqlite" }` (recommended) — the everything-store: KV + pages +
|
|
111
|
+
* lists in ONE `.db` per site (default `<root>/cancia.db`; override with
|
|
112
|
+
* `path`, relative paths resolve against the project root). Gitignore the
|
|
113
|
+
* `.db` for webhook-publish sites — it is NOT committed (contrast the
|
|
114
|
+
* git-backed model).
|
|
115
|
+
* - `{ kind: "json-file" }` — the JSON-file v2 default (files under
|
|
116
|
+
* `<root>/.cancia/`).
|
|
117
|
+
*
|
|
118
|
+
* Ignored when `git` is set (git-backed wins) or `storageV2` is passed as a
|
|
119
|
+
* live object (dev uses that eagerly).
|
|
120
|
+
*/
|
|
121
|
+
db?: {
|
|
122
|
+
kind: "sqlite" | "json-file";
|
|
123
|
+
path?: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Git-backed storage, described SERIALISABLY so a production SSR server can
|
|
127
|
+
* reconstruct the adapter in its own (fresh) process from this config + the
|
|
128
|
+
* `CANCIA_GITHUB_TOKEN` env var. Prefer this over passing a live
|
|
129
|
+
* `storageV2: createGitBackedAdapter(...)` object: a live adapter constructed
|
|
130
|
+
* during config evaluation cannot cross the build→deploy process boundary,
|
|
131
|
+
* so it would silently degrade to local-only in production. When `git` is
|
|
132
|
+
* set, Cancia builds the git-backed adapter (wrapping a JSON-file v2 local
|
|
133
|
+
* store) lazily at first request. The token is NEVER baked — it is read from
|
|
134
|
+
* `process.env.CANCIA_GITHUB_TOKEN` at runtime.
|
|
135
|
+
*/
|
|
136
|
+
git?: {
|
|
137
|
+
/** "owner/name" of the GitHub repo whose builds carry the content. */
|
|
138
|
+
repo: string;
|
|
139
|
+
/** Branch to commit onto. Default "main". */
|
|
140
|
+
branch?: string;
|
|
141
|
+
/** Optional committer identity for commits. */
|
|
142
|
+
committer?: GitHubCommitter;
|
|
143
|
+
/** Quiet window (ms) before a flush fires. Default 3000. */
|
|
144
|
+
debounceMs?: number;
|
|
145
|
+
/** Commit message for content updates. */
|
|
146
|
+
commitMessage?: string;
|
|
147
|
+
};
|
|
92
148
|
}
|
|
93
149
|
declare function canciaIntegration(opts?: CanciaIntegrationOptions): AstroIntegration;
|
|
94
150
|
|