@getstrata/core 0.5.100 → 0.7.3

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +67 -32
  2. package/README.md +16 -35
  3. package/dist/core/auth/abilityCatalog.d.ts +2 -2
  4. package/dist/core/auth/basicAuthGuard.d.ts +9 -0
  5. package/dist/core/auth/guard.d.ts +6 -0
  6. package/dist/core/auth/jwt.d.ts +19 -0
  7. package/dist/core/auth/jwtGuard.d.ts +14 -0
  8. package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
  9. package/dist/core/cache/tags.d.ts +6 -0
  10. package/dist/core/contracts/authUserDirectory.d.ts +4 -0
  11. package/dist/core/database/baseRepository.d.ts +5 -1
  12. package/dist/core/database/dialect.d.ts +18 -0
  13. package/dist/core/database/factory.d.ts +1 -0
  14. package/dist/core/database/index.d.ts +11 -3
  15. package/dist/core/database/model.d.ts +21 -2
  16. package/dist/core/database/mysqlConnection.d.ts +12 -0
  17. package/dist/core/database/namedConnections.d.ts +15 -0
  18. package/dist/core/database/relationQuery.d.ts +22 -3
  19. package/dist/core/database/relationships.d.ts +22 -2
  20. package/dist/core/database/repositoryQuery.d.ts +5 -1
  21. package/dist/core/database/sqliteConnection.d.ts +7 -0
  22. package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
  23. package/dist/core/http/resources.d.ts +2 -2
  24. package/dist/core/http/response.d.ts +2 -1
  25. package/dist/core/http/statelessAuth.d.ts +8 -0
  26. package/dist/core/http/throttleResponse.d.ts +2 -0
  27. package/dist/core/runtime/frontendMode.d.ts +10 -2
  28. package/dist/entries/auth/basicAuthGuard.js +137 -0
  29. package/dist/entries/auth/jwt.js +135 -0
  30. package/dist/entries/auth/jwtGuard.js +203 -0
  31. package/dist/entries/auth/sessionGuard.js +3 -21
  32. package/dist/entries/auth/tokenAbilityChecker.js +24 -0
  33. package/dist/entries/cache/tags.js +7 -1
  34. package/dist/entries/database/connectionContext.js +1 -0
  35. package/dist/entries/database/dialect.js +1 -0
  36. package/dist/entries/database/factory.js +5 -4
  37. package/dist/entries/database/model.js +189 -33
  38. package/dist/entries/database/mysqlConnection.js +35 -0
  39. package/dist/entries/database/namedConnections.js +1 -0
  40. package/dist/entries/database/query.js +28 -15
  41. package/dist/entries/database/relationships.js +43 -6
  42. package/dist/entries/database/repositoryQuery.js +142 -73
  43. package/dist/entries/database/schema.js +28 -15
  44. package/dist/entries/database/sqliteConnection.js +34 -0
  45. package/dist/entries/facades.js +1 -1
  46. package/dist/entries/http/contentNegotiation.js +5 -2
  47. package/dist/entries/http/csrfMiddleware.js +45 -0
  48. package/dist/entries/http/loginThrottleMiddleware.js +246 -7
  49. package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
  50. package/dist/entries/http/requireAbilityMiddleware.js +35 -11
  51. package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
  52. package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
  53. package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
  54. package/dist/entries/http/resources.js +4 -1
  55. package/dist/entries/http/response.js +46 -12
  56. package/dist/entries/http/statelessAuth.js +48 -0
  57. package/dist/entries/http/throttleMiddleware.js +208 -6
  58. package/dist/entries/http/webErrorResponse.js +35 -11
  59. package/dist/entries/http/webFormRequest.js +5 -2
  60. package/dist/entries/mail/mailer.js +1 -1
  61. package/dist/entries/openapi/generator.js +48 -5
  62. package/dist/entries/runtime/frontendMode.js +39 -10
  63. package/dist/framework/public-api.d.ts +11 -1
  64. package/dist/index.js +1068 -250
  65. package/package.json +56 -5
