@getstrata/core 2.2.12 → 2.2.14

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,15 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.14
4
+
5
+ Complete the scoped HTML rendering starter adoption: generated apps inject ViewRenderer through createViewProvider instead of copying renderPage; welcome pages opt into HTMX fragments with full-page fallbacks, auth retains existing contracts, and CORS preserves representation Vary headers. Includes generator, runtime auth and cache-header regressions.
6
+
7
+ Add optional typed resourceRoutes and strict mergeRouteMaps composition, preserving native Bun routing and app-owned middleware. Add explicit ViewRenderer.namespace prefixes with unchanged layout/Response/HTMX contracts; generated auth controllers use the namespace. Includes native HTTP, compile-time, rendering isolation and generator regressions.
8
+
9
+ ## 2.2.13
10
+
11
+ - Improve view rendering and better htmx support
12
+
3
13
  ## 2.2.12
4
14
 
5
15
  - Require Bun >=1.4.3; align CI, generated/existing example runtime pins and Docker stages to 1.4.3, with bun-types >=1.4.3 behind the official @types/bun shim.
@@ -14,8 +14,8 @@ export { parseMultipartUpload, sanitizeUploadFileName, validateUploadFile, } fro
14
14
  export { createRequireAuthMiddleware } from "./requireAuthMiddleware";
15
15
  export { serializeDate, toPaginatedResourceCollection, toResourceCollection } from "./resources";
16
16
  export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "./response";
17
- export type { RouteRequest } from "./route";
18
- export { getRouteParams } from "./route";
17
+ export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "./route";
18
+ export { getRouteParams, mergeRouteMaps, resourceRoutes } from "./route";
19
19
  export { withMiddleware } from "./routeMiddleware";
20
20
  export { bindRouteModel } from "./routeModelBinding";
21
21
  export { securedBindRouteModel, securedBindRouteModelByKey, } from "./securedRouteModelBinding";
@@ -1,3 +1,67 @@
1
+ import type { RouteHandler } from "./middleware";
2
+ type ResourceMemberHandler = RouteHandler<RouteRequest<{
3
+ id: string;
4
+ }>>;
5
+ interface ResourceActions {
6
+ index?: RouteHandler;
7
+ create?: RouteHandler;
8
+ store?: RouteHandler;
9
+ show?: ResourceMemberHandler;
10
+ edit?: ResourceMemberHandler;
11
+ update?: ResourceMemberHandler;
12
+ destroy?: ResourceMemberHandler;
13
+ }
14
+ type ResourceRouteMap<P extends string, A extends ResourceActions> = (A extends {
15
+ index: infer H;
16
+ } ? {
17
+ [K in P]: {
18
+ GET: H;
19
+ };
20
+ } : Record<never, never>) & (A extends {
21
+ store: infer H;
22
+ } ? {
23
+ [K in P]: {
24
+ POST: H;
25
+ };
26
+ } : Record<never, never>) & (A extends {
27
+ create: infer H;
28
+ } ? {
29
+ [K in `${P}/create`]: {
30
+ GET: H;
31
+ };
32
+ } : Record<never, never>) & (A extends {
33
+ show: infer H;
34
+ } ? {
35
+ [K in `${P}/:id`]: {
36
+ GET: H;
37
+ };
38
+ } : Record<never, never>) & (A extends {
39
+ edit: infer H;
40
+ } ? {
41
+ [K in `${P}/:id/edit`]: {
42
+ GET: H;
43
+ };
44
+ } : Record<never, never>) & (A extends {
45
+ update: infer H;
46
+ } ? {
47
+ [K in `${P}/:id`]: {
48
+ PUT: H;
49
+ PATCH: H;
50
+ };
51
+ } : Record<never, never>) & (A extends {
52
+ destroy: infer H;
53
+ } ? {
54
+ [K in `${P}/:id`]: {
55
+ DELETE: H;
56
+ };
57
+ } : Record<never, never>);
58
+ /** Expand only explicitly supplied actions into native Bun routes. */
59
+ declare function resourceRoutes<const P extends string, const A extends ResourceActions>(path: P, actions: A & Record<Exclude<keyof A, keyof ResourceActions>, never>): ResourceRouteMap<P, A>;
60
+ type Intersection<U> = (U extends unknown ? (value: U) => void : never) extends (value: infer I) => void ? I : never;
61
+ /** Strict composition: duplicate paths fail rather than silently replacing handlers. */
62
+ declare function mergeRouteMaps<const T extends readonly Record<string, unknown>[]>(...maps: T): Intersection<T[number]>;
63
+ export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap };
64
+ export { mergeRouteMaps, resourceRoutes };
1
65
  type RouteRequest<TParams extends Record<string, string>> = Request & {
2
66
  params: TParams;
3
67
  };
