@cancia/astro 0.10.0 → 0.12.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-4PYVIIOO.js +25 -0
- package/dist/{chunk-DIV2FFYX.js → chunk-5R3I5RLT.js} +15 -8
- package/dist/chunk-6GUOGDWO.js +172 -0
- package/dist/chunk-DEI3FBNY.js +65 -0
- package/dist/{chunk-ONKCUTPU.js → chunk-DOFLZ7KG.js} +6 -7
- package/dist/{chunk-GJIX7MWC.js → chunk-ECPYHIAW.js} +9 -1
- package/dist/{chunk-YGJ3JHXF.js → chunk-QQEUCOXQ.js} +11 -0
- package/dist/{chunk-NVXNJ4YC.js → chunk-R7VA3Z5N.js} +53 -11
- package/dist/{chunk-5IPHDIC6.js → chunk-RHFTQW4W.js} +10 -1
- package/dist/{chunk-2NVZWZPE.js → chunk-S5YLB6P3.js} +310 -7
- package/dist/{chunk-LJSSPOSW.js → chunk-VF3EXXEM.js} +139 -16
- package/dist/chunk-XINNLVZP.js +183 -0
- package/dist/chunk-XZCXWILA.js +145 -0
- package/dist/content.js +3 -3
- package/dist/{upload-DwCGjXbz.d.ts → email-BRK4WAO2.d.ts} +9 -1
- package/dist/endpoints/auth-callback.d.ts +1 -0
- package/dist/endpoints/auth-callback.js +9 -0
- package/dist/endpoints/auth-logout.d.ts +1 -0
- package/dist/endpoints/auth-logout.js +9 -0
- package/dist/endpoints/auth-request.d.ts +18 -0
- package/dist/endpoints/auth-request.js +9 -0
- package/dist/endpoints/auth-session.d.ts +1 -0
- package/dist/endpoints/auth-session.js +9 -0
- package/dist/endpoints/auth.js +10 -2
- package/dist/endpoints/content.js +12 -12
- package/dist/endpoints/lists.js +4 -1
- package/dist/endpoints/publish.js +8 -6
- package/dist/endpoints/schemas.js +6 -3
- package/dist/endpoints/upload.js +11 -10
- package/dist/{git-backed-DtiH52EI.d.ts → git-backed-CwTFfUS4.d.ts} +2 -2
- package/dist/{github-client-Db0pIrqE.d.ts → github-client-Db5EPcSL.d.ts} +1 -1
- package/dist/index.d.ts +32 -6
- package/dist/index.js +125 -35
- package/dist/loader/index.d.ts +34 -4
- package/dist/loader/index.js +3 -3
- package/dist/runtime.d.ts +69 -3
- package/dist/runtime.js +6 -4
- package/dist/schema/index.d.ts +14 -1
- package/dist/schema/index.js +1 -1
- package/dist/storage/index.d.ts +4 -4
- package/dist/storage/index.js +5 -5
- package/dist/{types-BMlLS-OS.d.ts → types-CKzfz5iu.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-MXVOJRAM.js +0 -504
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authorizeRequest
|
|
3
|
+
} from "./chunk-6GUOGDWO.js";
|
|
4
|
+
|
|
5
|
+
// src/auth/guard.ts
|
|
6
|
+
var unauthorized = () => new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
7
|
+
status: 401,
|
|
8
|
+
headers: { "Content-Type": "application/json" }
|
|
9
|
+
});
|
|
10
|
+
async function guardRequest(request, cfg) {
|
|
11
|
+
if (!cfg.secret && !cfg.auth) {
|
|
12
|
+
return { deny: null, email: null };
|
|
13
|
+
}
|
|
14
|
+
const result = await authorizeRequest({
|
|
15
|
+
request,
|
|
16
|
+
secret: cfg.secret,
|
|
17
|
+
auth: cfg.auth
|
|
18
|
+
});
|
|
19
|
+
if (!result.ok) return { deny: unauthorized(), email: null };
|
|
20
|
+
return { deny: null, email: result.email };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
guardRequest
|
|
25
|
+
};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
guardRequest
|
|
3
|
+
} from "./chunk-4PYVIIOO.js";
|
|
1
4
|
import {
|
|
2
5
|
loadListSchema
|
|
3
6
|
} from "./chunk-VL6FO446.js";
|
|
@@ -23,13 +26,8 @@ function getSite(request) {
|
|
|
23
26
|
function getLocale(request) {
|
|
24
27
|
return new URL(request.url).searchParams.get("locale");
|
|
25
28
|
}
|
|
26
|
-
function checkAuth(request, secret) {
|
|
27
|
-
|
|
28
|
-
const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
|
|
29
|
-
if (token !== secret) {
|
|
30
|
-
return json({ error: "Unauthorized" }, 401);
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
29
|
+
async function checkAuth(request, secret, auth) {
|
|
30
|
+
return (await guardRequest(request, { secret, auth })).deny;
|
|
33
31
|
}
|
|
34
32
|
function json(body, status = 200) {
|
|
35
33
|
return new Response(JSON.stringify(body), {
|
|
@@ -45,7 +43,7 @@ function makeListsRoutes(ctx) {
|
|
|
45
43
|
if (!ctx.storageV2) {
|
|
46
44
|
return json({ error: "Storage v2 not configured", code: "NO_STORAGE_V2" }, 503);
|
|
47
45
|
}
|
|
48
|
-
const deny = checkAuth(request, ctx.secret);
|
|
46
|
+
const deny = await checkAuth(request, ctx.secret, ctx.auth);
|
|
49
47
|
if (deny) return deny;
|
|
50
48
|
const parsed = parseListPath(request.url);
|
|
51
49
|
if (!parsed) return json({ error: "Bad request" }, 400);
|
|
@@ -103,6 +101,15 @@ function makeListsRoutes(ctx) {
|
|
|
103
101
|
if (!body || !Array.isArray(body.ids) || !body.ids.every((id2) => typeof id2 === "string")) {
|
|
104
102
|
return json({ error: "`ids` must be a string array" }, 400);
|
|
105
103
|
}
|
|
104
|
+
if (typeof body.knownCount === "number") {
|
|
105
|
+
const existing = await lists.translations(site, parsed.listName);
|
|
106
|
+
if (existing.length !== body.knownCount) {
|
|
107
|
+
return json({
|
|
108
|
+
error: "This list changed while you were reordering it. Close and reopen the panel to see the latest entries.",
|
|
109
|
+
code: "REV_CONFLICT"
|
|
110
|
+
}, 409);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
106
113
|
try {
|
|
107
114
|
await lists.reorder(site, parsed.listName, body.ids);
|
|
108
115
|
return json({ ok: true });
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/auth/cookie.ts
|
|
2
|
+
var SESSION_COOKIE = "cancia_session";
|
|
3
|
+
function serialiseSessionCookie(value, opts = {}) {
|
|
4
|
+
const parts = [
|
|
5
|
+
`${SESSION_COOKIE}=${value}`,
|
|
6
|
+
"Path=/",
|
|
7
|
+
"HttpOnly",
|
|
8
|
+
"SameSite=Lax",
|
|
9
|
+
`Max-Age=${opts.maxAgeSeconds ?? 60 * 60 * 24 * 30}`
|
|
10
|
+
];
|
|
11
|
+
if (opts.secure !== false) parts.push("Secure");
|
|
12
|
+
return parts.join("; ");
|
|
13
|
+
}
|
|
14
|
+
function clearSessionCookie(opts = {}) {
|
|
15
|
+
const parts = [
|
|
16
|
+
`${SESSION_COOKIE}=`,
|
|
17
|
+
"Path=/",
|
|
18
|
+
"HttpOnly",
|
|
19
|
+
"SameSite=Lax",
|
|
20
|
+
"Max-Age=0"
|
|
21
|
+
];
|
|
22
|
+
if (opts.secure !== false) parts.push("Secure");
|
|
23
|
+
return parts.join("; ");
|
|
24
|
+
}
|
|
25
|
+
function readSessionCookie(request) {
|
|
26
|
+
const header = request.headers.get("cookie");
|
|
27
|
+
if (!header) return void 0;
|
|
28
|
+
const values = [];
|
|
29
|
+
for (const pair of header.split(";")) {
|
|
30
|
+
const eq = pair.indexOf("=");
|
|
31
|
+
if (eq === -1) continue;
|
|
32
|
+
if (pair.slice(0, eq).trim() !== SESSION_COOKIE) continue;
|
|
33
|
+
const value = pair.slice(eq + 1).trim();
|
|
34
|
+
if (value !== "") values.push(value);
|
|
35
|
+
}
|
|
36
|
+
return values.length > 0 ? values[values.length - 1] : void 0;
|
|
37
|
+
}
|
|
38
|
+
function isSecureRequest(request) {
|
|
39
|
+
const proto = request.headers.get("x-forwarded-proto");
|
|
40
|
+
if (proto) return proto.split(",")[0].trim() === "https";
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(request.url);
|
|
43
|
+
if (url.protocol === "https:") return true;
|
|
44
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") return false;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/auth/tokens.ts
|
|
51
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
52
|
+
var TOKEN_BYTES = 32;
|
|
53
|
+
function mintToken() {
|
|
54
|
+
return randomBytes(TOKEN_BYTES).toString("base64url");
|
|
55
|
+
}
|
|
56
|
+
function hashToken(raw) {
|
|
57
|
+
return createHash("sha256").update(raw, "utf8").digest("hex");
|
|
58
|
+
}
|
|
59
|
+
function safeEqual(a, b) {
|
|
60
|
+
const bufA = Buffer.from(a, "utf8");
|
|
61
|
+
const bufB = Buffer.from(b, "utf8");
|
|
62
|
+
if (bufA.length !== bufB.length) {
|
|
63
|
+
timingSafeEqual(bufA, bufA);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return timingSafeEqual(bufA, bufB);
|
|
67
|
+
}
|
|
68
|
+
function normaliseEmail(email) {
|
|
69
|
+
return email.trim().toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/auth/magic-link.ts
|
|
73
|
+
var LOGIN_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
74
|
+
var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
75
|
+
var SESSION_TOUCH_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
76
|
+
async function requestLoginLink(store, site, rawEmail, now = Date.now()) {
|
|
77
|
+
const email = normaliseEmail(rawEmail);
|
|
78
|
+
if (!await store.isEditor(site, email)) {
|
|
79
|
+
return { token: null, email };
|
|
80
|
+
}
|
|
81
|
+
const token = mintToken();
|
|
82
|
+
await store.createLoginToken(site, {
|
|
83
|
+
hash: hashToken(token),
|
|
84
|
+
email,
|
|
85
|
+
expiresAt: new Date(now + LOGIN_TOKEN_TTL_MS).toISOString()
|
|
86
|
+
});
|
|
87
|
+
return { token, email };
|
|
88
|
+
}
|
|
89
|
+
var FAILED = { sessionToken: null, email: null, expiresAt: null };
|
|
90
|
+
async function redeemLoginToken(store, site, rawToken, now = Date.now()) {
|
|
91
|
+
if (!rawToken) return FAILED;
|
|
92
|
+
const record = await store.consumeLoginToken(site, hashToken(rawToken));
|
|
93
|
+
if (!record) return FAILED;
|
|
94
|
+
if (Date.parse(record.expiresAt) <= now) return FAILED;
|
|
95
|
+
if (!await store.isEditor(site, record.email)) return FAILED;
|
|
96
|
+
const sessionToken = mintToken();
|
|
97
|
+
const expiresAt = new Date(now + SESSION_TTL_MS).toISOString();
|
|
98
|
+
await store.createSession(site, {
|
|
99
|
+
id: hashToken(sessionToken),
|
|
100
|
+
email: record.email,
|
|
101
|
+
createdAt: new Date(now).toISOString(),
|
|
102
|
+
expiresAt
|
|
103
|
+
});
|
|
104
|
+
return { sessionToken, email: record.email, expiresAt };
|
|
105
|
+
}
|
|
106
|
+
var INVALID = { valid: false, email: null };
|
|
107
|
+
async function verifySession(store, site, rawToken, now = Date.now()) {
|
|
108
|
+
if (!rawToken) return INVALID;
|
|
109
|
+
const id = hashToken(rawToken);
|
|
110
|
+
const session = await store.getSession(site, id);
|
|
111
|
+
if (!session) return INVALID;
|
|
112
|
+
if (Date.parse(session.expiresAt) <= now) {
|
|
113
|
+
await store.deleteSession(site, id);
|
|
114
|
+
return INVALID;
|
|
115
|
+
}
|
|
116
|
+
if (!await store.isEditor(site, session.email)) {
|
|
117
|
+
await store.deleteSession(site, id);
|
|
118
|
+
return INVALID;
|
|
119
|
+
}
|
|
120
|
+
const elapsed = now - Date.parse(session.createdAt);
|
|
121
|
+
const remaining = Date.parse(session.expiresAt) - now;
|
|
122
|
+
if (elapsed > 0 && remaining < SESSION_TTL_MS - SESSION_TOUCH_AFTER_MS) {
|
|
123
|
+
const renewedExpiresAt = new Date(now + SESSION_TTL_MS).toISOString();
|
|
124
|
+
await store.touchSession(site, id, renewedExpiresAt);
|
|
125
|
+
return { valid: true, email: session.email, renewedExpiresAt };
|
|
126
|
+
}
|
|
127
|
+
return { valid: true, email: session.email };
|
|
128
|
+
}
|
|
129
|
+
async function revokeSession(store, site, rawToken) {
|
|
130
|
+
if (!rawToken) return;
|
|
131
|
+
await store.deleteSession(site, hashToken(rawToken));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/auth/authorize.ts
|
|
135
|
+
var DENY = { ok: false, email: null };
|
|
136
|
+
async function authorizeRequest(cfg) {
|
|
137
|
+
const { request, secret, auth, editorsConfigured } = cfg;
|
|
138
|
+
if (auth) {
|
|
139
|
+
const raw = readSessionCookie(request);
|
|
140
|
+
if (raw) {
|
|
141
|
+
const verdict = await verifySession(auth.store, auth.site, raw, cfg.now);
|
|
142
|
+
if (verdict.valid) {
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
email: verdict.email,
|
|
146
|
+
renewedExpiresAt: verdict.renewedExpiresAt
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const header = request.headers.get("Authorization");
|
|
152
|
+
if (header && secret) {
|
|
153
|
+
const token = header.replace("Bearer ", "").trim();
|
|
154
|
+
if (safeEqual(token, secret)) {
|
|
155
|
+
return { ok: true, email: null };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return DENY;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
serialiseSessionCookie,
|
|
163
|
+
clearSessionCookie,
|
|
164
|
+
readSessionCookie,
|
|
165
|
+
isSecureRequest,
|
|
166
|
+
normaliseEmail,
|
|
167
|
+
SESSION_TTL_MS,
|
|
168
|
+
requestLoginLink,
|
|
169
|
+
redeemLoginToken,
|
|
170
|
+
revokeSession,
|
|
171
|
+
authorizeRequest
|
|
172
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleLoginCallback,
|
|
3
|
+
handleLoginRequest,
|
|
4
|
+
handleLogout,
|
|
5
|
+
handleSessionCheck,
|
|
6
|
+
resolveLoginEmailSender
|
|
7
|
+
} from "./chunk-XZCXWILA.js";
|
|
8
|
+
import {
|
|
9
|
+
createRateLimiter
|
|
10
|
+
} from "./chunk-ECPYHIAW.js";
|
|
11
|
+
import {
|
|
12
|
+
authorizeRequest
|
|
13
|
+
} from "./chunk-6GUOGDWO.js";
|
|
14
|
+
|
|
15
|
+
// src/endpoints/auth-magic.ts
|
|
16
|
+
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
|
17
|
+
var limiter = createRateLimiter();
|
|
18
|
+
var notConfigured = () => new Response(JSON.stringify({ error: "Magic-link login is not configured for this site." }), {
|
|
19
|
+
status: 404,
|
|
20
|
+
headers: { "Content-Type": "application/json" }
|
|
21
|
+
});
|
|
22
|
+
function ctxFrom(rt) {
|
|
23
|
+
if (!rt.auth) return null;
|
|
24
|
+
return {
|
|
25
|
+
store: rt.auth.store,
|
|
26
|
+
site: rt.auth.site,
|
|
27
|
+
sendEmail: resolveLoginEmailSender(rt.sendLoginEmail),
|
|
28
|
+
limiter
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function requestLink({ request }) {
|
|
32
|
+
const rt = getCanciaRuntime();
|
|
33
|
+
const ctx = ctxFrom(rt);
|
|
34
|
+
if (!ctx) return notConfigured();
|
|
35
|
+
return handleLoginRequest(request, ctx, new URL(request.url).origin);
|
|
36
|
+
}
|
|
37
|
+
async function callback({ request }) {
|
|
38
|
+
const rt = getCanciaRuntime();
|
|
39
|
+
const ctx = ctxFrom(rt);
|
|
40
|
+
if (!ctx) return notConfigured();
|
|
41
|
+
return handleLoginCallback(request, ctx);
|
|
42
|
+
}
|
|
43
|
+
async function logout({ request }) {
|
|
44
|
+
const rt = getCanciaRuntime();
|
|
45
|
+
const ctx = ctxFrom(rt);
|
|
46
|
+
if (!ctx) return notConfigured();
|
|
47
|
+
return handleLogout(request, ctx);
|
|
48
|
+
}
|
|
49
|
+
async function session({ request }) {
|
|
50
|
+
const rt = getCanciaRuntime();
|
|
51
|
+
const ctx = ctxFrom(rt);
|
|
52
|
+
if (!ctx) return notConfigured();
|
|
53
|
+
return handleSessionCheck(
|
|
54
|
+
request,
|
|
55
|
+
ctx,
|
|
56
|
+
() => authorizeRequest({ request, secret: rt.secret, auth: rt.auth })
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
requestLink,
|
|
62
|
+
callback,
|
|
63
|
+
logout,
|
|
64
|
+
session
|
|
65
|
+
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
guardRequest
|
|
3
|
+
} from "./chunk-4PYVIIOO.js";
|
|
1
4
|
import {
|
|
2
5
|
loadSchemas
|
|
3
6
|
} from "./chunk-VL6FO446.js";
|
|
4
7
|
import {
|
|
5
8
|
describeList
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QQEUCOXQ.js";
|
|
7
10
|
|
|
8
11
|
// src/routes/schemas.ts
|
|
9
12
|
function json(body, status = 200) {
|
|
@@ -14,12 +17,8 @@ function json(body, status = 200) {
|
|
|
14
17
|
}
|
|
15
18
|
function makeSchemasRoute(ctx) {
|
|
16
19
|
return async function schemasRoute(request) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (token !== ctx.secret) {
|
|
20
|
-
return json({ error: "Unauthorized" }, 401);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
20
|
+
const deny = (await guardRequest(request, { secret: ctx.secret, auth: ctx.auth })).deny;
|
|
21
|
+
if (deny) return deny;
|
|
23
22
|
const schemas = await loadSchemas(ctx.projectRoot, ctx.schemasPath);
|
|
24
23
|
const descriptions = {};
|
|
25
24
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
@@ -5,7 +5,12 @@ function createRateLimiter() {
|
|
|
5
5
|
return /* @__PURE__ */ new Map();
|
|
6
6
|
}
|
|
7
7
|
function getIp(req) {
|
|
8
|
-
|
|
8
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
9
|
+
if (forwarded) {
|
|
10
|
+
const hops = forwarded.split(",").map((h) => h.trim()).filter(Boolean);
|
|
11
|
+
if (hops.length > 0) return hops[hops.length - 1];
|
|
12
|
+
}
|
|
13
|
+
return req.headers.get("x-real-ip") ?? "unknown";
|
|
9
14
|
}
|
|
10
15
|
function isRateLimited(limiter, ip, now = Date.now()) {
|
|
11
16
|
const bucket = limiter.get(ip);
|
|
@@ -51,5 +56,8 @@ async function runAuth(cfg) {
|
|
|
51
56
|
|
|
52
57
|
export {
|
|
53
58
|
createRateLimiter,
|
|
59
|
+
getIp,
|
|
60
|
+
isRateLimited,
|
|
61
|
+
recordFailure,
|
|
54
62
|
runAuth
|
|
55
63
|
};
|
|
@@ -45,6 +45,17 @@ var defineField = {
|
|
|
45
45
|
},
|
|
46
46
|
slug: (o) => z.string().regex(/^[a-z0-9-]+$/).meta({ widget: "slug", ...o }),
|
|
47
47
|
image: (o) => z.string().url().meta({ widget: "image", ...o }),
|
|
48
|
+
/**
|
|
49
|
+
* An uploaded document — a PDF today.
|
|
50
|
+
*
|
|
51
|
+
* Stores a URL exactly as `image` does, because the upload path is the same:
|
|
52
|
+
* the file goes to the configured store (R2, or public/uploads) and what
|
|
53
|
+
* comes back is a plain URL. A separate widget rather than reusing `image`
|
|
54
|
+
* so the editor renders a filename and a download affordance instead of a
|
|
55
|
+
* thumbnail, and so an `image` field cannot silently accept a PDF and
|
|
56
|
+
* render a broken <img>.
|
|
57
|
+
*/
|
|
58
|
+
file: (o) => z.string().url().meta({ widget: "file", ...o }),
|
|
48
59
|
select: (o) => z.enum(o.options).meta({ widget: "select", ...o }),
|
|
49
60
|
/**
|
|
50
61
|
* A repeatable list of a single member type. `member` is any Zod type,
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createJsonFileAdapterV2
|
|
3
|
-
|
|
2
|
+
createJsonFileAdapterV2,
|
|
3
|
+
createSqliteAdapterV2
|
|
4
|
+
} from "./chunk-S5YLB6P3.js";
|
|
4
5
|
import {
|
|
5
6
|
isDraft
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QQEUCOXQ.js";
|
|
8
|
+
|
|
9
|
+
// src/loader/index.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/storage/resolve.ts
|
|
14
|
+
import { isAbsolute, join } from "path";
|
|
15
|
+
function resolveDbPath(dbPath, projectRoot) {
|
|
16
|
+
return isAbsolute(dbPath) ? dbPath : join(projectRoot, dbPath);
|
|
17
|
+
}
|
|
18
|
+
function resolveReadStorage(desc, projectRoot) {
|
|
19
|
+
if (desc?.kind === "sqlite-v2") {
|
|
20
|
+
const dbPath = desc.dbPath ? resolveDbPath(desc.dbPath, projectRoot) : join(projectRoot, "cancia.db");
|
|
21
|
+
return createSqliteAdapterV2({ dbPath });
|
|
22
|
+
}
|
|
23
|
+
return createJsonFileAdapterV2({ projectRoot });
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
// src/loader/index.ts
|
|
9
|
-
import { join } from "path";
|
|
10
27
|
function makeId(locale, entryId) {
|
|
11
28
|
return `${locale}/${entryId}`;
|
|
12
29
|
}
|
|
13
|
-
async function syncOnce(ctx, lists, list, site, schema, includeDrafts) {
|
|
30
|
+
async function syncOnce(ctx, lists, list, site, schema, includeDrafts, usingJsonFile = false, projectRoot = "") {
|
|
14
31
|
ctx.store.clear();
|
|
15
32
|
const all = await lists.list(site, list);
|
|
16
33
|
const drafts = schema?.draftField && !includeDrafts ? all.filter((e) => isDraft(schema, e.data)) : [];
|
|
@@ -30,6 +47,11 @@ async function syncOnce(ctx, lists, list, site, schema, includeDrafts) {
|
|
|
30
47
|
digest: entry._rev
|
|
31
48
|
});
|
|
32
49
|
}
|
|
50
|
+
if (entries.length === 0 && usingJsonFile && existsSync(join2(projectRoot, "cancia.db"))) {
|
|
51
|
+
ctx.logger.warn(
|
|
52
|
+
`cancia: list "${list}" is empty, but a cancia.db exists at the project root and this loader is reading JSON files. Pass db: { kind: "sqlite" } to canciaLoader() to match your integration's db option \u2014 otherwise every entry the client saves is invisible to the build.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
33
55
|
const skipped = drafts.length ? ` (${drafts.length} draft${drafts.length === 1 ? "" : "s"} skipped)` : "";
|
|
34
56
|
ctx.logger.info(
|
|
35
57
|
`cancia: loaded ${entries.length} entr${entries.length === 1 ? "y" : "ies"} from list "${list}"${skipped}`
|
|
@@ -44,15 +66,33 @@ function canciaLoader(opts) {
|
|
|
44
66
|
name: `@cancia/astro/loader[${opts.list}]`,
|
|
45
67
|
async load(ctx) {
|
|
46
68
|
latestCtx = ctx;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
70
|
+
const detectedDb = !opts.storage && !opts.db && existsSync(join2(projectRoot, "cancia.db"));
|
|
71
|
+
const storage = opts.storage ?? resolveReadStorage(
|
|
72
|
+
opts.db ? { kind: opts.db.kind === "sqlite" ? "sqlite-v2" : "json-file", dbPath: opts.db.path } : detectedDb ? { kind: "sqlite-v2" } : void 0,
|
|
73
|
+
projectRoot
|
|
74
|
+
);
|
|
50
75
|
const { lists } = storage;
|
|
51
|
-
|
|
76
|
+
if (detectedDb) {
|
|
77
|
+
ctx.logger.info(
|
|
78
|
+
`cancia: reading list "${opts.list}" from cancia.db (detected). Pass db: { kind: "json-file" } if that is not what you want.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const usingJsonFile = !opts.storage && opts.db?.kind !== "sqlite" && !detectedDb;
|
|
82
|
+
await syncOnce(
|
|
83
|
+
ctx,
|
|
84
|
+
lists,
|
|
85
|
+
opts.list,
|
|
86
|
+
opts.site,
|
|
87
|
+
opts.schema,
|
|
88
|
+
opts.includeDrafts,
|
|
89
|
+
usingJsonFile,
|
|
90
|
+
projectRoot
|
|
91
|
+
);
|
|
52
92
|
if (ctx.watcher && !watcherAttached) {
|
|
53
93
|
watcherAttached = true;
|
|
54
94
|
const root = opts.projectRoot ?? process.cwd();
|
|
55
|
-
const watchDir =
|
|
95
|
+
const watchDir = join2(root, ".cancia", "lists", opts.list, opts.site);
|
|
56
96
|
ctx.watcher.add(watchDir);
|
|
57
97
|
const scheduleSync = () => {
|
|
58
98
|
if (pendingTimer) clearTimeout(pendingTimer);
|
|
@@ -66,7 +106,9 @@ function canciaLoader(opts) {
|
|
|
66
106
|
opts.list,
|
|
67
107
|
opts.site,
|
|
68
108
|
opts.schema,
|
|
69
|
-
opts.includeDrafts
|
|
109
|
+
opts.includeDrafts,
|
|
110
|
+
usingJsonFile,
|
|
111
|
+
projectRoot
|
|
70
112
|
).catch((err) => {
|
|
71
113
|
latestCtx.logger.error(`cancia: resync failed \u2014 ${err.message}`);
|
|
72
114
|
})
|
|
@@ -3,6 +3,15 @@ var SITE_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
|
3
3
|
function isValidSite(site) {
|
|
4
4
|
return SITE_RE.test(site);
|
|
5
5
|
}
|
|
6
|
+
function detectFileType(buf) {
|
|
7
|
+
if ([37, 80, 68, 70, 45].every((b, i) => buf[i] === b)) {
|
|
8
|
+
return { mime: "application/pdf", ext: "pdf" };
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function detectUploadType(buf) {
|
|
13
|
+
return detectImageType(buf) ?? detectFileType(buf);
|
|
14
|
+
}
|
|
6
15
|
function detectImageType(buf) {
|
|
7
16
|
const startsWith = (sig, offset = 0) => sig.every((b, i) => buf[offset + i] === b);
|
|
8
17
|
if (startsWith([255, 216, 255])) return { mime: "image/jpeg", ext: "jpg" };
|
|
@@ -15,5 +24,5 @@ function detectImageType(buf) {
|
|
|
15
24
|
|
|
16
25
|
export {
|
|
17
26
|
isValidSite,
|
|
18
|
-
|
|
27
|
+
detectUploadType
|
|
19
28
|
};
|