@getstrata/bootstrap 0.2.7 → 0.2.8

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.
@@ -0,0 +1,11 @@
1
+ import type { Policy } from "../../core/auth/policy";
2
+ import type { RouteRequest } from "../../core/http/route";
3
+ interface RouteModelAuthorization {
4
+ resource: string;
5
+ action: keyof Policy;
6
+ requireIfMatch?: boolean;
7
+ }
8
+ declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
9
+ declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
10
+ export type { RouteModelAuthorization };
11
+ export { securedBindRouteModel, securedBindRouteModelByKey };
@@ -10,6 +10,7 @@ export { collectProviders, createAppContext, runProviderPhase } from "./context.
10
10
  export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
11
11
  export { assertAppDependenciesComplete, getRequiredDependency, resolveService, ServiceContainer, } from "./contracts.ts";
12
12
  export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
13
+ export { type RouteModelAuthorization, securedBindRouteModel, securedBindRouteModelByKey, } from "./http/securedRouteModelBinding.ts";
13
14
  export { createHttpKernel, type HttpKernel, type MiddlewareGroupName } from "./httpKernel.ts";
14
15
  export { prefixRouteMap } from "./prefixRouteMap.ts";
15
16
  export { coreProviders } from "./providers/index.ts";
@@ -1,11 +1,2 @@
1
- import type { Policy } from "../auth/policy";
2
- import type { RouteRequest } from "./route";
3
- interface RouteModelAuthorization {
4
- resource: string;
5
- action: keyof Policy;
6
- requireIfMatch?: boolean;
7
- }
8
- declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
9
- declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
10
- export type { RouteModelAuthorization };
11
- export { securedBindRouteModel, securedBindRouteModelByKey };
1
+ export type { RouteModelAuthorization } from "../../bootstrap/http/securedRouteModelBinding.ts";
2
+ export { securedBindRouteModel, securedBindRouteModelByKey, } from "../../bootstrap/http/securedRouteModelBinding.ts";
@@ -527,6 +527,11 @@ class UnauthorizedError2 extends HttpError {
527
527
  super(401, message, details);
528
528
  }
529
529
  }
530
+ class PreconditionFailedError extends HttpError {
531
+ constructor(message = "Precondition Failed", details) {
532
+ super(412, message, details);
533
+ }
534
+ }
530
535
 
531
536
  // ../../src/core/auth/authContext.ts