@@ -1,8 +1,10 @@
1
- export type { LayoutDataResolver } from "./etaViewEngine";
1
+ export type { LayoutDataResolver, RenderOptions } from "./etaViewEngine";
2
2
  export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine } from "./etaViewEngine";
3
3
  export { htmlResponse, isHtmxRequest, redirectResponse, rssResponse, textResponse, xmlResponse, } from "./htmlResponse";
4
4
  export type { ViewEngine } from "./viewEngine";
5
+ export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "./viewRenderer";
6
+ export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer } from "./viewRenderer";
5
7
  export type { WebErrorViewInput, WebErrorViewOptions } from "./webErrorView";
6
8
  export { configureWebErrorView, errorTemplateName, htmlErrorResponse, notFoundHtmlResponse, renderKernelErrorChrome, renderWebErrorHtml, } from "./webErrorView";
7
9
  export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "./webLayoutData";
8
- export { configureWebLayoutData, resolveWebLayoutData } from "./webLayoutData";
10
+ export { configureWebLayoutData, createWebLayoutDataResolver, resolveWebLayoutData, } from "./webLayoutData";
@@ -0,0 +1,30 @@
1
+ import type { ViewEngine } from "./viewEngine";
2
+ interface ViewResponseOptions {
3
+ layout?: string | false;
4
+ request?: Request;
5
+ status?: number;
6
+ statusText?: string;
7
+ headers?: ConstructorParameters<typeof Headers>[0];
8
+ }
9
+ interface HtmxViewResponseOptions extends ViewResponseOptions {
10
+ /** Optional fragment template; otherwise render the page template without its layout. */
11
+ fragment?: string;
12
+ }
13
+ interface NamespacedViewRenderer {
14
+ render: ViewRenderer["render"];
15
+ renderHtmx: ViewRenderer["renderHtmx"];
16
+ }
17
+ /** App-owned renderer; request data stays in the engine's async resolver. */
18
+ declare class ViewRenderer {
19
+ private readonly engine;
20
+ private readonly defaultLayout?;
21
+ constructor(engine: ViewEngine, defaultLayout?: string | false | undefined);
22
+ /** Explicit template prefix; layout names and response contracts remain unchanged. */
23
+ namespace(prefix: string): NamespacedViewRenderer;
24
+ render(name: string, data?: Record<string, unknown>, options?: ViewResponseOptions): Promise<Response>;
25
+ /** Opt-in representation selection; auth/CSRF/tenancy remain the route's responsibility. */
26
+ renderHtmx(name: string, data?: Record<string, unknown>, options?: HtmxViewResponseOptions): Promise<Response>;
27
+ }
28
+ declare const CORE_VIEW_RENDERER_TOKEN: import("../contracts/container").ServiceToken<ViewRenderer>;
29
+ export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions };
30
+ export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer };
@@ -24,5 +24,7 @@ interface WebLayoutDataOptions {
24
24
  declare function configureWebLayoutData(options: WebLayoutDataOptions): void;
25
25
  declare function resetWebLayoutDataConfigForTests(): void;
26
26
  declare function resolveWebLayoutData(container: ServiceContainerLike, request?: Request, options?: WebLayoutDataOptions): Promise<Record<string, unknown>>;
27
+ /** Isolated configuration for one app; does not read legacy global defaults. */
28
+ declare function createWebLayoutDataResolver(container: ServiceContainerLike, options?: WebLayoutDataOptions): (request?: Request) => Promise<Record<string, unknown>>;
27
29
  export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey };
