@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/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
- const media = await mediaRepository.getByKey(key).catch(() => null);
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
- try {
1910
- await sendPasswordResetEmail({
1911
- to: normalizedEmail,
1912
- resetUrl,
1913
- locale: emailLocale,
1914
- apiKey: env.RESEND_API_KEY ?? "",
1915
- from: env.EMAIL_FROM,
1916
- isDev: env.ENV !== "production",
1917
- provider: env.EMAIL_PROVIDER,
1918
- smtpBaseUrl
1919
- });
1920
- } catch (error) {
1921
- if (env.ENV !== "production") {
1922
- console.error("[password-reset] Failed to send email:", error);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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;
@@ -1,5 +1,4 @@
1
1
  import type { AutomationMailParams } from '../email.types';
2
- /** Identity builder: automation payloads are already user-authored. */
3
2
  export declare function buildAutomationEmail(params: AutomationMailParams): {
4
3
  to: string;
5
4
  subject: 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, so enqueue only needs to hand off the message.
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<void>;
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<void>;
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.2",
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.2",
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",