532
537
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
@@ -0,0 +1,383 @@
1
+ // @bun
2
+ // ../../src/core/auth/authContext.ts
3
+ import { AsyncLocalStorage } from "async_hooks";
4
+ var authContext = new AsyncLocalStorage;
5
+ function currentAuthUser() {
6
+ return authContext.getStore() ?? null;
7
+ }
8
+
9
+ // ../../src/core/errors/http.ts
10
+ class HttpError extends Error {
11
+ status;
12
+ details;
13
+ constructor(status, message, details) {
14
+ super(message);
15
+ this.name = new.target.name;
16
+ this.status = status;
17
+ this.details = details;
18
+ }
19
+ }
20
+
21
+ class BadRequestError extends HttpError {
22
+ constructor(message = "Bad Request", details) {
23
+ super(400, message, details);
24
+ }
25
+ }
26
+ class ConflictError extends HttpError {
27
+ constructor(message = "Conflict", details) {
28
+ super(409, message, details);
29
+ }
30
+ }
31
+
32
+ class UnprocessableEntityError extends HttpError {
33
+ constructor(message = "Unprocessable Entity", details) {
34
+ super(422, message, details);
35
+ }
36
+ }
37
+ class ForbiddenError extends HttpError {
38
+ constructor(message = "Forbidden", details) {
39
+ super(403, message, details);
40
+ }
41
+ }
42
+
43
+ class UnauthorizedError extends HttpError {
44
+ constructor(message = "Unauthorized", details) {
45
+ super(401, message, details);
46
+ }
47
+ }
48
+ class PreconditionFailedError extends HttpError {
49
+ constructor(message = "Precondition Failed", details) {
50
+ super(412, message, details);
51
+ }
52
+ }
53
+
54
+ // ../../src/core/crypto/nonCryptographicHash.ts
55
+ function nonCryptographicDigest(input) {
56
+ return Bun.hash(input).toString(16);
57
+ }
58
+
59
+ // ../../src/core/http/etag.ts
60
+ function isEtagEnabled() {
61
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
62
+ }
63
+ function formatWeakEtag(digest) {
64
+ return `W/"${digest}"`;
65
+ }
66
+ function etagFromResource(resource) {
67
+ const version = resource.updated_at ?? resource.created_at ?? "";
68
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
69
+ const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
70
+ return formatWeakEtag(digest);
71
+ }
72
+ function normalizeEtag(value) {
73
+ return value.trim();
74
+ }
75
+ function etagValuesMatch(left, right) {
76
+ return normalizeEtag(left) === normalizeEtag(right);
77
+ }
78
+ function parseEtagList(header) {
79
+ if (!header) {
80
+ return [];
81
+ }
82
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
83
+ }
84
+ function ifNoneMatchSatisfied(request, etag) {
85
+ const header = request.headers.get("if-none-match");
86
+ if (!header) {
87
+ return false;
88
+ }
89
+ if (header.trim() === "*") {
90
+ return true;
91
+ }
92
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
93
+ }
94
+ function ifMatchSatisfied(request, etag) {
95
+ const header = request.headers.get("if-match");
96
+ if (!header) {
97
+ return false;
98
+ }
99
+ if (header.trim() === "*") {
100
+ return true;
101
+ }
102
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
103
+ }
104
+ function assertIfMatch(request, etag, options = {}) {
105
+ const header = request.headers.get("if-match");
106
+ if (!header) {
107
+ if (options.required) {
108
+ throw new PreconditionFailedError("If-Match header is required.");
109
+ }
110
+ return;
111
+ }
112
+ if (!ifMatchSatisfied(request, etag)) {
113
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
114
+ }
115
+ }
116
+ function applyEtagHeaders(headers, etag) {
117
+ const next = new Headers(headers);
118
+ next.set("ETag", etag);
119
+ next.set("Cache-Control", "private, must-revalidate");
120
+ next.append("Vary", "Authorization");
121
+ next.append("Vary", "X-Tenant-Id");
122
+ return next;
123
+ }
124
+ function notModifiedResponse(etag) {
125
+ return new Response(null, {
126
+ status: 304,
127
+ headers: applyEtagHeaders(new Headers, etag)
128
+ });
129
+ }
130
+ function applyConditionalGet(request, response, etag) {
131
+ if (!isEtagEnabled()) {
132
+ return response;
133
+ }
134
+ if (ifNoneMatchSatisfied(request, etag)) {
135
+ return notModifiedResponse(etag);
136
+ }
137
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
138
+ return new Response(response.body, {
139
+ status: response.status,
140
+ statusText: response.statusText,
141
+ headers
142
+ });
143
+ }
144
+
145
+ // ../../src/core/tenant/tenantContext.ts
146
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
147
+ var tenantContext = new AsyncLocalStorage2;
148
+
149
+ // ../../src/core/http/validation.ts
150
+ function parsePositiveIntParam(value, name = "id") {
151
+ const parsed = Number.parseInt(value, 10);
152
+ if (!Number.isInteger(parsed) || parsed <= 0) {
153
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
154
+ }
155
+ return parsed;
156
+ }
157
+
158
+ // ../../src/core/logging/logger.ts
159
+ class Logger {
160
+ channel;
161
+ constructor(channel = "app") {
162
+ this.channel = channel;
163
+ }
164
+ write(level, message, context = {}) {
165
+ const entry = {
166
+ level,
167
+ channel: this.channel,
168
+ message,
169
+ timestamp: new Date().toISOString(),
170
+ ...context
171
+ };
172
+ const line = JSON.stringify(entry);
173
+ if (level === "error") {
174
+ console.error(line);
175
+ return;
176
+ }
177
+ console.log(line);
178
+ }
179
+ debug(message, context) {
180
+ this.write("debug", message, context);
181
+ }
182
+ info(message, context) {
183
+ this.write("info", message, context);
184
+ }
185
+ warn(message, context) {
186
+ this.write("warn", message, context);
187
+ }
188
+ error(message, context) {
189
+ this.write("error", message, context);
190
+ }
191
+ }
192
+ var appLogger = new Logger("app");
193
+
194
+ // ../../src/bootstrap/config.ts
195
+ var APP_PORT_CONFIG_KEY = "app.port";
196
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
197
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
198
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
199
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
200
+ var DATABASE_URL_CONFIG_KEY = "database.url";
201
+ var CORE_CONFIG_TOKEN = "core.config";
202
+ var CORE_CACHE_TOKEN = "core.cache";
203
+ var CORE_QUEUE_TOKEN = "core.queue";
204
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
205
+ var CORE_AUTH_TOKEN = "core.auth";
206
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
207
+ var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
208
+ var DEFAULT_APP_PORT = 3000;
209
+ var DEFAULT_CACHE_TTL_MS = 3600000;
210
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
211
+ var DEFAULT_CACHE_DRIVER = "array";
212
+ var DEFAULT_API_TOKEN = "";
213
+ var DEFAULT_QUEUE_DRIVER = "sync";
214
+
215
+ // ../../src/bootstrap/contracts.ts
216
+ class ServiceContainer {
217
+ services = new Map;
218
+ singletonFactories = new Map;
219
+ bindings = new Map;
220
+ set(key, value) {
221
+ this.singletonFactories.delete(key);
222
+ this.bindings.delete(key);
223
+ this.services.set(key, value);
224
+ return value;
225
+ }
226
+ singleton(key, factory) {
227
+ this.bindings.delete(key);
228
+ this.services.delete(key);
229
+ this.singletonFactories.set(key, factory);
230
+ }
231
+ bind(key, factory) {
232
+ this.singletonFactories.delete(key);
233
+ this.services.delete(key);
234
+ this.bindings.set(key, factory);
235
+ }
236
+ get(key) {
237
+ if (this.services.has(key)) {
238
+ return this.services.get(key);
239
+ }
240
+ const singletonFactory = this.singletonFactories.get(key);
241
+ if (singletonFactory) {
242
+ const value = singletonFactory(this);
243
+ this.services.set(key, value);
244
+ return value;
245
+ }
246
+ const binding = this.bindings.get(key);
247
+ if (binding) {
248
+ return binding(this);
249
+ }
250
+ throw new Error(`Service "${key}" is not registered.`);
251
+ }
252
+ resolve(key) {
253
+ return this.get(key);
254
+ }
255
+ has(key) {
256
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
257
+ }
258
+ }
259
+
260
+ class ConfigStore {
261
+ values = new Map;
262
+ set(key, value) {
263
+ this.values.set(key, value);
264
+ return value;
265
+ }
266
+ get(key) {
267
+ return this.values.get(key);
268
+ }
269
+ require(key) {
270
+ if (!this.values.has(key)) {
271
+ throw new Error(`Config key "${key}" is not defined.`);
272
+ }
273
+ return this.values.get(key);
274
+ }
275
+ has(key) {
276
+ return this.values.has(key);
277
+ }
278
+ }
279
+ var requiredDependencyKeys = [
280
+ "container",
281
+ "cache",
282
+ "storage"
283
+ ];
284
+ function getRequiredDependency(dependencies, key) {
285
+ const dependency = dependencies[key];
286
+ if (dependency === undefined) {
287
+ throw new Error(`Required dependency "${key}" is not registered.`);
288
+ }
289
+ return dependency;
290
+ }
291
+ function assertAppDependenciesComplete(dependencies) {
292
+ for (const key of requiredDependencyKeys) {
293
+ getRequiredDependency(dependencies, key);
294
+ }
295
+ }
296
+ function resolveService(dependencies, token) {
297
+ return dependencies.container.resolve(token);
298
+ }
299
+
300
+ // ../../src/bootstrap/applicationRegistry.ts
301
+ var activeContext;
302
+ function setActiveApplicationContext(context) {
303
+ activeContext = context;
304
+ }
305
+ function requireActiveApplicationContext() {
306
+ if (!activeContext) {
307
+ throw new Error("The application context has not been bootstrapped.");
308
+ }
309
+ return activeContext;
310
+ }
311
+ function resolveApplicationCache() {
312
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
313
+ }
314
+ function resolveApplicationQueue() {
315
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
316
+ }
317
+ function resolveApplicationAuth() {
318
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
319
+ }
320
+ function resolveApplicationPolicyGate() {
321
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
322
+ }
323
+ function resolveApplicationConfig() {
324
+ return requireActiveApplicationContext().config;
325
+ }
326
+ function resolveApplicationLogger() {
327
+ return appLogger;
328
+ }
329
+ function resolveApplicationDependencies() {
330
+ return requireActiveApplicationContext().dependencies;
331
+ }
332
+
333
+ // ../../src/bootstrap/http/securedRouteModelBinding.ts
334
+ function isMutatingPolicyAction(action) {
335
+ return action === "update" || action === "delete";
336
+ }
337
+ function securedBindRouteModel(param, resolver, authorization, handler) {
338
+ return async (request) => {
339
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
340
+ const model = await resolver(id, request);
341
+ const gate = resolveApplicationPolicyGate();
342
+ const auth = resolveApplicationAuth();
343
+ const user = currentAuthUser() ?? await auth.resolve(request);
344
+ gate.authorize(authorization.resource, authorization.action, user, model);
345
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
346
+ assertIfMatch(request, etagFromResource(model), {
347
+ required: authorization.requireIfMatch ?? true
348
+ });
349
+ }
350
+ const response = await handler(request, model);
351
+ if (isEtagEnabled() && authorization.action === "view") {
352
+ return applyConditionalGet(request, response, etagFromResource(model));
353
+ }
354
+ return response;
355
+ };
356
+ }
357
+ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
358
+ return async (request) => {
359
+ const key = String(request.params[param] ?? "").trim();
360
+ if (!key) {
361
+ throw new BadRequestError(`Missing route parameter "${String(param)}".`);
362
+ }
363
+ const model = await resolver(key, request);
364
+ const gate = resolveApplicationPolicyGate();
365
+ const auth = resolveApplicationAuth();
366
+ const user = currentAuthUser() ?? await auth.resolve(request);
367
+ gate.authorize(authorization.resource, authorization.action, user, model);
368
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
369
+ assertIfMatch(request, etagFromResource(model), {
370
+ required: authorization.requireIfMatch ?? true
371
+ });
372
+ }
373
+ const response = await handler(request, model);
374
+ if (isEtagEnabled() && authorization.action === "view") {
375
+ return applyConditionalGet(request, response, etagFromResource(model));
376
+ }
377
+ return response;
378
+ };
379
+ }
380
+ export {
381
+ securedBindRouteModelByKey,
382
+ securedBindRouteModel
383
+ };
@@ -416,6 +416,11 @@ class UnauthorizedError2 extends HttpError {
416
416
  super(401, message, details);
417
417
  }
