@beechcms/api 0.6.2 → 0.6.4
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/assets/dashboard/assets/{chart-CboAhzSV.js → chart-CERC48Yu.js} +1 -1
- package/assets/dashboard/assets/{index-B19ig64T.js → index-BwTrD5DX.js} +81 -81
- package/assets/dashboard/assets/{pie-chart-recharts-B89zkqfa.js → pie-chart-recharts-DkdMjYy6.js} +1 -1
- package/assets/dashboard/assets/{timeseries-chart-recharts-kD3eDNrV.js → timeseries-chart-recharts-Co1oKyym.js} +1 -1
- package/assets/dashboard/index.html +1 -1
- package/dist/index.js +92 -38
- package/dist/shared/db/repositories/d1-user.repository.d.ts +1 -0
- package/dist/shared/email/templates/automation-mail.d.ts +0 -1
- package/dist/shared/services/queue/cloudflare-queue-service.d.ts +5 -2
- package/dist/shared/services/queue/in-memory-queue-service.d.ts +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -861,9 +861,15 @@ async function updateHandler(context) {
|
|
|
861
861
|
// src/shared/storage/upload.ts
|
|
862
862
|
async function deleteR2Objects(c, objectKeys) {
|
|
863
863
|
const { bucket, mediaRepository, systemStatsRepository } = c.var;
|
|
864
|
+
const untrackFailures = [];
|
|
864
865
|
await Promise.all(
|
|
865
866
|
objectKeys.map(async (key) => {
|
|
866
|
-
|
|
867
|
+
let media;
|
|
868
|
+
try {
|
|
869
|
+
media = await mediaRepository.getByKey(key);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
throw new Error(`Media lookup failed for key, not deleted: ${key}`, { cause: err });
|
|
872
|
+
}
|
|
867
873
|
if (!media) {
|
|
868
874
|
throw new Error(`Media object not found: ${key}`);
|
|
869
875
|
}
|
|
@@ -876,9 +882,15 @@ async function deleteR2Objects(c, objectKeys) {
|
|
|
876
882
|
}
|
|
877
883
|
} catch (err) {
|
|
878
884
|
console.warn(`Failed to untrack media object: ${key}`, err);
|
|
885
|
+
untrackFailures.push(key);
|
|
879
886
|
}
|
|
880
887
|
})
|
|
881
888
|
);
|
|
889
|
+
if (untrackFailures.length > 0) {
|
|
890
|
+
throw new Error(
|
|
891
|
+
`${untrackFailures.length} media row(s) now out of sync (R2 object deleted but DB untrack/decrement failed): ${untrackFailures.join(", ")}`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
882
894
|
}
|
|
883
895
|
|
|
884
896
|
// src/shared/utils/media-utils.ts
|
|
@@ -1863,7 +1875,7 @@ function resolveEmailLocale(raw) {
|
|
|
1863
1875
|
import { sha256hex as sha256hex3 } from "@beechcms/core";
|
|
1864
1876
|
var PASSWORD_RESET_TOKEN_EXPIRY_SECONDS = 30 * 60;
|
|
1865
1877
|
async function requestPasswordReset(context) {
|
|
1866
|
-
const { env, req } = context;
|
|
1878
|
+
const { env, req, executionCtx } = context;
|
|
1867
1879
|
const useSmtp = env.EMAIL_PROVIDER === "smtp";
|
|
1868
1880
|
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
1869
1881
|
return context.json({ error: "Service not available" }, 503);
|
|
@@ -1906,21 +1918,28 @@ async function requestPasswordReset(context) {
|
|
|
1906
1918
|
const baseUrl = (env.APP_URL ?? new URL(req.url).origin).replace(/\/$/, "");
|
|
1907
1919
|
const resetUrl = `${baseUrl}/admin/reset-password?token=${resetToken}`;
|
|
1908
1920
|
const smtpBaseUrl = env.SMTP_HOST ? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? "8025"}` : void 0;
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1921
|
+
const sendNotification = async () => {
|
|
1922
|
+
try {
|
|
1923
|
+
await sendPasswordResetEmail({
|
|
1924
|
+
to: normalizedEmail,
|
|
1925
|
+
resetUrl,
|
|
1926
|
+
locale: emailLocale,
|
|
1927
|
+
apiKey: env.RESEND_API_KEY ?? "",
|
|
1928
|
+
from: env.EMAIL_FROM,
|
|
1929
|
+
isDev: env.ENV !== "production",
|
|
1930
|
+
provider: env.EMAIL_PROVIDER,
|
|
1931
|
+
smtpBaseUrl
|
|
1932
|
+
});
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
if (env.ENV !== "production") {
|
|
1935
|
+
console.error("[password-reset] Failed to send email:", error);
|
|
1936
|
+
}
|
|
1923
1937
|
}
|
|
1938
|
+
};
|
|
1939
|
+
try {
|
|
1940
|
+
executionCtx.waitUntil(sendNotification());
|
|
1941
|
+
} catch {
|
|
1942
|
+
void sendNotification();
|
|
1924
1943
|
}
|
|
1925
1944
|
return context.json({ success: true });
|
|
1926
1945
|
}
|
|
@@ -2157,6 +2176,26 @@ setupApp.post("/auth/setup", async (context) => {
|
|
|
2157
2176
|
});
|
|
2158
2177
|
}
|
|
2159
2178
|
}
|
|
2179
|
+
const passwordHash = await context.get("hashProvider").hash(password);
|
|
2180
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
2181
|
+
const normalizedName = typeof name === "string" ? name.trim() : null;
|
|
2182
|
+
const normalizedSurname = typeof surname === "string" ? surname.trim() : null;
|
|
2183
|
+
const created = await context.get("userRepository").createInitialAdmin({
|
|
2184
|
+
id: context.get("idGenerator").uuid(),
|
|
2185
|
+
email: normalizedEmail,
|
|
2186
|
+
passwordHash,
|
|
2187
|
+
role: "admin",
|
|
2188
|
+
name: normalizedName,
|
|
2189
|
+
surname: normalizedSurname
|
|
2190
|
+
});
|
|
2191
|
+
if (!created) {
|
|
2192
|
+
return publicProblem(context, {
|
|
2193
|
+
type: "setup-already-done",
|
|
2194
|
+
title: "Setup already completed",
|
|
2195
|
+
status: 403,
|
|
2196
|
+
detail: "An administrator account already exists. Initial setup can only be performed once."
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2160
2199
|
if (track === "developer" && loadDemoData === true) {
|
|
2161
2200
|
await context.get("demoDataRepository").loadDemoData();
|
|
2162
2201
|
const layout = {
|
|
@@ -2242,18 +2281,6 @@ setupApp.post("/auth/setup", async (context) => {
|
|
|
2242
2281
|
};
|
|
2243
2282
|
await context.get("dashboardLayoutRepository").upsert("default", layout, "system");
|
|
2244
2283
|
}
|
|
2245
|
-
const passwordHash = await context.get("hashProvider").hash(password);
|
|
2246
|
-
const normalizedEmail = email.trim().toLowerCase();
|
|
2247
|
-
const normalizedName = typeof name === "string" ? name.trim() : null;
|
|
2248
|
-
const normalizedSurname = typeof surname === "string" ? surname.trim() : null;
|
|
2249
|
-
await context.get("userRepository").create({
|
|
2250
|
-
id: context.get("idGenerator").uuid(),
|
|
2251
|
-
email: normalizedEmail,
|
|
2252
|
-
passwordHash,
|
|
2253
|
-
role: "admin",
|
|
2254
|
-
name: normalizedName,
|
|
2255
|
-
surname: normalizedSurname
|
|
2256
|
-
});
|
|
2257
2284
|
if (track === "normal" && company && typeof company === "object") {
|
|
2258
2285
|
const c = company;
|
|
2259
2286
|
const companyName = c.name.trim();
|
|
@@ -3135,7 +3162,8 @@ async function deleteSeedMediaObjects(context, slug, seed, schemaMutator) {
|
|
|
3135
3162
|
}
|
|
3136
3163
|
}
|
|
3137
3164
|
if (r2Keys.length > 0) {
|
|
3138
|
-
await deleteR2Objects(context, r2Keys).catch(() => {
|
|
3165
|
+
await deleteR2Objects(context, r2Keys).catch((error) => {
|
|
3166
|
+
console.warn(`Seed drop for '${slug}' left media rows out of sync:`, error);
|
|
3139
3167
|
});
|
|
3140
3168
|
}
|
|
3141
3169
|
} catch {
|
|
@@ -3624,10 +3652,13 @@ function interpolate(template, context, defaultValue = "", onMissing) {
|
|
|
3624
3652
|
if (val == null || val === "") {
|
|
3625
3653
|
return defaultValue;
|
|
3626
3654
|
}
|
|
3627
|
-
return String(val);
|
|
3655
|
+
return escapeHtml(String(val));
|
|
3628
3656
|
};
|
|
3629
3657
|
return template.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, replacer);
|
|
3630
3658
|
}
|
|
3659
|
+
function escapeHtml(input) {
|
|
3660
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3661
|
+
}
|
|
3631
3662
|
function resolvePath(obj, path) {
|
|
3632
3663
|
if (path in obj && obj[path] !== void 0) {
|
|
3633
3664
|
return obj[path];
|
|
@@ -5050,7 +5081,8 @@ webhooksApp.post("/qstash", async (context) => {
|
|
|
5050
5081
|
try {
|
|
5051
5082
|
const isValid = await receiver.verify({
|
|
5052
5083
|
signature,
|
|
5053
|
-
body
|
|
5084
|
+
body,
|
|
5085
|
+
url: context.req.url
|
|
5054
5086
|
});
|
|
5055
5087
|
if (!isValid) {
|
|
5056
5088
|
return context.text("Invalid signature", 401);
|
|
@@ -5308,7 +5340,7 @@ function publicRateLimitMiddleware() {
|
|
|
5308
5340
|
const remaining = path.slice("/api/v1/public/".length);
|
|
5309
5341
|
const firstSegment = remaining.split("/")[0];
|
|
5310
5342
|
if (firstSegment && firstSegment !== "health" && firstSegment !== "schema" && firstSegment !== "schema.html") {
|
|
5311
|
-
seed = firstSegment;
|
|
5343
|
+
seed = c.get("seedRegistry").get(firstSegment) ? firstSegment : "invalid-seed";
|
|
5312
5344
|
}
|
|
5313
5345
|
}
|
|
5314
5346
|
const key = `${getClientIp(c.req)}:${seed}:${limiterName}`;
|
|
@@ -7332,6 +7364,18 @@ var D1UserRepository = class {
|
|
|
7332
7364
|
async create(user) {
|
|
7333
7365
|
await this.db.prepare("INSERT INTO users (id, email, password_hash, role, name, surname) VALUES (?, ?, ?, ?, ?, ?)").bind(user.id, user.email, user.passwordHash, user.role, user.name, user.surname).run();
|
|
7334
7366
|
}
|
|
7367
|
+
async createInitialAdmin(user) {
|
|
7368
|
+
try {
|
|
7369
|
+
await this.db.batch([
|
|
7370
|
+
this.db.prepare("INSERT INTO setup_completed (id) VALUES (1)"),
|
|
7371
|
+
this.db.prepare("INSERT INTO users (id, email, password_hash, role, name, surname) VALUES (?, ?, ?, ?, ?, ?)").bind(user.id, user.email, user.passwordHash, user.role, user.name, user.surname)
|
|
7372
|
+
]);
|
|
7373
|
+
return true;
|
|
7374
|
+
} catch (err) {
|
|
7375
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint failed")) return false;
|
|
7376
|
+
throw err;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7335
7379
|
async updateProfile(userId, fields) {
|
|
7336
7380
|
const columnAssignments = [];
|
|
7337
7381
|
const boundValues = [];
|
|
@@ -10073,8 +10117,10 @@ var CloudflareQueueService = class {
|
|
|
10073
10117
|
async enqueue(name, payload) {
|
|
10074
10118
|
try {
|
|
10075
10119
|
await this.queue.send({ name, payload });
|
|
10120
|
+
return true;
|
|
10076
10121
|
} catch (error) {
|
|
10077
10122
|
console.error(`CloudflareQueueService: failed to enqueue "${name}"`, error);
|
|
10123
|
+
return false;
|
|
10078
10124
|
}
|
|
10079
10125
|
}
|
|
10080
10126
|
};
|
|
@@ -10090,19 +10136,27 @@ var InMemoryQueueService = class {
|
|
|
10090
10136
|
context;
|
|
10091
10137
|
scheduleBackgroundTask;
|
|
10092
10138
|
async enqueue(name, payload) {
|
|
10093
|
-
const handler = this.jobs[name];
|
|
10139
|
+
const handler = Object.hasOwn(this.jobs, name) ? this.jobs[name] : void 0;
|
|
10094
10140
|
if (!handler) {
|
|
10095
10141
|
console.error(`InMemoryQueueService: no handler registered for "${name}"`);
|
|
10096
|
-
return;
|
|
10142
|
+
return false;
|
|
10143
|
+
}
|
|
10144
|
+
let run;
|
|
10145
|
+
try {
|
|
10146
|
+
const result = handler(payload, this.context);
|
|
10147
|
+
run = Promise.resolve(result).catch((error) => {
|
|
10148
|
+
console.error(`InMemoryQueueService: job "${name}" failed`, error);
|
|
10149
|
+
});
|
|
10150
|
+
} catch (error) {
|
|
10151
|
+
console.error(`InMemoryQueueService: job "${name}" failed synchronously`, error);
|
|
10152
|
+
return false;
|
|
10097
10153
|
}
|
|
10098
|
-
const run = handler(payload, this.context).catch((error) => {
|
|
10099
|
-
console.error(`InMemoryQueueService: job "${name}" failed`, error);
|
|
10100
|
-
});
|
|
10101
10154
|
if (this.scheduleBackgroundTask) {
|
|
10102
10155
|
this.scheduleBackgroundTask(run);
|
|
10103
|
-
return;
|
|
10156
|
+
return true;
|
|
10104
10157
|
}
|
|
10105
10158
|
await run;
|
|
10159
|
+
return true;
|
|
10106
10160
|
}
|
|
10107
10161
|
};
|
|
10108
10162
|
|
|
@@ -6,6 +6,7 @@ export declare class D1UserRepository implements IUserRepository {
|
|
|
6
6
|
findById(userId: string): Promise<UserRecord | null>;
|
|
7
7
|
findByEmail(email: string): Promise<UserRecord | null>;
|
|
8
8
|
create(user: NewUserInput): Promise<void>;
|
|
9
|
+
createInitialAdmin(user: NewUserInput): Promise<boolean>;
|
|
9
10
|
updateProfile(userId: string, fields: {
|
|
10
11
|
name?: string;
|
|
11
12
|
surname?: string;
|
|
@@ -2,10 +2,13 @@ import type { IQueueService, QueueMessage } from '@beechcms/core';
|
|
|
2
2
|
/**
|
|
3
3
|
* Production producer. Sends a QueueMessage envelope onto the Cloudflare Queue
|
|
4
4
|
* binding. Cloudflare guarantees at-least-once delivery + retries on the
|
|
5
|
-
* consumer side
|
|
5
|
+
* consumer side once a message is accepted, but `send()` itself rejects
|
|
6
|
+
* outright for transport-level failures (e.g. the 128 KiB per-message size
|
|
7
|
+
* limit) — those never reach the consumer, so callers MUST be told the
|
|
8
|
+
* message was dropped instead of assuming it was scheduled.
|
|
6
9
|
*/
|
|
7
10
|
export declare class CloudflareQueueService implements IQueueService {
|
|
8
11
|
private readonly queue;
|
|
9
12
|
constructor(queue: Queue<QueueMessage>);
|
|
10
|
-
enqueue<T>(name: string, payload: T): Promise<
|
|
13
|
+
enqueue<T>(name: string, payload: T): Promise<boolean>;
|
|
11
14
|
}
|
|
@@ -11,6 +11,6 @@ export declare class InMemoryQueueService implements IQueueService {
|
|
|
11
11
|
private readonly context;
|
|
12
12
|
private readonly scheduleBackgroundTask?;
|
|
13
13
|
constructor(jobs: JobRegistry, context: JobContext, scheduleBackgroundTask?: ScheduleBackgroundTask | undefined);
|
|
14
|
-
enqueue<T>(name: string, payload: T): Promise<
|
|
14
|
+
enqueue<T>(name: string, payload: T): Promise<boolean>;
|
|
15
15
|
}
|
|
16
16
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beechcms/api",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/factory.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@aws-sdk/client-s3": "^3.995.0",
|
|
22
22
|
"@aws-sdk/s3-request-presigner": "^3.995.0",
|
|
23
|
-
"@beechcms/core": "^0.6.
|
|
23
|
+
"@beechcms/core": "^0.6.4",
|
|
24
24
|
"@upstash/qstash": "^2.11.0",
|
|
25
25
|
"bcryptjs": "^2.4.3",
|
|
26
26
|
"hono": "^4.12.21",
|