@@ -70,21 +70,42 @@ async function withDatabaseErrorHandling(operation) {
70
70
  import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
71
71
 
72
72
  // ../../src/core/runtime/frontendMode.ts
73
- function readFrontendMode() {
74
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
75
- if (mode === "server-htmx") {
76
- return "server-htmx";
77
- }
78
- if (mode === "spa-react") {
79
- return "spa-react";
73
+ var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
74
+ var DEFAULT_SPA_PREFIX = "/app";
75
+ var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
76
+ function parseFrontendMode(value) {
77
+ const mode = (value ?? "api").trim();
78
+ if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
79
+ return mode;
80
80
  }
81
81
  return "api";
82
82
  }
83
+ function readFrontendMode() {
84
+ return parseFrontendMode(process.env.FRONTEND_MODE);
85
+ }
86
+ function isViewsMode(mode) {
87
+ return mode === "server-htmx" || mode === "hybrid";
88
+ }
89
+ function isSpaMode(mode) {
90
+ return mode === "spa-react" || mode === "hybrid";
91
+ }
83
92
  function isViewsEnabled() {
84
- return readFrontendMode() === "server-htmx";
93
+ return isViewsMode(readFrontendMode());
85
94
  }
86
95
  function isSpaEnabled() {
87
- return readFrontendMode() === "spa-react";
96
+ return isSpaMode(readFrontendMode());
97
+ }
98
+ function normalizeSpaPrefix(value) {
99
+ const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
100
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
101
+ const trimmed = withSlash.replace(/\/+$/, "");
102
+ if (trimmed.length === 0 || trimmed === "/") {
103
+ return DEFAULT_SPA_PREFIX;
104
+ }
105
+ return trimmed;
106
+ }
107
+ function readSpaPrefix() {
108
+ return normalizeSpaPrefix(process.env.SPA_PREFIX);
88
109
  }
89
110
 
90
111
  // ../../src/core/view/webErrorView.ts
@@ -217,6 +238,10 @@ function requestPrefersJson(request) {
217
238
  if (request.headers.get("HX-Request") === "true") {
218
239
  return false;
219
240
  }
241
+ const pathname = new URL(request.url).pathname;
242
+ if (pathname.startsWith("/api/")) {
243
+ return true;
244
+ }
220
245
  const accept = request.headers.get("accept")?.toLowerCase() ?? "";
221
246
  if (accept.includes("text/html")) {
222
247
  return false;
@@ -228,8 +253,7 @@ function requestPrefersJson(request) {
228
253
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
229
254
  return false;
230
255
  }
231
- const pathname = new URL(request.url).pathname;
232
- return pathname.startsWith("/api/");
256
+ return false;
233
257
  }
234
258
 
235
259
  // ../../src/core/http/safeInternalPath.ts
@@ -350,10 +374,20 @@ function withErrorHandling(handler) {
350
374
  }
351
375
  };
352
376
  }
377
+ function withJsonErrorHandling(handler) {
378
+ return async (...args) => {
379
+ try {
380
+ return await handler(...args);
381
+ } catch (error) {
382
+ return errorResponse(error);
383
+ }
384
+ };
385
+ }
353
386
  export {
354
387
  createdResponse,
355
388
  errorResponse,
356
389
  jsonResponse,
357
390
  noContentResponse,
358
- withErrorHandling
391
+ withErrorHandling,
392
+ withJsonErrorHandling
359
393
  };
@@ -0,0 +1,48 @@
1
+ // @bun
2
+ // ../../src/core/http/statelessAuth.ts
3
+ function authorizationScheme(request) {
4
+ const header = request.headers.get("authorization")?.trim() ?? "";
5
+ const scheme = header.split(/\s+/, 1)[0];
6
+ return scheme ? scheme.toLowerCase() : "";
7
+ }
8
+ function requestUsesHeaderCredentials(request) {
9
+ const scheme = authorizationScheme(request);
10
+ return scheme === "bearer" || scheme === "basic";
11
+ }
12
+ function readBearerToken(request) {
13
+ const header = request.headers.get("authorization")?.trim() ?? "";
14
+ if (!header.toLowerCase().startsWith("bearer ")) {
15
+ return null;
16
+ }
17
+ const token = header.slice("Bearer ".length).trim();
18
+ return token.length > 0 ? token : null;
19
+ }
20
+ function readBasicCredentials(request) {
21
+ const header = request.headers.get("authorization")?.trim() ?? "";
22
+ if (!header.toLowerCase().startsWith("basic ")) {
23
+ return null;
24
+ }
25
+ const encoded = header.slice("Basic ".length).trim();
26
+ if (!encoded) {
27
+ return null;
28
+ }
29
+ try {
30
+ const decoded = Buffer.from(encoded, "base64").toString("utf8");
31
+ const separator = decoded.indexOf(":");
32
+ if (separator < 0) {
33
+ return null;
34
+ }
35
+ return {
36
+ username: decoded.slice(0, separator),
37
+ password: decoded.slice(separator + 1)
38
+ };
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ export {
44
+ authorizationScheme,
45
+ readBasicCredentials,
46
+ readBearerToken,
47
+ requestUsesHeaderCredentials
48
+ };
@@ -73,6 +73,213 @@ function readClientIp(request, env = process.env) {
73
73
  return request.headers.get("x-real-ip")?.trim() || undefined;
74
74
  }
75
75
 
76
+ // ../../src/core/runtime/frontendMode.ts
77
+ var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
78
+ var DEFAULT_SPA_PREFIX = "/app";
79
+ var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
80
+ function parseFrontendMode(value) {
81
+ const mode = (value ?? "api").trim();
82
+ if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
83
+ return mode;
84
+ }
85
+ return "api";
86
+ }
87
+ function readFrontendMode() {
88
+ return parseFrontendMode(process.env.FRONTEND_MODE);
89
+ }
90
+ function isViewsMode(mode) {
91
+ return mode === "server-htmx" || mode === "hybrid";
92
+ }
93
+ function isSpaMode(mode) {
94
+ return mode === "spa-react" || mode === "hybrid";
95
+ }
96
+ function isViewsEnabled() {
97
+ return isViewsMode(readFrontendMode());
98
+ }
99
+ function isSpaEnabled() {
100
+ return isSpaMode(readFrontendMode());
101
+ }
102
+ function normalizeSpaPrefix(value) {
103
+ const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
104
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
105
+ const trimmed = withSlash.replace(/\/+$/, "");
106
+ if (trimmed.length === 0 || trimmed === "/") {
107
+ return DEFAULT_SPA_PREFIX;
108
+ }
109
+ return trimmed;
110
+ }
111
+ function readSpaPrefix() {
112
+ return normalizeSpaPrefix(process.env.SPA_PREFIX);
113
+ }
114
+
115
+ // ../../src/core/view/webErrorView.ts
116
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
117
+
118
+ // ../../src/core/view/htmlResponse.ts
119
+ function withCharset(contentType) {
120
+ return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
121
+ }
122
+ function htmlResponse(html, init = {}) {
123
+ return new Response(html, {
124
+ status: init.status ?? 200,
125
+ statusText: init.statusText,
126
+ headers: {
127
+ "Content-Type": "text/html; charset=utf-8"
128
+ }
129
+ });
130
+ }
131
+ function isHtmxRequest(request) {
132
+ return request.headers.get("HX-Request") === "true";
133
+ }
134
+ function redirectResponse(location, status = 302) {
135
+ return new Response(null, {
136
+ status,
137
+ headers: {
138
+ Location: location
139
+ }
140
+ });
141
+ }
142
+ function textResponse(body, init = {}) {
143
+ return new Response(body, {
144
+ status: init.status ?? 200,
145
+ headers: {
146
+ "Content-Type": "text/plain; charset=utf-8"
147
+ }
148
+ });
149
+ }
150
+ function xmlResponse(body, init = {}) {
151
+ return new Response(body, {
152
+ status: init.status ?? 200,
153
+ headers: {
154
+ "Content-Type": withCharset(init.contentType ?? "application/xml")
155
+ }
156
+ });
157
+ }
158
+ function rssResponse(body, init = {}) {
159
+ return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
160
+ }
161
+
162
+ // ../../src/core/view/webErrorView.ts
163
+ var configuredErrorView = {};
164
+ function configureWebErrorView(options) {
165
+ configuredErrorView = { ...options };
166
+ }
167
+ function escapeHtml(value) {
168
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
169
+ }
170
+ function renderKernelErrorChrome(input) {
171
+ const title = escapeHtml(input.title);
172
+ const message = escapeHtml(input.message);
173
+ const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
174
+ const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
175
+ const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
176
+ return `<!doctype html>
177
+ <html lang="en">
178
+ <head>
179
+ <meta charset="UTF-8" />
180
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
181
+ <title>${title}</title>
182
+ <link rel="stylesheet" href="/assets/app.css" />
183
+ </head>
184
+ <body>
185
+ <header class="site-header">
186
+ <a class="brand" href="/">Home</a>
187
+ </header>
188
+ <main class="site-main">
189
+ <section class="page-header">
190
+ <h1>${title}</h1>
191
+ <p>${message}</p>
192
+ ${details}
193
+ ${goBack}
194
+ </section>
195
+ </main>
196
+ </body>
197
+ </html>
198
+ `;
199
+ }
200
+ function errorTemplateName(status) {
201
+ if (status === 404) {
202
+ return "errors/not-found";
203
+ }
204
+ if (status === 403) {
205
+ return "errors/forbidden";
206
+ }
207
+ return "errors/error";
208
+ }
209
+ async function renderWebErrorHtml(input) {
210
+ const render = configuredErrorView.render;
211
+ if (!render) {
212
+ return renderKernelErrorChrome(input);
213
+ }
214
+ try {
215
+ return await render({
216
+ ...input,
217
+ request: input.request ?? currentRequestMeta().request
218
+ });
219
+ } catch {
220
+ return renderKernelErrorChrome(input);
221
+ }
222
+ }
223
+ async function htmlErrorResponse(input) {
224
+ return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
225
+ }
226
+ async function notFoundHtmlResponse(body) {
227
+ if (body !== undefined) {
228
+ return htmlResponse(body, { status: 404 });
229
+ }
230
+ return htmlErrorResponse({
231
+ status: 404,
232
+ title: "Not Found",
233
+ message: "The page you requested was not found."
234
+ });
235
+ }
236
+
237
+ // ../../src/core/http/contentNegotiation.ts
238
+ function requestPrefersJson(request) {
239
+ if (!request) {
240
+ return true;
241
+ }
242
+ if (request.headers.get("HX-Request") === "true") {
243
+ return false;
244
+ }
245
+ const pathname = new URL(request.url).pathname;
246
+ if (pathname.startsWith("/api/")) {
247
+ return true;
248
+ }
249
+ const accept = request.headers.get("accept")?.toLowerCase() ?? "";
250
+ if (accept.includes("text/html")) {
251
+ return false;
252
+ }
253
+ if (accept.includes("application/json")) {
254
+ return true;
255
+ }
256
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
257
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
258
+ return false;
259
+ }
260
+ return false;
261
+ }
262
+
263
+ // ../../src/core/http/throttleResponse.ts
264
+ async function tooManyRequestsResponse(request, message, decaySeconds) {
265
+ const retryAfter = { "retry-after": String(decaySeconds) };
266
+ if (requestPrefersJson(request) || !isViewsEnabled()) {
267
+ return Response.json({ error: message }, {
268
+ status: 429,
269
+ headers: retryAfter
270
+ });
271
+ }
272
+ const html = await htmlErrorResponse({
273
+ status: 429,
274
+ title: "Too Many Requests",
275
+ message,
276
+ request
277
+ });
278
+ const headers = new Headers(html.headers);
279
+ headers.set("retry-after", String(decaySeconds));
280
+ return new Response(html.body, { status: 429, headers });
281
+ }
282
+
76
283
  // ../../src/core/http/throttleMiddleware.ts
77
284
  function resolveThrottleIdentity(request) {
78
285
  const user = currentAuthUser();
@@ -97,12 +304,7 @@ function createThrottleMiddleware(options) {
97
304
  }
98
305
  const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
99
306
  if (attempts > maxAttempts) {
100
- return Response.json({ error: "Too many requests." }, {
101
- status: 429,
102
- headers: {
103
- "retry-after": String(options.decaySeconds)
104
- }
105
- });
307
+ return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
106
308
  }
107
309
  return await next();
108
310
  };
@@ -67,21 +67,42 @@ async function withDatabaseErrorHandling(operation) {
67
67
  }
68
68
 
69
69
  // ../../src/core/runtime/frontendMode.ts
70
- function readFrontendMode() {
71
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
72
- if (mode === "server-htmx") {
73
- return "server-htmx";
74
- }
75
- if (mode === "spa-react") {
76
- return "spa-react";
70
+ var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
71
+ var DEFAULT_SPA_PREFIX = "/app";
72
+ var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
73
+ function parseFrontendMode(value) {
74
+ const mode = (value ?? "api").trim();
75
+ if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
76
+ return mode;
77
77
  }
78
78
  return "api";
79
79
  }
80
+ function readFrontendMode() {
81
+ return parseFrontendMode(process.env.FRONTEND_MODE);
82
+ }
83
+ function isViewsMode(mode) {
84
+ return mode === "server-htmx" || mode === "hybrid";
85
+ }
86
+ function isSpaMode(mode) {
87
+ return mode === "spa-react" || mode === "hybrid";
88
+ }
80
89
  function isViewsEnabled() {
81
- return readFrontendMode() === "server-htmx";
90
+ return isViewsMode(readFrontendMode());
82
91
  }
83
92
  function isSpaEnabled() {
84
- return readFrontendMode() === "spa-react";
93
+ return isSpaMode(readFrontendMode());
94
+ }
95
+ function normalizeSpaPrefix(value) {
96
+ const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
97
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
98
+ const trimmed = withSlash.replace(/\/+$/, "");
99
+ if (trimmed.length === 0 || trimmed === "/") {
100
+ return DEFAULT_SPA_PREFIX;
101
+ }
102
+ return trimmed;
103
+ }
104
+ function readSpaPrefix() {
105
+ return normalizeSpaPrefix(process.env.SPA_PREFIX);
85
106
  }
86
107
 
87
108
  // ../../src/core/view/webErrorView.ts
@@ -214,6 +235,10 @@ function requestPrefersJson(request) {
214
235
  if (request.headers.get("HX-Request") === "true") {
215
236
  return false;
216
237
  }
238
+ const pathname = new URL(request.url).pathname;
239
+ if (pathname.startsWith("/api/")) {
240
+ return true;
241
+ }
217
242
  const accept = request.headers.get("accept")?.toLowerCase() ?? "";
218
243
  if (accept.includes("text/html")) {
219
244
  return false;
@@ -225,8 +250,7 @@ function requestPrefersJson(request) {
225
250
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
226
251
  return false;
227
252
  }
228
- const pathname = new URL(request.url).pathname;
229
- return pathname.startsWith("/api/");
253
+ return false;
230
254
  }
231
255
 
232
256
  // ../../src/core/http/safeInternalPath.ts
@@ -10,6 +10,10 @@ function requestPrefersJson(request) {
10
10
  if (request.headers.get("HX-Request") === "true") {
11
11
  return false;
12
12
  }
13
+ const pathname = new URL(request.url).pathname;
14
+ if (pathname.startsWith("/api/")) {
15
+ return true;
16
+ }
13
17
  const accept = request.headers.get("accept")?.toLowerCase() ?? "";
14
18
  if (accept.includes("text/html")) {
15
19
  return false;
@@ -21,8 +25,7 @@ function requestPrefersJson(request) {
21
25
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
22
26
  return false;
23
27
  }
24
- const pathname = new URL(request.url).pathname;
25
- return pathname.startsWith("/api/");
28
+ return false;
26
29
  }
27
30
 
28
31
  // ../../src/core/http/parseFormBody.ts
@@ -214,7 +214,7 @@ class LogMailDriver {
214
214
  to: message.to,
215
215
  subject: message.subject,
216
216
  body: message.body,
217
- ...message.html ? { html: message.html } : {}
217
+ ...message.html ? { htmlBytes: Buffer.byteLength(message.html, "utf8") } : {}
218
218
  }));
219
219
  }
220
220
  }
