@getstrata/core 1.0.3 → 1.0.4
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 +9 -0
- package/dist/core/runtime/appEnv.d.ts +16 -2
- package/dist/core/runtime/appKeyPrefix.d.ts +8 -1
- package/dist/entries/audit/exportAuditLogs.js +83 -18
- package/dist/entries/audit/siemFormatter.js +32 -3
- package/dist/entries/auth/intendedUrlCookie.js +33 -4
- package/dist/entries/auth/jwt.js +36 -4
- package/dist/entries/auth/jwtGuard.js +36 -4
- package/dist/entries/auth/oauth/providers.js +32 -3
- package/dist/entries/auth/oauth/samlProvider.js +32 -3
- package/dist/entries/auth/passwordConfirmCookie.js +35 -6
- package/dist/entries/auth/sessionCookie.js +35 -6
- package/dist/entries/auth/sessionGuard.js +35 -6
- package/dist/entries/auth/tokenHash.js +33 -4
- package/dist/entries/cache/createCacheStore.js +32 -3
- package/dist/entries/facades.js +32 -3
- package/dist/entries/http/corsMiddleware.js +16 -1
- package/dist/entries/http/csrfMiddleware.js +34 -5
- package/dist/entries/http/csrfToken.js +34 -5
- package/dist/entries/http/flashMiddleware.js +33 -4
- package/dist/entries/http/flashSession.js +33 -4
- package/dist/entries/http/loginThrottleMiddleware.js +1 -373
- package/dist/entries/http/memoryThrottleMiddleware.js +32 -58
- package/dist/entries/http/response.js +16 -56
- package/dist/entries/http/scimThrottleMiddleware.js +32 -3
- package/dist/entries/http/securityHeadersMiddleware.js +32 -3
- package/dist/entries/http/signedUrl.js +33 -4
- package/dist/entries/http/throttleMiddleware.js +32 -58
- package/dist/entries/http/webErrorResponse.js +1 -401
- package/dist/entries/jobs/exportAuditLogsJob.js +83 -18
- package/dist/entries/lifecycle/gracefulShutdown.js +1 -50
- package/dist/entries/mail/mailer.js +32 -3
- package/dist/entries/openapi/generator.js +32 -3
- package/dist/entries/queue/createAppQueue.js +32 -3
- package/dist/entries/queue/publicQueue.js +32 -3
- package/dist/entries/queue/queueMetrics.js +32 -3
- package/dist/entries/queue/redisQueue.js +32 -3
- package/dist/entries/runtime/appEnv.js +18 -1
- package/dist/entries/runtime/appKeyPrefix.js +1 -70
- package/dist/entries/security/oauthState.js +33 -4
- package/dist/entries/security/safeFetch.js +32 -9
- package/dist/entries/security/safeUrl.js +1 -105
- package/dist/entries/security/totp.js +32 -3
- package/dist/entries/tenant/databaseTenantContext.js +48 -6
- package/dist/entries/tracing/tracingMiddleware.js +32 -3
- package/dist/entries/view.js +1 -778
- package/dist/framework/public-api.d.ts +27 -18
- package/dist/index.js +442 -53
- package/package.json +2 -2
|
@@ -1,373 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
3
|
-
var {RedisClient } = globalThis.Bun;
|
|
4
|
-
|
|
5
|
-
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
-
function appKeyPrefix() {
|
|
7
|
-
return process.env.APP_KEY_PREFIX?.trim() || "strata";
|
|
8
|
-
}
|
|
9
|
-
function appCookieName(kind) {
|
|
10
|
-
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
-
}
|
|
12
|
-
function appDevSecret(kind) {
|
|
13
|
-
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
-
}
|
|
15
|
-
function namespacedRedisKey(kind) {
|
|
16
|
-
return `${appKeyPrefix()}:${kind}`;
|
|
17
|
-
}
|
|
18
|
-
function smtpEhloHost() {
|
|
19
|
-
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
20
|
-
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
21
|
-
return safe || "strata.local";
|
|
22
|
-
}
|
|
23
|
-
function siemEventType() {
|
|
24
|
-
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
25
|
-
}
|
|
26
|
-
function appUserAgent() {
|
|
27
|
-
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
28
|
-
}
|
|
29
|
-
function otelServiceName() {
|
|
30
|
-
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
31
|
-
}
|
|
32
|
-
function webhookSignatureHeader() {
|
|
33
|
-
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
34
|
-
}
|
|
35
|
-
function appDisplayName() {
|
|
36
|
-
return process.env.APP_NAME?.trim() || "Strata";
|
|
37
|
-
}
|
|
38
|
-
function appEnv() {
|
|
39
|
-
return process.env.APP_ENV?.trim() || "local";
|
|
40
|
-
}
|
|
41
|
-
function appUrl() {
|
|
42
|
-
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
43
|
-
}
|
|
44
|
-
function apiPrefix() {
|
|
45
|
-
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
46
|
-
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
47
|
-
const trimmed = withSlash.replace(/\/+$/, "");
|
|
48
|
-
return trimmed || "/api/v1";
|
|
49
|
-
}
|
|
50
|
-
function sdkClientClassName() {
|
|
51
|
-
const override = process.env.APP_SDK_CLASS?.trim();
|
|
52
|
-
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
53
|
-
return override;
|
|
54
|
-
}
|
|
55
|
-
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
56
|
-
return fromName ? `${fromName}Client` : "AppClient";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ../../src/core/http/clientIp.ts
|
|
60
|
-
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
61
|
-
function trustForwardedFor(env = process.env) {
|
|
62
|
-
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
63
|
-
}
|
|
64
|
-
function isPrivateAddress(address) {
|
|
65
|
-
const ip = address.replace(/^::ffff:/i, "");
|
|
66
|
-
return ip === "::1" || ip === "localhost" || /^127\./.test(ip) || /^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip) || /^169\.254\./.test(ip) || /^f[cd][0-9a-f]{2}:/i.test(ip) || /^fe80:/i.test(ip);
|
|
67
|
-
}
|
|
68
|
-
function forwardedClientIp(request) {
|
|
69
|
-
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
70
|
-
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
71
|
-
const hop = hops[index];
|
|
72
|
-
if (hop && !isPrivateAddress(hop)) {
|
|
73
|
-
return hop;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (hops.length > 0) {
|
|
77
|
-
return hops[hops.length - 1];
|
|
78
|
-
}
|
|
79
|
-
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
80
|
-
}
|
|
81
|
-
function readClientIp(request, env = process.env) {
|
|
82
|
-
if (trustForwardedFor(env)) {
|
|
83
|
-
const forwarded = forwardedClientIp(request);
|
|
84
|
-
if (forwarded) {
|
|
85
|
-
return forwarded;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return currentRequestMeta().ipAddress ?? undefined;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ../../src/core/runtime/frontendMode.ts
|
|
92
|
-
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
93
|
-
var DEFAULT_SPA_PREFIX = "/app";
|
|
94
|
-
var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
|
|
95
|
-
function parseFrontendMode(value) {
|
|
96
|
-
const mode = (value ?? "api").trim();
|
|
97
|
-
if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
|
|
98
|
-
return mode;
|
|
99
|
-
}
|
|
100
|
-
return "api";
|
|
101
|
-
}
|
|
102
|
-
function readFrontendMode() {
|
|
103
|
-
return parseFrontendMode(process.env.FRONTEND_MODE);
|
|
104
|
-
}
|
|
105
|
-
function isViewsMode(mode) {
|
|
106
|
-
return mode === "server-htmx" || mode === "hybrid";
|
|
107
|
-
}
|
|
108
|
-
function isSpaMode(mode) {
|
|
109
|
-
return mode === "spa-react" || mode === "hybrid";
|
|
110
|
-
}
|
|
111
|
-
function isViewsEnabled() {
|
|
112
|
-
return isViewsMode(readFrontendMode());
|
|
113
|
-
}
|
|
114
|
-
function isSpaEnabled() {
|
|
115
|
-
return isSpaMode(readFrontendMode());
|
|
116
|
-
}
|
|
117
|
-
function normalizeSpaPrefix(value) {
|
|
118
|
-
const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
|
|
119
|
-
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
120
|
-
const trimmed = withSlash.replace(/\/+$/, "");
|
|
121
|
-
if (trimmed.length === 0 || trimmed === "/") {
|
|
122
|
-
return DEFAULT_SPA_PREFIX;
|
|
123
|
-
}
|
|
124
|
-
return trimmed;
|
|
125
|
-
}
|
|
126
|
-
function readSpaPrefix() {
|
|
127
|
-
return normalizeSpaPrefix(process.env.SPA_PREFIX);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ../../src/core/view/webErrorView.ts
|
|
131
|
-
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
132
|
-
|
|
133
|
-
// ../../src/core/view/htmlResponse.ts
|
|
134
|
-
function withCharset(contentType) {
|
|
135
|
-
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
136
|
-
}
|
|
137
|
-
function htmlResponse(html, init = {}) {
|
|
138
|
-
return new Response(html, {
|
|
139
|
-
status: init.status ?? 200,
|
|
140
|
-
statusText: init.statusText,
|
|
141
|
-
headers: {
|
|
142
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function isHtmxRequest(request) {
|
|
147
|
-
return request.headers.get("HX-Request") === "true";
|
|
148
|
-
}
|
|
149
|
-
function redirectResponse(location, status = 302) {
|
|
150
|
-
return new Response(null, {
|
|
151
|
-
status,
|
|
152
|
-
headers: {
|
|
153
|
-
Location: location
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
function textResponse(body, init = {}) {
|
|
158
|
-
return new Response(body, {
|
|
159
|
-
status: init.status ?? 200,
|
|
160
|
-
headers: {
|
|
161
|
-
"Content-Type": "text/plain; charset=utf-8"
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
function xmlResponse(body, init = {}) {
|
|
166
|
-
return new Response(body, {
|
|
167
|
-
status: init.status ?? 200,
|
|
168
|
-
headers: {
|
|
169
|
-
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
function rssResponse(body, init = {}) {
|
|
174
|
-
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ../../src/core/view/webErrorView.ts
|
|
178
|
-
var configuredErrorView = {};
|
|
179
|
-
function configureWebErrorView(options) {
|
|
180
|
-
configuredErrorView = { ...options };
|
|
181
|
-
}
|
|
182
|
-
function escapeHtml(value) {
|
|
183
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
184
|
-
}
|
|
185
|
-
function renderKernelErrorChrome(input) {
|
|
186
|
-
const title = escapeHtml(input.title);
|
|
187
|
-
const message = escapeHtml(input.message);
|
|
188
|
-
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
189
|
-
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
190
|
-
const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
|
|
191
|
-
return `<!doctype html>
|
|
192
|
-
<html lang="en">
|
|
193
|
-
<head>
|
|
194
|
-
<meta charset="UTF-8" />
|
|
195
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
196
|
-
<title>${title}</title>
|
|
197
|
-
<link rel="stylesheet" href="/assets/app.css" />
|
|
198
|
-
</head>
|
|
199
|
-
<body>
|
|
200
|
-
<header class="site-header">
|
|
201
|
-
<a class="brand" href="/">Home</a>
|
|
202
|
-
</header>
|
|
203
|
-
<main class="site-main">
|
|
204
|
-
<section class="page-header">
|
|
205
|
-
<h1>${title}</h1>
|
|
206
|
-
<p>${message}</p>
|
|
207
|
-
${details}
|
|
208
|
-
${goBack}
|
|
209
|
-
</section>
|
|
210
|
-
</main>
|
|
211
|
-
</body>
|
|
212
|
-
</html>
|
|
213
|
-
`;
|
|
214
|
-
}
|
|
215
|
-
function errorTemplateName(status) {
|
|
216
|
-
if (status === 404) {
|
|
217
|
-
return "errors/not-found";
|
|
218
|
-
}
|
|
219
|
-
if (status === 403) {
|
|
220
|
-
return "errors/forbidden";
|
|
221
|
-
}
|
|
222
|
-
return "errors/error";
|
|
223
|
-
}
|
|
224
|
-
async function renderWebErrorHtml(input) {
|
|
225
|
-
const render = configuredErrorView.render;
|
|
226
|
-
if (!render) {
|
|
227
|
-
return renderKernelErrorChrome(input);
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
return await render({
|
|
231
|
-
...input,
|
|
232
|
-
request: input.request ?? currentRequestMeta2().request
|
|
233
|
-
});
|
|
234
|
-
} catch {
|
|
235
|
-
return renderKernelErrorChrome(input);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function htmlErrorResponse(input) {
|
|
239
|
-
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
240
|
-
}
|
|
241
|
-
async function notFoundHtmlResponse(body) {
|
|
242
|
-
if (body !== undefined) {
|
|
243
|
-
return htmlResponse(body, { status: 404 });
|
|
244
|
-
}
|
|
245
|
-
return htmlErrorResponse({
|
|
246
|
-
status: 404,
|
|
247
|
-
title: "Not Found",
|
|
248
|
-
message: "The page you requested was not found."
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
253
|
-
function requestPrefersJson(request) {
|
|
254
|
-
if (!request) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
const pathname = new URL(request.url).pathname;
|
|
261
|
-
if (pathname.startsWith("/api/")) {
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
265
|
-
if (accept.includes("text/html")) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
if (accept.includes("application/json")) {
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
272
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ../../src/core/http/throttleResponse.ts
|
|
279
|
-
async function tooManyRequestsResponse(request, message, decaySeconds) {
|
|
280
|
-
const retryAfter = { "retry-after": String(decaySeconds) };
|
|
281
|
-
if (requestPrefersJson(request) || !isViewsEnabled()) {
|
|
282
|
-
return Response.json({ error: message }, {
|
|
283
|
-
status: 429,
|
|
284
|
-
headers: retryAfter
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
const html = await htmlErrorResponse({
|
|
288
|
-
status: 429,
|
|
289
|
-
title: "Too Many Requests",
|
|
290
|
-
message,
|
|
291
|
-
request
|
|
292
|
-
});
|
|
293
|
-
const headers = new Headers(html.headers);
|
|
294
|
-
headers.set("retry-after", String(decaySeconds));
|
|
295
|
-
return new Response(html.body, { status: 429, headers });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
299
|
-
var memoryLoginBuckets = new Map;
|
|
300
|
-
function resolveLoginIdentity(request) {
|
|
301
|
-
return readClientIp(request) ?? "unknown";
|
|
302
|
-
}
|
|
303
|
-
async function resolveLoginEmail(request) {
|
|
304
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
305
|
-
try {
|
|
306
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
307
|
-
const formData = await request.clone().formData();
|
|
308
|
-
const email = formData.get("email");
|
|
309
|
-
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
310
|
-
}
|
|
311
|
-
const payload = await request.clone().json();
|
|
312
|
-
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
313
|
-
} catch {
|
|
314
|
-
return "unknown";
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
function consumeMemoryAttempt(key, decaySeconds) {
|
|
318
|
-
const now = Date.now();
|
|
319
|
-
const existing = memoryLoginBuckets.get(key);
|
|
320
|
-
if (!existing || existing.resetAt <= now) {
|
|
321
|
-
memoryLoginBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
322
|
-
return 1;
|
|
323
|
-
}
|
|
324
|
-
existing.count += 1;
|
|
325
|
-
return existing.count;
|
|
326
|
-
}
|
|
327
|
-
function createMemoryLoginThrottleMiddleware(options) {
|
|
328
|
-
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
329
|
-
return async (request, next) => {
|
|
330
|
-
const identity = resolveLoginIdentity(request);
|
|
331
|
-
const email = await resolveLoginEmail(request);
|
|
332
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
333
|
-
const attempts = consumeMemoryAttempt(throttleKey, options.decaySeconds);
|
|
334
|
-
if (attempts > options.maxAttempts) {
|
|
335
|
-
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
336
|
-
}
|
|
337
|
-
return await next();
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
function createRedisLoginThrottleMiddleware(options) {
|
|
341
|
-
const client = new RedisClient(options.redisUrl);
|
|
342
|
-
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
343
|
-
return async (request, next) => {
|
|
344
|
-
const identity = resolveLoginIdentity(request);
|
|
345
|
-
const email = await resolveLoginEmail(request);
|
|
346
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
347
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
348
|
-
if (attempts === 1) {
|
|
349
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
350
|
-
}
|
|
351
|
-
if (attempts > options.maxAttempts) {
|
|
352
|
-
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
353
|
-
}
|
|
354
|
-
return await next();
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
function createLoginThrottleMiddleware(options) {
|
|
358
|
-
const redisUrl = options.redisUrl?.trim() ?? "";
|
|
359
|
-
if (redisUrl) {
|
|
360
|
-
return createRedisLoginThrottleMiddleware({ ...options, redisUrl });
|
|
361
|
-
}
|
|
362
|
-
return createMemoryLoginThrottleMiddleware(options);
|
|
363
|
-
}
|
|
364
|
-
function resetMemoryLoginThrottleForTests() {
|
|
365
|
-
memoryLoginBuckets.clear();
|
|
366
|
-
}
|
|
367
|
-
export {
|
|
368
|
-
createLoginThrottleMiddleware,
|
|
369
|
-
createMemoryLoginThrottleMiddleware,
|
|
370
|
-
resetMemoryLoginThrottleForTests,
|
|
371
|
-
resolveLoginEmail,
|
|
372
|
-
resolveLoginIdentity
|
|
373
|
-
};
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/runtime/appEnv.ts
|
|
3
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
4
|
+
function normalizeEnvValue(value) {
|
|
5
|
+
return (value ?? "").trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function isProductionEnv(env = process.env) {
|
|
8
|
+
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
9
|
+
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
10
|
+
if (appEnv === "production" || nodeEnv === "production") {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (appEnv === "") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
17
|
+
}
|
|
18
|
+
function envFlagEnabled(value) {
|
|
19
|
+
return value === "true";
|
|
20
|
+
}
|
|
21
|
+
|
|
2
22
|
// ../../src/core/runtime/appKeyPrefix.ts
|
|
3
23
|
function appKeyPrefix() {
|
|
4
24
|
return process.env.APP_KEY_PREFIX?.trim() || "strata";
|
|
@@ -9,6 +29,18 @@ function appCookieName(kind) {
|
|
|
9
29
|
function appDevSecret(kind) {
|
|
10
30
|
return `${appKeyPrefix()}-dev-${kind}`;
|
|
11
31
|
}
|
|
32
|
+
function requireConfiguredSecret(names, devKind, env = process.env) {
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const value = env[name]?.trim();
|
|
35
|
+
if (value) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (isProductionEnv(env)) {
|
|
40
|
+
throw new Error(`${names[0]} must be set outside development. Do not derive secrets from the app name.`);
|
|
41
|
+
}
|
|
42
|
+
return appDevSecret(devKind);
|
|
43
|
+
}
|
|
12
44
|
function namespacedRedisKey(kind) {
|
|
13
45
|
return `${appKeyPrefix()}:${kind}`;
|
|
14
46
|
}
|
|
@@ -26,9 +58,6 @@ function appUserAgent() {
|
|
|
26
58
|
function otelServiceName() {
|
|
27
59
|
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
28
60
|
}
|
|
29
|
-
function webhookSignatureHeader() {
|
|
30
|
-
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
31
|
-
}
|
|
32
61
|
function appDisplayName() {
|
|
33
62
|
return process.env.APP_NAME?.trim() || "Strata";
|
|
34
63
|
}
|
|
@@ -128,9 +157,6 @@ function readSpaPrefix() {
|
|
|
128
157
|
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
129
158
|
|
|
130
159
|
// ../../src/core/view/htmlResponse.ts
|
|
131
|
-
function withCharset(contentType) {
|
|
132
|
-
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
133
|
-
}
|
|
134
160
|
function htmlResponse(html, init = {}) {
|
|
135
161
|
return new Response(html, {
|
|
136
162
|
status: init.status ?? 200,
|
|
@@ -140,42 +166,9 @@ function htmlResponse(html, init = {}) {
|
|
|
140
166
|
}
|
|
141
167
|
});
|
|
142
168
|
}
|
|
143
|
-
function isHtmxRequest(request) {
|
|
144
|
-
return request.headers.get("HX-Request") === "true";
|
|
145
|
-
}
|
|
146
|
-
function redirectResponse(location, status = 302) {
|
|
147
|
-
return new Response(null, {
|
|
148
|
-
status,
|
|
149
|
-
headers: {
|
|
150
|
-
Location: location
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function textResponse(body, init = {}) {
|
|
155
|
-
return new Response(body, {
|
|
156
|
-
status: init.status ?? 200,
|
|
157
|
-
headers: {
|
|
158
|
-
"Content-Type": "text/plain; charset=utf-8"
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
function xmlResponse(body, init = {}) {
|
|
163
|
-
return new Response(body, {
|
|
164
|
-
status: init.status ?? 200,
|
|
165
|
-
headers: {
|
|
166
|
-
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
function rssResponse(body, init = {}) {
|
|
171
|
-
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
172
|
-
}
|
|
173
169
|
|
|
174
170
|
// ../../src/core/view/webErrorView.ts
|
|
175
171
|
var configuredErrorView = {};
|
|
176
|
-
function configureWebErrorView(options) {
|
|
177
|
-
configuredErrorView = { ...options };
|
|
178
|
-
}
|
|
179
172
|
function escapeHtml(value) {
|
|
180
173
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
181
174
|
}
|
|
@@ -209,15 +202,6 @@ function renderKernelErrorChrome(input) {
|
|
|
209
202
|
</html>
|
|
210
203
|
`;
|
|
211
204
|
}
|
|
212
|
-
function errorTemplateName(status) {
|
|
213
|
-
if (status === 404) {
|
|
214
|
-
return "errors/not-found";
|
|
215
|
-
}
|
|
216
|
-
if (status === 403) {
|
|
217
|
-
return "errors/forbidden";
|
|
218
|
-
}
|
|
219
|
-
return "errors/error";
|
|
220
|
-
}
|
|
221
205
|
async function renderWebErrorHtml(input) {
|
|
222
206
|
const render = configuredErrorView.render;
|
|
223
207
|
if (!render) {
|
|
@@ -235,16 +219,6 @@ async function renderWebErrorHtml(input) {
|
|
|
235
219
|
async function htmlErrorResponse(input) {
|
|
236
220
|
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
237
221
|
}
|
|
238
|
-
async function notFoundHtmlResponse(body) {
|
|
239
|
-
if (body !== undefined) {
|
|
240
|
-
return htmlResponse(body, { status: 404 });
|
|
241
|
-
}
|
|
242
|
-
return htmlErrorResponse({
|
|
243
|
-
status: 404,
|
|
244
|
-
title: "Not Found",
|
|
245
|
-
message: "The page you requested was not found."
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
222
|
|
|
249
223
|
// ../../src/core/http/contentNegotiation.ts
|
|
250
224
|
function requestPrefersJson(request) {
|
|
@@ -110,8 +110,23 @@ import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/er
|
|
|
110
110
|
import { appLogger } from "@getstrata/core/logging/logger";
|
|
111
111
|
|
|
112
112
|
// ../../src/core/runtime/appEnv.ts
|
|
113
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
114
|
+
function normalizeEnvValue(value) {
|
|
115
|
+
return (value ?? "").trim().toLowerCase();
|
|
116
|
+
}
|
|
113
117
|
function isProductionEnv(env = process.env) {
|
|
114
|
-
|
|
118
|
+
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
119
|
+
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
120
|
+
if (appEnv === "production" || nodeEnv === "production") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (appEnv === "") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
127
|
+
}
|
|
128
|
+
function envFlagEnabled(value) {
|
|
129
|
+
return value === "true";
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
// ../../src/core/runtime/frontendMode.ts
|
|
@@ -157,9 +172,6 @@ function readSpaPrefix() {
|
|
|
157
172
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
158
173
|
|
|
159
174
|
// ../../src/core/view/htmlResponse.ts
|
|
160
|
-
function withCharset(contentType) {
|
|
161
|
-
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
162
|
-
}
|
|
163
175
|
function htmlResponse(html, init = {}) {
|
|
164
176
|
return new Response(html, {
|
|
165
177
|
status: init.status ?? 200,
|
|
@@ -169,42 +181,9 @@ function htmlResponse(html, init = {}) {
|
|
|
169
181
|
}
|
|
170
182
|
});
|
|
171
183
|
}
|
|
172
|
-
function isHtmxRequest(request) {
|
|
173
|
-
return request.headers.get("HX-Request") === "true";
|
|
174
|
-
}
|
|
175
|
-
function redirectResponse(location, status = 302) {
|
|
176
|
-
return new Response(null, {
|
|
177
|
-
status,
|
|
178
|
-
headers: {
|
|
179
|
-
Location: location
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
function textResponse(body, init = {}) {
|
|
184
|
-
return new Response(body, {
|
|
185
|
-
status: init.status ?? 200,
|
|
186
|
-
headers: {
|
|
187
|
-
"Content-Type": "text/plain; charset=utf-8"
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
function xmlResponse(body, init = {}) {
|
|
192
|
-
return new Response(body, {
|
|
193
|
-
status: init.status ?? 200,
|
|
194
|
-
headers: {
|
|
195
|
-
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
function rssResponse(body, init = {}) {
|
|
200
|
-
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
201
|
-
}
|
|
202
184
|
|
|
203
185
|
// ../../src/core/view/webErrorView.ts
|
|
204
186
|
var configuredErrorView = {};
|
|
205
|
-
function configureWebErrorView(options) {
|
|
206
|
-
configuredErrorView = { ...options };
|
|
207
|
-
}
|
|
208
187
|
function escapeHtml(value) {
|
|
209
188
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
210
189
|
}
|
|
@@ -238,15 +217,6 @@ function renderKernelErrorChrome(input) {
|
|
|
238
217
|
</html>
|
|
239
218
|
`;
|
|
240
219
|
}
|
|
241
|
-
function errorTemplateName(status) {
|
|
242
|
-
if (status === 404) {
|
|
243
|
-
return "errors/not-found";
|
|
244
|
-
}
|
|
245
|
-
if (status === 403) {
|
|
246
|
-
return "errors/forbidden";
|
|
247
|
-
}
|
|
248
|
-
return "errors/error";
|
|
249
|
-
}
|
|
250
220
|
async function renderWebErrorHtml(input) {
|
|
251
221
|
const render = configuredErrorView.render;
|
|
252
222
|
if (!render) {
|
|
@@ -264,16 +234,6 @@ async function renderWebErrorHtml(input) {
|
|
|
264
234
|
async function htmlErrorResponse(input) {
|
|
265
235
|
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
266
236
|
}
|
|
267
|
-
async function notFoundHtmlResponse(body) {
|
|
268
|
-
if (body !== undefined) {
|
|
269
|
-
return htmlResponse(body, { status: 404 });
|
|
270
|
-
}
|
|
271
|
-
return htmlErrorResponse({
|
|
272
|
-
status: 404,
|
|
273
|
-
title: "Not Found",
|
|
274
|
-
message: "The page you requested was not found."
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
237
|
|
|
278
238
|
// ../../src/core/http/contentNegotiation.ts
|
|
279
239
|
function requestPrefersJson(request) {
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
3
3
|
var {RedisClient } = globalThis.Bun;
|
|
4
4
|
|
|
5
|
+
// ../../src/core/runtime/appEnv.ts
|
|
6
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
7
|
+
function normalizeEnvValue(value) {
|
|
8
|
+
return (value ?? "").trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
function isProductionEnv(env = process.env) {
|
|
11
|
+
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
12
|
+
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
13
|
+
if (appEnv === "production" || nodeEnv === "production") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (appEnv === "") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
20
|
+
}
|
|
21
|
+
function envFlagEnabled(value) {
|
|
22
|
+
return value === "true";
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
26
|
function appKeyPrefix() {
|
|
7
27
|
return process.env.APP_KEY_PREFIX?.trim() || "strata";
|
|
@@ -12,6 +32,18 @@ function appCookieName(kind) {
|
|
|
12
32
|
function appDevSecret(kind) {
|
|
13
33
|
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
34
|
}
|
|
35
|
+
function requireConfiguredSecret(names, devKind, env = process.env) {
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const value = env[name]?.trim();
|
|
38
|
+
if (value) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (isProductionEnv(env)) {
|
|
43
|
+
throw new Error(`${names[0]} must be set outside development. Do not derive secrets from the app name.`);
|
|
44
|
+
}
|
|
45
|
+
return appDevSecret(devKind);
|
|
46
|
+
}
|
|
15
47
|
function namespacedRedisKey(kind) {
|
|
16
48
|
return `${appKeyPrefix()}:${kind}`;
|
|
17
49
|
}
|
|
@@ -29,9 +61,6 @@ function appUserAgent() {
|
|
|
29
61
|
function otelServiceName() {
|
|
30
62
|
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
31
63
|
}
|
|
32
|
-
function webhookSignatureHeader() {
|
|
33
|
-
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
34
|
-
}
|
|
35
64
|
function appDisplayName() {
|
|
36
65
|
return process.env.APP_NAME?.trim() || "Strata";
|
|
37
66
|
}
|