@cancia/astro 0.1.0 → 0.2.1
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-5ELSN6LI.js +244 -0
- package/dist/chunk-7MPOERVU.js +245 -0
- package/dist/github-client-BAZ1pW24.d.ts +48 -0
- package/dist/index.d.ts +25 -1
- package/dist/index.js +62 -154
- package/dist/runtime.d.ts +46 -1
- package/dist/runtime.js +7 -1
- package/dist/storage/index.d.ts +65 -1
- package/dist/storage/index.js +6 -0
- package/package.json +1 -1
- package/dist/chunk-DGCGIEFD.js +0 -14
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/storage/github-client.ts
|
|
2
|
+
function toBase64(text) {
|
|
3
|
+
if (typeof Buffer !== "undefined") {
|
|
4
|
+
return Buffer.from(text, "utf-8").toString("base64");
|
|
5
|
+
}
|
|
6
|
+
const bytes = new TextEncoder().encode(text);
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
}
|
|
11
|
+
function createGitHubClient(opts) {
|
|
12
|
+
const { repo, branch, token, committer } = opts;
|
|
13
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
14
|
+
const apiBase = (opts.apiBase ?? "https://api.github.com").replace(/\/$/, "");
|
|
15
|
+
if (!doFetch) {
|
|
16
|
+
throw new Error("createGitHubClient: no fetch available (pass opts.fetch)");
|
|
17
|
+
}
|
|
18
|
+
const headers = () => ({
|
|
19
|
+
Authorization: `Bearer ${token}`,
|
|
20
|
+
Accept: "application/vnd.github+json",
|
|
21
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
22
|
+
});
|
|
23
|
+
const contentsUrl = (path) => {
|
|
24
|
+
const encoded = path.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
25
|
+
return `${apiBase}/repos/${repo}/contents/${encoded}`;
|
|
26
|
+
};
|
|
27
|
+
async function getFileSha(path) {
|
|
28
|
+
const url = `${contentsUrl(path)}?ref=${encodeURIComponent(branch)}`;
|
|
29
|
+
const res = await doFetch(url, { method: "GET", headers: headers() });
|
|
30
|
+
if (res.status === 404) return null;
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const detail = await res.text().catch(() => "");
|
|
33
|
+
throw new Error(`GitHub getFileSha ${path} failed: ${res.status} ${detail}`);
|
|
34
|
+
}
|
|
35
|
+
const body = await res.json();
|
|
36
|
+
return body.sha ?? null;
|
|
37
|
+
}
|
|
38
|
+
async function putFile(file, message) {
|
|
39
|
+
const sha = await getFileSha(file.path);
|
|
40
|
+
const payload = {
|
|
41
|
+
message,
|
|
42
|
+
content: toBase64(file.content),
|
|
43
|
+
branch
|
|
44
|
+
};
|
|
45
|
+
if (sha) payload.sha = sha;
|
|
46
|
+
if (committer) payload.committer = committer;
|
|
47
|
+
const res = await doFetch(contentsUrl(file.path), {
|
|
48
|
+
method: "PUT",
|
|
49
|
+
headers: headers(),
|
|
50
|
+
body: JSON.stringify(payload)
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const detail = await res.text().catch(() => "");
|
|
54
|
+
throw new Error(`GitHub commit ${file.path} failed: ${res.status} ${detail}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function commitFiles(files, message) {
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
await putFile(file, message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { getFileSha, commitFiles };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/storage/git-backed.ts
|
|
66
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
67
|
+
import { join, relative } from "path";
|
|
68
|
+
function createGitBackedAdapter(opts) {
|
|
69
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
70
|
+
const branch = opts.branch ?? "main";
|
|
71
|
+
const debounceMs = opts.debounceMs ?? 3e3;
|
|
72
|
+
const commitMessage = opts.commitMessage ?? "Cancia: content update";
|
|
73
|
+
const warn = opts.warn ?? ((m) => console.warn(m));
|
|
74
|
+
const onError = opts.onError ?? ((m, e) => console.error(m, e));
|
|
75
|
+
const kvPath = opts.contentPaths?.kvPath ?? join(projectRoot, "cancia-content.json");
|
|
76
|
+
const pagesPath = opts.contentPaths?.pagesPath ?? join(projectRoot, ".cancia", "pages.json");
|
|
77
|
+
const listsDir = opts.contentPaths?.listsDir ?? join(projectRoot, ".cancia", "lists");
|
|
78
|
+
const token = opts.token ?? process.env.CANCIA_GITHUB_TOKEN ?? "";
|
|
79
|
+
let client = null;
|
|
80
|
+
if (opts.client) {
|
|
81
|
+
client = opts.client;
|
|
82
|
+
} else if (token) {
|
|
83
|
+
client = createGitHubClient({
|
|
84
|
+
repo: opts.repo,
|
|
85
|
+
branch,
|
|
86
|
+
token,
|
|
87
|
+
committer: opts.committer,
|
|
88
|
+
fetch: opts.fetch,
|
|
89
|
+
apiBase: opts.apiBase
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const gitEnabled = client !== null;
|
|
93
|
+
if (!gitEnabled) {
|
|
94
|
+
warn(
|
|
95
|
+
"[cancia] Git-backed storage: no GitHub token (CANCIA_GITHUB_TOKEN) \u2014 running local-only. Edits save to disk but are NOT committed/pushed."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
99
|
+
let timer = null;
|
|
100
|
+
let flushing = null;
|
|
101
|
+
let rerunRequested = false;
|
|
102
|
+
function toRepoPath(absPath) {
|
|
103
|
+
return relative(projectRoot, absPath).split("\\").join("/");
|
|
104
|
+
}
|
|
105
|
+
function markDirty(absPath) {
|
|
106
|
+
dirty.add(absPath);
|
|
107
|
+
}
|
|
108
|
+
function markListDirty(listName, site) {
|
|
109
|
+
const siteDir = join(listsDir, listName, site);
|
|
110
|
+
if (!existsSync(siteDir)) return;
|
|
111
|
+
const walk = (dir) => {
|
|
112
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
113
|
+
const full = join(dir, entry.name);
|
|
114
|
+
if (entry.isDirectory()) walk(full);
|
|
115
|
+
else if (entry.isFile()) markDirty(full);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
walk(siteDir);
|
|
119
|
+
}
|
|
120
|
+
function scheduleFlush() {
|
|
121
|
+
if (!gitEnabled) return;
|
|
122
|
+
if (timer) clearTimeout(timer);
|
|
123
|
+
timer = setTimeout(() => {
|
|
124
|
+
timer = null;
|
|
125
|
+
void runFlush();
|
|
126
|
+
}, debounceMs);
|
|
127
|
+
}
|
|
128
|
+
async function runFlush() {
|
|
129
|
+
if (flushing) {
|
|
130
|
+
rerunRequested = true;
|
|
131
|
+
return flushing;
|
|
132
|
+
}
|
|
133
|
+
flushing = doFlush().finally(() => {
|
|
134
|
+
flushing = null;
|
|
135
|
+
if (rerunRequested) {
|
|
136
|
+
rerunRequested = false;
|
|
137
|
+
void runFlush();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return flushing;
|
|
141
|
+
}
|
|
142
|
+
async function doFlush() {
|
|
143
|
+
if (!client || dirty.size === 0) return;
|
|
144
|
+
const batch = [...dirty];
|
|
145
|
+
const files = [];
|
|
146
|
+
for (const abs of batch) {
|
|
147
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) continue;
|
|
148
|
+
files.push({ path: toRepoPath(abs), content: readFileSync(abs, "utf-8") });
|
|
149
|
+
}
|
|
150
|
+
if (files.length === 0) {
|
|
151
|
+
for (const abs of batch) dirty.delete(abs);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
await client.commitFiles(files, commitMessage);
|
|
156
|
+
for (const abs of batch) dirty.delete(abs);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
onError(
|
|
159
|
+
"[cancia] Git-backed storage: commit failed \u2014 data saved locally, will retry on next flush.",
|
|
160
|
+
err
|
|
161
|
+
);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function flush() {
|
|
166
|
+
if (!gitEnabled) return;
|
|
167
|
+
if (timer) {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
timer = null;
|
|
170
|
+
}
|
|
171
|
+
await runFlush();
|
|
172
|
+
}
|
|
173
|
+
const kv = {
|
|
174
|
+
get: (site, key, lang) => opts.local.kv.get(site, key, lang),
|
|
175
|
+
getAll: (site) => opts.local.kv.getAll(site),
|
|
176
|
+
async set(site, key, lang, value) {
|
|
177
|
+
await opts.local.kv.set(site, key, lang, value);
|
|
178
|
+
markDirty(kvPath);
|
|
179
|
+
scheduleFlush();
|
|
180
|
+
},
|
|
181
|
+
async delete(site, key, lang) {
|
|
182
|
+
await opts.local.kv.delete(site, key, lang);
|
|
183
|
+
markDirty(kvPath);
|
|
184
|
+
scheduleFlush();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const pages = {
|
|
188
|
+
get: (site, route) => opts.local.pages.get(site, route),
|
|
189
|
+
list: (site) => opts.local.pages.list(site),
|
|
190
|
+
async set(site, route, meta, rev) {
|
|
191
|
+
const result = await opts.local.pages.set(site, route, meta, rev);
|
|
192
|
+
markDirty(pagesPath);
|
|
193
|
+
scheduleFlush();
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
async delete(site, route) {
|
|
197
|
+
await opts.local.pages.delete(site, route);
|
|
198
|
+
markDirty(pagesPath);
|
|
199
|
+
scheduleFlush();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const lists = {
|
|
203
|
+
list: (site, listName, locale) => opts.local.lists.list(site, listName, locale),
|
|
204
|
+
get: (site, listName, id, locale) => opts.local.lists.get(site, listName, id, locale),
|
|
205
|
+
translations: (site, listName) => opts.local.lists.translations(site, listName),
|
|
206
|
+
async create(site, listName, data, locale, id) {
|
|
207
|
+
const entry = await opts.local.lists.create(site, listName, data, locale, id);
|
|
208
|
+
markListDirty(listName, site);
|
|
209
|
+
scheduleFlush();
|
|
210
|
+
return entry;
|
|
211
|
+
},
|
|
212
|
+
async update(site, listName, id, locale, data, rev) {
|
|
213
|
+
const entry = await opts.local.lists.update(site, listName, id, locale, data, rev);
|
|
214
|
+
markListDirty(listName, site);
|
|
215
|
+
scheduleFlush();
|
|
216
|
+
return entry;
|
|
217
|
+
},
|
|
218
|
+
async delete(site, listName, id, locale) {
|
|
219
|
+
await opts.local.lists.delete(site, listName, id, locale);
|
|
220
|
+
markListDirty(listName, site);
|
|
221
|
+
scheduleFlush();
|
|
222
|
+
},
|
|
223
|
+
async reorder(site, listName, ids) {
|
|
224
|
+
await opts.local.lists.reorder(site, listName, ids);
|
|
225
|
+
markListDirty(listName, site);
|
|
226
|
+
scheduleFlush();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const git = {
|
|
230
|
+
flush,
|
|
231
|
+
get gitEnabled() {
|
|
232
|
+
return gitEnabled;
|
|
233
|
+
},
|
|
234
|
+
pendingPaths() {
|
|
235
|
+
return [...dirty].map(toRepoPath);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return { kv, pages, lists, git };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export {
|
|
242
|
+
createGitHubClient,
|
|
243
|
+
createGitBackedAdapter
|
|
244
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGitBackedAdapter
|
|
3
|
+
} from "./chunk-5ELSN6LI.js";
|
|
4
|
+
import {
|
|
5
|
+
createJsonFileAdapter,
|
|
6
|
+
createJsonFileAdapterV2
|
|
7
|
+
} from "./chunk-ST44VULL.js";
|
|
8
|
+
import {
|
|
9
|
+
detectImageType,
|
|
10
|
+
isValidSite
|
|
11
|
+
} from "./chunk-5IPHDIC6.js";
|
|
12
|
+
|
|
13
|
+
// src/routes/upload.ts
|
|
14
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
function makeLocalUploadHandler(opts) {
|
|
18
|
+
const uploadDir = opts.uploadDir ?? join(process.cwd(), "public/uploads");
|
|
19
|
+
const publicUrlBase = opts.publicUrlBase ?? "/uploads";
|
|
20
|
+
const maxBytes = (opts.maxMB ?? 10) * 1024 * 1024;
|
|
21
|
+
return async (file, site, detected) => {
|
|
22
|
+
const dest = join(uploadDir, site);
|
|
23
|
+
await mkdir(dest, { recursive: true });
|
|
24
|
+
const name = `${randomUUID()}.${detected.ext}`;
|
|
25
|
+
await writeFile(join(dest, name), Buffer.from(await file.arrayBuffer()));
|
|
26
|
+
return `${publicUrlBase}/${site}/${name}`;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function makeUploadRoute(uploadHandler, secret, maxMB = 10) {
|
|
30
|
+
return async ({ request }) => {
|
|
31
|
+
if (secret) {
|
|
32
|
+
const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
|
|
33
|
+
if (token !== secret) {
|
|
34
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const form = await request.formData().catch(() => null);
|
|
38
|
+
const file = form?.get("file");
|
|
39
|
+
const site = form?.get("site");
|
|
40
|
+
if (!(file instanceof File)) {
|
|
41
|
+
return new Response(JSON.stringify({ error: "Missing file field" }), { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
if (typeof site !== "string" || !isValidSite(site)) {
|
|
44
|
+
return new Response(
|
|
45
|
+
JSON.stringify({ error: "Invalid site (allowed: a-z, 0-9, and . _ -; must start alphanumeric)" }),
|
|
46
|
+
{ status: 400 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (file.size > maxMB * 1024 * 1024) {
|
|
50
|
+
return new Response(JSON.stringify({ error: `File too large (max ${maxMB}MB)` }), { status: 413 });
|
|
51
|
+
}
|
|
52
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
53
|
+
const detected = detectImageType(buf);
|
|
54
|
+
if (!detected) {
|
|
55
|
+
return new Response(JSON.stringify({ error: "File type not allowed" }), { status: 415 });
|
|
56
|
+
}
|
|
57
|
+
const url = await uploadHandler(file, site, detected);
|
|
58
|
+
return new Response(JSON.stringify({ url }), {
|
|
59
|
+
headers: { "Content-Type": "application/json" }
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/routes/upload-r2.ts
|
|
65
|
+
import { createHmac, createHash } from "crypto";
|
|
66
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
67
|
+
function sha256hex(data) {
|
|
68
|
+
return createHash("sha256").update(data).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
function hmacSha256(key, data) {
|
|
71
|
+
return createHmac("sha256", key).update(data).digest();
|
|
72
|
+
}
|
|
73
|
+
function getSigningKey(secretKey, date, region, service) {
|
|
74
|
+
const kDate = hmacSha256(Buffer.from(`AWS4${secretKey}`, "utf8"), date);
|
|
75
|
+
const kRegion = hmacSha256(kDate, region);
|
|
76
|
+
const kService = hmacSha256(kRegion, service);
|
|
77
|
+
const kSigning = hmacSha256(kService, "aws4_request");
|
|
78
|
+
return kSigning;
|
|
79
|
+
}
|
|
80
|
+
async function signedPutRequest(opts) {
|
|
81
|
+
const { endpoint, bucket, key, body, contentType, accessKeyId, secretAccessKey } = opts;
|
|
82
|
+
const region = "auto";
|
|
83
|
+
const service = "s3";
|
|
84
|
+
const now = /* @__PURE__ */ new Date();
|
|
85
|
+
const isoDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "").slice(0, 15) + "Z";
|
|
86
|
+
const shortDate = isoDate.slice(0, 8);
|
|
87
|
+
const url = `${endpoint}/${bucket}/${key}`;
|
|
88
|
+
const host = new URL(endpoint).host;
|
|
89
|
+
const payloadHash = sha256hex(body);
|
|
90
|
+
const headers = {
|
|
91
|
+
"content-type": contentType,
|
|
92
|
+
"host": host,
|
|
93
|
+
"x-amz-content-sha256": payloadHash,
|
|
94
|
+
"x-amz-date": isoDate
|
|
95
|
+
};
|
|
96
|
+
const signedHeaders = Object.keys(headers).sort().join(";");
|
|
97
|
+
const canonicalHeaders = Object.keys(headers).sort().map((k) => `${k}:${headers[k]}
|
|
98
|
+
`).join("");
|
|
99
|
+
const canonicalRequest = [
|
|
100
|
+
"PUT",
|
|
101
|
+
`/${bucket}/${key}`,
|
|
102
|
+
"",
|
|
103
|
+
canonicalHeaders,
|
|
104
|
+
signedHeaders,
|
|
105
|
+
payloadHash
|
|
106
|
+
].join("\n");
|
|
107
|
+
const credentialScope = `${shortDate}/${region}/${service}/aws4_request`;
|
|
108
|
+
const stringToSign = [
|
|
109
|
+
"AWS4-HMAC-SHA256",
|
|
110
|
+
isoDate,
|
|
111
|
+
credentialScope,
|
|
112
|
+
sha256hex(canonicalRequest)
|
|
113
|
+
].join("\n");
|
|
114
|
+
const signingKey = getSigningKey(secretAccessKey, shortDate, region, service);
|
|
115
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
116
|
+
const authHeader = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
117
|
+
let res;
|
|
118
|
+
try {
|
|
119
|
+
res = await fetch(url, {
|
|
120
|
+
method: "PUT",
|
|
121
|
+
headers: { ...headers, Authorization: authHeader },
|
|
122
|
+
body: new Uint8Array(body)
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`R2 upload: network error reaching ${url} \u2014 ${err}`);
|
|
126
|
+
}
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const text = await res.text().catch(() => res.statusText);
|
|
129
|
+
throw new Error(`R2 upload failed (${res.status}): ${text}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function makeR2UploadHandler(opts) {
|
|
133
|
+
const {
|
|
134
|
+
accountId,
|
|
135
|
+
bucket,
|
|
136
|
+
accessKeyId,
|
|
137
|
+
secretAccessKey,
|
|
138
|
+
publicUrl,
|
|
139
|
+
prefix = "uploads"
|
|
140
|
+
} = opts;
|
|
141
|
+
const endpoint = `https://${accountId}.r2.cloudflarestorage.com`;
|
|
142
|
+
return async (file, _site, detected) => {
|
|
143
|
+
const key = `${prefix}/${randomUUID2()}.${detected.ext}`;
|
|
144
|
+
const body = Buffer.from(await file.arrayBuffer());
|
|
145
|
+
await signedPutRequest({
|
|
146
|
+
endpoint,
|
|
147
|
+
bucket,
|
|
148
|
+
key,
|
|
149
|
+
body,
|
|
150
|
+
contentType: file.type,
|
|
151
|
+
accessKeyId,
|
|
152
|
+
secretAccessKey
|
|
153
|
+
});
|
|
154
|
+
return `${publicUrl.replace(/\/$/, "")}/${key}`;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/runtime.ts
|
|
159
|
+
var _runtime = null;
|
|
160
|
+
var _bakedConfig = null;
|
|
161
|
+
function setBakedConfig(config) {
|
|
162
|
+
_bakedConfig = config;
|
|
163
|
+
}
|
|
164
|
+
function setCanciaRuntime(runtime) {
|
|
165
|
+
_runtime = runtime;
|
|
166
|
+
}
|
|
167
|
+
function buildRuntimeFromBaked(baked) {
|
|
168
|
+
const projectRoot = baked.projectRoot || process.cwd();
|
|
169
|
+
const token = process.env.CANCIA_TOKEN?.trim() || "";
|
|
170
|
+
const secret = baked.public ? void 0 : token || void 0;
|
|
171
|
+
const deployHook = process.env.CANCIA_DEPLOY_HOOK || void 0;
|
|
172
|
+
const storageV2 = buildStorageV2(baked, projectRoot);
|
|
173
|
+
const storage = storageV2 ? storageV2.kv : lazyJsonFileAdapter(projectRoot);
|
|
174
|
+
const uploadHandler = buildUploadHandler(baked, projectRoot);
|
|
175
|
+
return {
|
|
176
|
+
storage,
|
|
177
|
+
storageV2,
|
|
178
|
+
projectRoot,
|
|
179
|
+
schemasPath: baked.schemasPath,
|
|
180
|
+
defaultLocale: baked.defaultLocale,
|
|
181
|
+
locales: baked.locales,
|
|
182
|
+
secret,
|
|
183
|
+
uploadHandler,
|
|
184
|
+
deployHook,
|
|
185
|
+
maxUploadMB: baked.maxUploadMB
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function lazyJsonFileAdapter(projectRoot) {
|
|
189
|
+
return createJsonFileAdapter(projectRoot + "/cancia-content.json");
|
|
190
|
+
}
|
|
191
|
+
function buildStorageV2(baked, projectRoot) {
|
|
192
|
+
const desc = baked.storage;
|
|
193
|
+
if (!desc) return void 0;
|
|
194
|
+
if (desc.kind === "git-backed") {
|
|
195
|
+
const local = createJsonFileAdapterV2({ projectRoot });
|
|
196
|
+
return createGitBackedAdapter({
|
|
197
|
+
local,
|
|
198
|
+
repo: desc.repo,
|
|
199
|
+
branch: desc.branch,
|
|
200
|
+
token: process.env.CANCIA_GITHUB_TOKEN,
|
|
201
|
+
committer: desc.committer,
|
|
202
|
+
projectRoot,
|
|
203
|
+
debounceMs: desc.debounceMs,
|
|
204
|
+
commitMessage: desc.commitMessage
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return createJsonFileAdapterV2({ projectRoot });
|
|
208
|
+
}
|
|
209
|
+
function buildUploadHandler(baked, projectRoot) {
|
|
210
|
+
if (baked.r2) {
|
|
211
|
+
const env = process.env;
|
|
212
|
+
const resolved = {
|
|
213
|
+
accountId: baked.r2.accountId ?? env.R2_ACCOUNT_ID ?? "",
|
|
214
|
+
bucket: baked.r2.bucket ?? env.R2_BUCKET_NAME ?? env.R2_BUCKET ?? "",
|
|
215
|
+
accessKeyId: env.R2_ACCESS_KEY_ID ?? "",
|
|
216
|
+
secretAccessKey: env.R2_SECRET_ACCESS_KEY ?? "",
|
|
217
|
+
publicUrl: baked.r2.publicUrl ?? env.R2_PUBLIC_URL ?? "",
|
|
218
|
+
prefix: baked.r2.prefix
|
|
219
|
+
};
|
|
220
|
+
return makeR2UploadHandler(resolved);
|
|
221
|
+
}
|
|
222
|
+
return makeLocalUploadHandler({
|
|
223
|
+
uploadDir: projectRoot + "/public/uploads",
|
|
224
|
+
maxMB: baked.maxUploadMB
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function getCanciaRuntime() {
|
|
228
|
+
if (_runtime) return _runtime;
|
|
229
|
+
if (_bakedConfig) {
|
|
230
|
+
_runtime = buildRuntimeFromBaked(_bakedConfig);
|
|
231
|
+
return _runtime;
|
|
232
|
+
}
|
|
233
|
+
throw new Error(
|
|
234
|
+
"[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."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
makeLocalUploadHandler,
|
|
240
|
+
makeUploadRoute,
|
|
241
|
+
makeR2UploadHandler,
|
|
242
|
+
setBakedConfig,
|
|
243
|
+
setCanciaRuntime,
|
|
244
|
+
getCanciaRuntime
|
|
245
|
+
};
|
|
@@ -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 { createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter } from './storage/index.js';
|
|
9
|
+
export { GitBackedContentPaths, GitBackedControls, GitBackedOptions, GitBackedStorage, createGitBackedAdapter, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter } from './storage/index.js';
|
|
9
10
|
export { z } from 'zod';
|
|
10
11
|
import 'astro/loaders';
|
|
11
12
|
import './portable-text-BikSqS9T.js';
|
|
@@ -89,6 +90,29 @@ interface CanciaIntegrationOptions {
|
|
|
89
90
|
* (named or default) — a record of list name → defineList() output.
|
|
90
91
|
*/
|
|
91
92
|
schemasPath?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Git-backed storage, described SERIALISABLY so a production SSR server can
|
|
95
|
+
* reconstruct the adapter in its own (fresh) process from this config + the
|
|
96
|
+
* `CANCIA_GITHUB_TOKEN` env var. Prefer this over passing a live
|
|
97
|
+
* `storageV2: createGitBackedAdapter(...)` object: a live adapter constructed
|
|
98
|
+
* during config evaluation cannot cross the build→deploy process boundary,
|
|
99
|
+
* so it would silently degrade to local-only in production. When `git` is
|
|
100
|
+
* set, Cancia builds the git-backed adapter (wrapping a JSON-file v2 local
|
|
101
|
+
* store) lazily at first request. The token is NEVER baked — it is read from
|
|
102
|
+
* `process.env.CANCIA_GITHUB_TOKEN` at runtime.
|
|
103
|
+
*/
|
|
104
|
+
git?: {
|
|
105
|
+
/** "owner/name" of the GitHub repo whose builds carry the content. */
|
|
106
|
+
repo: string;
|
|
107
|
+
/** Branch to commit onto. Default "main". */
|
|
108
|
+
branch?: string;
|
|
109
|
+
/** Optional committer identity for commits. */
|
|
110
|
+
committer?: GitHubCommitter;
|
|
111
|
+
/** Quiet window (ms) before a flush fires. Default 3000. */
|
|
112
|
+
debounceMs?: number;
|
|
113
|
+
/** Commit message for content updates. */
|
|
114
|
+
commitMessage?: string;
|
|
115
|
+
};
|
|
92
116
|
}
|
|
93
117
|
declare function canciaIntegration(opts?: CanciaIntegrationOptions): AstroIntegration;
|
|
94
118
|
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
} from "./chunk-IIGDU5SV.js";
|
|
7
7
|
import "./chunk-NG5GJME5.js";
|
|
8
8
|
import {
|
|
9
|
+
makeLocalUploadHandler,
|
|
10
|
+
makeR2UploadHandler,
|
|
11
|
+
makeUploadRoute,
|
|
9
12
|
setCanciaRuntime
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-7MPOERVU.js";
|
|
11
14
|
import {
|
|
12
15
|
defineField,
|
|
13
16
|
defineList,
|
|
@@ -20,6 +23,9 @@ import {
|
|
|
20
23
|
import {
|
|
21
24
|
createSQLiteAdapter
|
|
22
25
|
} from "./chunk-AE4SIY24.js";
|
|
26
|
+
import {
|
|
27
|
+
createGitBackedAdapter
|
|
28
|
+
} from "./chunk-5ELSN6LI.js";
|
|
23
29
|
import {
|
|
24
30
|
createJsonFileAdapter,
|
|
25
31
|
createJsonFileAdapterV2
|
|
@@ -28,14 +34,11 @@ import {
|
|
|
28
34
|
RevConflictError
|
|
29
35
|
} from "./chunk-7IA5B5CF.js";
|
|
30
36
|
import "./chunk-BOIQNZAO.js";
|
|
31
|
-
import
|
|
32
|
-
detectImageType,
|
|
33
|
-
isValidSite
|
|
34
|
-
} from "./chunk-5IPHDIC6.js";
|
|
37
|
+
import "./chunk-5IPHDIC6.js";
|
|
35
38
|
|
|
36
39
|
// src/integration.ts
|
|
37
40
|
import { loadEnv } from "vite";
|
|
38
|
-
import { fileURLToPath } from "url";
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
39
42
|
|
|
40
43
|
// src/routes/content.ts
|
|
41
44
|
function makeContentRoute(storage, secret) {
|
|
@@ -83,57 +86,6 @@ function makeContentRoute(storage, secret) {
|
|
|
83
86
|
};
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
// src/routes/upload.ts
|
|
87
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
88
|
-
import { join } from "path";
|
|
89
|
-
import { randomUUID } from "crypto";
|
|
90
|
-
function makeLocalUploadHandler(opts) {
|
|
91
|
-
const uploadDir = opts.uploadDir ?? join(process.cwd(), "public/uploads");
|
|
92
|
-
const publicUrlBase = opts.publicUrlBase ?? "/uploads";
|
|
93
|
-
const maxBytes = (opts.maxMB ?? 10) * 1024 * 1024;
|
|
94
|
-
return async (file, site, detected) => {
|
|
95
|
-
const dest = join(uploadDir, site);
|
|
96
|
-
await mkdir(dest, { recursive: true });
|
|
97
|
-
const name = `${randomUUID()}.${detected.ext}`;
|
|
98
|
-
await writeFile(join(dest, name), Buffer.from(await file.arrayBuffer()));
|
|
99
|
-
return `${publicUrlBase}/${site}/${name}`;
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function makeUploadRoute(uploadHandler, secret, maxMB = 10) {
|
|
103
|
-
return async ({ request }) => {
|
|
104
|
-
if (secret) {
|
|
105
|
-
const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
|
|
106
|
-
if (token !== secret) {
|
|
107
|
-
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const form = await request.formData().catch(() => null);
|
|
111
|
-
const file = form?.get("file");
|
|
112
|
-
const site = form?.get("site");
|
|
113
|
-
if (!(file instanceof File)) {
|
|
114
|
-
return new Response(JSON.stringify({ error: "Missing file field" }), { status: 400 });
|
|
115
|
-
}
|
|
116
|
-
if (typeof site !== "string" || !isValidSite(site)) {
|
|
117
|
-
return new Response(
|
|
118
|
-
JSON.stringify({ error: "Invalid site (allowed: a-z, 0-9, and . _ -; must start alphanumeric)" }),
|
|
119
|
-
{ status: 400 }
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
if (file.size > maxMB * 1024 * 1024) {
|
|
123
|
-
return new Response(JSON.stringify({ error: `File too large (max ${maxMB}MB)` }), { status: 413 });
|
|
124
|
-
}
|
|
125
|
-
const buf = new Uint8Array(await file.arrayBuffer());
|
|
126
|
-
const detected = detectImageType(buf);
|
|
127
|
-
if (!detected) {
|
|
128
|
-
return new Response(JSON.stringify({ error: "File type not allowed" }), { status: 415 });
|
|
129
|
-
}
|
|
130
|
-
const url = await uploadHandler(file, site, detected);
|
|
131
|
-
return new Response(JSON.stringify({ url }), {
|
|
132
|
-
headers: { "Content-Type": "application/json" }
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
89
|
// src/routes/publish.ts
|
|
138
90
|
function makePublishRoute(deployHook, secret) {
|
|
139
91
|
return async ({ request }) => {
|
|
@@ -226,109 +178,15 @@ function makeAuthRoute(secret) {
|
|
|
226
178
|
};
|
|
227
179
|
}
|
|
228
180
|
|
|
229
|
-
// src/routes/upload-r2.ts
|
|
230
|
-
import { createHmac, createHash } from "crypto";
|
|
231
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
232
|
-
function sha256hex(data) {
|
|
233
|
-
return createHash("sha256").update(data).digest("hex");
|
|
234
|
-
}
|
|
235
|
-
function hmacSha256(key, data) {
|
|
236
|
-
return createHmac("sha256", key).update(data).digest();
|
|
237
|
-
}
|
|
238
|
-
function getSigningKey(secretKey, date, region, service) {
|
|
239
|
-
const kDate = hmacSha256(Buffer.from(`AWS4${secretKey}`, "utf8"), date);
|
|
240
|
-
const kRegion = hmacSha256(kDate, region);
|
|
241
|
-
const kService = hmacSha256(kRegion, service);
|
|
242
|
-
const kSigning = hmacSha256(kService, "aws4_request");
|
|
243
|
-
return kSigning;
|
|
244
|
-
}
|
|
245
|
-
async function signedPutRequest(opts) {
|
|
246
|
-
const { endpoint, bucket, key, body, contentType, accessKeyId, secretAccessKey } = opts;
|
|
247
|
-
const region = "auto";
|
|
248
|
-
const service = "s3";
|
|
249
|
-
const now = /* @__PURE__ */ new Date();
|
|
250
|
-
const isoDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "").slice(0, 15) + "Z";
|
|
251
|
-
const shortDate = isoDate.slice(0, 8);
|
|
252
|
-
const url = `${endpoint}/${bucket}/${key}`;
|
|
253
|
-
const host = new URL(endpoint).host;
|
|
254
|
-
const payloadHash = sha256hex(body);
|
|
255
|
-
const headers = {
|
|
256
|
-
"content-type": contentType,
|
|
257
|
-
"host": host,
|
|
258
|
-
"x-amz-content-sha256": payloadHash,
|
|
259
|
-
"x-amz-date": isoDate
|
|
260
|
-
};
|
|
261
|
-
const signedHeaders = Object.keys(headers).sort().join(";");
|
|
262
|
-
const canonicalHeaders = Object.keys(headers).sort().map((k) => `${k}:${headers[k]}
|
|
263
|
-
`).join("");
|
|
264
|
-
const canonicalRequest = [
|
|
265
|
-
"PUT",
|
|
266
|
-
`/${bucket}/${key}`,
|
|
267
|
-
"",
|
|
268
|
-
canonicalHeaders,
|
|
269
|
-
signedHeaders,
|
|
270
|
-
payloadHash
|
|
271
|
-
].join("\n");
|
|
272
|
-
const credentialScope = `${shortDate}/${region}/${service}/aws4_request`;
|
|
273
|
-
const stringToSign = [
|
|
274
|
-
"AWS4-HMAC-SHA256",
|
|
275
|
-
isoDate,
|
|
276
|
-
credentialScope,
|
|
277
|
-
sha256hex(canonicalRequest)
|
|
278
|
-
].join("\n");
|
|
279
|
-
const signingKey = getSigningKey(secretAccessKey, shortDate, region, service);
|
|
280
|
-
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
281
|
-
const authHeader = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
282
|
-
let res;
|
|
283
|
-
try {
|
|
284
|
-
res = await fetch(url, {
|
|
285
|
-
method: "PUT",
|
|
286
|
-
headers: { ...headers, Authorization: authHeader },
|
|
287
|
-
body: new Uint8Array(body)
|
|
288
|
-
});
|
|
289
|
-
} catch (err) {
|
|
290
|
-
throw new Error(`R2 upload: network error reaching ${url} \u2014 ${err}`);
|
|
291
|
-
}
|
|
292
|
-
if (!res.ok) {
|
|
293
|
-
const text = await res.text().catch(() => res.statusText);
|
|
294
|
-
throw new Error(`R2 upload failed (${res.status}): ${text}`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function makeR2UploadHandler(opts) {
|
|
298
|
-
const {
|
|
299
|
-
accountId,
|
|
300
|
-
bucket,
|
|
301
|
-
accessKeyId,
|
|
302
|
-
secretAccessKey,
|
|
303
|
-
publicUrl,
|
|
304
|
-
prefix = "uploads"
|
|
305
|
-
} = opts;
|
|
306
|
-
const endpoint = `https://${accountId}.r2.cloudflarestorage.com`;
|
|
307
|
-
return async (file, _site, detected) => {
|
|
308
|
-
const key = `${prefix}/${randomUUID2()}.${detected.ext}`;
|
|
309
|
-
const body = Buffer.from(await file.arrayBuffer());
|
|
310
|
-
await signedPutRequest({
|
|
311
|
-
endpoint,
|
|
312
|
-
bucket,
|
|
313
|
-
key,
|
|
314
|
-
body,
|
|
315
|
-
contentType: file.type,
|
|
316
|
-
accessKeyId,
|
|
317
|
-
secretAccessKey
|
|
318
|
-
});
|
|
319
|
-
return `${publicUrl.replace(/\/$/, "")}/${key}`;
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
181
|
// src/token.ts
|
|
324
182
|
import { randomBytes } from "crypto";
|
|
325
183
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
326
|
-
import { join
|
|
184
|
+
import { join } from "path";
|
|
327
185
|
function generateToken() {
|
|
328
186
|
return randomBytes(32).toString("hex");
|
|
329
187
|
}
|
|
330
188
|
function ensureToken(rootPath) {
|
|
331
|
-
const envPath =
|
|
189
|
+
const envPath = join(rootPath, ".env");
|
|
332
190
|
let contents = "";
|
|
333
191
|
if (existsSync(envPath)) {
|
|
334
192
|
contents = readFileSync(envPath, "utf-8");
|
|
@@ -355,6 +213,7 @@ function canciaIntegration(opts = {}) {
|
|
|
355
213
|
let resolvedUploadHandler;
|
|
356
214
|
let resolvedDeployHook;
|
|
357
215
|
let resolvedLocales = ["en"];
|
|
216
|
+
let bakedConfig = null;
|
|
358
217
|
return {
|
|
359
218
|
name: "@cancia/astro",
|
|
360
219
|
hooks: {
|
|
@@ -384,6 +243,42 @@ function canciaIntegration(opts = {}) {
|
|
|
384
243
|
\x1B[32mcancia\x1B[0m No CANCIA_TOKEN found \u2014 generated one for you.`);
|
|
385
244
|
}
|
|
386
245
|
const hasDeployHook = !!(opts.deployHook ?? env.CANCIA_DEPLOY_HOOK ?? process.env.CANCIA_DEPLOY_HOOK);
|
|
246
|
+
let storageDescriptor;
|
|
247
|
+
if (opts.git) {
|
|
248
|
+
storageDescriptor = {
|
|
249
|
+
kind: "git-backed",
|
|
250
|
+
repo: opts.git.repo,
|
|
251
|
+
branch: opts.git.branch,
|
|
252
|
+
committer: opts.git.committer,
|
|
253
|
+
debounceMs: opts.git.debounceMs,
|
|
254
|
+
commitMessage: opts.git.commitMessage
|
|
255
|
+
};
|
|
256
|
+
} else if (opts.storageV2 === null) {
|
|
257
|
+
storageDescriptor = void 0;
|
|
258
|
+
} else {
|
|
259
|
+
storageDescriptor = { kind: "json-file", dbPath: opts.dbPath };
|
|
260
|
+
}
|
|
261
|
+
let r2Baked;
|
|
262
|
+
if (opts.r2) {
|
|
263
|
+
const r2Opts = typeof opts.r2 === "function" ? opts.r2() : opts.r2;
|
|
264
|
+
r2Baked = {
|
|
265
|
+
accountId: r2Opts.accountId,
|
|
266
|
+
bucket: r2Opts.bucket,
|
|
267
|
+
publicUrl: r2Opts.publicUrl,
|
|
268
|
+
prefix: r2Opts.prefix
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
bakedConfig = {
|
|
272
|
+
projectRoot: resolvedRootPath,
|
|
273
|
+
schemasPath: opts.schemasPath,
|
|
274
|
+
defaultLocale: resolvedLocales[0],
|
|
275
|
+
locales: resolvedLocales,
|
|
276
|
+
maxUploadMB: opts.maxUploadMB ?? 10,
|
|
277
|
+
public: opts.public ?? false,
|
|
278
|
+
hasDeployHook,
|
|
279
|
+
storage: storageDescriptor,
|
|
280
|
+
r2: r2Baked
|
|
281
|
+
};
|
|
387
282
|
injectScript(
|
|
388
283
|
"head-inline",
|
|
389
284
|
`window.__CANCIA__=${JSON.stringify({
|
|
@@ -409,6 +304,8 @@ function canciaIntegration(opts = {}) {
|
|
|
409
304
|
injectRoute({ pattern: "/api/cancia/schemas", entrypoint: endpointPath("schemas"), prerender: false });
|
|
410
305
|
injectRoute({ pattern: "/api/cancia/lists/[listName]", entrypoint: endpointPath("lists"), prerender: false });
|
|
411
306
|
injectRoute({ pattern: "/api/cancia/lists/[listName]/[id]", entrypoint: endpointPath("lists"), prerender: false });
|
|
307
|
+
const runtimeModulePath = fileURLToPath(new URL("./runtime.js", import.meta.url));
|
|
308
|
+
const RESOLVED_VIRTUAL_ID = "\0virtual:cancia/runtime";
|
|
412
309
|
updateConfig({
|
|
413
310
|
vite: {
|
|
414
311
|
plugins: [
|
|
@@ -416,8 +313,18 @@ function canciaIntegration(opts = {}) {
|
|
|
416
313
|
name: "vite-plugin-cancia-runtime",
|
|
417
314
|
resolveId(id) {
|
|
418
315
|
if (id === "virtual:cancia/runtime") {
|
|
419
|
-
return
|
|
316
|
+
return RESOLVED_VIRTUAL_ID;
|
|
420
317
|
}
|
|
318
|
+
},
|
|
319
|
+
load(id) {
|
|
320
|
+
if (id !== RESOLVED_VIRTUAL_ID) return;
|
|
321
|
+
const importUrl = pathToFileURL(runtimeModulePath).href;
|
|
322
|
+
const bakedLiteral = JSON.stringify(bakedConfig ?? null);
|
|
323
|
+
return [
|
|
324
|
+
`import { setBakedConfig, getCanciaRuntime } from ${JSON.stringify(importUrl)};`,
|
|
325
|
+
`setBakedConfig(${bakedLiteral});`,
|
|
326
|
+
`export { getCanciaRuntime };`
|
|
327
|
+
].join("\n");
|
|
421
328
|
}
|
|
422
329
|
}
|
|
423
330
|
]
|
|
@@ -645,6 +552,7 @@ export {
|
|
|
645
552
|
RevConflictError,
|
|
646
553
|
canciaIntegration,
|
|
647
554
|
canciaLoader,
|
|
555
|
+
createGitBackedAdapter,
|
|
648
556
|
createJsonFileAdapter,
|
|
649
557
|
createJsonFileAdapterV2,
|
|
650
558
|
createSQLiteAdapter,
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { C as CanciaStorage, a as CanciaStorageV2 } from './types-BMlLS-OS.js';
|
|
2
2
|
import { U as UploadHandler } from './upload-DwCGjXbz.js';
|
|
3
|
+
import { G as GitHubCommitter } from './github-client-BAZ1pW24.js';
|
|
3
4
|
|
|
4
5
|
interface CanciaRuntime {
|
|
5
6
|
/** v1 KV-only storage. Kept for the existing /content endpoint. */
|
|
@@ -28,7 +29,51 @@ interface CanciaRuntime {
|
|
|
28
29
|
deployHook: string | undefined;
|
|
29
30
|
maxUploadMB: number;
|
|
30
31
|
}
|
|
32
|
+
/** Describes which storage adapter to build lazily in the server process. */
|
|
33
|
+
type BakedStorageDescriptor = {
|
|
34
|
+
kind: "json-file";
|
|
35
|
+
dbPath?: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "git-backed";
|
|
38
|
+
repo: string;
|
|
39
|
+
branch?: string;
|
|
40
|
+
committer?: GitHubCommitter;
|
|
41
|
+
debounceMs?: number;
|
|
42
|
+
commitMessage?: string;
|
|
43
|
+
};
|
|
44
|
+
/** R2 config baked minus its secrets (accessKeyId/secretAccessKey come from env). */
|
|
45
|
+
interface BakedR2Config {
|
|
46
|
+
accountId?: string;
|
|
47
|
+
bucket?: string;
|
|
48
|
+
publicUrl?: string;
|
|
49
|
+
prefix?: string;
|
|
50
|
+
}
|
|
51
|
+
interface BakedConfig {
|
|
52
|
+
projectRoot: string;
|
|
53
|
+
schemasPath: string | undefined;
|
|
54
|
+
defaultLocale: string;
|
|
55
|
+
locales: string[];
|
|
56
|
+
maxUploadMB: number;
|
|
57
|
+
/** When true, endpoints skip auth (public demos). Baked, never a secret. */
|
|
58
|
+
public: boolean;
|
|
59
|
+
/** Whether a deploy hook exists — the value itself is read from env. */
|
|
60
|
+
hasDeployHook: boolean;
|
|
61
|
+
/** How to build the v2 storage adapter at runtime. Absent = no v2 storage. */
|
|
62
|
+
storage: BakedStorageDescriptor | undefined;
|
|
63
|
+
/** When set, build an R2 upload handler; secrets come from env at runtime. */
|
|
64
|
+
r2: BakedR2Config | undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Called by the generated virtual:cancia/runtime module at import time with the
|
|
68
|
+
* build-baked, non-secret config. Enables lazy self-init in a fresh process.
|
|
69
|
+
*/
|
|
70
|
+
declare function setBakedConfig(config: BakedConfig): void;
|
|
71
|
+
/**
|
|
72
|
+
* Eagerly set the runtime. Used in dev (astro:server:setup) and in-process
|
|
73
|
+
* builds where the resolved runtime already exists. Production falls through
|
|
74
|
+
* to lazy init instead.
|
|
75
|
+
*/
|
|
31
76
|
declare function setCanciaRuntime(runtime: CanciaRuntime): void;
|
|
32
77
|
declare function getCanciaRuntime(): CanciaRuntime;
|
|
33
78
|
|
|
34
|
-
export { type CanciaRuntime, getCanciaRuntime, setCanciaRuntime };
|
|
79
|
+
export { type BakedConfig, type BakedR2Config, type BakedStorageDescriptor, type CanciaRuntime, getCanciaRuntime, setBakedConfig, setCanciaRuntime };
|
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCanciaRuntime,
|
|
3
|
+
setBakedConfig,
|
|
3
4
|
setCanciaRuntime
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7MPOERVU.js";
|
|
6
|
+
import "./chunk-5ELSN6LI.js";
|
|
7
|
+
import "./chunk-ST44VULL.js";
|
|
8
|
+
import "./chunk-7IA5B5CF.js";
|
|
9
|
+
import "./chunk-5IPHDIC6.js";
|
|
5
10
|
export {
|
|
6
11
|
getCanciaRuntime,
|
|
12
|
+
setBakedConfig,
|
|
7
13
|
setCanciaRuntime
|
|
8
14
|
};
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { C as CanciaStorage, a as CanciaStorageV2 } from '../types-BMlLS-OS.js';
|
|
2
2
|
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';
|
|
3
|
+
import { G as GitHubCommitter, a as GitHubClient, F as FetchLike } from '../github-client-BAZ1pW24.js';
|
|
4
|
+
export { C as CommitFile, b as GitHubClientOptions, c as createGitHubClient } from '../github-client-BAZ1pW24.js';
|
|
3
5
|
|
|
4
6
|
declare function createJsonFileAdapter(filePath?: string): CanciaStorage;
|
|
5
7
|
|
|
@@ -17,4 +19,66 @@ declare function createJsonFileAdapterV2(opts?: JsonFileV2Options): CanciaStorag
|
|
|
17
19
|
|
|
18
20
|
declare function createSQLiteAdapter(dbPath?: string): CanciaStorage;
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
interface GitBackedContentPaths {
|
|
23
|
+
/** KV file path. Default <projectRoot>/cancia-content.json. */
|
|
24
|
+
kvPath?: string;
|
|
25
|
+
/** Pages file path. Default <projectRoot>/.cancia/pages.json. */
|
|
26
|
+
pagesPath?: string;
|
|
27
|
+
/** Lists directory. Default <projectRoot>/.cancia/lists. */
|
|
28
|
+
listsDir?: string;
|
|
29
|
+
}
|
|
30
|
+
interface GitBackedOptions {
|
|
31
|
+
/** The wrapped local adapter — the on-disk source of truth. */
|
|
32
|
+
local: CanciaStorageV2;
|
|
33
|
+
/** "owner/name" of the GitHub repo whose builds carry the content. */
|
|
34
|
+
repo: string;
|
|
35
|
+
/** Branch to commit onto. Default "main". */
|
|
36
|
+
branch?: string;
|
|
37
|
+
/**
|
|
38
|
+
* GitHub PAT. Reads CANCIA_GITHUB_TOKEN if omitted. When absent entirely the
|
|
39
|
+
* adapter runs in local-only mode (disk writes only; no commits) + warns once.
|
|
40
|
+
*/
|
|
41
|
+
token?: string;
|
|
42
|
+
/** Optional committer identity for commits. */
|
|
43
|
+
committer?: GitHubCommitter;
|
|
44
|
+
/**
|
|
45
|
+
* Project root the local adapter writes under — needed to turn absolute
|
|
46
|
+
* on-disk paths into repo-relative commit paths. Default process.cwd().
|
|
47
|
+
*/
|
|
48
|
+
projectRoot?: string;
|
|
49
|
+
/** Override where the local adapter's content lives (must match `local`). */
|
|
50
|
+
contentPaths?: GitBackedContentPaths;
|
|
51
|
+
/** Quiet window (ms) before a flush fires. Default 3000. */
|
|
52
|
+
debounceMs?: number;
|
|
53
|
+
/** Commit message for content updates. */
|
|
54
|
+
commitMessage?: string;
|
|
55
|
+
/** Injected GitHub client (tests pass a mock). Overrides token/fetch. */
|
|
56
|
+
client?: GitHubClient;
|
|
57
|
+
/** Injected fetch, forwarded to the default GitHub client. */
|
|
58
|
+
fetch?: FetchLike;
|
|
59
|
+
/** API base override, forwarded to the default GitHub client (tests). */
|
|
60
|
+
apiBase?: string;
|
|
61
|
+
/** Warn sink (tests capture). Default console.warn. */
|
|
62
|
+
warn?: (msg: string) => void;
|
|
63
|
+
/** Error sink for push failures (tests capture). Default console.error. */
|
|
64
|
+
onError?: (msg: string, err: unknown) => void;
|
|
65
|
+
}
|
|
66
|
+
/** The extra control surface the git adapter adds on top of CanciaStorageV2. */
|
|
67
|
+
interface GitBackedControls {
|
|
68
|
+
/**
|
|
69
|
+
* Force any pending dirty files to commit now, bypassing the debounce.
|
|
70
|
+
* Resolves once the flush completes (or rejects if the push failed — the
|
|
71
|
+
* files stay dirty for the next flush). For tests + graceful shutdown.
|
|
72
|
+
*/
|
|
73
|
+
flush(): Promise<void>;
|
|
74
|
+
/** True if git commits are active (token present). */
|
|
75
|
+
readonly gitEnabled: boolean;
|
|
76
|
+
/** Snapshot of currently-dirty repo-relative paths (for tests/inspection). */
|
|
77
|
+
pendingPaths(): string[];
|
|
78
|
+
}
|
|
79
|
+
type GitBackedStorage = CanciaStorageV2 & {
|
|
80
|
+
git: GitBackedControls;
|
|
81
|
+
};
|
|
82
|
+
declare function createGitBackedAdapter(opts: GitBackedOptions): GitBackedStorage;
|
|
83
|
+
|
|
84
|
+
export { CanciaStorage, CanciaStorageV2, type GitBackedContentPaths, type GitBackedControls, type GitBackedOptions, type GitBackedStorage, GitHubClient, GitHubCommitter, createGitBackedAdapter, createJsonFileAdapter, createJsonFileAdapterV2, createSQLiteAdapter };
|
package/dist/storage/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSQLiteAdapter
|
|
3
3
|
} from "../chunk-AE4SIY24.js";
|
|
4
|
+
import {
|
|
5
|
+
createGitBackedAdapter,
|
|
6
|
+
createGitHubClient
|
|
7
|
+
} from "../chunk-5ELSN6LI.js";
|
|
4
8
|
import {
|
|
5
9
|
createJsonFileAdapter,
|
|
6
10
|
createJsonFileAdapterV2
|
|
@@ -10,6 +14,8 @@ import {
|
|
|
10
14
|
} from "../chunk-7IA5B5CF.js";
|
|
11
15
|
export {
|
|
12
16
|
RevConflictError,
|
|
17
|
+
createGitBackedAdapter,
|
|
18
|
+
createGitHubClient,
|
|
13
19
|
createJsonFileAdapter,
|
|
14
20
|
createJsonFileAdapterV2,
|
|
15
21
|
createSQLiteAdapter
|
package/package.json
CHANGED
package/dist/chunk-DGCGIEFD.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// src/runtime.ts
|
|
2
|
-
var _runtime = null;
|
|
3
|
-
function setCanciaRuntime(runtime) {
|
|
4
|
-
_runtime = runtime;
|
|
5
|
-
}
|
|
6
|
-
function getCanciaRuntime() {
|
|
7
|
-
if (!_runtime) throw new Error("[cancia] Runtime not initialised. This is a bug.");
|
|
8
|
-
return _runtime;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
setCanciaRuntime,
|
|
13
|
-
getCanciaRuntime
|
|
14
|
-
};
|