@lastshotlabs/bunshot 0.0.20 → 0.0.25

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 (122) hide show
  1. package/README.md +3035 -1249
  2. package/dist/adapters/localStorage.d.ts +6 -0
  3. package/dist/adapters/localStorage.js +44 -0
  4. package/dist/adapters/memoryAuth.d.ts +7 -0
  5. package/dist/adapters/memoryAuth.js +144 -0
  6. package/dist/adapters/memoryStorage.d.ts +3 -0
  7. package/dist/adapters/memoryStorage.js +44 -0
  8. package/dist/adapters/mongoAuth.js +120 -0
  9. package/dist/adapters/s3Storage.d.ts +14 -0
  10. package/dist/adapters/s3Storage.js +126 -0
  11. package/dist/adapters/sqliteAuth.d.ts +7 -0
  12. package/dist/adapters/sqliteAuth.js +199 -0
  13. package/dist/app.d.ts +100 -3
  14. package/dist/app.js +248 -47
  15. package/dist/cli.js +118 -38
  16. package/dist/index.d.ts +49 -7
  17. package/dist/index.js +35 -5
  18. package/dist/lib/HttpError.d.ts +5 -0
  19. package/dist/lib/HttpError.js +7 -0
  20. package/dist/lib/appConfig.d.ts +44 -0
  21. package/dist/lib/appConfig.js +16 -0
  22. package/dist/lib/auditLog.d.ts +52 -0
  23. package/dist/lib/auditLog.js +201 -0
  24. package/dist/lib/authAdapter.d.ts +69 -0
  25. package/dist/lib/constants.d.ts +4 -0
  26. package/dist/lib/constants.js +4 -0
  27. package/dist/lib/context.d.ts +19 -1
  28. package/dist/lib/context.js +17 -3
  29. package/dist/lib/createRoute.d.ts +28 -2
  30. package/dist/lib/createRoute.js +54 -3
  31. package/dist/lib/deletionCancelToken.d.ts +12 -0
  32. package/dist/lib/deletionCancelToken.js +88 -0
  33. package/dist/lib/groups.d.ts +113 -0
  34. package/dist/lib/groups.js +133 -0
  35. package/dist/lib/idempotency.d.ts +22 -0
  36. package/dist/lib/idempotency.js +182 -0
  37. package/dist/lib/metrics.d.ts +14 -0
  38. package/dist/lib/metrics.js +158 -0
  39. package/dist/lib/pagination.d.ts +119 -0
  40. package/dist/lib/pagination.js +166 -0
  41. package/dist/lib/session.d.ts +4 -0
  42. package/dist/lib/session.js +56 -2
  43. package/dist/lib/signing.d.ts +52 -0
  44. package/dist/lib/signing.js +180 -0
  45. package/dist/lib/storageAdapter.d.ts +30 -0
  46. package/dist/lib/storageAdapter.js +1 -0
  47. package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
  48. package/dist/lib/stripUnreferencedSchemas.js +79 -0
  49. package/dist/lib/tenant.js +2 -2
  50. package/dist/lib/upload.d.ts +35 -0
  51. package/dist/lib/upload.js +87 -0
  52. package/dist/lib/validate.js +2 -2
  53. package/dist/lib/ws.d.ts +1 -0
  54. package/dist/lib/ws.js +21 -0
  55. package/dist/lib/wsHeartbeat.d.ts +12 -0
  56. package/dist/lib/wsHeartbeat.js +57 -0
  57. package/dist/lib/wsMessages.d.ts +40 -0
  58. package/dist/lib/wsMessages.js +330 -0
  59. package/dist/lib/wsPresence.d.ts +25 -0
  60. package/dist/lib/wsPresence.js +99 -0
  61. package/dist/middleware/auditLog.d.ts +22 -0
  62. package/dist/middleware/auditLog.js +39 -0
  63. package/dist/middleware/cacheResponse.js +5 -1
  64. package/dist/middleware/csrf.js +10 -0
  65. package/dist/middleware/identify.js +57 -9
  66. package/dist/middleware/metrics.d.ts +9 -0
  67. package/dist/middleware/metrics.js +26 -0
  68. package/dist/middleware/requestId.d.ts +3 -0
  69. package/dist/middleware/requestId.js +7 -0
  70. package/dist/middleware/requestLogger.d.ts +38 -0
  71. package/dist/middleware/requestLogger.js +68 -0
  72. package/dist/middleware/requestSigning.d.ts +20 -0
  73. package/dist/middleware/requestSigning.js +99 -0
  74. package/dist/middleware/requireMfaSetup.d.ts +16 -0
  75. package/dist/middleware/requireMfaSetup.js +36 -0
  76. package/dist/middleware/requireRole.d.ts +9 -3
  77. package/dist/middleware/requireRole.js +23 -36
  78. package/dist/middleware/upload.d.ts +5 -0
  79. package/dist/middleware/upload.js +27 -0
  80. package/dist/middleware/webhookAuth.d.ts +30 -0
  81. package/dist/middleware/webhookAuth.js +57 -0
  82. package/dist/models/AuditLog.d.ts +30 -0
  83. package/dist/models/AuditLog.js +39 -0
  84. package/dist/models/Group.d.ts +21 -0
  85. package/dist/models/Group.js +28 -0
  86. package/dist/models/GroupMembership.d.ts +21 -0
  87. package/dist/models/GroupMembership.js +25 -0
  88. package/dist/routes/auth.js +84 -6
  89. package/dist/routes/groups.d.ts +21 -0
  90. package/dist/routes/groups.js +346 -0
  91. package/dist/routes/jobs.js +47 -45
  92. package/dist/routes/metrics.d.ts +7 -0
  93. package/dist/routes/metrics.js +52 -0
  94. package/dist/routes/mfa.js +4 -0
  95. package/dist/routes/uploads.d.ts +2 -0
  96. package/dist/routes/uploads.js +135 -0
  97. package/dist/server.d.ts +26 -0
  98. package/dist/server.js +46 -3
  99. package/dist/ws/index.js +3 -0
  100. package/docs/sections/auth-flow/full.md +779 -634
  101. package/docs/sections/auth-flow/overview.md +2 -2
  102. package/docs/sections/auth-security-examples/full.md +365 -0
  103. package/docs/sections/authentication/full.md +130 -0
  104. package/docs/sections/authentication/overview.md +5 -0
  105. package/docs/sections/cli/full.md +13 -1
  106. package/docs/sections/configuration/full.md +17 -0
  107. package/docs/sections/configuration/overview.md +1 -0
  108. package/docs/sections/exports/full.md +34 -3
  109. package/docs/sections/logging/full.md +83 -0
  110. package/docs/sections/metrics/full.md +127 -0
  111. package/docs/sections/oauth/full.md +189 -189
  112. package/docs/sections/oauth/overview.md +1 -1
  113. package/docs/sections/pagination/full.md +93 -0
  114. package/docs/sections/roles/full.md +224 -135
  115. package/docs/sections/roles/overview.md +3 -1
  116. package/docs/sections/signing/full.md +203 -0
  117. package/docs/sections/uploads/full.md +199 -0
  118. package/docs/sections/versioning/full.md +85 -0
  119. package/docs/sections/webhook-auth/full.md +100 -0
  120. package/docs/sections/websocket/full.md +83 -0
  121. package/docs/sections/websocket-rooms/full.md +6 -1
  122. package/package.json +16 -4