@@ -65,6 +65,12 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
65
65
  "GET /auth/tokens": "List API tokens",
66
66
  "POST /auth/tokens": "Create API token",
67
67
  "DELETE /auth/tokens/:id": "Revoke API token",
68
+ "POST /auth/token": "Mint a short-lived JWT with email and password",
69
+ "POST /login": "Login with email and password",
70
+ "GET /careers": "List public career postings",
71
+ "GET /careers/:id": "Show a public career posting",
72
+ "GET /integrations/ping": "Partner heartbeat (requires integrations:ping)",
73
+ "GET /audit-logs/export": "Download audit events as JSON or CEF",
68
74
  "GET /users/me/export": "GDPR export of user data",
69
75
  "GET /users/me/current-organization": "Current organization",
70
76
  "PUT /users/me/current-organization": "Switch current organization",
@@ -94,10 +100,47 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
94
100
  "POST /billing/webhooks/stripe": "Stripe webhook receiver (stub)",
95
101
  "GET /scim/v2/Users": "SCIM list users",
96
102
  "POST /scim/v2/Users": "SCIM create user",
97
- "GET /scim/v2/Groups": "SCIM list groups (organizations)",
98
103
  "GET /health": "Liveness probe",
99
104
  "GET /ready": "Readiness probe",
