@lastshotlabs/bunshot 0.0.21 → 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.
- package/README.md +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +44 -0
- package/dist/adapters/memoryAuth.d.ts +7 -0
- package/dist/adapters/memoryAuth.js +144 -0
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +120 -0
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +7 -0
- package/dist/adapters/sqliteAuth.js +199 -0
- package/dist/app.d.ts +100 -3
- package/dist/app.js +247 -46
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +49 -7
- package/dist/index.js +35 -5
- package/dist/lib/HttpError.d.ts +5 -0
- package/dist/lib/HttpError.js +7 -0
- package/dist/lib/appConfig.d.ts +44 -0
- package/dist/lib/appConfig.js +16 -0
- package/dist/lib/auditLog.d.ts +52 -0
- package/dist/lib/auditLog.js +201 -0
- package/dist/lib/authAdapter.d.ts +69 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +19 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- package/dist/lib/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -0
- package/dist/lib/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- package/dist/lib/session.d.ts +4 -0
- package/dist/lib/session.js +56 -2
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +180 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +35 -0
- package/dist/lib/upload.js +87 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +21 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/csrf.js +10 -0
- package/dist/middleware/identify.js +57 -9
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +99 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +36 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- package/dist/middleware/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +57 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -0
- package/dist/routes/auth.js +84 -6
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +47 -45
- package/dist/routes/metrics.d.ts +7 -0
- package/dist/routes/metrics.js +52 -0
- package/dist/routes/mfa.js +4 -0
- package/dist/routes/uploads.d.ts +2 -0
- package/dist/routes/uploads.js +135 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- package/dist/ws/index.js +3 -0
- package/docs/sections/auth-flow/full.md +779 -634
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +365 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +127 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +199 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +83 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- 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 {
|
|
11
|
-
import {
|
|
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";
|
|
@@ -22,6 +24,7 @@ import { connectRedis } from "./lib/redis";
|
|
|
22
24
|
import { setSessionStore } from "./lib/session";
|
|
23
25
|
import { setCacheStore } from "./middleware/cacheResponse";
|
|
24
26
|
import { maybeAutoRegister } from "./lib/createRoute";
|
|
27
|
+
import { setStorageAdapter, setUploadConfig } from "./lib/upload";
|
|
25
28
|
export const createApp = async (config) => {
|
|
26
29
|
const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
|
|
27
30
|
const appName = appConfig.name ?? "Bun Core API";
|
|
@@ -136,6 +139,8 @@ export const createApp = async (config) => {
|
|
|
136
139
|
setPasswordResetConfig(passwordReset ?? null);
|
|
137
140
|
setPasswordPolicy(authConfig.passwordPolicy ?? {});
|
|
138
141
|
setPasswordResetStore(sessions);
|
|
142
|
+
const { setDeletionCancelTokenStore } = await import("./lib/deletionCancelToken");
|
|
143
|
+
setDeletionCancelTokenStore(sessions);
|
|
139
144
|
setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
|
|
140
145
|
setMaxSessions(sessionPolicy.maxSessions ?? 6);
|
|
141
146
|
setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
|
|
@@ -146,6 +151,31 @@ export const createApp = async (config) => {
|
|
|
146
151
|
if (oauthProviders)
|
|
147
152
|
initOAuthProviders(oauthProviders);
|
|
148
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
|
+
}
|
|
149
179
|
// OAuth paths must bypass bearer auth — initiation and link routes are browser redirects,
|
|
150
180
|
// callbacks come from external providers; none can send a bearer token header.
|
|
151
181
|
const oauthBypass = configuredOAuth.flatMap((p) => [
|
|
@@ -153,10 +183,42 @@ export const createApp = async (config) => {
|
|
|
153
183
|
`/auth/${p}/callback`,
|
|
154
184
|
`/auth/${p}/link`,
|
|
155
185
|
]);
|
|
156
|
-
const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/"];
|
|
157
|
-
|
|
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];
|
|
158
192
|
const app = new OpenAPIHono();
|
|
159
|
-
app.use(
|
|
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
|
+
}
|
|
160
222
|
const headerOpts = {};
|
|
161
223
|
if (securityConfig.headers?.contentSecurityPolicy) {
|
|
162
224
|
headerOpts["Content-Security-Policy"] = securityConfig.headers.contentSecurityPolicy;
|
|
@@ -176,7 +238,7 @@ export const createApp = async (config) => {
|
|
|
176
238
|
const corsAllowHeaders = ["Content-Type", "Authorization", HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN];
|
|
177
239
|
if (securityConfig.csrf?.enabled)
|
|
178
240
|
corsAllowHeaders.push(HEADER_CSRF_TOKEN);
|
|
179
|
-
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 }));
|
|
180
242
|
if ((botCfg.blockList?.length ?? 0) > 0) {
|
|
181
243
|
const { botProtection } = await import("./middleware/botProtection");
|
|
182
244
|
app.use(botProtection({ blockList: botCfg.blockList }));
|
|
@@ -192,6 +254,10 @@ export const createApp = async (config) => {
|
|
|
192
254
|
});
|
|
193
255
|
}
|
|
194
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
|
+
}
|
|
195
261
|
// CSRF protection (after identify so we can check for auth cookie presence)
|
|
196
262
|
if (securityConfig.csrf?.enabled) {
|
|
197
263
|
setCsrfEnabled(true);
|
|
@@ -224,6 +290,10 @@ export const createApp = async (config) => {
|
|
|
224
290
|
}
|
|
225
291
|
for (const mw of middleware)
|
|
226
292
|
app.use(mw);
|
|
293
|
+
if (authConfig.mfa?.required) {
|
|
294
|
+
const { requireMfaSetup } = await import("./middleware/requireMfaSetup");
|
|
295
|
+
app.use(requireMfaSetup);
|
|
296
|
+
}
|
|
227
297
|
setAppName(appName);
|
|
228
298
|
// Schema pre-loading — import shared schema files before routes so registerSchema /
|
|
229
299
|
// registerSchemas calls run first, guaranteeing $ref instead of inline shapes.
|
|
@@ -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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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.
|
|
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
|
|
4
|
-
`)}}if(!process.stdin.isTTY){console.log(z),
|
|
5
|
-
`)break;else if(
|
|
6
|
-
`),process.stdin.setRawMode(!1),process.exit(0);else{let
|
|
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=
|
|
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
|
-
}`}
|
|
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
|
-
`,
|
|
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 = ${
|
|
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
|
-
|
|
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
|
-
`,
|
|
127
|
+
`,Yz=`import { createServer } from "@lastshotlabs/bunshot";
|
|
60
128
|
import { appConfig } from "@config/index";
|
|
61
129
|
|
|
62
130
|
await createServer(appConfig);
|
|
63
|
-
`,
|
|
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
|
-
${
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 ${
|
|
184
|
-
`);
|
|
185
|
-
`,"utf-8");var
|
|
186
|
-
`,"utf-8");
|
|
187
|
-
DB config:`);console.log(` mongo: ${
|
|
188
|
-
|
|
189
|
-
|
|
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 ${
|
|
272
|
+
`);console.log(` cd ${X}`);console.log(" # fill in .env");console.log(` bun dev
|
|
193
273
|
`);
|