package/dist/app.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { cors } from "hono/cors";
3
- import { logger } from "hono/logger";
4
3
  import { secureHeaders } from "hono/secure-headers";
5
4
  import { Scalar } from "@scalar/hono-api-reference";
6
- import { HttpError } from "./lib/HttpError";
5
+ import { HttpError, ValidationError } from "./lib/HttpError";
7
6
  import { rateLimit } from "./middleware/rateLimit";
8
7
  import { bearerAuth } from "./middleware/bearerAuth";
9
8
  import { identify } from "./middleware/identify";
10
- import { HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN, HEADER_CSRF_TOKEN } from "./lib/constants";
11
- import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setPasswordPolicy, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig, setCsrfEnabled } from "./lib/appConfig";
9
+ import { defaultValidationErrorFormatter } from "./lib/context";
10
+ import { HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN, HEADER_CSRF_TOKEN, HEADER_REQUEST_ID } from "./lib/constants";
11
+ import { requestId } from "./middleware/requestId";
12
+ import { requestLogger } from "./middleware/requestLogger";
13
+ import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setPasswordPolicy, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig, setCsrfEnabled, setSigningConfig } from "./lib/appConfig";
12
14
  import { setEmailVerificationStore } from "./lib/emailVerification";
13
15
  import { setPasswordResetStore } from "./lib/resetPassword";
14
16
  import { setAuthRateLimitStore } from "./lib/authRateLimit";
@@ -17,12 +19,12 @@ import { mongoAuthAdapter } from "./adapters/mongoAuth";
17
19
  import { memoryAuthAdapter } from "./adapters/memoryAuth";
18
20
  import { initOAuthProviders, getConfiguredOAuthProviders, setOAuthStateStore } from "./lib/oauth";
19
21
  import { setOAuthCodeStore } from "./lib/oauthCode";
20
- import { createOAuthRouter } from "./routes/oauth";
21
22
  import { connectMongo, connectAuthMongo, connectAppMongo } from "./lib/mongo";
22
23
  import { connectRedis } from "./lib/redis";
23
24
  import { setSessionStore } from "./lib/session";
24
25
  import { setCacheStore } from "./middleware/cacheResponse";
25
26
  import { maybeAutoRegister } from "./lib/createRoute";
