@webstir-io/webstir-backend 0.1.15 → 0.1.16

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 (123) hide show
  1. package/README.md +106 -79
  2. package/dist/add.d.ts +59 -0
  3. package/dist/add.js +626 -0
  4. package/dist/build/artifacts.d.ts +115 -1
  5. package/dist/build/artifacts.js +4 -4
  6. package/dist/build/entries.js +1 -1
  7. package/dist/build/pipeline.d.ts +33 -1
  8. package/dist/build/pipeline.js +307 -65
  9. package/dist/cache/diff.js +9 -8
  10. package/dist/cache/reporters.js +1 -1
  11. package/dist/deploy-cli.d.ts +2 -0
  12. package/dist/deploy-cli.js +86 -0
  13. package/dist/diagnostics/summary.js +2 -2
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +4 -0
  16. package/dist/manifest/pipeline.js +103 -32
  17. package/dist/provider.js +35 -17
  18. package/dist/runtime/bun.d.ts +51 -0
  19. package/dist/runtime/bun.js +499 -0
  20. package/dist/runtime/core.d.ts +141 -0
  21. package/dist/runtime/core.js +316 -0
  22. package/dist/runtime/deploy-backend.d.ts +20 -0
  23. package/dist/runtime/deploy-backend.js +175 -0
  24. package/dist/runtime/deploy-shared.d.ts +43 -0
  25. package/dist/runtime/deploy-shared.js +75 -0
  26. package/dist/runtime/deploy-static.d.ts +2 -0
  27. package/dist/runtime/deploy-static.js +161 -0
  28. package/dist/runtime/deploy.d.ts +3 -0
  29. package/dist/runtime/deploy.js +91 -0
  30. package/dist/runtime/forms.d.ts +73 -0
  31. package/dist/runtime/forms.js +236 -0
  32. package/dist/runtime/request-hooks.d.ts +47 -0
  33. package/dist/runtime/request-hooks.js +102 -0
  34. package/dist/runtime/session-metadata.d.ts +13 -0
  35. package/dist/runtime/session-metadata.js +98 -0
  36. package/dist/runtime/session-runtime.d.ts +28 -0
  37. package/dist/runtime/session-runtime.js +180 -0
  38. package/dist/runtime/session.d.ts +83 -0
  39. package/dist/runtime/session.js +396 -0
  40. package/dist/runtime/views.d.ts +74 -0
  41. package/dist/runtime/views.js +221 -0
  42. package/dist/scaffold/assets.js +25 -21
  43. package/dist/testing/context.js +1 -1
  44. package/dist/testing/index.d.ts +1 -1
  45. package/dist/testing/index.js +100 -56
  46. package/dist/utils/bun.d.ts +2 -0
  47. package/dist/utils/bun.js +13 -0
  48. package/dist/watch.d.ts +13 -1
  49. package/dist/watch.js +345 -97
  50. package/dist/workspace.d.ts +8 -0
  51. package/dist/workspace.js +44 -3
  52. package/package.json +49 -14
  53. package/scripts/publish.sh +2 -92
  54. package/scripts/smoke.mjs +282 -107
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/add.ts +964 -0
  57. package/src/build/artifacts.ts +49 -46
  58. package/src/build/entries.ts +12 -12
  59. package/src/build/pipeline.ts +779 -403
  60. package/src/cache/diff.ts +111 -105
  61. package/src/cache/reporters.ts +26 -26
  62. package/src/deploy-cli.ts +111 -0
  63. package/src/diagnostics/summary.ts +28 -22
  64. package/src/index.ts +11 -0
  65. package/src/manifest/pipeline.ts +328 -215
  66. package/src/provider.ts +115 -98
  67. package/src/runtime/bun.ts +793 -0
  68. package/src/runtime/core.ts +598 -0
  69. package/src/runtime/deploy-backend.ts +239 -0
  70. package/src/runtime/deploy-shared.ts +136 -0
  71. package/src/runtime/deploy-static.ts +191 -0
  72. package/src/runtime/deploy.ts +143 -0
  73. package/src/runtime/forms.ts +364 -0
  74. package/src/runtime/request-hooks.ts +165 -0
  75. package/src/runtime/session-metadata.ts +135 -0
  76. package/src/runtime/session-runtime.ts +267 -0
  77. package/src/runtime/session.ts +642 -0
  78. package/src/runtime/views.ts +385 -0
  79. package/src/scaffold/assets.ts +77 -73
  80. package/src/testing/context.js +8 -9
  81. package/src/testing/context.ts +9 -9
  82. package/src/testing/index.d.ts +14 -3
  83. package/src/testing/index.js +254 -175
  84. package/src/testing/index.ts +298 -195
  85. package/src/testing/types.d.ts +18 -19
  86. package/src/testing/types.ts +18 -18
  87. package/src/utils/bun.ts +26 -0
  88. package/src/watch.ts +503 -99
  89. package/src/workspace.ts +59 -3
  90. package/templates/backend/.env.example +15 -0
  91. package/templates/backend/auth/adapter.ts +335 -36
  92. package/templates/backend/db/connection.ts +190 -65
  93. package/templates/backend/db/migrate.ts +149 -43
  94. package/templates/backend/db/types.d.ts +1 -1
  95. package/templates/backend/env.ts +132 -20
  96. package/templates/backend/functions/hello/index.ts +1 -2
  97. package/templates/backend/index.ts +15 -508
  98. package/templates/backend/jobs/nightly/index.ts +1 -1
  99. package/templates/backend/jobs/runtime.ts +24 -11
  100. package/templates/backend/jobs/scheduler.ts +208 -46
  101. package/templates/backend/module.ts +227 -13
  102. package/templates/backend/observability/logger.ts +2 -12
  103. package/templates/backend/observability/metrics.ts +8 -5
  104. package/templates/backend/session/sqlite.ts +152 -0
  105. package/templates/backend/session/store.ts +45 -0
  106. package/templates/backend/tsconfig.json +1 -1
  107. package/tests/add.test.js +327 -0
  108. package/tests/authAdapter.test.js +315 -0
  109. package/tests/bundlerParity.test.js +217 -0
  110. package/tests/cacheReporter.test.js +10 -10
  111. package/tests/dbConnection.test.js +209 -0
  112. package/tests/deploy.test.js +357 -0
  113. package/tests/envLoader.test.js +271 -17
  114. package/tests/integration.test.js +2432 -3
  115. package/tests/jobsScheduler.test.js +253 -0
  116. package/tests/manifest.test.js +287 -12
  117. package/tests/migrationRunner.test.js +249 -0
  118. package/tests/sessionScaffoldStore.test.js +752 -0
  119. package/tests/sessionStore.test.js +490 -0
  120. package/tests/testing.test.js +252 -0
  121. package/tests/watch.test.js +192 -32
  122. package/tsconfig.json +3 -10
  123. package/templates/backend/server/fastify.ts +0 -288
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import { existsSync, readFileSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -6,6 +7,8 @@ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
6
7
 
7
8
  export interface AuthSecrets {
8
9
  jwtSecret?: string;
10
+ jwtPublicKey?: string;
11
+ jwksUrl?: string;
9
12
  jwtIssuer?: string;
10
13
  jwtAudience?: string;
11
14
  serviceTokens: string[];
@@ -26,6 +29,17 @@ export interface DatabaseConfig {
26
29
  migrationsTable: string;
27
30
  }
28
31
 
32
+ export interface HttpConfig {
33
+ bodyLimitBytes: number;
34
+ }
35
+
36
+ export interface SessionConfig {
37
+ secret: string;
38
+ cookieName: string;
39
+ secure: boolean;
40
+ maxAgeSeconds: number;
41
+ }
42
+
29
43
  export interface AppEnv {
30
44
  NODE_ENV: string;
31
45
  PORT: number;
@@ -34,10 +48,15 @@ export interface AppEnv {
34
48
  logging: LoggingConfig;
35
49
  metrics: MetricsConfig;
36
50
  database: DatabaseConfig;
51
+ http: HttpConfig;
52
+ sessions: SessionConfig;
37
53
  }
38
54
 
39
55
  const ENV_FILES = ['.env.local', '.env'];
56
+ const WORKSPACE_ROOT_PATTERN = /^(.*)[/\\](?:src|build)[/\\]backend(?:[/\\].*)?$/;
40
57
  const WORKSPACE_ROOT = resolveWorkspaceRoot();
58
+ const DEFAULT_REQUEST_BODY_MAX_BYTES = 1024 * 1024;
59
+ const GENERATED_SESSION_SECRET = crypto.randomBytes(32).toString('hex');
41
60
  let envLoaded = false;
42
61
 
43
62
  export function loadEnv(): AppEnv {
@@ -46,26 +65,40 @@ export function loadEnv(): AppEnv {
46
65
  envLoaded = true;
47
66
  }
48
67
 
49
- const NODE_ENV = process.env.NODE_ENV ?? 'development';
68
+ const NODE_ENV = readNodeEnv();
50
69
  const PORT = parsePort(process.env.PORT ?? '4000');
51
70
  const API_BASE_URL = requireEnv('API_BASE_URL', 'http://localhost:4000');
52
71
  const auth: AuthSecrets = {
53
72
  jwtSecret: process.env.AUTH_JWT_SECRET,
73
+ jwtPublicKey: resolveJwtPublicKey(),
74
+ jwksUrl: normalizeOptionalText(process.env.AUTH_JWKS_URL),
54
75
  jwtIssuer: process.env.AUTH_JWT_ISSUER,
55
76
  jwtAudience: process.env.AUTH_JWT_AUDIENCE,
56
- serviceTokens: parseList(process.env.AUTH_SERVICE_TOKENS)
77
+ serviceTokens: parseList(process.env.AUTH_SERVICE_TOKENS),
57
78
  };
58
79
  const logging: LoggingConfig = {
59
80
  level: parseLogLevel(process.env.LOG_LEVEL),
60
- serviceName: process.env.LOG_SERVICE_NAME ?? 'backend-template'
81
+ serviceName: process.env.LOG_SERVICE_NAME ?? 'backend-template',
61
82
  };
62
83
  const metrics: MetricsConfig = {
63
84
  enabled: parseBoolean(process.env.METRICS_ENABLED, true),
64
- windowSize: parsePositiveInt(process.env.METRICS_WINDOW, 200)
85
+ windowSize: parsePositiveInt(process.env.METRICS_WINDOW, 200),
65
86
  };
66
87
  const database: DatabaseConfig = {
67
88
  url: process.env.DATABASE_URL ?? 'file:./data/dev.sqlite',
68
- migrationsTable: process.env.DATABASE_MIGRATIONS_TABLE ?? '_webstir_migrations'
89
+ migrationsTable: process.env.DATABASE_MIGRATIONS_TABLE ?? '_webstir_migrations',
90
+ };
91
+ const http: HttpConfig = {
92
+ bodyLimitBytes: parsePositiveInt(
93
+ process.env.REQUEST_BODY_MAX_BYTES,
94
+ DEFAULT_REQUEST_BODY_MAX_BYTES,
95
+ ),
96
+ };
97
+ const sessions: SessionConfig = {
98
+ secret: resolveSessionSecret(NODE_ENV),
99
+ cookieName: process.env.SESSION_COOKIE_NAME ?? 'webstir_session',
100
+ secure: parseBoolean(process.env.SESSION_COOKIE_SECURE, NODE_ENV === 'production'),
101
+ maxAgeSeconds: parsePositiveInt(process.env.SESSION_MAX_AGE, 60 * 60 * 24),
69
102
  };
70
103
 
71
104
  return {
@@ -75,10 +108,53 @@ export function loadEnv(): AppEnv {
75
108
  auth,
76
109
  logging,
77
110
  metrics,
78
- database
111
+ database,
112
+ http,
113
+ sessions,
79
114
  };
80
115
  }
81
116
 
117
+ function resolveSessionSecret(nodeEnv: string): string {
118
+ const explicitSecret = normalizeOptionalText(process.env.SESSION_SECRET);
119
+ if (explicitSecret) {
120
+ return explicitSecret;
121
+ }
122
+
123
+ if (nodeEnv.trim().toLowerCase() === 'production') {
124
+ throw new Error('SESSION_SECRET is required when NODE_ENV=production.');
125
+ }
126
+
127
+ return process.env.AUTH_JWT_SECRET ?? GENERATED_SESSION_SECRET;
128
+ }
129
+
130
+ function readNodeEnv(): string {
131
+ const value = Reflect.get(process.env, 'NODE_ENV');
132
+ return typeof value === 'string' && value.length > 0 ? value : 'development';
133
+ }
134
+
135
+ function resolveJwtPublicKey(): string | undefined {
136
+ const inline = normalizeOptionalText(process.env.AUTH_JWT_PUBLIC_KEY);
137
+ if (inline) {
138
+ return inline;
139
+ }
140
+
141
+ const filePath = normalizeOptionalText(process.env.AUTH_JWT_PUBLIC_KEY_FILE);
142
+ if (!filePath) {
143
+ return undefined;
144
+ }
145
+
146
+ const resolvedPath = path.isAbsolute(filePath)
147
+ ? path.resolve(filePath)
148
+ : path.resolve(WORKSPACE_ROOT, filePath);
149
+ try {
150
+ return readFileSync(resolvedPath, 'utf8');
151
+ } catch (error) {
152
+ throw new Error(
153
+ `Failed to read AUTH_JWT_PUBLIC_KEY_FILE at ${resolvedPath}: ${(error as Error).message}`,
154
+ );
155
+ }
156
+ }
157
+
82
158
  function loadEnvFiles(): void {
83
159
  for (const file of ENV_FILES) {
84
160
  const full = path.resolve(WORKSPACE_ROOT, file);
@@ -100,7 +176,10 @@ function applyEnvFile(filePath: string): void {
100
176
  const key = line.slice(0, idx).trim();
101
177
  if (!key || process.env[key] !== undefined) continue;
102
178
  let value = line.slice(idx + 1).trim();
103
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
179
+ if (
180
+ (value.startsWith('"') && value.endsWith('"')) ||
181
+ (value.startsWith("'") && value.endsWith("'"))
182
+ ) {
104
183
  value = value.slice(1, -1);
105
184
  }
106
185
  process.env[key] = value;
@@ -131,9 +210,21 @@ function parseList(value: string | undefined): string[] {
131
210
  .filter((item) => item.length > 0);
132
211
  }
133
212
 
213
+ function normalizeOptionalText(value: string | undefined): string | undefined {
214
+ const normalized = value?.trim();
215
+ return normalized || undefined;
216
+ }
217
+
134
218
  function parseLogLevel(value: string | undefined): LogLevel {
135
219
  const normalized = (value ?? 'info').toLowerCase();
136
- if (normalized === 'trace' || normalized === 'debug' || normalized === 'info' || normalized === 'warn' || normalized === 'error' || normalized === 'fatal') {
220
+ if (
221
+ normalized === 'trace' ||
222
+ normalized === 'debug' ||
223
+ normalized === 'info' ||
224
+ normalized === 'warn' ||
225
+ normalized === 'error' ||
226
+ normalized === 'fatal'
227
+ ) {
137
228
  return normalized;
138
229
  }
139
230
  return 'info';
@@ -155,20 +246,41 @@ function parsePositiveInt(value: string | undefined, fallback: number): number {
155
246
  }
156
247
 
157
248
  export function resolveWorkspaceRoot(): string {
158
- if (process.env.WORKSPACE_ROOT) {
159
- return process.env.WORKSPACE_ROOT;
249
+ const envWorkspaceRoot = resolveEnvWorkspaceRoot(process.env);
250
+ if (envWorkspaceRoot) {
251
+ return path.resolve(envWorkspaceRoot);
252
+ }
253
+ const inferredRoot = inferWorkspaceRootFromImportMetaUrl(import.meta.url);
254
+ if (inferredRoot) {
255
+ return inferredRoot;
256
+ }
257
+ return path.resolve(process.cwd());
258
+ }
259
+
260
+ function resolveEnvWorkspaceRoot(env: NodeJS.ProcessEnv): string | undefined {
261
+ const workspaceRoot = env.WORKSPACE_ROOT?.trim();
262
+ if (workspaceRoot) {
263
+ return workspaceRoot;
160
264
  }
265
+
266
+ const webstirWorkspaceRoot = env.WEBSTIR_WORKSPACE_ROOT?.trim();
267
+ return webstirWorkspaceRoot || undefined;
268
+ }
269
+
270
+ function inferWorkspaceRootFromImportMetaUrl(importMetaUrl: string): string | undefined {
161
271
  try {
162
- const filePath = fileURLToPath(import.meta.url);
163
- const dir = path.dirname(filePath);
164
- if (dir.endsWith(`${path.sep}src${path.sep}backend`)) {
165
- return path.resolve(dir, '..', '..');
166
- }
167
- if (dir.endsWith(`${path.sep}build${path.sep}backend`)) {
168
- return path.resolve(dir, '..', '..');
169
- }
272
+ return inferWorkspaceRootFromFilePath(fileURLToPath(importMetaUrl));
170
273
  } catch {
171
- // ignore
274
+ return undefined;
172
275
  }
173
- return process.cwd();
276
+ }
277
+
278
+ function inferWorkspaceRootFromFilePath(filePath: string): string | undefined {
279
+ const normalizedFilePath = path.resolve(path.dirname(filePath));
280
+ const match = normalizedFilePath.match(WORKSPACE_ROOT_PATTERN);
281
+ if (!match) {
282
+ return undefined;
283
+ }
284
+
285
+ return match[1] || path.parse(normalizedFilePath).root;
174
286
  }
@@ -7,7 +7,7 @@ export async function run(): Promise<void> {
7
7
  console.info('[function:hello] ran at', new Date().toISOString());
8
8
  }
9
9
 
10
- // Execute when launched directly: `node build/backend/functions/hello/index.js`
10
+ // Execute when launched directly: `bun build/backend/functions/hello/index.js`
11
11
  const isMain = (() => {
12
12
  try {
13
13
  const argv1 = process.argv?.[1];
@@ -26,4 +26,3 @@ if (isMain) {
26
26
  process.exitCode = 1;
27
27
  });
28
28
  }
29
-