28
- export { configureWebLayoutData, resetWebLayoutDataConfigForTests, resolveWebLayoutData };
30
+ export { configureWebLayoutData, createWebLayoutDataResolver, resetWebLayoutDataConfigForTests, resolveWebLayoutData, };
@@ -141,7 +141,16 @@ function createCorsMiddleware(options = {}) {
141
141
  const response = await next();
142
142
  const headers = new Headers(response.headers);
143
143
  for (const [key, value] of buildCorsHeaders(request, captured)) {
144
- headers.set(key, value);
144
+ if (key === "vary") {
145
+ const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
146
+ if (fields.includes("*"))
147
+ continue;
148
+ if (!fields.some((field) => field.toLowerCase() === "origin"))
149
+ fields.push(value);
150
+ headers.set("Vary", fields.join(", "));
151
+ } else {
152
+ headers.set(key, value);
153
+ }
145
154
  }
146
155
  return new Response(response.body, {
147
156
  status: response.status,
@@ -1,8 +1,52 @@
1
1
  // @bun
2
2
  // ../../src/core/http/route.ts
3
+ function resourceRoutes(path, actions) {
4
+ if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
5
+ throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
6
+ }
7
+ const routes = {};
8
+ const definitions = {
9
+ index: [path, ["GET"]],
10
+ create: [`${path}/create`, ["GET"]],
11
+ store: [path, ["POST"]],
12
+ show: [`${path}/:id`, ["GET"]],
13
+ edit: [`${path}/:id/edit`, ["GET"]],
14
+ update: [`${path}/:id`, ["PUT", "PATCH"]],
15
+ destroy: [`${path}/:id`, ["DELETE"]]
16
+ };
17
+ for (const [action, handler] of Object.entries(actions)) {
18
+ if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
19
+ throw new TypeError(`Invalid resource action: ${action}`);
20
+ }
21
+ const [route, methods] = definitions[action];
22
+ const methodMap = routes[route] ?? {};
23
+ routes[route] = methodMap;
24
+ for (const method of methods)
25
+ methodMap[method] = handler;
26
+ }
27
+ return routes;
28
+ }
29
+ function mergeRouteMaps(...maps) {
30
+ const routes = {};
31
+ for (const map of maps) {
32
+ for (const [path, handler] of Object.entries(map)) {
33
+ if (Object.hasOwn(routes, path))
34
+ throw new Error(`Duplicate route path: ${path}`);
35
+ Object.defineProperty(routes, path, {
36
+ value: handler,
37
+ enumerable: true,
38
+ writable: true,
39
+ configurable: true
40
+ });
41
+ }
42
+ }
43
+ return routes;
44
+ }
3
45
  function getRouteParams(request) {
4
46
  return request.params;
5
47
  }
6
48
  export {
7
- getRouteParams
49
+ getRouteParams,
50
+ mergeRouteMaps,
51
+ resourceRoutes
8
52
  };
@@ -107,8 +107,8 @@ export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAd
107
107
  export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
108
108
  export { JsonResource, ResourceCollection, serializeDate, toPaginatedResourceCollection, toResourceCollection, whenLoaded, } from "../core/http/resources.ts";
109
109
  export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "../core/http/response.ts";
110
- export type { RouteRequest } from "../core/http/route.ts";
111
- export { getRouteParams } from "../core/http/route.ts";
110
+ export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "../core/http/route.ts";
111
+ export { getRouteParams, mergeRouteMaps, resourceRoutes } from "../core/http/route.ts";
112
112
  export { loginRedirectLocation, safeInternalRedirectPath, sanitizeInternalPath, } from "../core/http/safeInternalPath.ts";
113
113
  export { createScimThrottleMiddleware } from "../core/http/scimThrottleMiddleware.ts";
114
114
  export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
@@ -169,6 +169,7 @@ export type { TracingOptions, TracingRuntime } from "../core/tracing/tracingMidd
169
169
  export { acquireTracingRuntime, createTracingMiddleware, createTracingRuntime, getTracingRuntime, } from "../core/tracing/tracingMiddleware.ts";
170
170
  export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
171
171
  export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
172
- export { configureWebErrorView, configureWebLayoutData, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, errorTemplateName, htmlErrorResponse, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, renderKernelErrorChrome, renderWebErrorHtml, resolveWebLayoutData, rssResponse, textResponse, xmlResponse, } from "../core/view/index.ts";
172
+ export { CORE_VIEW_RENDERER_TOKEN, configureWebErrorView, configureWebLayoutData, createWebLayoutDataResolver, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, errorTemplateName, htmlErrorResponse, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, renderKernelErrorChrome, renderWebErrorHtml, resolveWebLayoutData, rssResponse, textResponse, ViewRenderer, xmlResponse, } from "../core/view/index.ts";
173
173
  export type { ViewEngine } from "../core/view/viewEngine.ts";
174
+ export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "../core/view/viewRenderer.ts";
174
175
  export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "../core/view/webLayoutData.ts";
package/dist/index.js CHANGED
@@ -11352,7 +11352,16 @@ function createCorsMiddleware(options = {}) {
11352
11352
  const response = await next();
11353
11353
  const headers = new Headers(response.headers);
11354
11354
  for (const [key, value] of buildCorsHeaders(request, captured)) {
11355
- headers.set(key, value);
11355
+ if (key === "vary") {
11356
+ const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
11357
+ if (fields.includes("*"))
11358
+ continue;
11359
+ if (!fields.some((field) => field.toLowerCase() === "origin"))
11360
+ fields.push(value);
11361
+ headers.set("Vary", fields.join(", "));
11362
+ } else {
11363
+ headers.set(key, value);
11364
+ }
11356
11365
  }
11357
11366
  return new Response(response.body, {
11358
11367
  status: response.status,
@@ -12390,6 +12399,48 @@ function withErrorHandling(handler) {
12390
12399
  };
12391
12400
  }
12392
12401
  // ../../src/core/http/route.ts
12402
+ function resourceRoutes(path, actions) {
12403
+ if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
12404
+ throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
12405
+ }
12406
+ const routes = {};
12407
+ const definitions = {
12408
+ index: [path, ["GET"]],
12409
+ create: [`${path}/create`, ["GET"]],
12410
+ store: [path, ["POST"]],
12411
+ show: [`${path}/:id`, ["GET"]],
12412
+ edit: [`${path}/:id/edit`, ["GET"]],
12413
+ update: [`${path}/:id`, ["PUT", "PATCH"]],
12414
+ destroy: [`${path}/:id`, ["DELETE"]]
12415
+ };
12416
+ for (const [action, handler] of Object.entries(actions)) {
12417
+ if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
12418
+ throw new TypeError(`Invalid resource action: ${action}`);
12419
+ }
12420
+ const [route, methods] = definitions[action];
12421
+ const methodMap = routes[route] ?? {};
12422
+ routes[route] = methodMap;
12423
+ for (const method of methods)
12424
+ methodMap[method] = handler;
12425
+ }
12426
+ return routes;
12427
+ }
12428
+ function mergeRouteMaps(...maps) {
12429
+ const routes = {};
12430
+ for (const map of maps) {
12431
+ for (const [path, handler] of Object.entries(map)) {
12432
+ if (Object.hasOwn(routes, path))
12433
+ throw new Error(`Duplicate route path: ${path}`);
12434
+ Object.defineProperty(routes, path, {
12435
+ value: handler,
12436
+ enumerable: true,
12437
+ writable: true,
12438
+ configurable: true
12439
+ });
12440
+ }
12441
+ }
12442
+ return routes;
12443
+ }
12393
12444
  function getRouteParams(request) {
12394
12445
  return request.params;
12395
12446
  }
@@ -16118,6 +16169,75 @@ class EtaViewEngine {
16118
16169
  });