27
+ import { setStorageAdapter, setUploadConfig } from "./lib/upload";
26
28
  export const createApp = async (config) => {
27
29
  const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
28
30
  const appName = appConfig.name ?? "Bun Core API";
@@ -137,6 +139,8 @@ export const createApp = async (config) => {
137
139
  setPasswordResetConfig(passwordReset ?? null);
138
140
  setPasswordPolicy(authConfig.passwordPolicy ?? {});
139
141
  setPasswordResetStore(sessions);
142
+ const { setDeletionCancelTokenStore } = await import("./lib/deletionCancelToken");
143
+ setDeletionCancelTokenStore(sessions);
140
144
  setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
141
145
  setMaxSessions(sessionPolicy.maxSessions ?? 6);
142
146
  setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
@@ -147,6 +151,31 @@ export const createApp = async (config) => {
147
151
  if (oauthProviders)
148
152
  initOAuthProviders(oauthProviders);
149
153
  const configuredOAuth = getConfiguredOAuthProviders();
154
+ // Start the account deletion worker when queued deletion is configured.
155
+ // The worker runs in-process alongside the API server.
156
+ if (authConfig.accountDeletion?.queued && enableAuthRoutes) {
157
+ try {
158
+ const { createWorker } = await import("./lib/queue");
159
+ const appName_ = appName;
160
+ const accountDeletion_ = authConfig.accountDeletion;
161
+ createWorker(`${appName_}:account-deletions`, async (job) => {
162
+ const { userId } = job.data;
163
+ const adapter_ = authAdapter;
164
+ if (accountDeletion_.onBeforeDelete)
165
+ await accountDeletion_.onBeforeDelete(userId);
166
+ if (adapter_.deleteUser)
167
+ await adapter_.deleteUser(userId);
168
+ if (accountDeletion_.onAfterDelete)
169
+ await accountDeletion_.onAfterDelete(userId);
170
+ }, { concurrency: 1 });
171
+ }
172
+ catch (err) {
173
+ if (err?.message?.includes("bullmq is not installed")) {
174
+ throw new Error("createApp: accountDeletion.queued requires BullMQ. Run: bun add bullmq");
175
+ }
176
+ throw err;
177
+ }
178
+ }
150
179
  // OAuth paths must bypass bearer auth — initiation and link routes are browser redirects,
151
180
  // callbacks come from external providers; none can send a bearer token header.
152
181
  const oauthBypass = configuredOAuth.flatMap((p) => [
@@ -154,10 +183,42 @@ export const createApp = async (config) => {
154
183
  `/auth/${p}/callback`,
155
184
  `/auth/${p}/link`,
156
185
  ]);
157
- const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/"];
158
- const bearerAuthBypass = [...DEFAULT_BYPASS, ...oauthBypass, ...extraBypass];
186
+ const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/", "/metrics"];
187
+ // Add per-version docs/spec paths when versioning is configured
188
+ const versionBypass = config.versioning
189
+ ? config.versioning.versions.flatMap((v) => [`/${v}/docs`, `/${v}/openapi.json`])
190
+ : [];
191
+ const bearerAuthBypass = [...DEFAULT_BYPASS, ...versionBypass, ...oauthBypass, ...extraBypass];
159
192
  const app = new OpenAPIHono();
160
- app.use(logger());
193
+ app.use(requestId);
194
+ // Set the validation error formatter on context so defaultHook and onError both pick it up
195
+ const validationFormatter = config.validation?.formatError ?? defaultValidationErrorFormatter;
196
+ app.use("*", async (c, next) => {
197
+ c.set("validationErrorFormatter", validationFormatter);
198
+ await next();
199
+ });
200
+ // Metrics collection middleware (before requestLogger so it captures all requests)
201
+ if (config.metrics?.enabled) {
202
+ if (!config.metrics.auth || config.metrics.auth === "none") {
203
+ if (process.env.NODE_ENV === "production") {
204
+ console.warn("[security] /metrics endpoint is enabled without auth. Configure metrics.auth to restrict access in production.");
205
+ }
206
+ }
207
+ const { metricsCollector } = await import("./middleware/metrics");
208
+ app.use(metricsCollector({
209
+ excludePaths: config.metrics.excludePaths,
210
+ normalizePath: config.metrics.normalizePath,
211
+ }));
212
+ }
213
+ const loggingConfig = config.logging ?? {};
214
+ if (loggingConfig.enabled !== false) {
215
+ app.use(requestLogger({
216
+ onLog: loggingConfig.onLog,
217
+ level: loggingConfig.level,
218
+ excludePaths: loggingConfig.excludePaths,
219
+ excludeMethods: loggingConfig.excludeMethods,
220
+ }));
221
+ }
161
222
  const headerOpts = {};
162
223
  if (securityConfig.headers?.contentSecurityPolicy) {
163
224
  headerOpts["Content-Security-Policy"] = securityConfig.headers.contentSecurityPolicy;
@@ -177,7 +238,7 @@ export const createApp = async (config) => {
177
238
  const corsAllowHeaders = ["Content-Type", "Authorization", HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN];
178
239
  if (securityConfig.csrf?.enabled)
179
240
  corsAllowHeaders.push(HEADER_CSRF_TOKEN);
180
- app.use(cors({ origin: corsOrigins, allowHeaders: corsAllowHeaders, exposeHeaders: ["x-cache"], credentials: true }));
241
+ app.use(cors({ origin: corsOrigins, allowHeaders: corsAllowHeaders, exposeHeaders: ["x-cache", HEADER_REQUEST_ID], credentials: true }));
181
242
  if ((botCfg.blockList?.length ?? 0) > 0) {
182
243
  const { botProtection } = await import("./middleware/botProtection");
183
244
  app.use(botProtection({ blockList: botCfg.blockList }));
@@ -193,6 +254,10 @@ export const createApp = async (config) => {
193
254
  });
194
255
  }
195
256
  app.use(identify);
257
+ // Signing config — make available to pagination, identify, and other lib modules
258
+ if (securityConfig.signing) {
259
+ setSigningConfig(securityConfig.signing);
260
+ }
196
261
  // CSRF protection (after identify so we can check for auth cookie presence)
197
262
  if (securityConfig.csrf?.enabled) {
198
263
  setCsrfEnabled(true);
@@ -225,6 +290,10 @@ export const createApp = async (config) => {
225
290
  }
226
291
  for (const mw of middleware)
227
292
  app.use(mw);
293
+ if (authConfig.mfa?.required) {
294
+ const { requireMfaSetup } = await import("./middleware/requireMfaSetup");
295
+ app.use(requireMfaSetup);
296
+ }
228
297
  setAppName(appName);
229
298
  // Schema pre-loading — import shared schema files before routes so registerSchema /
230
299
  // registerSchemas calls run first, guaranteeing $ref instead of inline shapes.
@@ -283,6 +352,7 @@ export const createApp = async (config) => {
283
352
  app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens }));
284
353
  }
285
354
  if (configuredOAuth.length > 0) {
355
+ const { createOAuthRouter } = await import(`${coreRoutesDir}/oauth`);
286
356
  app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
287
357
  }
288
358
  if (authConfig.mfa && enableAuthRoutes) {
@@ -299,49 +369,180 @@ export const createApp = async (config) => {
299
369
  const { createJobsRouter } = await import(`${coreRoutesDir}/jobs`);
300
370
  app.route("/", createJobsRouter(config.jobs));
301
371
  }
302
- // Service routes — collect all, sort by optional exported `priority`, then mount
303
- const serviceGlob = new Bun.Glob("**/*.ts");
304
- const serviceFiles = [];
305
- for await (const file of serviceGlob.scan({ cwd: routesDir })) {
306
- serviceFiles.push(file);
307
- }
308
- const serviceMods = await Promise.all(serviceFiles.map(async (file) => ({
309
- file,
310
- mod: await import(`${routesDir}/${file}`),
311
- })));
312
- serviceMods
313
- .sort((a, b) => (a.mod.priority ?? Infinity) - (b.mod.priority ?? Infinity))
314
- .forEach(({ mod }) => {
315
- if (mod.router)
316
- app.route("/", mod.router);
317
- });
372
+ if (config.metrics?.enabled) {
373
+ const { createMetricsRouter } = await import(`${coreRoutesDir}/metrics`);
374
+ app.route("/", createMetricsRouter({
375
+ auth: config.metrics.auth,
376
+ queues: config.metrics.queues,
377
+ }));
378
+ }
379
+ if (config.groups?.managementRoutes) {
380
+ const { createGroupsRouter } = await import(`${coreRoutesDir}/groups`);
381
+ app.route("/", createGroupsRouter(config.groups));
382
+ }
383
+ if (config.upload) {
384
+ const { storage, presignedUrls, ...uploadOpts } = config.upload;
385
+ setStorageAdapter(storage);
386
+ setUploadConfig(uploadOpts);
387
+ if (presignedUrls) {
388
+ const { createUploadsRouter } = await import(`${coreRoutesDir}/uploads`);
389
+ const presignConfig = presignedUrls === true ? {} : presignedUrls;
390
+ app.route("/", createUploadsRouter(presignConfig));
391
+ }
392
+ }
393
+ // Helper to register standard security schemes on an OpenAPI registry
394
+ const registerSecuritySchemes = (registry) => {
395
+ registry.registerComponent("securitySchemes", "cookieAuth", {
396
+ type: "apiKey",
397
+ in: "cookie",
398
+ name: "token",
399
+ description: "Session cookie set automatically on login/register.",
400
+ });
401
+ registry.registerComponent("securitySchemes", "userToken", {
402
+ type: "apiKey",
403
+ in: "header",
404
+ name: "x-user-token",
405
+ description: "JWT session token passed as the x-user-token request header (alternative to the session cookie).",
406
+ });
407
+ registry.registerComponent("securitySchemes", "bearerAuth", {
408
+ type: "http",
409
+ scheme: "bearer",
410
+ description: "API key passed as Authorization: Bearer <token>. Required on all endpoints unless bearer auth is disabled in CreateAppConfig or the path is in the bypass list.",
411
+ });
412
+ };
413
+ if (config.versioning) {
414
+ // Version-aware route discovery — each version gets its own OpenAPIHono instance
415
+ const { versions, sharedDir = "shared", defaultVersion = versions[versions.length - 1] } = config.versioning;
416
+ const { setVersionPrefix, clearVersionPrefix, getVersionToken, drainCapturedTokens, assertCapturedTokens } = await import("./lib/createRoute");
417
+ const { defaultHook } = await import("./lib/context");
418
+ const { stripUnreferencedSchemas } = await import("./lib/stripUnreferencedSchemas");
419
+ // Import shared routes with no prefix — schemas stay unprefixed (version-agnostic)
420
+ let sharedMods = [];
421
+ if (sharedDir !== false) {
422
+ const sharedRoutesDir = `${routesDir}/${sharedDir}`;
423
+ try {
424
+ const sharedGlob = new Bun.Glob("**/*.ts");
425
+ const sharedFiles = [];
426
+ for await (const file of sharedGlob.scan({ cwd: sharedRoutesDir })) {
427
+ sharedFiles.push(file);
428
+ }
429
+ sharedMods = await Promise.all(sharedFiles.map(async (file) => ({ file, mod: await import(`${sharedRoutesDir}/${file}`) })));
430
+ }
431
+ catch {
432
+ // sharedDir doesn't exist — fine
433
+ }
434
+ }
435
+ // Drain any tokens captured during shared route imports (token=null, correct since no prefix was set)
436
+ // to prevent null tokens from bleeding into per-version assertions below.
437
+ drainCapturedTokens();
438
+ // For each version sequentially: set prefix, import routes, mount on isolated OpenAPIHono
439
+ for (const version of versions) {
440
+ setVersionPrefix(version);
441
+ const expectedToken = getVersionToken();
442
+ const vApp = new OpenAPIHono({ defaultHook });
443
+ const versionRoutesDir = `${routesDir}/${version}`;
444
+ const versionFiles = [];
445
+ try {
446
+ const versionGlob = new Bun.Glob("**/*.ts");
447
+ for await (const file of versionGlob.scan({ cwd: versionRoutesDir })) {
448
+ versionFiles.push(file);
449
+ }
450
+ }
451
+ catch {
452
+ // version dir doesn't exist — fine
453
+ }
454
+ // Import all version route files in parallel
455
+ const versionMods = await Promise.all(versionFiles.map(async (file) => ({ file, mod: await import(`${versionRoutesDir}/${file}`) })));
456
+ // Assert version token to catch top-level await interleaving bugs at startup
457
+ assertCapturedTokens(drainCapturedTokens(), expectedToken);
458
+ // Mount version-specific routes (sorted by priority)
459
+ versionMods
460
+ .sort((a, b) => (a.mod.priority ?? Infinity) - (b.mod.priority ?? Infinity))
461
+ .forEach(({ mod }) => {
462
+ if (mod.router)
463
+ vApp.route("/", mod.router);
464
+ });
465
+ // Mount shared routes on this versioned app
466
+ for (const { mod } of sharedMods) {
467
+ if (mod.router)
468
+ vApp.route("/", mod.router);
469
+ }
470
+ registerSecuritySchemes(vApp.openAPIRegistry);
471
+ // Serve per-version spec stripped of schemas from other versions
472
+ vApp.get("/openapi.json", (c) => {
473
+ const spec = vApp.getOpenAPIDocument({
474
+ openapi: "3.0.0",
475
+ info: { title: `${appName} ${version.toUpperCase()}`, version: openApiVersion },
476
+ });
477
+ return c.json(stripUnreferencedSchemas(spec));
478
+ });
479
+ // Per-version Scalar docs
480
+ vApp.get("/docs", Scalar({ url: `/${version}/openapi.json` }));
481
+ clearVersionPrefix();
482
+ // Mount versioned app under /v1, /v2, etc.
483
+ app.route(`/${version}`, vApp);
484
+ }
485
+ // Root /docs → version selector page
486
+ app.get("/docs", (c) => {
487
+ const links = versions
488
+ .map((v) => `<li><a href="/${v}/docs" style="font-size:1.1em">${v.toUpperCase()}</a></li>`)
489
+ .join("\n");
490
+ const html = `<!DOCTYPE html>
491
+ <html lang="en">
492
+ <head><meta charset="utf-8"><title>${appName} — API Docs</title>
493
+ <style>body{font-family:sans-serif;padding:2rem}ul{list-style:none;padding:0}li{margin:.5rem 0}</style>
494
+ </head>
495
+ <body>
496
+ <h1>${appName}</h1>
497
+ <h2>API Documentation</h2>
498
+ <ul>${links}</ul>
499
+ </body></html>`;
500
+ return c.html(html);
501
+ });
502
+ // Root /openapi.json → 302 to default version (no merged spec exists)
503
+ app.get("/openapi.json", (c) => c.redirect(`/${defaultVersion}/openapi.json`, 302));
504
+ }
505
+ else {
506
+ // Non-versioned path — existing behavior unchanged
507
+ // Service routes — collect all, sort by optional exported `priority`, then mount
508
+ const serviceGlob = new Bun.Glob("**/*.ts");
509
+ const serviceFiles = [];
510
+ for await (const file of serviceGlob.scan({ cwd: routesDir })) {
511
+ serviceFiles.push(file);
512
+ }
513
+ const serviceMods = await Promise.all(serviceFiles.map(async (file) => ({
514
+ file,
515
+ mod: await import(`${routesDir}/${file}`),
516
+ })));
517
+ serviceMods
518
+ .sort((a, b) => (a.mod.priority ?? Infinity) - (b.mod.priority ?? Infinity))
519
+ .forEach(({ mod }) => {
520
+ if (mod.router)
521
+ app.route("/", mod.router);
522
+ });
523
+ registerSecuritySchemes(app.openAPIRegistry);
524
+ app.doc("/openapi.json", { openapi: "3.0.0", info: { title: appName, version: openApiVersion } });
525
+ app.get("/docs", Scalar({ url: "/openapi.json" }));
526
+ }
318
527
  app.onError((err, c) => {
528
+ const reqId = c.get("requestId") ?? "unknown";
529
+ // ValidationError extends HttpError — must check first or the details payload is lost
530
+ if (err instanceof ValidationError) {
531
+ const fmt = c.get("validationErrorFormatter") ?? defaultValidationErrorFormatter;
532
+ try {
533
+ return c.json(fmt(err.issues, reqId), 400);
534
+ }
535
+ catch {
536
+ return c.json(defaultValidationErrorFormatter(err.issues, reqId), 400);
537
+ }
538
+ }
319
539
  if (err instanceof HttpError) {
320
- return c.json({ error: err.message }, err.status);
540
+ return c.json({ error: err.message, requestId: reqId }, err.status);
321
541
  }
322
542
  console.error(err);
323
- return c.json({ error: "Internal Server Error" }, 500);
324
- });
325
- app.notFound((c) => c.json({ error: "Not Found" }, 404));
326
- app.openAPIRegistry.registerComponent("securitySchemes", "cookieAuth", {
327
- type: "apiKey",
328
- in: "cookie",
329
- name: "token",
330
- description: "Session cookie set automatically on login/register.",
331
- });
332
- app.openAPIRegistry.registerComponent("securitySchemes", "userToken", {
333
- type: "apiKey",
334
- in: "header",
335
- name: "x-user-token",
336
- description: "JWT session token passed as the x-user-token request header (alternative to the session cookie).",
337
- });
338
- app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
339
- type: "http",
340
- scheme: "bearer",
341
- description: "API key passed as Authorization: Bearer <token>. Required on all endpoints unless bearer auth is disabled in CreateAppConfig or the path is in the bypass list.",
543
+ return c.json({ error: "Internal Server Error", requestId: reqId }, 500);
342
544
  });
343
- app.doc("/openapi.json", { openapi: "3.0.0", info: { title: appName, version: openApiVersion } });
344
- app.get("/docs", Scalar({ url: "/openapi.json" }));
545
+ app.notFound((c) => c.json({ error: "Not Found", requestId: c.get("requestId") ?? "unknown" }, 404));
345
546
  app.get("/sw.js", (c) => c.body("", 200, { "Content-Type": "application/javascript" }));
346
547
  return app;
347
548
  };
package/dist/cli.js CHANGED
@@ -1,21 +1,96 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var D=import.meta.require;import{existsSync as N,mkdirSync as X,writeFileSync as q,readSync as b,rmSync as h}from"fs";import{join as B}from"path";import{spawnSync as M}from"child_process";function w(z){process.stdout.write(z);let H=Buffer.alloc(1024),Q=b(0,H,0,H.length,null);return H.subarray(0,Q).toString().trim().replace(/\r/g,"")}function E(z,H,Q=0){let J=Q;function Z($=!1){if(!$)process.stdout.write(`\x1B[${H.length}A`);for(let A=0;A<H.length;A++){let V=A===J,R=V?"\x1B[36m>\x1B[0m":" ",g=V?`\x1B[1m${H[A]}\x1B[0m`:`\x1B[2m${H[A]}\x1B[0m`;process.stdout.write(`\x1B[2K ${R} ${g}
4
- `)}}if(!process.stdin.isTTY){console.log(z),H.forEach((V,R)=>console.log(` ${R+1}) ${V}`));let $=w(` Choose [${Q+1}]: `);if(!$)return Q;let A=parseInt($);if(A>=1&&A<=H.length)return A-1;return Q}console.log(z),process.stdout.write("\x1B[?25l"),Z(!0),process.stdin.setRawMode(!0);let _=Buffer.alloc(16);try{while(!0){let $=b(0,_,0,_.length,null),A=_.subarray(0,$).toString();if(A==="\r"||A===`
5
- `)break;else if(A==="\x1B[A"||A==="\x1BOA")J=(J-1+H.length)%H.length,Z();else if(A==="\x1B[B"||A==="\x1BOB")J=(J+1)%H.length,Z();else if(A==="\x03")process.stdout.write(`\x1B[?25h
6
- `),process.stdin.setRawMode(!1),process.exit(0);else{let V=parseInt(A);if(V>=1&&V<=H.length){J=V-1,Z();break}}}}finally{process.stdin.setRawMode(!1),process.stdout.write("\x1B[?25h")}return J}var f=process.argv[2],m=process.argv[3],O=f||w("App name: ");if(!O)console.error("App name is required."),process.exit(1);var I=O.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),G=m||(f?I:w(`Directory (${I}): `))||I,K=!1,W=!1,P="mongo",v="redis",T="redis",F="redis";console.log("");var x=E("Database setup:",["Full stack (MongoDB + Redis \u2014 production ready)","SQLite (single file, no external services)","Memory (ephemeral, great for prototyping/tests)","Custom (choose each store individually)"]);if(x===0)K=E("MongoDB connection mode:",["Single (auth + app data share one connection)","Separate (auth on its own cluster)"])===0?"single":"separate",W=!0,P="mongo",v="redis",T="redis",F="redis";else if(x===1)K=!1,W=!1,P="sqlite",v="sqlite",T="sqlite",F="sqlite";else if(x===2)K=!1,W=!1,P="memory",v="memory",T="memory",F="memory";else{console.log(`
3
+ var a=import.meta.require;import{existsSync as D,mkdirSync as E,writeFileSync as I,readSync as g,rmSync as r}from"fs";import{join as K}from"path";import{spawnSync as w}from"child_process";function b(z){process.stdout.write(z);let G=Buffer.alloc(1024),U=g(0,G,0,G.length,null);return G.subarray(0,U).toString().trim().replace(/\r/g,"")}function Q(z,G,U=0){let J=U;function $(_=!1){if(!_)process.stdout.write(`\x1B[${G.length}A`);for(let H=0;H<G.length;H++){let Z=H===J,F=Z?"\x1B[36m>\x1B[0m":" ",L=Z?`\x1B[1m${G[H]}\x1B[0m`:`\x1B[2m${G[H]}\x1B[0m`;process.stdout.write(`\x1B[2K ${F} ${L}
4
+ `)}}if(!process.stdin.isTTY){console.log(z),G.forEach((Z,F)=>console.log(` ${F+1}) ${Z}`));let _=b(` Choose [${U+1}]: `);if(!_)return U;let H=parseInt(_);if(H>=1&&H<=G.length)return H-1;return U}console.log(z),process.stdout.write("\x1B[?25l"),$(!0),process.stdin.setRawMode(!0);let W=Buffer.alloc(16);try{while(!0){let _=g(0,W,0,W.length,null),H=W.subarray(0,_).toString();if(H==="\r"||H===`
5
+ `)break;else if(H==="\x1B[A"||H==="\x1BOA")J=(J-1+G.length)%G.length,$();else if(H==="\x1B[B"||H==="\x1BOB")J=(J+1)%G.length,$();else if(H==="\x03")process.stdout.write(`\x1B[?25h
6
+ `),process.stdin.setRawMode(!1),process.exit(0);else{let Z=parseInt(H);if(Z>=1&&Z<=G.length){J=Z-1,$();break}}}}finally{process.stdin.setRawMode(!1),process.stdout.write("\x1B[?25h")}return J}var y=process.argv[2],n=process.argv[3],N=y||b("App name: ");if(!N)console.error("App name is required."),process.exit(1);var x=N.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),X=n||(y?x:b(`Directory (${x}): `))||x,Y=!1,B=!1,T="mongo",q="redis",v="redis",O="redis";console.log("");var C=Q("Database setup:",["Full stack (MongoDB + Redis \u2014 production ready)","SQLite (single file, no external services)","Memory (ephemeral, great for prototyping/tests)","Custom (choose each store individually)"]);if(C===0)Y=Q("MongoDB connection mode:",["Single (auth + app data share one connection)","Separate (auth on its own cluster)"])===0?"single":"separate",B=!0,T="mongo",q="redis",v="redis",O="redis";else if(C===1)Y=!1,B=!1,T="sqlite",q="sqlite",v="sqlite",O="sqlite";else if(C===2)Y=!1,B=!1,T="memory",q="memory",v="memory",O="memory";else{console.log(`
7
7
  Configure each store:
8
- `);let z=E("MongoDB:",["Single (one connection for auth + app data)","Separate (auth on its own cluster)","None (no MongoDB)"]);if(z===0)K="single";else if(z===1)K="separate";else K=!1;W=E("Redis:",["Yes","No"])===0;let Q=[],J=[];if(W)Q.push("redis"),J.push("Redis");if(K)Q.push("mongo"),J.push("MongoDB");Q.push("sqlite","memory"),J.push("SQLite","Memory");let Z=[],_=[];if(K)Z.push("mongo"),_.push("MongoDB");Z.push("sqlite","memory"),_.push("SQLite","Memory");let $=E("Auth store:",_);P=Z[$];let A=E("Sessions store:",J);v=Q[A];let V=E("Cache store:",J);T=Q[V];let R=E("OAuth state store:",J);F=Q[R]}var S=P==="sqlite"||v==="sqlite"||T==="sqlite"||F==="sqlite",U=B(process.cwd(),G),Y=B(U,"src"),k=B(Y,"config"),j=B(Y,"lib"),p=B(Y,"routes"),l=B(Y,"workers"),u=B(Y,"queues"),d=B(Y,"ws"),c=B(Y,"services"),a=B(Y,"middleware"),r=B(Y,"models");if(N(U))console.error(`Directory "${G}" already exists.`),process.exit(1);function n(){let z=[];if(K)z.push(` mongo: "${K}",`);else z.push(" mongo: false,");if(z.push(` redis: ${W},`),z.push(` auth: "${P}",`),z.push(` sessions: "${v}",`),z.push(` oauthState: "${F}",`),z.push(` cache: "${T}",`),S)z.push(' sqlite: path.join(import.meta.dir, "../../data.db"),');return`{
8
+ `);let z=Q("MongoDB:",["Single (one connection for auth + app data)","Separate (auth on its own cluster)","None (no MongoDB)"]);if(z===0)Y="single";else if(z===1)Y="separate";else Y=!1;B=Q("Redis:",["Yes","No"])===0;let U=[],J=[];if(B)U.push("redis"),J.push("Redis");if(Y)U.push("mongo"),J.push("MongoDB");U.push("sqlite","memory"),J.push("SQLite","Memory");let $=[],W=[];if(Y)$.push("mongo"),W.push("MongoDB");$.push("sqlite","memory"),W.push("SQLite","Memory");let _=Q("Auth store:",W);T=$[_];let H=Q("Sessions store:",J);q=U[H];let Z=Q("Cache store:",J);v=U[Z];let F=Q("OAuth state store:",J);O=U[F]}var s=T==="sqlite"||q==="sqlite"||v==="sqlite"||O==="sqlite";console.log("");var R="web-saas",m=null,t=Q("How would you like to configure auth?",["Use a preset (pick a security posture, get sensible defaults)","Step by step (choose features individually)"]);if(t===0){let z=Q("Which best describes your app?",["Web app / SaaS (CSRF, refresh tokens, botProtection)","Internal / admin (MFA required, no refresh tokens, tight limits)",'Mobile / API only (no CSRF, cors: "*", header auth)',"Dev / prototype (permissive \u2014 iterate fast, no rate limits)"]);R=["web-saas","internal","mobile-api","dev"][z]}else{R="custom";let z=Q("Password policy:",["Relaxed (8 chars)","Strong (12+ chars, special required)","Minimal (dev only)"]),G=["relaxed","strong","minimal"][z],J=Q("Email verification:",["Yes","No"])===0,W=Q("Password reset:",["Yes","No"])===0,H=Q("Refresh tokens:",["Yes","No"])===0,Z=Q("MFA:",["None","Optional (users opt in)","Required (all users)"]),F=["none","optional","required"][Z],h=Q("CSRF protection:",["Yes","No"])===0,P=[],d=["Google","GitHub","Apple","Microsoft"];while(!0){let j=[...d.filter((c)=>!P.includes(c)),"None (done)"],u=Q("OAuth providers (select all that apply):",j),f=j[u];if(f==="None (done)")break;P.push(f)}m={passwordPolicy:G,emailVerification:J,passwordReset:W,refreshTokens:H,mfa:F,csrf:h,oauthProviders:P}}var A=K(process.cwd(),X),V=K(A,"src"),S=K(V,"config"),l=K(V,"lib"),i=K(V,"routes"),o=K(V,"workers"),e=K(V,"queues"),zz=K(V,"ws"),Gz=K(V,"services"),Hz=K(V,"middleware"),Jz=K(V,"models");if(D(A))console.error(`Directory "${X}" already exists.`),process.exit(1);function Kz(){let z=[];if(Y)z.push(` mongo: "${Y}",`);else z.push(" mongo: false,");if(z.push(` redis: ${B},`),z.push(` auth: "${T}",`),z.push(` sessions: "${q}",`),z.push(` oauthState: "${O}",`),z.push(` cache: "${v}",`),s)z.push(' sqlite: path.join(import.meta.dir, "../../data.db"),');return`{
9
9
  ${z.join(`
10
10
  `)}
11
- }`}var s=`export const APP_NAME = "${O}";
11
+ }`}function Qz(){if(R==="web-saas")return`export const auth: AuthConfig = {
12
+ roles: Object.values(USER_ROLES),
13
+ defaultRole: USER_ROLES.USER,
14
+ passwordPolicy: { minLength: 8, requireLetter: true, requireDigit: true },
15
+ // Uncomment to require email verification before login:
16
+ // emailVerification: {
17
+ // required: true,
18
+ // onSend: async (email, token) => { /* send via Resend, SendGrid, etc. */ },
19
+ // },
20
+ // Uncomment to enable password reset:
21
+ // passwordReset: {
22
+ // onSend: async (email, token) => { /* send via Resend, SendGrid, etc. */ },
23
+ // },
24
+ refreshTokens: { accessTokenExpiry: 900, refreshTokenExpiry: 2_592_000 },
25
+ sessionPolicy: { trackLastActive: true },
26
+ // Uncomment to enable opt-in MFA (TOTP + email OTP):
27
+ // mfa: { issuer: APP_NAME },
28
+ };
29
+
30
+ export const security: SecurityConfig = {
31
+ cors: ["https://myapp.com"], // TODO: replace with your domain
32
+ trustProxy: 1,
33
+ csrf: { enabled: true },
34
+ botProtection: { fingerprintRateLimit: true },
35
+ };`;if(R==="internal")return`export const auth: AuthConfig = {
36
+ roles: ["superadmin", "admin", "viewer"],
37
+ defaultRole: "viewer",
38
+ passwordPolicy: { minLength: 14, requireLetter: true, requireDigit: true, requireSpecial: true },
39
+ mfa: { issuer: APP_NAME, required: true },
40
+ sessionPolicy: {
41
+ maxSessions: 2,
42
+ trackLastActive: true,
43
+ persistSessionMetadata: true,
44
+ includeInactiveSessions: true,
45
+ },
46
+ rateLimit: { login: { windowMs: 15 * 60 * 1000, max: 5 } },
47
+ };
48
+
49
+ export const security: SecurityConfig = {
50
+ cors: ["https://admin.myapp.com"], // TODO: replace with your domain
51
+ trustProxy: 1,
52
+ csrf: { enabled: true },
53
+ rateLimit: { windowMs: 60_000, max: 30 },
54
+ };`;if(R==="mobile-api")return`export const auth: AuthConfig = {
55
+ roles: Object.values(USER_ROLES),
56
+ defaultRole: USER_ROLES.USER,
57
+ refreshTokens: { accessTokenExpiry: 900, refreshTokenExpiry: 2_592_000, rotationGraceSeconds: 60 },
58
+ sessionPolicy: { maxSessions: 5 },
59
+ };
60
+
61
+ export const security: SecurityConfig = {
62
+ cors: "*",
63
+ trustProxy: 1,
64
+ botProtection: { fingerprintRateLimit: true },
65
+ };`;if(R==="dev")return`export const auth: AuthConfig = {
66
+ roles: Object.values(USER_ROLES),
67
+ defaultRole: USER_ROLES.USER,
68
+ passwordPolicy: { minLength: 1, requireLetter: false, requireDigit: false, requireSpecial: false },
69
+ rateLimit: {
70
+ login: { windowMs: 60_000, max: 10_000 },
71
+ register: { windowMs: 60_000, max: 10_000 },
72
+ },
73
+ };
74
+
75
+ export const security: SecurityConfig = {
76
+ cors: "*",
77
+ bearerAuth: false,
78
+ };`;let z=m,G=[" roles: Object.values(USER_ROLES),"," defaultRole: USER_ROLES.USER,"];if(z.passwordPolicy==="relaxed")G.push(" passwordPolicy: { minLength: 8, requireLetter: true, requireDigit: true },");else if(z.passwordPolicy==="strong")G.push(" passwordPolicy: { minLength: 12, requireLetter: true, requireDigit: true, requireSpecial: true },");else G.push(" passwordPolicy: { minLength: 1, requireLetter: false, requireDigit: false, requireSpecial: false },");if(z.emailVerification)G.push(" emailVerification: {"),G.push(" required: true,"),G.push(" onSend: async (email, token) => { /* send via Resend, SendGrid, etc. */ },"),G.push(" },");if(z.passwordReset)G.push(" passwordReset: {"),G.push(" onSend: async (email, token) => { /* send via Resend, SendGrid, etc. */ },"),G.push(" },");if(z.refreshTokens)G.push(" refreshTokens: { accessTokenExpiry: 900, refreshTokenExpiry: 2_592_000 },");if(z.mfa==="optional")G.push(" mfa: { issuer: APP_NAME },");else if(z.mfa==="required")G.push(" mfa: { issuer: APP_NAME, required: true },");G.push(" sessionPolicy: { trackLastActive: true },");let U=`export const auth: AuthConfig = {
79
+ ${G.join(`
80
+ `)}
81
+ };`,J=[' cors: "*",'];if(z.csrf)J.push(" csrf: { enabled: true },");let $=`export const security: SecurityConfig = {
82
+ ${J.join(`
83
+ `)}
84
+ };`;return`${U}
85
+
86
+ ${$}`}var Uz=`export const APP_NAME = "${N}";
12
87
  export const APP_VERSION = "1.0.0";
13
88
 
14
89
  export const USER_ROLES = {
15
90
  ADMIN: "admin",
16
91
  USER: "user",
17
92
  };
18
- `,t=`import path from "path";
93
+ `,Xz=`import path from "path";
19
94
  import {
20
95
  type AppMeta,
21
96
  type AuthConfig,
@@ -36,16 +111,9 @@ export const workersDir = path.join(import.meta.dir, "../workers");
36
111
 
37
112
  export const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
38
113
 
39
- export const db: DbConfig = ${n()};
40
-
41
- export const auth: AuthConfig = {
42
- roles: Object.values(USER_ROLES),
43
- defaultRole: USER_ROLES.USER,
44
- };
114
+ export const db: DbConfig = ${Kz()};
45
115
 
46
- export const security: SecurityConfig = {
47
- cors: ["*"],
48
- };
116
+ ${Qz()}
49
117
 
50
118
  export const appConfig: CreateServerConfig = {
51
119
  app,
@@ -56,11 +124,11 @@ export const appConfig: CreateServerConfig = {
56
124
  auth,
57
125
  security,
58
126
  };
59
- `,i=`import { createServer } from "@lastshotlabs/bunshot";
127
+ `,Yz=`import { createServer } from "@lastshotlabs/bunshot";
60
128
  import { appConfig } from "@config/index";
61
129
 
62
130
  await createServer(appConfig);
63
- `,o=`# ${O}
131
+ `,Zz=`# ${N}
64
132
 
65
133
  Built with [@lastshotlabs/bunshot](https://github.com/Last-Shot-Labs/bunshot).
66
134
 
@@ -105,7 +173,7 @@ export const router = createRouter();
105
173
 
106
174
  router.get("/products", (c) => c.json({ products: [] }));
107
175
  \`\`\`
108
- ${K?`
176
+ ${Y?`
109
177
  ## Adding models
110
178
 
111
179
  \`\`\`ts
@@ -123,7 +191,7 @@ export const Product = appConnection.model("Product", ProductSchema);
123
191
  ## Environment variables
124
192
 
125
193
  See \`.env\` \u2014 fill in the values before running.
126
- `;function e(){let z=["NODE_ENV=development","PORT=3000"];if(K==="single")z.push(`
194
+ `;function $z(){let z=["NODE_ENV=development","PORT=3000"];if(Y==="single")z.push(`
127
195
  # MongoDB
128
196
  MONGO_USER_DEV=
129
197
  MONGO_PW_DEV=
@@ -132,7 +200,7 @@ MONGO_DB_DEV=
132
200
  MONGO_USER_PROD=
133
201
  MONGO_PW_PROD=
134
202
  MONGO_HOST_PROD=
135
- MONGO_DB_PROD=`);else if(K==="separate")z.push(`
203
+ MONGO_DB_PROD=`);else if(Y==="separate")z.push(`
136
204
  # MongoDB (app data)
137
205
  MONGO_USER_DEV=
138
206
  MONGO_PW_DEV=
@@ -151,7 +219,7 @@ MONGO_AUTH_DB_DEV=
151
219
  MONGO_AUTH_USER_PROD=
152
220
  MONGO_AUTH_PW_PROD=
153
221
  MONGO_AUTH_HOST_PROD=
154
- MONGO_AUTH_DB_PROD=`);if(W)z.push(`
222
+ MONGO_AUTH_DB_PROD=`);if(B)z.push(`
155
223
  # Redis
156
224
  REDIS_HOST_DEV=
157
225
  REDIS_USER_DEV=
@@ -168,26 +236,38 @@ BEARER_TOKEN_DEV=
168
236
  BEARER_TOKEN_PROD=
169
237
 
170
238
  # OAuth \u2014 Google (optional)
171
- GOOGLE_CLIENT_ID=
172
- GOOGLE_CLIENT_SECRET=
173
- GOOGLE_REDIRECT_URI=
239
+ # GOOGLE_CLIENT_ID=
240
+ # GOOGLE_CLIENT_SECRET=
241
+ # GOOGLE_REDIRECT_URI=
174
242
 
175
243
  # OAuth \u2014 Apple (optional)
176
- APPLE_CLIENT_ID=
177
- APPLE_TEAM_ID=
178
- APPLE_KEY_ID=
179
- APPLE_PRIVATE_KEY=
180
- APPLE_REDIRECT_URI=`),z.join(`
244
+ # APPLE_CLIENT_ID=
245
+ # APPLE_TEAM_ID=
246
+ # APPLE_KEY_ID=
247
+ # APPLE_PRIVATE_KEY=
248
+ # APPLE_REDIRECT_URI=
249
+
250
+ # OAuth \u2014 GitHub (optional)
251
+ # GITHUB_CLIENT_ID=
252
+ # GITHUB_CLIENT_SECRET=
253
+ # GITHUB_REDIRECT_URI=
254
+
255
+ # OAuth \u2014 Microsoft (optional)
256
+ # MICROSOFT_TENANT_ID=
257
+ # MICROSOFT_CLIENT_ID=
258
+ # MICROSOFT_CLIENT_SECRET=
259
+ # MICROSOFT_REDIRECT_URI=`),z.join(`
181
260
  `)+`
182
261
  `}console.log(`
183
- @lastshotlabs/bunshot \u2014 creating ${G}
184
- `);X(U,{recursive:!0});console.log(" Running bun init...");M("bun",["init","-y"],{cwd:U,stdio:"inherit"});var C=B(U,"index.ts");if(N(C))h(C);var y=B(U,"package.json"),L=JSON.parse(D("fs").readFileSync(y,"utf-8"));L.module="src/index.ts";L.scripts={dev:"bun --watch src/index.ts",start:"bun src/index.ts"};L.dependencies={...L.dependencies,"@lastshotlabs/bunshot":"*"};q(y,JSON.stringify(L,null,2)+`
185
- `,"utf-8");var zz=B(U,"tsconfig.json"),Az={compilerOptions:{lib:["ESNext"],target:"ESNext",module:"Preserve",moduleDetection:"force",jsx:"react-jsx",allowJs:!0,moduleResolution:"bundler",allowImportingTsExtensions:!0,verbatimModuleSyntax:!0,noEmit:!0,strict:!0,skipLibCheck:!0,noFallthroughCasesInSwitch:!0,noUncheckedIndexedAccess:!0,noImplicitOverride:!0,noUnusedLocals:!1,noUnusedParameters:!1,noPropertyAccessFromIndexSignature:!1,paths:{"@lib/*":["./src/lib/*"],"@middleware/*":["./src/middleware/*"],"@models/*":["./src/models/*"],"@queues/*":["./src/queues/*"],"@routes/*":["./src/routes/*"],"@scripts/*":["./src/scripts/*"],"@services/*":["./src/services/*"],"@workers/*":["./src/workers/*"],"@service-facades/*":["./src/service-facades/*"],"@config/*":["./src/config/*"],"@constants/*":["./src/lib/constants/*"]}}};q(zz,JSON.stringify(Az,null,2)+`
186
- `,"utf-8");X(k,{recursive:!0});X(j,{recursive:!0});X(p,{recursive:!0});X(l,{recursive:!0});X(u,{recursive:!0});X(d,{recursive:!0});X(c,{recursive:!0});X(a,{recursive:!0});X(r,{recursive:!0});q(B(j,"constants.ts"),s,"utf-8");q(B(k,"index.ts"),t,"utf-8");q(B(Y,"index.ts"),i,"utf-8");q(B(U,".env"),e(),"utf-8");q(B(U,"README.md"),o,"utf-8");console.log(" Created:");console.log(` + ${G}/src/index.ts`);console.log(` + ${G}/src/config/index.ts`);console.log(` + ${G}/src/lib/constants.ts`);console.log(` + ${G}/src/routes/`);console.log(` + ${G}/src/workers/`);console.log(` + ${G}/src/queues/`);console.log(` + ${G}/src/ws/`);console.log(` + ${G}/src/services/`);console.log(` + ${G}/src/middleware/`);console.log(` + ${G}/src/models/`);console.log(` + ${G}/.env`);console.log(` + ${G}/README.md`);console.log(`
187
- DB config:`);console.log(` mongo: ${K||"none"} | redis: ${W}`);console.log(` auth: ${P} | sessions: ${v} | cache: ${T} | oauthState: ${F}`);console.log(`
188
- Initializing git...`);var Bz=M("git",["init"],{cwd:U,stdio:"inherit"});if(Bz.status!==0)console.error(" git init failed \u2014 skipping.");console.log(`
189
- Installing dependencies...`);var Gz=M("bun",["install"],{cwd:U,stdio:"inherit"});if(Gz.status!==0)console.error(`
262
+ @lastshotlabs/bunshot \u2014 creating ${X}
263
+ `);E(A,{recursive:!0});console.log(" Running bun init...");w("bun",["init","-y"],{cwd:A,stdio:"inherit"});var k=K(A,"index.ts");if(D(k))r(k);var p=K(A,"package.json"),M=JSON.parse(a("fs").readFileSync(p,"utf-8"));M.module="src/index.ts";M.scripts={dev:"bun --watch src/index.ts",start:"bun src/index.ts"};M.dependencies={...M.dependencies,"@lastshotlabs/bunshot":"*"};I(p,JSON.stringify(M,null,2)+`
264
+ `,"utf-8");var Az=K(A,"tsconfig.json"),Ez={compilerOptions:{lib:["ESNext"],target:"ESNext",module:"Preserve",moduleDetection:"force",jsx:"react-jsx",allowJs:!0,moduleResolution:"bundler",allowImportingTsExtensions:!0,verbatimModuleSyntax:!0,noEmit:!0,strict:!0,skipLibCheck:!0,noFallthroughCasesInSwitch:!0,noUncheckedIndexedAccess:!0,noImplicitOverride:!0,noUnusedLocals:!1,noUnusedParameters:!1,noPropertyAccessFromIndexSignature:!1,paths:{"@lib/*":["./src/lib/*"],"@middleware/*":["./src/middleware/*"],"@models/*":["./src/models/*"],"@queues/*":["./src/queues/*"],"@routes/*":["./src/routes/*"],"@scripts/*":["./src/scripts/*"],"@services/*":["./src/services/*"],"@workers/*":["./src/workers/*"],"@service-facades/*":["./src/service-facades/*"],"@config/*":["./src/config/*"],"@constants/*":["./src/lib/constants/*"]}}};I(Az,JSON.stringify(Ez,null,2)+`
265
+ `,"utf-8");E(S,{recursive:!0});E(l,{recursive:!0});E(i,{recursive:!0});E(o,{recursive:!0});E(e,{recursive:!0});E(zz,{recursive:!0});E(Gz,{recursive:!0});E(Hz,{recursive:!0});E(Jz,{recursive:!0});I(K(l,"constants.ts"),Uz,"utf-8");I(K(S,"index.ts"),Xz,"utf-8");I(K(V,"index.ts"),Yz,"utf-8");I(K(A,".env"),$z(),"utf-8");I(K(A,"README.md"),Zz,"utf-8");console.log(" Created:");console.log(` + ${X}/src/index.ts`);console.log(` + ${X}/src/config/index.ts`);console.log(` + ${X}/src/lib/constants.ts`);console.log(` + ${X}/src/routes/`);console.log(` + ${X}/src/workers/`);console.log(` + ${X}/src/queues/`);console.log(` + ${X}/src/ws/`);console.log(` + ${X}/src/services/`);console.log(` + ${X}/src/middleware/`);console.log(` + ${X}/src/models/`);console.log(` + ${X}/.env`);console.log(` + ${X}/README.md`);console.log(`
266
+ DB config:`);console.log(` mongo: ${Y||"none"} | redis: ${B}`);console.log(` auth: ${T} | sessions: ${q} | cache: ${v} | oauthState: ${O}`);console.log(`
267
+ Auth config:`);console.log(` posture: ${R}`);console.log(`
268
+ Initializing git...`);var Vz=w("git",["init"],{cwd:A,stdio:"inherit"});if(Vz.status!==0)console.error(" git init failed \u2014 skipping.");console.log(`
269
+ Installing dependencies...`);var Wz=w("bun",["install"],{cwd:A,stdio:"inherit"});if(Wz.status!==0)console.error(`
190
270
  bun install failed. Run it manually inside the directory.`),process.exit(1);console.log(`
191
271
  Done! Next steps:
192
- `);console.log(` cd ${G}`);console.log(" # fill in .env");console.log(` bun dev
272
+ `);console.log(` cd ${X}`);console.log(" # fill in .env");console.log(` bun dev
193
273
  `);