@cancia/astro 0.8.0 → 0.10.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-L2VKQJPY.js → chunk-2NVZWZPE.js} +62 -33
- package/dist/chunk-GJIX7MWC.js +55 -0
- package/dist/{chunk-AGNPUTKS.js → chunk-LJSSPOSW.js} +2 -2
- package/dist/{chunk-GNWD7EL2.js → chunk-MXVOJRAM.js} +22 -39
- package/dist/{chunk-3B3CXOMK.js → chunk-NVXNJ4YC.js} +2 -2
- package/dist/{chunk-HQIGNRIU.js → chunk-ONKCUTPU.js} +1 -1
- package/dist/{chunk-WVHTCFE6.js → chunk-YGJ3JHXF.js} +8 -1
- package/dist/content.js +8 -8
- package/dist/endpoints/auth.js +7 -48
- package/dist/endpoints/schemas.js +2 -2
- package/dist/index.js +12 -59
- package/dist/loader/index.js +3 -3
- package/dist/runtime.js +3 -3
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.js +1 -1
- package/dist/storage/index.js +2 -2
- package/dist/transform/text-helper.js +4 -5
- package/package.json +2 -2
|
@@ -67,6 +67,52 @@ function hashRev(value) {
|
|
|
67
67
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmSync } from "fs";
|
|
68
68
|
import { dirname as dirname2, join } from "path";
|
|
69
69
|
import { randomUUID } from "crypto";
|
|
70
|
+
|
|
71
|
+
// src/storage/order.ts
|
|
72
|
+
function applyOrder(order, ids) {
|
|
73
|
+
const result = [];
|
|
74
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const id of order) {
|
|
76
|
+
if (ids.has(id)) {
|
|
77
|
+
result.push(id);
|
|
78
|
+
seen.add(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
82
|
+
return [...result, ...extras];
|
|
83
|
+
}
|
|
84
|
+
function orderAfterCreate(order, id) {
|
|
85
|
+
return order.includes(id) ? order : [...order, id];
|
|
86
|
+
}
|
|
87
|
+
function orderAfterDelete(order, id, stillExistsElsewhere) {
|
|
88
|
+
if (stillExistsElsewhere) return order;
|
|
89
|
+
const next = order.filter((existingId) => existingId !== id);
|
|
90
|
+
return next.length === order.length ? order : next;
|
|
91
|
+
}
|
|
92
|
+
var UnknownEntryError = class extends Error {
|
|
93
|
+
constructor(id, listName) {
|
|
94
|
+
super(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
95
|
+
this.id = id;
|
|
96
|
+
this.listName = listName;
|
|
97
|
+
this.name = "UnknownEntryError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function planReorder(ids, allIds, listName) {
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
if (!allIds.has(id)) throw new UnknownEntryError(id, listName);
|
|
103
|
+
}
|
|
104
|
+
const keep = new Set(ids);
|
|
105
|
+
const dropped = [...allIds].filter((id) => !keep.has(id));
|
|
106
|
+
return { order: ids, dropped };
|
|
107
|
+
}
|
|
108
|
+
function entryExistsMessage(id, listName, locale) {
|
|
109
|
+
return `List entry "${id}" already exists in "${listName}" (${locale})`;
|
|
110
|
+
}
|
|
111
|
+
function entryNotFoundMessage(id, listName, locale) {
|
|
112
|
+
return `List entry "${id}" not found in "${listName}" (${locale})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/storage/json-file-v2.ts
|
|
70
116
|
function readJsonFile(filePath, fallback) {
|
|
71
117
|
if (!existsSync2(filePath)) return fallback;
|
|
72
118
|
try {
|
|
@@ -168,18 +214,6 @@ function makeListStore(listsDir) {
|
|
|
168
214
|
function writeEntry(site, listName, locale, entry) {
|
|
169
215
|
writeJsonFile(entryPath(listsDir, listName, site, locale, entry.id), entry);
|
|
170
216
|
}
|
|
171
|
-
function applyOrder(order, ids) {
|
|
172
|
-
const result = [];
|
|
173
|
-
const seen = /* @__PURE__ */ new Set();
|
|
174
|
-
for (const id of order) {
|
|
175
|
-
if (ids.has(id)) {
|
|
176
|
-
result.push(id);
|
|
177
|
-
seen.add(id);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
181
|
-
return [...result, ...extras];
|
|
182
|
-
}
|
|
183
217
|
return {
|
|
184
218
|
async list(site, listName, locale) {
|
|
185
219
|
const order = readOrder(listsDir, listName, site);
|
|
@@ -218,7 +252,7 @@ function makeListStore(listsDir) {
|
|
|
218
252
|
const finalId = id ?? randomUUID();
|
|
219
253
|
const existing = readEntry(site, listName, finalId, locale);
|
|
220
254
|
if (existing) {
|
|
221
|
-
throw new Error(
|
|
255
|
+
throw new Error(entryExistsMessage(finalId, listName, locale));
|
|
222
256
|
}
|
|
223
257
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
224
258
|
const entry = {
|
|
@@ -230,15 +264,14 @@ function makeListStore(listsDir) {
|
|
|
230
264
|
};
|
|
231
265
|
writeEntry(site, listName, locale, entry);
|
|
232
266
|
const order = readOrder(listsDir, listName, site);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
267
|
+
const nextOrder = orderAfterCreate(order, finalId);
|
|
268
|
+
if (nextOrder !== order) writeOrder(listsDir, listName, site, nextOrder);
|
|
236
269
|
return toEntry(entry);
|
|
237
270
|
},
|
|
238
271
|
async update(site, listName, id, locale, data, rev) {
|
|
239
272
|
const existing = readEntry(site, listName, id, locale);
|
|
240
273
|
if (!existing) {
|
|
241
|
-
throw new Error(
|
|
274
|
+
throw new Error(entryNotFoundMessage(id, listName, locale));
|
|
242
275
|
}
|
|
243
276
|
if (hashRev(existing.data) !== rev) throw new RevConflictError();
|
|
244
277
|
const updated = {
|
|
@@ -254,13 +287,9 @@ function makeListStore(listsDir) {
|
|
|
254
287
|
if (!existsSync2(path)) return;
|
|
255
288
|
rmSync(path);
|
|
256
289
|
const stillExists = listLocales(listsDir, listName, site).some((loc) => existsSync2(entryPath(listsDir, listName, site, loc, id)));
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (next.length !== order.length) {
|
|
261
|
-
writeOrder(listsDir, listName, site, next);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
290
|
+
const order = readOrder(listsDir, listName, site);
|
|
291
|
+
const next = orderAfterDelete(order, id, stillExists);
|
|
292
|
+
if (next !== order) writeOrder(listsDir, listName, site, next);
|
|
264
293
|
},
|
|
265
294
|
async reorder(site, listName, ids) {
|
|
266
295
|
const locales = listLocales(listsDir, listName, site);
|
|
@@ -270,20 +299,14 @@ function makeListStore(listsDir) {
|
|
|
270
299
|
allIds.add(id);
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
throw new Error(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const supplied = new Set(ids);
|
|
279
|
-
for (const id of allIds) {
|
|
280
|
-
if (supplied.has(id)) continue;
|
|
302
|
+
const { order, dropped } = planReorder(ids, allIds, listName);
|
|
303
|
+
for (const id of dropped) {
|
|
281
304
|
for (const loc of locales) {
|
|
282
305
|
const path = entryPath(listsDir, listName, site, loc, id);
|
|
283
306
|
if (existsSync2(path)) rmSync(path);
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
|
-
writeOrder(listsDir, listName, site,
|
|
309
|
+
writeOrder(listsDir, listName, site, order);
|
|
287
310
|
},
|
|
288
311
|
async translations(site, listName) {
|
|
289
312
|
const locales = listLocales(listsDir, listName, site);
|
|
@@ -320,5 +343,11 @@ export {
|
|
|
320
343
|
createJsonFileAdapter,
|
|
321
344
|
canonicalize,
|
|
322
345
|
hashRev,
|
|
346
|
+
applyOrder,
|
|
347
|
+
orderAfterCreate,
|
|
348
|
+
orderAfterDelete,
|
|
349
|
+
planReorder,
|
|
350
|
+
entryExistsMessage,
|
|
351
|
+
entryNotFoundMessage,
|
|
323
352
|
createJsonFileAdapterV2
|
|
324
353
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/auth-core.ts
|
|
2
|
+
var MAX_FAILURES = 5;
|
|
3
|
+
var WINDOW_MS = 6e4;
|
|
4
|
+
function createRateLimiter() {
|
|
5
|
+
return /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
function getIp(req) {
|
|
8
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
9
|
+
}
|
|
10
|
+
function isRateLimited(limiter, ip, now = Date.now()) {
|
|
11
|
+
const bucket = limiter.get(ip);
|
|
12
|
+
if (!bucket || now > bucket.resetAt) return false;
|
|
13
|
+
return bucket.failures >= MAX_FAILURES;
|
|
14
|
+
}
|
|
15
|
+
function recordFailure(limiter, ip, now = Date.now()) {
|
|
16
|
+
const bucket = limiter.get(ip);
|
|
17
|
+
if (!bucket || now > bucket.resetAt) {
|
|
18
|
+
limiter.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
19
|
+
} else {
|
|
20
|
+
bucket.failures += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function clearFailures(limiter, ip) {
|
|
24
|
+
limiter.delete(ip);
|
|
25
|
+
}
|
|
26
|
+
var json = (body, status) => new Response(JSON.stringify(body), {
|
|
27
|
+
status,
|
|
28
|
+
headers: { "Content-Type": "application/json" }
|
|
29
|
+
});
|
|
30
|
+
async function runAuth(cfg) {
|
|
31
|
+
const { request, secret, limiter } = cfg;
|
|
32
|
+
const now = cfg.now ?? Date.now();
|
|
33
|
+
const ip = getIp(request);
|
|
34
|
+
if (isRateLimited(limiter, ip, now)) {
|
|
35
|
+
return json({ error: "Too many attempts. Try again in a minute." }, 429);
|
|
36
|
+
}
|
|
37
|
+
let token;
|
|
38
|
+
try {
|
|
39
|
+
const body = await request.json();
|
|
40
|
+
token = body?.token;
|
|
41
|
+
} catch {
|
|
42
|
+
return json({ error: "Invalid request body" }, 400);
|
|
43
|
+
}
|
|
44
|
+
if (!token || !secret || token !== secret) {
|
|
45
|
+
recordFailure(limiter, ip, now);
|
|
46
|
+
return json({ error: "Invalid token" }, 401);
|
|
47
|
+
}
|
|
48
|
+
clearFailures(limiter, ip);
|
|
49
|
+
return json({ ok: true }, 200);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
createRateLimiter,
|
|
54
|
+
runAuth
|
|
55
|
+
};
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
createGitBackedAdapter,
|
|
7
7
|
createSqliteAdapterV2
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MXVOJRAM.js";
|
|
9
9
|
import {
|
|
10
10
|
createJsonFileAdapter,
|
|
11
11
|
createJsonFileAdapterV2
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-2NVZWZPE.js";
|
|
13
13
|
|
|
14
14
|
// src/routes/upload.ts
|
|
15
15
|
import { writeFile, mkdir } from "fs/promises";
|
|
@@ -2,8 +2,14 @@ import {
|
|
|
2
2
|
createGitHubClient
|
|
3
3
|
} from "./chunk-U7V53JX7.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
applyOrder,
|
|
6
|
+
entryExistsMessage,
|
|
7
|
+
entryNotFoundMessage,
|
|
8
|
+
hashRev,
|
|
9
|
+
orderAfterCreate,
|
|
10
|
+
orderAfterDelete,
|
|
11
|
+
planReorder
|
|
12
|
+
} from "./chunk-2NVZWZPE.js";
|
|
7
13
|
import {
|
|
8
14
|
RevConflictError
|
|
9
15
|
} from "./chunk-7IA5B5CF.js";
|
|
@@ -119,18 +125,6 @@ function rowToEntry(row) {
|
|
|
119
125
|
_rev: hashRev(data)
|
|
120
126
|
};
|
|
121
127
|
}
|
|
122
|
-
function applyOrder(order, ids) {
|
|
123
|
-
const result = [];
|
|
124
|
-
const seen = /* @__PURE__ */ new Set();
|
|
125
|
-
for (const id of order) {
|
|
126
|
-
if (ids.has(id)) {
|
|
127
|
-
result.push(id);
|
|
128
|
-
seen.add(id);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
132
|
-
return [...result, ...extras];
|
|
133
|
-
}
|
|
134
128
|
function makeListStore(db) {
|
|
135
129
|
const getOrderStmt = db.prepare("SELECT ids FROM list_order WHERE site=? AND list=?");
|
|
136
130
|
const upsertOrderStmt = db.prepare(
|
|
@@ -178,32 +172,25 @@ function makeListStore(db) {
|
|
|
178
172
|
(site, listName, id, locale, dataJson, now) => {
|
|
179
173
|
insertEntryStmt.run(site, listName, id, locale, dataJson, now, now);
|
|
180
174
|
const order = readOrder(site, listName);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
175
|
+
const next = orderAfterCreate(order, id);
|
|
176
|
+
if (next !== order) writeOrder(site, listName, next);
|
|
184
177
|
}
|
|
185
178
|
);
|
|
186
179
|
const deleteTx = db.transaction((site, listName, id, locale) => {
|
|
187
180
|
deleteEntryStmt.run(site, listName, id, locale);
|
|
188
181
|
const stillExists = distinctIds(site, listName).has(id);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (next.length !== order.length) {
|
|
193
|
-
writeOrder(site, listName, next);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
182
|
+
const order = readOrder(site, listName);
|
|
183
|
+
const next = orderAfterDelete(order, id, stillExists);
|
|
184
|
+
if (next !== order) writeOrder(site, listName, next);
|
|
196
185
|
});
|
|
197
|
-
const reorderTx = db.transaction(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const id of all) {
|
|
201
|
-
if (!supplied.has(id)) {
|
|
186
|
+
const reorderTx = db.transaction(
|
|
187
|
+
(site, listName, order, dropped) => {
|
|
188
|
+
for (const id of dropped) {
|
|
202
189
|
deleteEntryAllLocalesStmt.run(site, listName, id);
|
|
203
190
|
}
|
|
191
|
+
writeOrder(site, listName, order);
|
|
204
192
|
}
|
|
205
|
-
|
|
206
|
-
});
|
|
193
|
+
);
|
|
207
194
|
return {
|
|
208
195
|
async list(site, listName, locale) {
|
|
209
196
|
const order = readOrder(site, listName);
|
|
@@ -244,7 +231,7 @@ function makeListStore(db) {
|
|
|
244
231
|
const finalId = id ?? randomUUID();
|
|
245
232
|
const existing = getEntryStmt.get(site, listName, finalId, locale);
|
|
246
233
|
if (existing) {
|
|
247
|
-
throw new Error(
|
|
234
|
+
throw new Error(entryExistsMessage(finalId, listName, locale));
|
|
248
235
|
}
|
|
249
236
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
237
|
createTx(site, listName, finalId, locale, JSON.stringify(data), now);
|
|
@@ -260,7 +247,7 @@ function makeListStore(db) {
|
|
|
260
247
|
async update(site, listName, id, locale, data, rev) {
|
|
261
248
|
const existing = getEntryStmt.get(site, listName, id, locale);
|
|
262
249
|
if (!existing) {
|
|
263
|
-
throw new Error(
|
|
250
|
+
throw new Error(entryNotFoundMessage(id, listName, locale));
|
|
264
251
|
}
|
|
265
252
|
const existingData = JSON.parse(existing.data);
|
|
266
253
|
if (hashRev(existingData) !== rev) throw new RevConflictError();
|
|
@@ -282,12 +269,8 @@ function makeListStore(db) {
|
|
|
282
269
|
},
|
|
283
270
|
async reorder(site, listName, ids) {
|
|
284
271
|
const all = distinctIds(site, listName);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new Error(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
reorderTx(site, listName, ids);
|
|
272
|
+
const { order, dropped } = planReorder(ids, all, listName);
|
|
273
|
+
reorderTx(site, listName, order, dropped);
|
|
291
274
|
},
|
|
292
275
|
async translations(site, listName) {
|
|
293
276
|
const rows = allEntriesStmt.all(site, listName);
|
|
@@ -93,7 +93,14 @@ var defineField = {
|
|
|
93
93
|
*/
|
|
94
94
|
link: (o) => z.object({
|
|
95
95
|
label: z.string(),
|
|
96
|
-
href:
|
|
96
|
+
// An EMPTY href is allowed: it means "this link has no destination
|
|
97
|
+
// yet", which is a state the editor must be able to save — otherwise a
|
|
98
|
+
// client cannot clear a URL at all, and the form rejects a field they
|
|
99
|
+
// deliberately emptied. `isSafeHref("")` is false (correct for
|
|
100
|
+
// rendering, where an empty href must not become an anchor), so the
|
|
101
|
+
// empty case is permitted here explicitly rather than by loosening the
|
|
102
|
+
// guard. Anything non-empty still has to pass it.
|
|
103
|
+
href: z.string().refine((h) => h === "" || isSafeHref(h), "unsafe or unsupported URL scheme")
|
|
97
104
|
}).meta({ widget: "link", ...o }),
|
|
98
105
|
/**
|
|
99
106
|
* A constrained rich-text body. Stored as a Portable-Text SUBSET array (D4):
|
package/dist/content.js
CHANGED
|
@@ -2,9 +2,9 @@ import "./chunk-FOSOWSXV.js";
|
|
|
2
2
|
import "./chunk-CJDIVWO3.js";
|
|
3
3
|
import {
|
|
4
4
|
createSqliteAdapterV2
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-MXVOJRAM.js";
|
|
6
6
|
import "./chunk-U7V53JX7.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-2NVZWZPE.js";
|
|
8
8
|
import "./chunk-7IA5B5CF.js";
|
|
9
9
|
|
|
10
10
|
// src/content.ts
|
|
@@ -20,17 +20,17 @@ function isSafeHref(href) {
|
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
22
|
function parseLink(raw, fallback) {
|
|
23
|
-
if (
|
|
23
|
+
if (raw === void 0) return fallback;
|
|
24
|
+
if (raw === "") return { label: "", href: "" };
|
|
24
25
|
const trimmed = raw.trim();
|
|
25
26
|
if (trimmed.startsWith("{")) {
|
|
26
27
|
try {
|
|
27
28
|
const p = JSON.parse(trimmed);
|
|
28
29
|
if (p && typeof p === "object") {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
30
|
+
const label = p.label === void 0 ? fallback.label : String(p.label);
|
|
31
|
+
const rawHref = p.href === void 0 ? void 0 : String(p.href);
|
|
32
|
+
const href = rawHref === void 0 ? fallback.href : rawHref === "" ? "" : isSafeHref(rawHref) ? rawHref : fallback.href;
|
|
33
|
+
return { label, href };
|
|
34
34
|
}
|
|
35
35
|
} catch {
|
|
36
36
|
}
|
package/dist/endpoints/auth.js
CHANGED
|
@@ -1,55 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRateLimiter,
|
|
3
|
+
runAuth
|
|
4
|
+
} from "../chunk-GJIX7MWC.js";
|
|
5
|
+
|
|
1
6
|
// src/endpoints/auth.ts
|
|
2
7
|
import { getCanciaRuntime } from "virtual:cancia/runtime";
|
|
3
|
-
var
|
|
4
|
-
var MAX_FAILURES = 5;
|
|
5
|
-
var WINDOW_MS = 6e4;
|
|
6
|
-
function getIp(req) {
|
|
7
|
-
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
8
|
-
}
|
|
9
|
-
function isRateLimited(ip) {
|
|
10
|
-
const bucket = buckets.get(ip);
|
|
11
|
-
if (!bucket || Date.now() > bucket.resetAt) return false;
|
|
12
|
-
return bucket.failures >= MAX_FAILURES;
|
|
13
|
-
}
|
|
14
|
-
function recordFailure(ip) {
|
|
15
|
-
const now = Date.now();
|
|
16
|
-
const bucket = buckets.get(ip);
|
|
17
|
-
if (!bucket || now > bucket.resetAt) {
|
|
18
|
-
buckets.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
19
|
-
} else {
|
|
20
|
-
bucket.failures += 1;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
8
|
+
var limiter = createRateLimiter();
|
|
23
9
|
async function POST({ request }) {
|
|
24
10
|
const { secret } = getCanciaRuntime();
|
|
25
|
-
|
|
26
|
-
if (isRateLimited(ip))
|
|
27
|
-
return new Response(
|
|
28
|
-
JSON.stringify({ error: "Too many attempts. Try again in a minute." }),
|
|
29
|
-
{ status: 429, headers: { "Content-Type": "application/json" } }
|
|
30
|
-
);
|
|
31
|
-
let token;
|
|
32
|
-
try {
|
|
33
|
-
const body = await request.json();
|
|
34
|
-
token = body?.token;
|
|
35
|
-
} catch {
|
|
36
|
-
return new Response(
|
|
37
|
-
JSON.stringify({ error: "Invalid request body" }),
|
|
38
|
-
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
if (!token || token !== secret) {
|
|
42
|
-
recordFailure(ip);
|
|
43
|
-
return new Response(
|
|
44
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
45
|
-
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
buckets.delete(ip);
|
|
49
|
-
return new Response(
|
|
50
|
-
JSON.stringify({ ok: true }),
|
|
51
|
-
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
52
|
-
);
|
|
11
|
+
return runAuth({ request, secret, limiter });
|
|
53
12
|
}
|
|
54
13
|
export {
|
|
55
14
|
POST
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
makeSchemasRoute
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ONKCUTPU.js";
|
|
4
4
|
import {
|
|
5
5
|
runPublish
|
|
6
6
|
} from "./chunk-5RCLBWRH.js";
|
|
7
|
+
import {
|
|
8
|
+
createRateLimiter,
|
|
9
|
+
runAuth
|
|
10
|
+
} from "./chunk-GJIX7MWC.js";
|
|
7
11
|
import {
|
|
8
12
|
makeListsRoutes
|
|
9
13
|
} from "./chunk-DIV2FFYX.js";
|
|
@@ -12,7 +16,7 @@ import {
|
|
|
12
16
|
makeR2UploadHandler,
|
|
13
17
|
makeUploadRoute,
|
|
14
18
|
setCanciaRuntime
|
|
15
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LJSSPOSW.js";
|
|
16
20
|
import "./chunk-5IPHDIC6.js";
|
|
17
21
|
import {
|
|
18
22
|
createSQLiteAdapter
|
|
@@ -21,16 +25,16 @@ import {
|
|
|
21
25
|
closeSqliteAdapterV2,
|
|
22
26
|
createGitBackedAdapter,
|
|
23
27
|
createSqliteAdapterV2
|
|
24
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-MXVOJRAM.js";
|
|
25
29
|
import "./chunk-U7V53JX7.js";
|
|
26
30
|
import "./chunk-VL6FO446.js";
|
|
27
31
|
import {
|
|
28
32
|
canciaLoader
|
|
29
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-NVXNJ4YC.js";
|
|
30
34
|
import {
|
|
31
35
|
createJsonFileAdapter,
|
|
32
36
|
createJsonFileAdapterV2
|
|
33
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-2NVZWZPE.js";
|
|
34
38
|
import {
|
|
35
39
|
RevConflictError
|
|
36
40
|
} from "./chunk-7IA5B5CF.js";
|
|
@@ -39,7 +43,7 @@ import {
|
|
|
39
43
|
defineList,
|
|
40
44
|
describeList,
|
|
41
45
|
z
|
|
42
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-YGJ3JHXF.js";
|
|
43
47
|
import "./chunk-KHCFVVYV.js";
|
|
44
48
|
|
|
45
49
|
// src/integration.ts
|
|
@@ -115,61 +119,10 @@ function makePublishRoute(deployHook, secret, hookOptions, dispatch, mode) {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
// src/routes/auth.ts
|
|
118
|
-
var buckets = /* @__PURE__ */ new Map();
|
|
119
|
-
var MAX_FAILURES = 5;
|
|
120
|
-
var WINDOW_MS = 6e4;
|
|
121
|
-
function getIp(req) {
|
|
122
|
-
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
123
|
-
}
|
|
124
|
-
function isRateLimited(ip) {
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
const bucket = buckets.get(ip);
|
|
127
|
-
if (!bucket || now > bucket.resetAt) return false;
|
|
128
|
-
return bucket.failures >= MAX_FAILURES;
|
|
129
|
-
}
|
|
130
|
-
function recordFailure(ip) {
|
|
131
|
-
const now = Date.now();
|
|
132
|
-
const bucket = buckets.get(ip);
|
|
133
|
-
if (!bucket || now > bucket.resetAt) {
|
|
134
|
-
buckets.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
135
|
-
} else {
|
|
136
|
-
bucket.failures += 1;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function clearFailures(ip) {
|
|
140
|
-
buckets.delete(ip);
|
|
141
|
-
}
|
|
142
122
|
function makeAuthRoute(secret) {
|
|
123
|
+
const limiter = createRateLimiter();
|
|
143
124
|
return async function authRoute(req) {
|
|
144
|
-
|
|
145
|
-
if (isRateLimited(ip)) {
|
|
146
|
-
return new Response(
|
|
147
|
-
JSON.stringify({ error: "Too many attempts. Try again in a minute." }),
|
|
148
|
-
{ status: 429, headers: { "Content-Type": "application/json" } }
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
let token;
|
|
152
|
-
try {
|
|
153
|
-
const body = await req.json();
|
|
154
|
-
token = body?.token;
|
|
155
|
-
} catch {
|
|
156
|
-
return new Response(
|
|
157
|
-
JSON.stringify({ error: "Invalid request body" }),
|
|
158
|
-
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
if (!token || token !== secret) {
|
|
162
|
-
recordFailure(ip);
|
|
163
|
-
return new Response(
|
|
164
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
165
|
-
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
clearFailures(ip);
|
|
169
|
-
return new Response(
|
|
170
|
-
JSON.stringify({ ok: true }),
|
|
171
|
-
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
172
|
-
);
|
|
125
|
+
return runAuth({ request: req, secret, limiter });
|
|
173
126
|
};
|
|
174
127
|
}
|
|
175
128
|
|
package/dist/loader/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canciaLoader
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-NVXNJ4YC.js";
|
|
4
|
+
import "../chunk-2NVZWZPE.js";
|
|
5
5
|
import "../chunk-7IA5B5CF.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-YGJ3JHXF.js";
|
|
7
7
|
import "../chunk-KHCFVVYV.js";
|
|
8
8
|
export {
|
|
9
9
|
canciaLoader
|
package/dist/runtime.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
getCanciaRuntime,
|
|
4
4
|
setBakedConfig,
|
|
5
5
|
setCanciaRuntime
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LJSSPOSW.js";
|
|
7
7
|
import "./chunk-5IPHDIC6.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-MXVOJRAM.js";
|
|
9
9
|
import "./chunk-U7V53JX7.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-2NVZWZPE.js";
|
|
11
11
|
import "./chunk-7IA5B5CF.js";
|
|
12
12
|
export {
|
|
13
13
|
__resetRuntimeForTests,
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -123,7 +123,9 @@ interface ListSchema<TFields extends Record<string, z.ZodTypeAny> = Record<strin
|
|
|
123
123
|
* to "published", matching the field's own default and keeping entries that
|
|
124
124
|
* predate the draftField visible.
|
|
125
125
|
*/
|
|
126
|
-
declare function isDraft(schema:
|
|
126
|
+
declare function isDraft(schema: {
|
|
127
|
+
draftField?: string;
|
|
128
|
+
}, data: Record<string, unknown>): boolean;
|
|
127
129
|
declare function defineList<TFields extends Record<string, z.ZodTypeAny>>(opts: ListSchemaOptions<TFields>): ListSchema<TFields>;
|
|
128
130
|
/**
|
|
129
131
|
* One field as seen by the modal form. Plain JSON, no Zod refs.
|
package/dist/schema/index.js
CHANGED
package/dist/storage/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
closeSqliteAdapterV2,
|
|
7
7
|
createGitBackedAdapter,
|
|
8
8
|
createSqliteAdapterV2
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-MXVOJRAM.js";
|
|
10
10
|
import {
|
|
11
11
|
createGitHubClient
|
|
12
12
|
} from "../chunk-U7V53JX7.js";
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createJsonFileAdapter,
|
|
16
16
|
createJsonFileAdapterV2,
|
|
17
17
|
hashRev
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-2NVZWZPE.js";
|
|
19
19
|
import {
|
|
20
20
|
RevConflictError
|
|
21
21
|
} from "../chunk-7IA5B5CF.js";
|
|
@@ -44,11 +44,10 @@ function __canciaLink(cms, key, labelFallback, hrefFallback) {
|
|
|
44
44
|
if (trimmed.startsWith("{")) {
|
|
45
45
|
try {
|
|
46
46
|
const parsed = JSON.parse(trimmed);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
47
|
+
const label = parsed.label === void 0 ? labelFallback : String(parsed.label);
|
|
48
|
+
const rawHref = parsed.href === void 0 ? void 0 : String(parsed.href);
|
|
49
|
+
const href = rawHref === void 0 ? hrefFallback : rawHref === "" ? "" : isSafeHref(rawHref) ? rawHref : hrefFallback;
|
|
50
|
+
return { label, href };
|
|
52
51
|
} catch {
|
|
53
52
|
return fallback;
|
|
54
53
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cancia/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Astro integration for Cancia CMS — inline editing with zero separate server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/ninuzdellalb/cancia",
|
|
8
|
+
"url": "git+https://github.com/ninuzdellalb/cancia.git",
|
|
9
9
|
"directory": "packages/astro"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|