@getstrata/bootstrap 0.2.7 → 0.2.9

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.
@@ -262,6 +262,7 @@ var db = new Proxy(function database() {}, {
262
262
  return typeof value === "function" ? value.bind(connection) : value;
263
263
  }
264
264
  });
265
+ var connection_default = db;
265
266
 
266
267
  // ../../src/modules/user/apiTokenTable.ts
267
268
  import { defineTable } from "@getstrata/core/database";
@@ -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
+ };