100
- "GET /metrics": "Prometheus metrics"
105
+ "GET /metrics": "Prometheus metrics",
106
+ "GET /api/user": "Current authenticated HiroApp user",
107
+ "POST /api/login": "Login with email and password",
108
+ "POST /api/auth/token": "Mint a short-lived JWT with email and password",
109
+ "POST /api/apply/login": "Candidate portal login (opaque token)",
110
+ "POST /api/apply/logout": "Revoke the candidate portal token",
111
+ "GET /api/apply/me": "Candidate portal current user",
112
+ "GET /api/apply/positions": "Published jobs for the candidate portal",
113
+ "GET /api/apply/applications": "Candidate portal applications",
114
+ "POST /api/apply/applications": "Apply from the candidate portal",
115
+ "GET /api/apply/interviews": "Candidate portal interviews",
116
+ "GET /api/apply/offers": "Candidate portal offers",
117
+ "PATCH /api/apply/profile": "Update candidate portal profile",
118
+ "GET /api/kiosk/scorecards": "List on-site kiosk scorecards",
119
+ "POST /api/kiosk/scorecards": "Store an on-site kiosk scorecard",
120
+ "POST /api/kiosk/sync": "Sync kiosk scorecards into Postgres",
121
+ "GET /api/integrations/ping": "Partner heartbeat (requires integrations:ping)",
122
+ "GET /api/audit-logs/export": "Download audit events as JSON or CEF",
123
+ "GET /api/careers": "List public career postings",
124
+ "GET /api/careers/:id": "Show a public career posting",
125
+ "POST /api/logout": "Log out the current cookie session",
126
+ "POST /api/auth/two-factor-challenge": "Complete staff two-factor login challenge",
127
+ "GET /api/users/me": "Current user profile",
128
+ "PATCH /api/users/me": "Update profile name and email",
129
+ "PUT /api/users/me/password": "Change password",
130
+ "GET /api/users/me/sessions": "List cookie browser sessions",
131
+ "DELETE /api/users/me/sessions/:id": "Revoke one cookie browser session",
132
+ "GET /api/auth/tokens": "List API tokens",
133
+ "POST /api/auth/tokens": "Create API token",
134
+ "DELETE /api/auth/tokens/:id": "Revoke API token",
135
+ "GET /api/applications": "List applications",
136
+ "POST /api/applications": "Submit an application",
137
+ "GET /api/positions": "List hiring positions",
138
+ "GET /api/departments": "List departments",
139
+ "GET /api/webhooks": "List hiring webhooks",
140
+ "POST /api/webhooks": "Create a hiring webhook",
141
+ "GET /api/audit-logs": "List hiring audit log entries",
142
+ "GET /api/billing/subscription": "Current tenant subscription",
143
+ "GET /scim/v2/Groups": "SCIM list groups (departments)"
101
144
  };
