@getstrata/core 2.0.3 → 2.0.5

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,13 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.0.5
4
+
5
+ - Allow explicitly approved application headers in CORS
6
+
7
+ ## 2.0.4
8
+
9
+ - Place cache rollout guidance beside outbox docs without overlapping API additions
10
+
3
11
  ## 2.0.3
4
12
 
5
13
  - Persist transactional events with leased tenant-aware SQL outbox workers
@@ -21,6 +21,8 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
21
21
  protected readonly table: TableDefinition<TEntity, PrimaryKey>;
22
22
  protected readonly connection: DatabaseConnection;
23
23
  constructor(table: TableDefinition<TEntity, PrimaryKey>, connection?: DatabaseConnection);
24
+ private assertTransactionalEvent;
25
+ private runModelMutation;
24
26
  count(options?: ExtendedQueryOptions<TEntity>): Promise<number>;
25
27
  findAll(options?: ExtendedQueryOptions<TEntity>): Promise<TEntity[]>;
26
28
  paginate(options: {
@@ -1,6 +1,10 @@
1
1
  type EventListener = (payload: unknown) => void | Promise<void>;
2
2
  declare class EventBus {
3
3
  constructor();
4
+ private readonly transactionalListeners;
5
+ listenTransactional(event: string, listener: EventListener): () => void;
6
+ hasTransactionalListeners(event: string): boolean;
7
+ dispatchTransactional(event: string, payload: unknown): Promise<void>;
4
8
  private readonly listeners;
5
9
  on(event: string, listener: EventListener): () => void;
6
10
  listen(event: string, listener: EventListener): () => void;
@@ -30,6 +30,7 @@ declare class SqlOutbox {
30
30
  private readonly maxPayloadBytes;
31
31
  private readonly tenantResolver;
32
32
  constructor(options: SqlOutboxOptions);
33
+ hasListener(name: string, event?: string): boolean;
33
34
  publish(name: string, payload: unknown, options?: {
34
35
  id?: string;
35
36
  version?: number;
@@ -5,6 +5,11 @@ interface CorsConfig {
5
5
  allowedHeaders: string[];
6
6
  maxAgeSeconds: number;
7
7
  }
8
- declare function resolveCorsConfig(): CorsConfig;
9
- declare function createCorsMiddleware(): Middleware;
8
+ /** Additional application-approved request headers; defaults and origins remain unchanged. */
9
+ interface CorsOptions {
10
+ additionalAllowedHeaders?: readonly string[];
11
+ }
12
+ declare function resolveCorsConfig(options?: CorsOptions): CorsConfig;
13
+ declare function createCorsMiddleware(options?: CorsOptions): Middleware;
14
+ export type { CorsConfig, CorsOptions };
10
15
  export { createCorsMiddleware, resolveCorsConfig };
@@ -1,6 +1,45 @@
1
+ /** JSON Schema keywords remain extensible; metadata cannot contain executable values. */
2
+ type OpenApiJson = null | boolean | number | string | OpenApiJson[] | {
3
+ [key: string]: OpenApiJson | undefined;
4
+ };
5
+ type OpenApiSchema = boolean | {
6
+ [keyword: string]: OpenApiJson | undefined;
7
+ };
8
+ interface OpenApiParameter {
9
+ name: string;
10
+ in: "header" | "query" | "path" | "cookie";
11
+ required?: boolean;
12
+ description?: string;
13
+ schema: OpenApiSchema;
14
+ }
15
+ interface OpenApiMediaType {
16
+ schema?: OpenApiSchema;
17
+ example?: OpenApiJson;
18
+ }
19
+ interface OpenApiResponse {
20
+ description: string;
21
+ content?: Record<string, OpenApiMediaType>;
22
+ }
23
+ interface OpenApiOperation {
24
+ summary?: string;
25
+ description?: string;
26
+ operationId?: string;
27
+ tags?: string[];
28
+ deprecated?: boolean;
29
+ parameters?: OpenApiParameter[];
30
+ requestBody?: {
31
+ required?: boolean;
32
+ description?: string;
33
+ content: Record<string, OpenApiMediaType>;
34
+ };
35
+ responses?: Record<string, OpenApiResponse>;
36
+ security?: Array<Record<string, string[]>>;
37
+ }
38
+ type OpenApiRouteMap = Record<string, Partial<Record<"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS", OpenApiOperation>>>;
1
39
  interface RegisteredRoute {
2
40
  method: string;
3
41
  path: string;
4
42
  middleware: string[];
43
+ openApi?: OpenApiOperation;
5
44
  }
6
- export type { RegisteredRoute };
45
+ export type { OpenApiJson, OpenApiMediaType, OpenApiOperation, OpenApiParameter, OpenApiResponse, OpenApiRouteMap, OpenApiSchema, RegisteredRoute, };
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -155,6 +155,24 @@ var init_tenantContext = __esm(() => {
155
155
  // ../../src/core/events/eventBus.ts
156
156
  class EventBus {
157
157
  constructor() {}
158
+ transactionalListeners = new Map;
159
+ listenTransactional(event, listener) {
160
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
161
+ handlers.add(listener);
162
+ this.transactionalListeners.set(event, handlers);
163
+ return () => {
164
+ handlers.delete(listener);
165
+ if (!handlers.size)
166
+ this.transactionalListeners.delete(event);
167
+ };
168
+ }
169
+ hasTransactionalListeners(event) {
170
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
171
+ }
172
+ async dispatchTransactional(event, payload) {
173
+ for (const listener of this.transactionalListeners.get(event) ?? [])
174
+ await listener(payload);
175
+ }
158
176
  listeners = new Map;
159
177
  on(event, listener) {
160
178
  return this.listen(event, listener);
@@ -86,10 +86,28 @@ function sdkClientClassName() {
86
86
  }
87
87
 
88
88
  // ../../src/core/http/corsMiddleware.ts
89
+ function additionalHeaders(options) {
90
+ const configured = process.env.CORS_ADDITIONAL_ALLOWED_HEADERS?.trim();
91
+ const supplied = options.additionalAllowedHeaders === undefined ? [] : options.additionalAllowedHeaders;
92
+ if (!Array.isArray(supplied)) {
93
+ throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
94
+ }
95
+ const names = [
96
+ ...configured ? configured.split(",").map((name) => name.trim()) : [],
97
+ ...supplied
98
+ ];
99
+ return names.map((name) => {
100
+ if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
101
+ throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
102
+ }
103
+ return name;
104
+ });
105
+ }
89
106
  function defaultAllowedOrigins() {
90
107
  return appUrl();
91
108
  }
92
- function resolveCorsConfig() {
109
+ function resolveCorsConfig(options = {}) {
110
+ const extraHeaders = additionalHeaders(options);
93
111
  return {
94
112
  allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
95
113
  allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
@@ -102,22 +120,27 @@ function resolveCorsConfig() {
102
120
  "X-Authenticated-User-Role",
103
121
  "If-Match",
104
122
  "If-None-Match",
105
- "X-CSRF-Token"
106
- ],
123
+ "X-CSRF-Token",
124
+ ...extraHeaders
125
+ ].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
107
126
  maxAgeSeconds: 86400
108
127
  };
109
128
  }
110
- function createCorsMiddleware() {
129
+ function createCorsMiddleware(options = {}) {
130
+ resolveCorsConfig(options);
131
+ const captured = {
132
+ additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
133
+ };
111
134
  return async (request, next) => {
112
135
  if (request.method === "OPTIONS") {
113
136
  return new Response(null, {
114
137
  status: 204,
115
- headers: buildCorsHeaders(request)
138
+ headers: buildCorsHeaders(request, captured)
116
139
  });
117
140
  }
118
141
  const response = await next();
119
142
  const headers = new Headers(response.headers);
120
- for (const [key, value] of buildCorsHeaders(request)) {
143
+ for (const [key, value] of buildCorsHeaders(request, captured)) {
121
144
  headers.set(key, value);
122
145
  }
123
146
  return new Response(response.body, {
@@ -127,10 +150,10 @@ function createCorsMiddleware() {
127
150
  });
128
151
  };
129
152
  }
130
- function buildCorsHeaders(request) {
153
+ function buildCorsHeaders(request, options) {
131
154
  const headers = new Headers;
132
155
  const origin = request.headers.get("origin");
133
- const corsConfig = resolveCorsConfig();
156
+ const corsConfig = resolveCorsConfig(options);
134
157
  const allowedOrigins = corsConfig.allowedOrigins;
135
158
  headers.set("Vary", "Origin");
136
159
  const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -168,7 +168,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
168
168
  "GET /scim/v2/Groups": "SCIM list groups (departments)"
169
169
  };
170
170
  function toOpenApiPath(path) {
171
- return path.replace(/:([A-Za-z_]+)/g, "{$1}");
171
+ return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
172
172
  }
173
173
  function toRelativeApiPath(path) {
174
174
  const prefix = apiPrefix();
@@ -196,6 +196,16 @@ function requiresBearerAuth(path, method) {
196
196
  }
197
197
  return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || relative === "/user" || relative.startsWith("/integrations/") || relative.startsWith("/audit-logs") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
198
198
  }
199
+ var DEFAULT_RESPONSES = {
200
+ "200": { description: "OK" },
201
+ "201": { description: "Created" },
202
+ "204": { description: "No Content" },
203
+ "400": { description: "Bad Request" },
204
+ "401": { description: "Unauthorized" },
205
+ "403": { description: "Forbidden" },
206
+ "404": { description: "Not Found" },
207
+ "422": { description: "Validation Error" }
208
+ };
199
209
  function generateOpenApiSpec(routes) {
200
210
  const paths = {};
201
211
  for (const route of routes) {
@@ -206,16 +216,8 @@ function generateOpenApiSpec(routes) {
206
216
  paths[openApiPath][method] = {
207
217
  summary: description,
208
218
  ...requiresBearerAuth(route.path, route.method) ? { security: [{ bearerAuth: [] }] } : {},
209
- responses: {
210
- "200": { description: "OK" },
211
- "201": { description: "Created" },
212
- "204": { description: "No Content" },
213
- "400": { description: "Bad Request" },
214
- "401": { description: "Unauthorized" },
215
- "403": { description: "Forbidden" },
216
- "404": { description: "Not Found" },
217
- "422": { description: "Validation Error" }
218
- }
219
+ ...route.openApi,
220
+ responses: route.openApi?.responses ?? structuredClone(DEFAULT_RESPONSES)
219
221
  };
220
222
  }
221
223
  return {
@@ -297,10 +299,38 @@ function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
297
299
  " }",
298
300
  ""
299
301
  ];
302
+ const contracts = Object.fromEntries(Object.entries(spec.paths).map(([path, methods]) => [
303
+ path,
304
+ Object.fromEntries(Object.entries(methods).filter(([, value]) => {
305
+ const operation = value;
306
+ return operation.parameters || operation.requestBody || operation.operationId || JSON.stringify(operation.responses) !== JSON.stringify(DEFAULT_RESPONSES);
307
+ }))
308
+ ]).filter(([, methods]) => Object.keys(methods).length));
309
+ if (Object.keys(contracts).length)
310
+ lines.splice(1, 0, ` static readonly operationContracts = ${JSON.stringify(contracts)} as const;`);
300
311
  for (const [path, methods] of Object.entries(spec.paths)) {
301
312
  const requestPath = toRequestPath(path, prefix);
302
- for (const method of Object.keys(methods)) {
313
+ for (const [method, operation] of Object.entries(methods)) {
314
+ const metadata = operation;
315
+ const headers = metadata.parameters?.filter((parameter) => parameter.in === "header") ?? [];
303
316
  const functionName = toMethodName(method, path, prefix);
317
+ const pathParameters = metadata.parameters?.filter((parameter) => parameter.in === "path") ?? [];
318
+ const queryParameters = metadata.parameters?.filter((parameter) => parameter.in === "query") ?? [];
319
+ const groups = [
320
+ { field: "operationHeaders", parameters: headers },
321
+ { field: "operationPath", parameters: pathParameters },
322
+ { field: "operationQuery", parameters: queryParameters }
323
+ ].filter((group) => group.parameters.length);
324
+ if (groups.length) {
325
+ const type = groups.map((group) => `${group.field}${group.parameters.some((parameter) => parameter.required) ? "" : "?"}: { ${group.parameters.map((parameter) => `${JSON.stringify(parameter.name)}${parameter.required ? "" : "?"}: ${parameter.schema?.type === "integer" || parameter.schema?.type === "number" ? "number" : parameter.schema?.type === "boolean" ? "boolean" : "string"}`).join("; ")} }`).join("; ");
326
+ const required = groups.some((group) => group.parameters.some((parameter) => parameter.required));
327
+ lines.push(` async ${functionName}(init: RequestInit & { ${type} }${required ? "" : " = {}"}): Promise<Response> {`, " const headers = new Headers(init.headers);", ` let path = ${JSON.stringify(requestPath)};`, ...groups.flatMap((group) => group.parameters.filter((parameter) => parameter.required).map((parameter) => ` if (init.${group.field}?.[${JSON.stringify(parameter.name)}] === undefined) throw new TypeError(${JSON.stringify(`Required operation ${group.field === "operationHeaders" ? "header" : group.field === "operationPath" ? "path parameter" : "query parameter"}: ${parameter.name}`)});`)), ...headers.map((parameter) => ` if (init.operationHeaders?.[${JSON.stringify(parameter.name)}] !== undefined) headers.set(${JSON.stringify(parameter.name)}, String(init.operationHeaders[${JSON.stringify(parameter.name)}]));`), ...pathParameters.map((parameter) => ` if (init.operationPath?.[${JSON.stringify(parameter.name)}] !== undefined) path = path.replaceAll(${JSON.stringify(`{${parameter.name}}`)}, encodeURIComponent(String(init.operationPath[${JSON.stringify(parameter.name)}])));`), ...queryParameters.length ? [
328
+ " const query = new URLSearchParams();",
329
+ ...queryParameters.map((parameter) => ` if (init.operationQuery?.[${JSON.stringify(parameter.name)}] !== undefined) query.set(${JSON.stringify(parameter.name)}, String(init.operationQuery[${JSON.stringify(parameter.name)}]));`),
330
+ " if (query.size) path += '?' + query.toString();"
331
+ ] : [], ` const { ${groups.map((group) => `${group.field}: _${group.field}`).join(", ")}, ...requestInit } = init;`, ` return await this.request(path, { ...requestInit, headers, method: ${JSON.stringify(method.toUpperCase())} });`, " }", "");
332
+ continue;
333
+ }
304
334
  lines.push(` async ${functionName}(init: RequestInit = {}): Promise<Response> {`, ` return await this.request("${requestPath}", { ...init, method: "${method.toUpperCase()}" });`, " }", "");
305
335
  }
306
336
  }
@@ -13,6 +13,16 @@ function validateOpenApiSpec(spec) {
13
13
  errors.push(`Path must start with '/': ${path}`);
14
14
  }
15
15
  for (const [method, operation] of Object.entries(methods)) {
16
+ const details = operation;
17
+ const seen = new Set;
18
+ for (const parameter of details.parameters ?? []) {
19
+ const identity = `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`;
20
+ if (seen.has(identity))
21
+ errors.push(`Duplicate parameter ${parameter.name} on ${method.toUpperCase()} ${path}.`);
22
+ seen.add(identity);
23
+ if (parameter.in === "path" && (!parameter.required || !path.includes(`{${parameter.name}}`)))
24
+ errors.push(`Path parameter ${parameter.name} must be required and match ${path}.`);
25
+ }
16
26
  if (!("responses" in operation)) {
17
27
  errors.push(`Operation ${method.toUpperCase()} ${path} is missing responses.`);
18
28
  }
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -133,6 +133,24 @@ var init_tenantContext = __esm(() => {
133
133
  // ../../src/core/events/eventBus.ts
134
134
  class EventBus {
135
135
  constructor() {}
136
+ transactionalListeners = new Map;
137
+ listenTransactional(event, listener) {
138
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
139
+ handlers.add(listener);
140
+ this.transactionalListeners.set(event, handlers);
141
+ return () => {
142
+ handlers.delete(listener);
143
+ if (!handlers.size)
144
+ this.transactionalListeners.delete(event);
145
+ };
146
+ }
147
+ hasTransactionalListeners(event) {
148
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
149
+ }
150
+ async dispatchTransactional(event, payload) {
151
+ for (const listener of this.transactionalListeners.get(event) ?? [])
152
+ await listener(payload);
153
+ }
136
154
  listeners = new Map;
137
155
  on(event, listener) {
138
156
  return this.listen(event, listener);
@@ -81,7 +81,7 @@ export { conditionalJsonResponse } from "../core/http/conditionalResponse.ts";
81
81
  export type { ContentSecurityPolicyDirectives, ContentSecurityPolicyOptions, } from "../core/http/contentSecurityPolicy.ts";
82
82
  export { configureContentSecurityPolicy, generateCspNonce, HTMX_2_0_4_INDICATOR_STYLE_HASH, resetContentSecurityPolicyForTests, resolveContentSecurityPolicy, resolveHtmlContentSecurityPolicy, serverHtmxContentSecurityPolicy, spaContentSecurityPolicy, strictApiContentSecurityPolicy, } from "../core/http/contentSecurityPolicy.ts";
83
83
  export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
84
- export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
84
+ export { type CorsConfig, type CorsOptions, createCorsMiddleware, } from "../core/http/corsMiddleware.ts";
85
85
  export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
86
86
  export { createCsrfProtection } from "../core/http/csrfProtection.ts";
87
87
  export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
package/dist/index.js CHANGED
@@ -535,6 +535,24 @@ var init_transactionControl = __esm(() => {
535
535
  // ../../src/core/events/eventBus.ts
536
536
  class EventBus {
537
537
  constructor() {}
538
+ transactionalListeners = new Map;
539
+ listenTransactional(event, listener) {
540
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
541
+ handlers.add(listener);
542
+ this.transactionalListeners.set(event, handlers);
543
+ return () => {
544
+ handlers.delete(listener);
545
+ if (!handlers.size)
546
+ this.transactionalListeners.delete(event);
547
+ };
548
+ }
549
+ hasTransactionalListeners(event) {
550
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
551
+ }
552
+ async dispatchTransactional(event, payload) {
553
+ for (const listener of this.transactionalListeners.get(event) ?? [])
554
+ await listener(payload);
555
+ }
538
556
  listeners = new Map;
539
557
  on(event, listener) {
540
558
  return this.listen(event, listener);
@@ -621,6 +639,7 @@ async function dispatchQueuedModelEvents(queued) {
621
639
  }
622
640
  }
623
641
  async function dispatchModelEvent(event, payload) {
642
+ await eventBus.dispatchTransactional(event, payload);
624
643
  const state = deferredModelEvents.getStore();
625
644
  const frame = state?.frames[state.frames.length - 1];
626
645
  if (frame) {
@@ -3810,6 +3829,9 @@ class ConfigStore {
3810
3829
  return this.values.has(key);
3811
3830
  }
3812
3831
  }
3832
+ // ../../src/core/database/baseRepository.ts
3833
+ init_eventBus();
3834
+
3813
3835
  // ../../src/core/events/index.ts
3814
3836
  init_deferredModelEvents();
3815
3837
  init_eventBus();
@@ -3828,6 +3850,9 @@ function buildPaginationMeta(input) {
3828
3850
  };
3829
3851
  }
3830
3852
 
3853
+ // ../../src/core/database/baseRepository.ts
3854
+ init_connectionContext();
3855
+
3831
3856
  // ../../src/core/database/query.ts
3832
3857
  init_dialect();
3833
3858
  function quoteIdentifier(identifier) {
@@ -4952,6 +4977,9 @@ class RepositoryQuery {
4952
4977
  }
4953
4978
 
4954
4979
  // ../../src/core/database/baseRepository.ts
4980
+ init_transaction();
4981
+ init_transactionControl();
4982
+
4955
4983
  class BaseRepository {
4956
4984
  table;
4957
4985
  connection;
@@ -4959,6 +4987,17 @@ class BaseRepository {
4959
4987
  this.table = table;
4960
4988
  this.connection = connection;
4961
4989
  }
4990
+ assertTransactionalEvent(action) {
4991
+ if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
4992
+ return;
4993
+ if (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))
4994
+ throw new Error("Transactional model listeners require runInTransaction and the framework-bound repository connection before writing.");
4995
+ }
4996
+ async runModelMutation(action, operation) {
4997
+ if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
4998
+ return operation();
4999
+ return runInTransaction(operation);
5000
+ }
4962
5001
  async count(options = {}) {
4963
5002
  const { whereNodes, where, ...rest } = options;
4964
5003
  return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
@@ -5116,7 +5155,8 @@ class BaseRepository {
5116
5155
  return await this.incrementById(id, column, -amount, extra);
5117
5156
  }
5118
5157
  async create(values) {
5119
- return await withDatabaseErrorHandling(async () => {
5158
+ this.assertTransactionalEvent("created");
5159
+ return await this.runModelMutation("created", () => withDatabaseErrorHandling(async () => {
5120
5160
  const { text, params } = buildInsertQuery(this.table, values);
5121
5161
  const [record] = await this.connection.unsafe(text, params);
5122
5162
  if (!record) {
@@ -5125,10 +5165,11 @@ class BaseRepository {
5125
5165
  const entity = record;
5126
5166
  await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
5127
5167
  return entity;
5128
- });
5168
+ }));
5129
5169
  }
5130
5170
  async updateById(id, changes) {
5131
- return await withDatabaseErrorHandling(async () => {
5171
+ this.assertTransactionalEvent("updated");
5172
+ return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
5132
5173
  const { text, params } = buildUpdateQuery(this.table, id, changes);
5133
5174
  const [record] = await this.connection.unsafe(text, params);
5134
5175
  const entity = record ?? null;
@@ -5136,7 +5177,7 @@ class BaseRepository {
5136
5177
  await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
5137
5178
  }
5138
5179
  return entity;
5139
- });
5180
+ }));
5140
5181
  }
5141
5182
  async updateByIdOrThrow(id, changes, errorFactory) {
5142
5183
  const record = await this.updateById(id, changes);
@@ -5152,7 +5193,8 @@ class BaseRepository {
5152
5193
  return await this.forceDeleteById(id);
5153
5194
  }
5154
5195
  async softDeleteById(id) {
5155
- return await withDatabaseErrorHandling(async () => {
5196
+ this.assertTransactionalEvent("deleted");
5197
+ return await this.runModelMutation("deleted", () => withDatabaseErrorHandling(async () => {
5156
5198
  const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
5157
5199
  const [record] = await this.connection.unsafe(text, params);
5158
5200
  if (!record) {
@@ -5160,10 +5202,11 @@ class BaseRepository {
5160
5202
  }
5161
5203
  await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
5162
5204
  return true;
5163
- });
5205
+ }));
5164
5206
  }
5165
5207
  async forceDeleteById(id) {
5166
- return await withDatabaseErrorHandling(async () => {
5208
+ this.assertTransactionalEvent("force-deleted");
5209
+ return await this.runModelMutation("force-deleted", () => withDatabaseErrorHandling(async () => {
5167
5210
  const { text, params } = buildDeleteByIdQuery(this.table, id);
5168
5211
  const [row] = await this.connection.unsafe(text, params);
5169
5212
  if (!row) {
@@ -5173,10 +5216,11 @@ class BaseRepository {
5173
5216
  id
5174
5217
  });
5175
5218
  return true;
5176
- });
5219
+ }));
5177
5220
  }
5178
5221
  async restoreById(id) {
5179
- return await withDatabaseErrorHandling(async () => {
5222
+ this.assertTransactionalEvent("restored");
5223
+ return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
5180
5224
  const { text, params } = buildRestoreByIdQuery(this.table, id);
5181
5225
  const [record] = await this.connection.unsafe(text, params);
5182
5226
  if (!record) {
@@ -5185,7 +5229,7 @@ class BaseRepository {
5185
5229
  const entity = record;
5186
5230
  await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
5187
5231
  return entity;
5188
- });
5232
+ }));
5189
5233
  }
5190
5234
  withConnection(connection) {
5191
5235
  const clone = Object.create(Object.getPrototypeOf(this));
@@ -8417,6 +8461,10 @@ class SqlOutbox {
8417
8461
  this.listeners.set(listener.name, listener);
8418
8462
  }
8419
8463
  }
8464
+ hasListener(name, event) {
8465
+ const listener = this.listeners.get(name);
8466
+ return listener !== undefined && (event === undefined || listener.event === event);
8467
+ }
8420
8468
  async publish(name, payload, options = {}) {
8421
8469
  if (!hasActiveTransaction())
8422
8470
  throw new Error("Durable publication requires an active business transaction.");
@@ -9418,10 +9466,28 @@ function readBunRequestCookie(request, name) {
9418
9466
  return request.cookies.get(name) ?? readRequestCookie(request, name);
9419
9467
  }
9420
9468
  // ../../src/core/http/corsMiddleware.ts
9469
+ function additionalHeaders(options) {
9470
+ const configured = process.env.CORS_ADDITIONAL_ALLOWED_HEADERS?.trim();
9471
+ const supplied = options.additionalAllowedHeaders === undefined ? [] : options.additionalAllowedHeaders;
9472
+ if (!Array.isArray(supplied)) {
9473
+ throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
9474
+ }
9475
+ const names = [
9476
+ ...configured ? configured.split(",").map((name) => name.trim()) : [],
9477
+ ...supplied
9478
+ ];
9479
+ return names.map((name) => {
9480
+ if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
9481
+ throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
9482
+ }
9483
+ return name;
9484
+ });
9485
+ }
9421
9486
  function defaultAllowedOrigins() {
9422
9487
  return appUrl();
9423
9488
  }
9424
- function resolveCorsConfig() {
9489
+ function resolveCorsConfig(options = {}) {
9490
+ const extraHeaders = additionalHeaders(options);
9425
9491
  return {
9426
9492
  allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
9427
9493
  allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
@@ -9434,22 +9500,27 @@ function resolveCorsConfig() {
9434
9500
  "X-Authenticated-User-Role",
9435
9501
  "If-Match",
9436
9502
  "If-None-Match",
9437
- "X-CSRF-Token"
9438
- ],
9503
+ "X-CSRF-Token",
9504
+ ...extraHeaders
9505
+ ].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
9439
9506
  maxAgeSeconds: 86400
9440
9507
  };
9441
9508
  }
9442
- function createCorsMiddleware() {
9509
+ function createCorsMiddleware(options = {}) {
9510
+ resolveCorsConfig(options);
9511
+ const captured = {
9512
+ additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
9513
+ };
9443
9514
  return async (request, next) => {
9444
9515
  if (request.method === "OPTIONS") {
9445
9516
  return new Response(null, {
9446
9517
  status: 204,
9447
- headers: buildCorsHeaders(request)
9518
+ headers: buildCorsHeaders(request, captured)
9448
9519
  });
9449
9520
  }
9450
9521
  const response = await next();
9451
9522
  const headers = new Headers(response.headers);
9452
- for (const [key, value] of buildCorsHeaders(request)) {
9523
+ for (const [key, value] of buildCorsHeaders(request, captured)) {
9453
9524
  headers.set(key, value);
9454
9525
  }
9455
9526
  return new Response(response.body, {
@@ -9459,10 +9530,10 @@ function createCorsMiddleware() {
9459
9530
  });
9460
9531
  };
9461
9532
  }
9462
- function buildCorsHeaders(request) {
9533
+ function buildCorsHeaders(request, options) {
9463
9534
  const headers = new Headers;
9464
9535
  const origin = request.headers.get("origin");
9465
- const corsConfig = resolveCorsConfig();
9536
+ const corsConfig = resolveCorsConfig(options);
9466
9537
  const allowedOrigins = corsConfig.allowedOrigins;
9467
9538
  headers.set("Vary", "Origin");
9468
9539
  const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",