@getstrata/core 2.2.16 → 2.2.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.17
4
+
5
+ Add explicit development-only migration reset context while preserving guarded rollback and encryption archival; align generated fresh entries and reject production/staging or unsupported reset arguments before provisioning. Replace CSP-blocked error-page JavaScript navigation with safe same-origin links, and correct ambiguous Postgres/SQLite foreign-key messages without changing their HTTP status.
6
+
3
7
  ## 2.2.16
4
8
 
5
9
  Allow managed outbox workers to reuse a configured instance, preserving application tenant resolution, listener registration and delivery settings. Keep generated listeners-only defaults compatible, reject ambiguous worker sources, and cover configured SQL delivery, cancellation, cleanup and TypeScript contracts.
@@ -20,6 +20,7 @@ declare function rollbackDatabase(db: MigrationDatabase, migrations: Migration[]
20
20
  declare function freshDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
21
21
  advisoryLock?: boolean;
22
22
  onMigration?: (name: string) => void;
23
+ destructive?: boolean;
23
24
  }): Promise<void>;
24
25
  export { withMigrationLock } from "./advisoryLock.ts";
25
26
  export { ensureMigrationsTable, freshDatabase, getAppliedMigrations, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, runPendingMigrations, };
@@ -2,14 +2,18 @@ interface MigrationDatabase {
2
2
  unsafe<T = unknown>(query: string, params?: readonly unknown[]): Promise<T[]>;
3
3
  schema?: import("../schema/schema.ts").SchemaBuilder;
4
4
  }
5
+ /** Explicit destructive reset is distinct from an ordinary rollback. */
6
+ interface MigrationDownContext {
7
+ operation: "fresh";
8
+ }
5
9
  interface Migration {
6
10
  name: string;
7
11
  up(db: MigrationDatabase): Promise<void>;
8
- down(db: MigrationDatabase): Promise<void>;
12
+ down(db: MigrationDatabase, context?: MigrationDownContext): Promise<void>;
9
13
  }
10
14
  type MigrationStatus = {
11
15
  name: string;
12
16
  status: "up" | "pending";
13
17
  batch: number | null;
14
18
  };
15
- export type { Migration, MigrationDatabase, MigrationStatus };
19
+ export type { Migration, MigrationDatabase, MigrationDownContext, MigrationStatus };
@@ -362,7 +362,7 @@ function mapSqliteError(error) {
362
362
  return new ConflictError("A record with these values already exists.");
363
363
  }
364
364
  if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
365
- return new UnprocessableEntityError("Referenced record does not exist.");
365
+ return new UnprocessableEntityError("Operation conflicts with related records.");
366
366
  }