102
145
  function toOpenApiPath(path) {
103
146
  return path.replace(/:([A-Za-z_]+)/g, "{$1}");
@@ -114,7 +157,7 @@ function toRelativeApiPath(path) {
114
157
  }
115
158
  function requiresBearerAuth(path, method) {
116
159
  const relative = toRelativeApiPath(path);
117
- if (relative.startsWith("/auth/login") || relative.startsWith("/auth/two-factor-challenge") || relative.startsWith("/auth/register") || relative.startsWith("/auth/forgot-password") || relative.startsWith("/auth/reset-password") || relative.startsWith("/auth/email/verification-notification") || relative.startsWith("/auth/oauth")) {
160
+ if (relative.startsWith("/auth/login") || relative.startsWith("/auth/two-factor-challenge") || relative.startsWith("/auth/register") || relative.startsWith("/auth/forgot-password") || relative.startsWith("/auth/reset-password") || relative.startsWith("/auth/email/verification-notification") || relative.startsWith("/auth/oauth") || relative === "/auth/token" || relative === "/apply/login" || relative === "/login" || path === "/login" || path === "/api/login" || path === "/api/apply/login") {
118
161
  return false;
119
162
  }
120
163
  if (relative.startsWith("/scim/") || relative.startsWith("/billing/webhooks/") || path.startsWith("/scim/") || path.startsWith("/billing/webhooks/")) {
@@ -123,10 +166,10 @@ function requiresBearerAuth(path, method) {
123
166
  if (["/health", "/ready", "/metrics", "/"].includes(relative) || ["/health", "/ready", "/metrics", "/"].includes(path)) {
124
167
  return false;
125
168
  }
126
- if (method === "GET" && ["/organizations", "/projects", "/tasks", "/search"].some((prefix) => relative.startsWith(prefix) || path.startsWith(prefix))) {
169
+ if (method === "GET" && ["/organizations", "/projects", "/tasks", "/search", "/careers"].some((prefix) => relative.startsWith(prefix) || path.startsWith(prefix))) {
127
170
  return false;
128
171
  }
129
- return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
172
+ 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);
130
173
  }
131
174
  function generateOpenApiSpec(routes) {
132
175
  const paths = {};
@@ -1,23 +1,52 @@
1
1
  // @bun
2
2
  // ../../src/core/runtime/frontendMode.ts
3
- function readFrontendMode() {
4
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
5
- if (mode === "server-htmx") {
6
- return "server-htmx";
7
- }
8
- if (mode === "spa-react") {
9
- return "spa-react";
3
+ var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
4
+ var DEFAULT_SPA_PREFIX = "/app";
5
+ var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
6
+ function parseFrontendMode(value) {
7
+ const mode = (value ?? "api").trim();
8
+ if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
9
+ return mode;
10
10
  }
11
11
  return "api";
12
12
  }
13
+ function readFrontendMode() {
14
+ return parseFrontendMode(process.env.FRONTEND_MODE);
15
+ }
16
+ function isViewsMode(mode) {
17
+ return mode === "server-htmx" || mode === "hybrid";
18
+ }
19
+ function isSpaMode(mode) {
20
+ return mode === "spa-react" || mode === "hybrid";
21
+ }
13
22
  function isViewsEnabled() {
14
- return readFrontendMode() === "server-htmx";
23
+ return isViewsMode(readFrontendMode());
15
24
  }
16
25
  function isSpaEnabled() {
17
- return readFrontendMode() === "spa-react";
26
+ return isSpaMode(readFrontendMode());
27
+ }
28
+ function normalizeSpaPrefix(value) {
29
+ const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
30
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
31
+ const trimmed = withSlash.replace(/\/+$/, "");
32
+ if (trimmed.length === 0 || trimmed === "/") {
33
+ return DEFAULT_SPA_PREFIX;
34
+ }
35
+ return trimmed;
36
+ }
37
+ function readSpaPrefix() {
38
+ return normalizeSpaPrefix(process.env.SPA_PREFIX);
18
39
  }
19
40
  export {
41
+ DEFAULT_SPA_PREFIX,
42
+ FRONTEND_MODES,
43
+ FRONTEND_MODE_PATTERN,
20
44
  isSpaEnabled,
45
+ isSpaMode,
21
46
  isViewsEnabled,
22
- readFrontendMode
47
+ isViewsMode,
48
+ normalizeSpaPrefix,
49
+ parseFrontendMode,
50
+ readFrontendMode,
51
+ readSpaPrefix
23
52
  };