418
418
  }
419
+ class PreconditionFailedError extends HttpError {
420
+ constructor(message = "Precondition Failed", details) {
421
+ super(412, message, details);
422
+ }
423
+ }
419
424
 
420
425
  // ../../src/core/auth/authContext.ts
421
426
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
@@ -19,6 +19,7 @@ export { appendOrganizationScope, appendProjectScope, assertOrganizationReadable
19
19
  export { default as MembershipService, resolveMembershipService, } from "../core/auth/membershipService.ts";
20
20
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
21
21
  export { createScimAuthMiddleware } from "../core/auth/scimAuthMiddleware.ts";
22
+ export { type CacheDriver, type CreateCacheStoreOptions, createCacheStore, } from "../core/cache/createCacheStore.ts";
22
23
  export { default as CacheRepository } from "../core/cache/repository.ts";
23
24
  export { CACHE_TAGS } from "../core/cache/tags.ts";
24
25
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
package/dist/index.js CHANGED
@@ -179,6 +179,11 @@ class UnauthorizedError extends HttpError {
179
179
  super(401, message, details);
180
180
  }
181
181
  }
182
+ class PreconditionFailedError extends HttpError {
183
+ constructor(message = "Precondition Failed", details) {
184
+ super(412, message, details);
185
+ }
186
+ }
182
187
 
