@getstrata/bootstrap 0.2.13 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ // @bun
2
+ // ../../src/bootstrap/web/server.ts
3
+ function wrapRouteHandler(handler) {
4
+ return async (request) => {
5
+ const response = await handler(request);
6
+ return response ?? new Response("Not Found", { status: 404 });
7
+ };
8
+ }
9
+ function convertAppRoutesToBunRoutes(routes) {
10
+ const bunRoutes = {};
11
+ for (const [path, handler] of Object.entries(routes)) {
12
+ if (typeof handler === "function") {
13
+ bunRoutes[path] = { GET: wrapRouteHandler(handler) };
14
+ continue;
15
+ }
16
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
17
+ const methods = {};
18
+ for (const [method, methodHandler] of Object.entries(handler)) {
19
+ if (typeof methodHandler !== "function") {
20
+ continue;
21
+ }
22
+ methods[method.toUpperCase()] = wrapRouteHandler(methodHandler);
23
+ }
24
+ if (Object.keys(methods).length > 0) {
25
+ bunRoutes[path] = methods;
26
+ }
27
+ }
28
+ }
29
+ return bunRoutes;
30
+ }
31
+ function createWebServer(options) {
32
+ const publicDir = options.publicDir ?? "./public";
33
+ const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes) : undefined;
34
+ return Bun.serve({
35
+ port: options.port,
36
+ ...bunRoutes ? { routes: bunRoutes } : {},
37
+ async fetch(request) {
38
+ await options.onRequest?.(request);
39
+ const url = new URL(request.url);
40
+ if (url.pathname.startsWith("/assets/")) {
41
+ const file = Bun.file(`${publicDir}${url.pathname}`);
42
+ if (await file.exists()) {
43
+ return new Response(file);
44
+ }
45
+ }
46
+ if (options.handle) {
47
+ const response = await options.handle(request);
48
+ return response ?? new Response("Not Found", { status: 404 });
49
+ }
50
+ return new Response("Not Found", { status: 404 });
51
+ }
52
+ });
53
+ }
54
+ export {
55
+ createWebServer,
56
+ convertAppRoutesToBunRoutes
57
+ };
@@ -0,0 +1,99 @@
1
+ // @bun
2
+ // ../../src/bootstrap/web/session.ts
3
+ import { createHash, randomBytes } from "crypto";
4
+
5
+ // ../../src/core/http/cookies.ts
6
+ function readRequestCookie(request, name) {
7
+ const cookies = request.cookies;
8
+ if (cookies && typeof cookies.get === "function") {
9
+ const value = cookies.get(name);
10
+ if (value) {
11
+ return value;
12
+ }
13
+ }
14
+ const header = request.headers.get("cookie");
15
+ if (!header) {
16
+ return null;
17
+ }
18
+ for (const part of header.split(";")) {
19
+ const idx = part.indexOf("=");
20
+ if (idx === -1)
21
+ continue;
22
+ const cookieName = part.slice(0, idx).trim();
23
+ if (cookieName !== name)
24
+ continue;
25
+ return decodeURIComponent(part.slice(idx + 1).trim());
26
+ }
27
+ return null;
28
+ }
29
+
30
+ // ../../src/bootstrap/web/session.ts
31
+ class CookieSessionStore {
32
+ sql;
33
+ secret;
34
+ cookieName;
35
+ maxAgeSeconds;
36
+ constructor(sql, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
37
+ this.sql = sql;
38
+ this.secret = secret;
39
+ this.cookieName = cookieName;
40
+ this.maxAgeSeconds = maxAgeSeconds;
41
+ }
42
+ cookieHeader(_user, sessionId) {
43
+ const payload = `${sessionId}.${this.sign(sessionId)}`;
44
+ return this.withSecureFlag(`${this.cookieName}=${payload}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${this.maxAgeSeconds}`);
45
+ }
46
+ clearCookieHeader() {
47
+ return this.withSecureFlag(`${this.cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
48
+ }
49
+ withSecureFlag(header) {
50
+ if (true) {
51
+ return header;
52
+ }
53
+ return header.includes("Secure") ? header : `${header}; Secure`;
54
+ }
55
+ async create(user) {
56
+ const id = randomBytes(32).toString("hex");
57
+ const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
58
+ await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
59
+ id,
60
+ user.id,
61
+ expires
62
+ ]);
63
+ return id;
64
+ }
65
+ async destroy(sessionId) {
66
+ await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
67
+ }
68
+ async read(request) {
69
+ const cookie = readRequestCookie(request, this.cookieName);
70
+ const raw = cookie ?? null;
71
+ if (!raw)
72
+ return null;
73
+ const [sessionId, signature] = raw.split(".");
74
+ if (!sessionId || !signature || signature !== this.sign(sessionId)) {
75
+ return null;
76
+ }
77
+ const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
78
+ COALESCE(u.is_admin, false) AS is_admin
79
+ FROM sessions s
80
+ INNER JOIN users u ON u.id = s.user_id
81
+ WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
82
+ const row = rows[0];
83
+ if (!row)
84
+ return null;
85
+ return {
86
+ id: row.user_id,
87
+ name: row.name,
88
+ email: row.email,
89
+ learn_subscriber: row.learn_subscriber,
90
+ is_admin: row.is_admin
91
+ };
92
+ }
93
+ sign(value) {
94
+ return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
95
+ }
96
+ }
97
+ export {
98
+ CookieSessionStore
99
+ };
@@ -0,0 +1,8 @@
1
+ // @bun
2
+ // ../../src/bootstrap/web/slug.ts
3
+ function slugify(value) {
4
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
5
+ }
6
+ export {
7
+ slugify
8
+ };
@@ -19,7 +19,6 @@ export { default as MembershipService, resolveMembershipService, } from "../core
19
19
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
20
20
  export { createScimAuthMiddleware } from "../core/auth/scimAuthMiddleware.ts";
21
21
  export { type CacheDriver, type CreateCacheStoreOptions, createCacheStore, } from "../core/cache/createCacheStore.ts";
22
- export { cacheTagsForModelWrite, discoverModelTableNames } from "../core/cache/modelCacheTags.ts";
23
22
  export { default as CacheRepository } from "../core/cache/repository.ts";
24
23
  export { CACHE_TAGS } from "../core/cache/tags.ts";
25
24
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
package/dist/index.js CHANGED
@@ -3582,7 +3582,6 @@ var failedJobService_default = FailedJobService;
3582
3582
 
3583
3583
  // ../../src/core/queue/jobRegistry.ts
3584
3584
  class JobRegistry {
3585
- constructor() {}
3586
3585
  factories = new Map;
3587
3586
  instances = new WeakMap;
3588
3587
  register(name, factory) {
@@ -3606,7 +3605,17 @@ class JobRegistry {
3606
3605
  return [...this.factories.keys()];
3607
3606
  }
3608
3607
  }
3609
- var jobRegistry = new JobRegistry;
3608
+ var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
3609
+ function readSharedJobRegistry() {
3610
+ const globalRegistry = globalThis[JOB_REGISTRY_KEY];
3611
+ if (globalRegistry) {
3612
+ return globalRegistry;
3613
+ }
3614
+ const registry = new JobRegistry;
3615
+ globalThis[JOB_REGISTRY_KEY] = registry;
3616
+ return registry;
3617
+ }
3618
+ var jobRegistry = readSharedJobRegistry();
3610
3619
 
3611
3620
  // ../../src/core/queue/jobRunner.ts
3612
3621
  async function runQueueJob(envelope, failedJobs) {
@@ -3722,75 +3731,6 @@ function createProductionQueue(driver, options = {}) {
3722
3731
  return new ResilientQueue(failedJobs, driver === "async");
3723
3732
  }
3724
3733
 
3725
- // ../../src/core/jobs/dispatchWebhookJob.ts
3726
- import { createHmac as createHmac2 } from "crypto";
3727
- class DispatchWebhookJob extends Job {
3728
- maxAttempts = 3;
3729
- backoffMs = 2000;
3730
- async handle(payload) {
3731
- const rows = await repositoryConnection`
3732
- SELECT id, url, secret
3733
- FROM webhook
3734
- WHERE id = ${payload.webhookId} AND active = TRUE
3735
- LIMIT 1
3736
- `;
3737
- const webhook = rows[0];
3738
- if (!webhook) {
3739
- return;
3740
- }
3741
- const body = JSON.stringify({ event: payload.event, payload: payload.payload });
3742
- const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
3743
- assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
3744
- let responseStatus = null;
3745
- let errorMessage = null;
3746
- try {
3747
- const response = await safeFetch(webhook.url, {
3748
- method: "POST",
3749
- headers: {
3750
- "content-type": "application/json",
3751
- "x-workhub-signature": signature
3752
- },
3753
- body
3754
- }, { allowHttp: appConfig.env !== "production" });
3755
- responseStatus = response.status;
3756
- if (!response.ok) {
3757
- throw new Error(`Webhook delivery failed with status ${response.status}.`);
3758
- }
3759
- } catch (error) {
3760
- errorMessage = error instanceof Error ? error.message : String(error);
3761
- await repositoryConnection`
3762
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
3763
- VALUES (
3764
- ${webhook.id},
3765
- ${payload.event},
3766
- ${JSON.stringify(payload.payload)}::jsonb,
3767
- ${responseStatus},
3768
- ${errorMessage}
3769
- )
3770
- `;
3771
- throw error instanceof Error ? error : new Error(errorMessage);
3772
- }
3773
- await repositoryConnection`
3774
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
3775
- VALUES (
3776
- ${webhook.id},
3777
- ${payload.event},
3778
- ${JSON.stringify(payload.payload)}::jsonb,
3779
- ${responseStatus}
3780
- )
3781
- `;
3782
- }
3783
- }
3784
- var dispatchWebhookJob_default = DispatchWebhookJob;
3785
-
3786
- // ../../src/bootstrap/queue/defaultJobs.ts
3787
- function registerDefaultJobs() {
3788
- jobRegistry.register("cache.invalidate-tags", () => {
3789
- return new invalidateCacheTagsJob_default(resolveApplicationCache());
3790
- });
3791
- jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
3792
- }
3793
-
3794
3734
  // ../../src/core/queue/createAppQueue.ts
3795
3735
  var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
3796
3736
  function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
@@ -3896,6 +3836,75 @@ var policyProvider = {
3896
3836
  };
3897
3837
  var policy_default = policyProvider;
3898
3838
 
3839
+ // ../../src/core/jobs/dispatchWebhookJob.ts
3840
+ import { createHmac as createHmac2 } from "crypto";
3841
+ class DispatchWebhookJob extends Job {
3842
+ maxAttempts = 3;
3843
+ backoffMs = 2000;
3844
+ async handle(payload) {
3845
+ const rows = await repositoryConnection`
3846
+ SELECT id, url, secret
3847
+ FROM webhook
3848
+ WHERE id = ${payload.webhookId} AND active = TRUE
3849
+ LIMIT 1
3850
+ `;
3851
+ const webhook = rows[0];
3852
+ if (!webhook) {
3853
+ return;
3854
+ }
3855
+ const body = JSON.stringify({ event: payload.event, payload: payload.payload });
3856
+ const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
3857
+ assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
3858
+ let responseStatus = null;
3859
+ let errorMessage = null;
3860
+ try {
3861
+ const response = await safeFetch(webhook.url, {
3862
+ method: "POST",
3863
+ headers: {
3864
+ "content-type": "application/json",
3865
+ "x-workhub-signature": signature
3866
+ },
3867
+ body
3868
+ }, { allowHttp: appConfig.env !== "production" });
3869
+ responseStatus = response.status;
3870
+ if (!response.ok) {
3871
+ throw new Error(`Webhook delivery failed with status ${response.status}.`);
3872
+ }
3873
+ } catch (error) {
3874
+ errorMessage = error instanceof Error ? error.message : String(error);
3875
+ await repositoryConnection`
3876
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
3877
+ VALUES (
3878
+ ${webhook.id},
3879
+ ${payload.event},
3880
+ ${JSON.stringify(payload.payload)}::jsonb,
3881
+ ${responseStatus},
3882
+ ${errorMessage}
3883
+ )
3884
+ `;
3885
+ throw error instanceof Error ? error : new Error(errorMessage);
3886
+ }
3887
+ await repositoryConnection`
3888
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
3889
+ VALUES (
3890
+ ${webhook.id},
3891
+ ${payload.event},
3892
+ ${JSON.stringify(payload.payload)}::jsonb,
3893
+ ${responseStatus}
3894
+ )
3895
+ `;
3896
+ }
3897
+ }
3898
+ var dispatchWebhookJob_default = DispatchWebhookJob;
3899
+
3900
+ // ../../src/bootstrap/queue/defaultJobs.ts
3901
+ function registerDefaultJobs() {
3902
+ jobRegistry.register("cache.invalidate-tags", () => {
3903
+ return new invalidateCacheTagsJob_default(resolveApplicationCache());
3904
+ });
3905
+ jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
3906
+ }
3907
+
3899
3908
  // ../../src/bootstrap/providers/queue.ts
3900
3909
  var queueProvider = {
3901
3910
  name: "core.queue",
@@ -4840,7 +4849,7 @@ function parsePositiveIntParam(value, name = "id") {
4840
4849
  return parsed;
4841
4850
  }
4842
4851
 
4843
- // ../../src/bootstrap/http/securedRouteModelBinding.ts
4852
+ // ../../src/core/http/securedRouteModelBinding.ts
4844
4853
  function isMutatingPolicyAction(action) {
4845
4854
  return action === "update" || action === "delete";
4846
4855
  }
@@ -5021,7 +5030,7 @@ class MembershipService {
5021
5030
  }
5022
5031
  var membershipService_default = MembershipService;
5023
5032
 
5024
- // ../../src/bootstrap/membershipService.ts
5033
+ // ../../src/core/auth/resolveMembershipService.ts
5025
5034
  function resolveMembershipService() {
5026
5035
  const dependencies = resolveApplicationDependencies();
5027
5036
  if (dependencies.container.has("core.membership")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.13",
3
+ "version": "0.2.17",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,11 @@
20
20
  "import": "./dist/entries/applicationRegistry.js",
21
21
  "default": "./dist/entries/applicationRegistry.js"
22
22
  },
23
+ "./cache/modelCacheTags": {
24
+ "types": "./dist/bootstrap/cache/modelCacheTags.d.ts",
25
+ "import": "./dist/entries/cache/modelCacheTags.js",
26
+ "default": "./dist/entries/cache/modelCacheTags.js"
27
+ },
23
28
  "./config": {
24
29
  "types": "./dist/bootstrap/config.d.ts",
25
30
  "import": "./dist/entries/config.js",
@@ -69,6 +74,31 @@
69
74
  "types": "./dist/bootstrap/providers/view/index.d.ts",
70
75
  "import": "./dist/entries/providers/view.js",
71
76
  "default": "./dist/entries/providers/view.js"
77
+ },
78
+ "./web/forms": {
79
+ "types": "./dist/bootstrap/web/forms.d.ts",
80
+ "import": "./dist/entries/web/forms.js",
81
+ "default": "./dist/entries/web/forms.js"
82
+ },
83
+ "./web/routing": {
84
+ "types": "./dist/bootstrap/web/routing.d.ts",
85
+ "import": "./dist/entries/web/routing.js",
86
+ "default": "./dist/entries/web/routing.js"
87
+ },
88
+ "./web/server": {
89
+ "types": "./dist/bootstrap/web/server.d.ts",
90
+ "import": "./dist/entries/web/server.js",
91
+ "default": "./dist/entries/web/server.js"
92
+ },
93
+ "./web/session": {
94
+ "types": "./dist/bootstrap/web/session.d.ts",
95
+ "import": "./dist/entries/web/session.js",
96
+ "default": "./dist/entries/web/session.js"
97
+ },
98
+ "./web/slug": {
99
+ "types": "./dist/bootstrap/web/slug.d.ts",
100
+ "import": "./dist/entries/web/slug.js",
101
+ "default": "./dist/entries/web/slug.js"
72
102
  }
73
103
  },
74
104
  "main": "./dist/index.js",
@@ -82,14 +112,14 @@
82
112
  "build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta --external @getstrata/core",
83
113
  "build:types": "tsc -p tsconfig.types.json",
84
114
  "prepublishOnly": "bun run build",
85
- "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/membershipService.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
115
+ "build:subpaths": "bun build entries/applicationRegistry.ts entries/cache/modelCacheTags.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/membershipService.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts entries/web/forms.ts entries/web/routing.ts entries/web/server.ts entries/web/session.ts entries/web/slug.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
86
116
  "build:shims": "true"
87
117
  },
88
118
  "publishConfig": {
89
119
  "access": "public"
90
120
  },
91
121
  "peerDependencies": {
92
- "@getstrata/core": "^0.5.21",
122
+ "@getstrata/core": "^0.5.25",
93
123
  "typescript": "^5.9.0"
94
124
  }
95
125
  }
@@ -1 +0,0 @@
1
- export { cacheTagsForModelWrite, discoverModelTableNames, } from "../../bootstrap/cache/modelCacheTags.ts";