16119
16170
  }
16120
16171
  }
16172
+ // ../../src/core/view/viewRenderer.ts
16173
+ function validateRelativeViewName(name) {
16174
+ if (!/^[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(name) || name.split("/").some((part) => part === "." || part === "..")) {
16175
+ throw new TypeError("View namespace and names must be relative template paths without dot segments.");
16176
+ }
16177
+ }
16178
+ var HTMX_VARY_HEADERS = ["HX-Request", "HX-Boosted", "HX-History-Restore-Request"];
16179
+ function varyByHtmx(headers) {
16180
+ const existing = (headers.get("Vary") ?? "").split(",").map((field) => field.trim());
16181
+ if (existing.includes("*"))
16182
+ return;
16183
+ const fields = new Map;
16184
+ for (const field of [...existing, ...HTMX_VARY_HEADERS]) {
16185
+ if (field)
16186
+ fields.set(field.toLowerCase(), field);
16187
+ }
16188
+ headers.set("Vary", [...fields.values()].join(", "));
16189
+ }
16190
+
16191
+ class ViewRenderer {
16192
+ engine;
16193
+ defaultLayout;
16194
+ constructor(engine, defaultLayout) {
16195
+ this.engine = engine;
16196
+ this.defaultLayout = defaultLayout;
16197
+ }
16198
+ namespace(prefix) {
16199
+ validateRelativeViewName(prefix);
16200
+ const qualify = (name) => {
16201
+ validateRelativeViewName(name);
16202
+ return `${prefix}/${name}`;
16203
+ };
16204
+ return {
16205
+ render: async (name, data, options) => this.render(qualify(name), data, options),
16206
+ renderHtmx: async (name, data, options = {}) => this.renderHtmx(qualify(name), data, {
16207
+ ...options,
16208
+ ...options.fragment === undefined ? {} : { fragment: qualify(options.fragment) }
16209
+ })
16210
+ };
16211
+ }
16212
+ async render(name, data = {}, options = {}) {
16213
+ const html = await this.engine.render(name, data, {
16214
+ request: options.request,
16215
+ layout: options.layout ?? this.defaultLayout
16216
+ });
16217
+ const headers = new Headers(options.headers);
16218
+ headers.set("Content-Type", "text/html; charset=utf-8");
16219
+ return new Response(html, {
16220
+ status: options.status ?? 200,
16221
+ statusText: options.statusText,
16222
+ headers
16223
+ });
16224
+ }
16225
+ async renderHtmx(name, data = {}, options = {}) {
16226
+ const request = options.request ?? currentRequestMeta().request;
16227
+ if (!request)
16228
+ throw new Error("HTMX view rendering requires a request.");
16229
+ const fragment = request.headers.get("HX-Request") === "true" && request.headers.get("HX-Boosted") !== "true" && request.headers.get("HX-History-Restore-Request") !== "true";
16230
+ const headers = new Headers(options.headers);
16231
+ varyByHtmx(headers);
16232
+ return this.render(fragment ? options.fragment ?? name : name, data, {
16233
+ ...options,
16234
+ request,
16235
+ headers,
16236
+ layout: fragment ? false : options.layout
16237
+ });
16238
+ }
16239
+ }
16240
+ var CORE_VIEW_RENDERER_TOKEN = createServiceToken("core.viewRenderer");
16121
16241
  // ../../src/core/view/webLayoutData.ts
16122
16242
  var configuredLayoutOptions = {};
16123
16243
  function configureWebLayoutData(options) {
@@ -16156,6 +16276,16 @@ async function defaultLoadLayoutUser(container, _request) {
16156
16276
  }
16157
16277
  async function resolveWebLayoutData(container, request, options = {}) {
16158
16278
  const resolved = { ...configuredLayoutOptions, ...options };
16279
+ return loadWebLayoutData(container, request, resolved);
16280
+ }
16281
+ function createWebLayoutDataResolver(container, options = {}) {
16282
+ const scoped = {
16283
+ ...options,
16284
+ extra: typeof options.extra === "function" ? options.extra : { ...options.extra }
16285
+ };
16286
+ return (request) => loadWebLayoutData(container, request, scoped);
16287
+ }
16288
+ async function loadWebLayoutData(container, request, resolved) {
16159
16289
  const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
16160
16290
  const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
16161
16291
  const userKey = layoutUserKey(resolved);
@@ -16183,6 +16313,7 @@ export {
16183
16313
  BelongsToRelationQuery,
16184
16314
  Blueprint,
16185
16315
  CACHE_TAGS,
16316
+ CORE_VIEW_RENDERER_TOKEN,
16186
16317
  repository_default as CacheRepository,
16187
16318
  ColumnDefinition,
16188
16319
  CompositeGuard,
@@ -16256,6 +16387,7 @@ export {
16256
16387
  UnprocessableEntityError,
16257
16388
  UnsupportedSchemaFeatureError,
16258
16389
  ValidationError,
16390
+ ViewRenderer,
16259
16391
  WebFormRequest,
16260
16392
  WhereBuilder,
16261
16393
  absoluteTemporarySignedUrl,
@@ -16374,6 +16506,7 @@ export {
16374
16506
  createTrackedJob,
16375
16507
  createTransactionAcquisitionMetrics,
16376
16508
  createValidateSignatureMiddleware,
16509
+ createWebLayoutDataResolver,
16377
16510
  createdResponse,
16378
16511
  currentAuthUser,
16379
16512
  currentCredentialSource,
@@ -16471,6 +16604,7 @@ export {
16471
16604
  maxLength,
16472
16605
  membershipContext,
16473
16606
  membershipRepository,
16607
+ mergeRouteMaps,
16474
16608
  migrateDatabase,
16475
16609
  minLength,
16476
16610
  modelEventName,
@@ -16573,6 +16707,7 @@ export {
16573
16707
  resolveUserId,
16574
16708
  resolveUserTenantId,
16575
16709
  resolveWebLayoutData,
16710
+ resourceRoutes,
16576
16711
  rollbackDatabase,
16577
16712
  rssResponse,
16578
16713
  runDueScheduledTasks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",