183
188
  // ../../src/core/security/safeUrl.ts
184
189
  var dnsLookup = dnsLookupImpl;
@@ -1818,6 +1823,12 @@ function resolveApplicationCache() {
1818
1823
  function resolveApplicationQueue() {
1819
1824
  return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
1820
1825
  }
1826
+ function resolveApplicationAuth() {
1827
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
1828
+ }
1829
+ function resolveApplicationPolicyGate() {
1830
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
1831
+ }
1821
1832
 
1822
1833
  // ../../src/core/jobs/dispatchWebhookJob.ts
1823
1834
  import { createHmac as createHmac2 } from "crypto";
@@ -4702,6 +4713,157 @@ function mergeWebRoutes(dependencies, routes) {
4702
4713
  ...routes
4703
4714
  };
4704
4715
  }
4716
+ // ../../src/core/crypto/nonCryptographicHash.ts
4717
+ function nonCryptographicDigest(input) {
4718
+ return Bun.hash(input).toString(16);
4719
+ }
4720
+
4721
+ // ../../src/core/http/etag.ts
4722
+ function isEtagEnabled() {
4723
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
4724
+ }
4725
+ function formatWeakEtag(digest) {
4726
+ return `W/"${digest}"`;
4727
+ }
4728
+ function etagFromResource(resource) {
4729
+ const version = resource.updated_at ?? resource.created_at ?? "";
4730
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
4731
+ const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
4732
+ return formatWeakEtag(digest);
4733
+ }
4734
+ function normalizeEtag(value) {
4735
+ return value.trim();
4736
+ }
4737
+ function etagValuesMatch(left, right) {
4738
+ return normalizeEtag(left) === normalizeEtag(right);
4739
+ }
4740
+ function parseEtagList(header) {
4741
+ if (!header) {
4742
+ return [];
4743
+ }
4744
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
4745
+ }
4746
+ function ifNoneMatchSatisfied(request, etag) {
4747
+ const header = request.headers.get("if-none-match");
4748
+ if (!header) {
4749
+ return false;
4750
+ }
4751
+ if (header.trim() === "*") {
4752
+ return true;
4753
+ }
4754
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4755
+ }
4756
+ function ifMatchSatisfied(request, etag) {
4757
+ const header = request.headers.get("if-match");
4758
+ if (!header) {
4759
+ return false;
4760
+ }
4761
+ if (header.trim() === "*") {
4762
+ return true;
4763
+ }
4764
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4765
+ }
4766
+ function assertIfMatch(request, etag, options = {}) {
4767
+ const header = request.headers.get("if-match");
4768
+ if (!header) {
4769
+ if (options.required) {
4770
+ throw new PreconditionFailedError("If-Match header is required.");
4771
+ }
4772
+ return;
4773
+ }
4774
+ if (!ifMatchSatisfied(request, etag)) {
4775
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
4776
+ }
4777
+ }
4778
+ function applyEtagHeaders(headers, etag) {
4779
+ const next = new Headers(headers);
4780
+ next.set("ETag", etag);
4781
+ next.set("Cache-Control", "private, must-revalidate");
4782
+ next.append("Vary", "Authorization");
4783
+ next.append("Vary", "X-Tenant-Id");
4784
+ return next;
4785
+ }
4786
+ function notModifiedResponse(etag) {
4787
+ return new Response(null, {
4788
+ status: 304,
4789
+ headers: applyEtagHeaders(new Headers, etag)
4790
+ });
4791
+ }
4792
+ function applyConditionalGet(request, response, etag) {
4793
+ if (!isEtagEnabled()) {
4794
+ return response;
4795
+ }
4796
+ if (ifNoneMatchSatisfied(request, etag)) {
4797
+ return notModifiedResponse(etag);
4798
+ }
4799
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
4800
+ return new Response(response.body, {
4801
+ status: response.status,
4802
+ statusText: response.statusText,
4803
+ headers
4804
+ });
4805
+ }
4806
+
4807
+ // ../../src/core/tenant/tenantContext.ts
4808
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4809
+ var tenantContext = new AsyncLocalStorage4;
4810
+
4811
+ // ../../src/core/http/validation.ts
4812
+ function parsePositiveIntParam(value, name = "id") {
4813
+ const parsed = Number.parseInt(value, 10);
4814
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4815
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
4816
+ }
4817
+ return parsed;
4818
+ }
4819
+
4820
+ // ../../src/bootstrap/http/securedRouteModelBinding.ts
4821
+ function isMutatingPolicyAction(action) {
4822
+ return action === "update" || action === "delete";
4823
+ }
4824
+ function securedBindRouteModel(param, resolver, authorization, handler) {
4825
+ return async (request) => {
4826
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
4827
+ const model = await resolver(id, request);
4828
+ const gate = resolveApplicationPolicyGate();
4829
+ const auth = resolveApplicationAuth();
4830
+ const user = currentAuthUser() ?? await auth.resolve(request);
4831
+ gate.authorize(authorization.resource, authorization.action, user, model);
4832
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4833
+ assertIfMatch(request, etagFromResource(model), {
4834
+ required: authorization.requireIfMatch ?? true
4835
+ });
4836
+ }
4837
+ const response = await handler(request, model);
4838
+ if (isEtagEnabled() && authorization.action === "view") {
4839
+ return applyConditionalGet(request, response, etagFromResource(model));
4840
+ }
4841
+ return response;
4842
+ };
4843
+ }
4844
+ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
4845
+ return async (request) => {
4846
+ const key = String(request.params[param] ?? "").trim();
4847
+ if (!key) {
4848
+ throw new BadRequestError(`Missing route parameter "${String(param)}".`);
4849
+ }
4850
+ const model = await resolver(key, request);
4851
+ const gate = resolveApplicationPolicyGate();
4852
+ const auth = resolveApplicationAuth();
4853
+ const user = currentAuthUser() ?? await auth.resolve(request);
4854
+ gate.authorize(authorization.resource, authorization.action, user, model);
4855
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4856
+ assertIfMatch(request, etagFromResource(model), {
4857
+ required: authorization.requireIfMatch ?? true
4858
+ });
4859
+ }
4860
+ const response = await handler(request, model);
4861
+ if (isEtagEnabled() && authorization.action === "view") {
4862
+ return applyConditionalGet(request, response, etagFromResource(model));
4863
+ }
4864
+ return response;
4865
+ };
4866
+ }
4705
4867
  // ../../src/bootstrap/prefixRouteMap.ts