367
367
  if (code === "SQLITE_CONSTRAINT_NOTNULL") {
368
368
  return new BadRequestError("Required field is missing.");
@@ -407,7 +407,7 @@ function mapDatabaseError(error) {
407
407
  if (error.detail) {
408
408
  console.error("[database] foreign-key violation", error.detail);
409
409
  }
410
- return new UnprocessableEntityError("Referenced record does not exist.");
410
+ return new UnprocessableEntityError("Operation conflicts with related records.");
411
411
  case "23502":
412
412
  if (error.detail) {
413
413
  console.error("[database] not-null violation", error.detail);
@@ -362,7 +362,7 @@ function mapSqliteError(error) {
362
362
  return new ConflictError("A record with these values already exists.");
363
363
  }
364
364
  if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
365
- return new UnprocessableEntityError("Referenced record does not exist.");
365
+ return new UnprocessableEntityError("Operation conflicts with related records.");
366
366
  }
367
367
  if (code === "SQLITE_CONSTRAINT_NOTNULL") {
368
368
  return new BadRequestError("Required field is missing.");
@@ -407,7 +407,7 @@ function mapDatabaseError(error) {
407
407
  if (error.detail) {
408
408
  console.error("[database] foreign-key violation", error.detail);
409
409
  }
410
- return new UnprocessableEntityError("Referenced record does not exist.");
410
+ return new UnprocessableEntityError("Operation conflicts with related records.");
411
411
  case "23502":
412
412
  if (error.detail) {
413
413
  console.error("[database] not-null violation", error.detail);
@@ -38,7 +38,7 @@ function mapSqliteError(error) {
38
38
  return new ConflictError("A record with these values already exists.");
39
39
  }
40
40
  if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
41
- return new UnprocessableEntityError("Referenced record does not exist.");
41
+ return new UnprocessableEntityError("Operation conflicts with related records.");
42
42
  }
43
43
  if (code === "SQLITE_CONSTRAINT_NOTNULL") {
44
44
  return new BadRequestError("Required field is missing.");
@@ -83,7 +83,7 @@ function mapDatabaseError(error) {
83
83
  if (error.detail) {
84
84
  console.error("[database] foreign-key violation", error.detail);
85
85
  }
86
- return new UnprocessableEntityError("Referenced record does not exist.");
86
+ return new UnprocessableEntityError("Operation conflicts with related records.");
87
87
  case "23502":
88
88
  if (error.detail) {
89
89
  console.error("[database] not-null violation", error.detail);
@@ -228,11 +228,33 @@ var init_dialect = __esm(() => {
228
228
  });
229
229
 
230
230
  // ../../src/core/database/migrations/runner.ts
231
- init_dialect();
232
231
  import { readdir } from "fs/promises";
233
232
  import { join } from "path";
234
233
  import { pathToFileURL } from "url";
235
234
 
235
+ // ../../src/core/runtime/appEnv.ts
236
+ var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
237
+ function normalizeEnvValue(value) {
238
+ return (value ?? "").trim().toLowerCase();
239
+ }
240
+ function isProductionEnv(env = process.env) {
241
+ const appEnv = normalizeEnvValue(env.APP_ENV);
242
+ const nodeEnv = normalizeEnvValue(env.NODE_ENV);
243
+ if (appEnv === "production" || nodeEnv === "production") {
244
+ return true;
245
+ }
246
+ if (appEnv === "") {
247
+ return false;
248
+ }
249
+ return !NON_PRODUCTION_APP_ENVS.has(appEnv);
250
+ }
251
+ function envFlagEnabled(value) {
252
+ return value === "true";
253
+ }
254
+
255
+ // ../../src/core/database/migrations/runner.ts
256
+ init_dialect();
257
+
236
258
  // ../../src/core/database/migrations/advisoryLock.ts
237
259
  var MIGRATION_LOCK_KEY = 42424242;
238
260
  async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
@@ -346,13 +368,19 @@ async function rollbackDatabase(db, migrations, options = {}) {
346
368
  return rolledBack;
347
369
  }
348
370
  async function freshDatabase(db, migrations, options = {}) {
371
+ if (options.destructive && isProductionEnv()) {
372
+ throw new Error("Destructive migration reset is only available in development/test environments.");
373
+ }
349
374
  const runFresh = async () => {
350
375
  const applied = await getAppliedMigrations(db);
351
376
  const appliedNames = new Set(applied.map(({ name }) => name));
352
377
  const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
353
378
  for (const migration of [...appliedMigrations].reverse()) {
354
379
  options.onMigration?.(migration.name);
355
- await migration.down(db);
380
+ if (options.destructive)
381
+ await migration.down(db, { operation: "fresh" });
382
+ else
383
+ await migration.down(db);
356
384
  }
357
385
  if (appliedMigrations.length > 0) {
358
386
  await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
@@ -345,12 +345,25 @@ var configuredErrorView = {};
345
345
  function escapeHtml(value) {
346
346
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
347
347
  }
348
+ function errorReturnPath(request) {
349
+ if (!request)
350
+ return "/";
351
+ try {
352
+ const current = new URL(request.url);
353
+ const previous = new URL(request.headers.get("referer") ?? "");
354
+ if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
355
+ return "/";
356
+ return sanitizeInternalPath(previous.pathname, "/");
357
+ } catch {
358
+ return "/";
359
+ }
360
+ }
348
361
  function renderKernelErrorChrome(input) {
349
362
  const title = escapeHtml(input.title);
350
363
  const message = escapeHtml(input.message);
351
364
  const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
352
365
  const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
353
- const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
366
+ const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta3().request))}">Go back</a></p>` : "";
354
367
  return `<!doctype html>
355
368
  <html lang="en">
356
369
  <head>
@@ -162,7 +162,7 @@ function mapSqliteError(error) {
162
162
  return new ConflictError("A record with these values already exists.");
163
163
  }
164
164
  if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
165
- return new UnprocessableEntityError("Referenced record does not exist.");
165
+ return new UnprocessableEntityError("Operation conflicts with related records.");
166
166
  }
167
167
  if (code === "SQLITE_CONSTRAINT_NOTNULL") {
168
168
  return new BadRequestError("Required field is missing.");
@@ -207,7 +207,7 @@ function mapDatabaseError(error) {
207
207
  if (error.detail) {
208
208
  console.error("[database] foreign-key violation", error.detail);
209
209
  }
210
- return new UnprocessableEntityError("Referenced record does not exist.");
210
+ return new UnprocessableEntityError("Operation conflicts with related records.");
211
211
  case "23502":
212
212
  if (error.detail) {
213
213
  console.error("[database] not-null violation", error.detail);
@@ -403,12 +403,25 @@ var configuredErrorView = {};
403
403
  function escapeHtml(value) {
404
404
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
405
405
  }
406
+ function errorReturnPath(request) {
407
+ if (!request)
408
+ return "/";
409
+ try {
410
+ const current = new URL(request.url);
411
+ const previous = new URL(request.headers.get("referer") ?? "");
412
+ if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
413
+ return "/";
414
+ return sanitizeInternalPath(previous.pathname, "/");
415
+ } catch {
416
+ return "/";
417
+ }
418
+ }
406
419
  function renderKernelErrorChrome(input) {
407
420
  const title = escapeHtml(input.title);
408
421
  const message = escapeHtml(input.message);
409
422
  const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
410
423
  const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
411
- const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
424
+ const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta2().request))}">Go back</a></p>` : "";
412
425
  return `<!doctype html>
413
426
  <html lang="en">
414
427
  <head>
@@ -399,12 +399,25 @@ var configuredErrorView = {};
399
399
  function escapeHtml(value) {
400
400
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
401
401
  }
402
+ function errorReturnPath(request) {
403
+ if (!request)
404
+ return "/";
405
+ try {
406
+ const current = new URL(request.url);
407
+ const previous = new URL(request.headers.get("referer") ?? "");
408
+ if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
409
+ return "/";
410
+ return sanitizeInternalPath(previous.pathname, "/");
411
+ } catch {
412
+ return "/";
413
+ }
414
+ }
402
415
  function renderKernelErrorChrome(input) {
403
416
  const title = escapeHtml(input.title);
404
417
  const message = escapeHtml(input.message);
405
418
  const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
406
419
  const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
407
- const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
420
+ const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta3().request))}">Go back</a></p>` : "";
408
421
  return `<!doctype html>
409
422
  <html lang="en">
410
423
  <head>
@@ -284,12 +284,25 @@ var configuredErrorView = {};
284
284
  function escapeHtml(value) {
285
285
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
286
286
  }
287
+ function errorReturnPath(request) {
288
+ if (!request)
289
+ return "/";
290
+ try {
291
+ const current = new URL(request.url);
292
+ const previous = new URL(request.headers.get("referer") ?? "");
293
+ if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
294
+ return "/";
295
+ return sanitizeInternalPath(previous.pathname, "/");
296
+ } catch {
297
+ return "/";
298
+ }
299
+ }
287
300
  function renderKernelErrorChrome(input) {
288
301
  const title = escapeHtml(input.title);
289
302
  const message = escapeHtml(input.message);
290
303
  const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
291
304
  const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
292
- const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
305
+ const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta3().request))}">Go back</a></p>` : "";
293
306
  return `<!doctype html>
294
307
  <html lang="en">
295
308
  <head>
package/dist/index.js CHANGED
@@ -3215,7 +3215,7 @@ function mapSqliteError(error) {
3215
3215
  return new ConflictError("A record with these values already exists.");
3216
3216
  }
3217
3217
  if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
3218
- return new UnprocessableEntityError("Referenced record does not exist.");
3218
+ return new UnprocessableEntityError("Operation conflicts with related records.");
3219
3219
  }
3220
3220
  if (code === "SQLITE_CONSTRAINT_NOTNULL") {
3221
3221
  return new BadRequestError("Required field is missing.");
@@ -3260,7 +3260,7 @@ function mapDatabaseError(error) {
3260
3260
  if (error.detail) {
3261
3261
  console.error("[database] foreign-key violation", error.detail);
3262
3262
  }
3263
- return new UnprocessableEntityError("Referenced record does not exist.");
3263
+ return new UnprocessableEntityError("Operation conflicts with related records.");
3264
3264
  case "23502":
3265
3265
  if (error.detail) {
3266
3266
  console.error("[database] not-null violation", error.detail);
@@ -7164,7 +7164,13 @@ function createFieldEncryptionRotationMigration(name, driver, options = {}) {
7164
7164
  if (options.rls)
7165
7165
  await db.unsafe(enableTenantRlsSql(checkpointTable.name));
7166
7166
  },
7167
- async down() {
7167
+ async down(db, context) {
7168
+ if (context?.operation === "fresh") {
7169
+ await Schema.run(db, driver, (schema) => {
7170
+ schema.drop(checkpointTable.name);
7171
+ });
7172
+ return;
7173
+ }
7168
7174
  throw new Error("Encryption rotation checkpoints require explicit administrative archival.");
7169
7175
  }
7170
7176
  };
@@ -7520,10 +7526,10 @@ async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
7520
7526
  }
7521
7527
  }
7522
7528
  // ../../src/core/database/migrations/runner.ts
7523
- init_dialect();
7524
7529
  import { readdir } from "fs/promises";
7525
7530
  import { join } from "path";
7526
7531
  import { pathToFileURL } from "url";
7532
+ init_dialect();
7527
7533
  var MIGRATIONS_TABLE = "framework_migrations";
7528
7534
  function migrationsNameColumn() {
7529
7535
  return currentSqlDialect().driver === "mysql" ? "VARCHAR(255) PRIMARY KEY" : "TEXT PRIMARY KEY";
@@ -7625,13 +7631,19 @@ async function rollbackDatabase(db, migrations, options = {}) {
7625
7631
  return rolledBack;
7626
7632
  }
7627
7633
  async function freshDatabase(db, migrations, options = {}) {
7634
+ if (options.destructive && isProductionEnv()) {
7635
+ throw new Error("Destructive migration reset is only available in development/test environments.");
7636
+ }
7628
7637
  const runFresh = async () => {
7629
7638
  const applied = await getAppliedMigrations(db);
7630
7639
  const appliedNames = new Set(applied.map(({ name }) => name));
7631
7640
  const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
7632
7641
  for (const migration of [...appliedMigrations].reverse()) {
7633
7642
  options.onMigration?.(migration.name);
7634
- await migration.down(db);
7643
+ if (options.destructive)
7644
+ await migration.down(db, { operation: "fresh" });
7645
+ else
7646
+ await migration.down(db);
7635
7647
  }
7636
7648
  if (appliedMigrations.length > 0) {
7637
7649
  await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
@@ -12339,12 +12351,25 @@ function configureWebErrorView(options) {
12339
12351
  function escapeHtml(value) {
12340
12352
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
12341
12353
  }
12354
+ function errorReturnPath(request) {
12355
+ if (!request)
12356
+ return "/";
12357
+ try {
12358
+ const current = new URL(request.url);
12359
+ const previous = new URL(request.headers.get("referer") ?? "");
12360
+ if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
12361
+ return "/";
12362
+ return sanitizeInternalPath(previous.pathname, "/");
12363
+ } catch {
12364
+ return "/";
12365
+ }
12366
+ }
12342
12367
  function renderKernelErrorChrome(input) {
12343
12368
  const title = escapeHtml(input.title);
12344
12369
  const message = escapeHtml(input.message);
12345
12370
  const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
12346
12371
  const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
12347
- const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
12372
+ const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta().request))}">Go back</a></p>` : "";
12348
12373
  return `<!doctype html>
12349
12374
  <html lang="en">
12350
12375
  <head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.16",
3
+ "version": "2.2.17",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",