4706
4868
  function prefixRouteMap(prefix, routes) {
4707
4869
  const normalizedPrefix = prefix.replace(/\/$/, "");
@@ -4761,7 +4923,7 @@ async function parseFormBody(request) {
4761
4923
  return { fields, files };
4762
4924
  }
4763
4925
  // ../../src/bootstrap/web/routing.ts
4764
- import { securedBindRouteModelByKey, withErrorHandling } from "@getstrata/core";
4926
+ import { withErrorHandling } from "@getstrata/core";
4765
4927
  function routeParams(request) {
4766
4928
  const normalized = {};
4767
4929
  const raw = request.params;
@@ -4936,6 +5098,8 @@ export {
4936
5098
  toRouteRequest,
4937
5099
  slugify,
4938
5100
  setActiveApplicationContext2 as setActiveApplicationContext,
5101
+ securedBindRouteModelByKey,
5102
+ securedBindRouteModel,
4939
5103
  scheduleRunCommand,
4940
5104
  runProviderPhase,
4941
5105
  runDueScheduledTasks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,11 @@
40
40
  "import": "./dist/entries/createWebRoutes.js",
41
41
  "default": "./dist/entries/createWebRoutes.js"
42
42
  },
43
+ "./http/securedRouteModelBinding": {
44
+ "types": "./dist/bootstrap/http/securedRouteModelBinding.d.ts",
45
+ "import": "./dist/entries/http/securedRouteModelBinding.js",
46
+ "default": "./dist/entries/http/securedRouteModelBinding.js"
47
+ },
43
48
  "./httpKernel": {
44
49
  "types": "./dist/bootstrap/httpKernel.d.ts",
45
50
  "import": "./dist/entries/httpKernel.js",
@@ -67,14 +72,14 @@
67
72
  "build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta --external @getstrata/core",
68
73
  "build:types": "tsc -p tsconfig.types.json",
69
74
  "prepublishOnly": "bun run build",
70
- "build:subpaths": "bun build entries/applicationRegistry.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/httpKernel.ts entries/providers.ts entries/providers/view.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
75
+ "build:subpaths": "bun build entries/applicationRegistry.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/providers.ts entries/providers/view.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
71
76
  "build:shims": "true"
72
77
  },
73
78
  "publishConfig": {
74
79
  "access": "public"
75
80
  },
76
81
  "peerDependencies": {
77
- "@getstrata/core": "^0.5.15",
82
+ "@getstrata/core": "^0.5.16",
78
83
  "typescript": "^5.9.0"
79
84
  }
80
85
  }