chadstart 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -53,3 +53,23 @@ TOKEN_SECRET_KEY=replace-with-a-long-random-secret
53
53
  # šŸ’” Bugsink (https://www.bugsink.com) is a self-hosted alternative to Sentry
54
54
  # that uses the same Sentry SDK — just point SENTRY_DSN at your Bugsink instance.
55
55
  # SENTRY_DSN=https://xxxxx@oXXXXX.ingest.sentry.io/XXXXXXX
56
+
57
+ # Optional: OAuth / Social Login (powered by grant)
58
+ # For each provider, set OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET.
59
+ # Provider names must be uppercase (e.g. GOOGLE, GITHUB, FACEBOOK).
60
+ # See docs/oauth.md for the full list of 200+ supported providers.
61
+ #
62
+ # OAUTH_GOOGLE_KEY=your-google-client-id
63
+ # OAUTH_GOOGLE_SECRET=your-google-client-secret
64
+ # OAUTH_GITHUB_KEY=your-github-client-id
65
+ # OAUTH_GITHUB_SECRET=your-github-client-secret
66
+ # OAUTH_FACEBOOK_KEY=your-facebook-app-id
67
+ # OAUTH_FACEBOOK_SECRET=your-facebook-app-secret
68
+ # OAUTH_DISCORD_KEY=your-discord-client-id
69
+ # OAUTH_DISCORD_SECRET=your-discord-client-secret
70
+ # OAUTH_APPLE_KEY=your-apple-client-id
71
+ # OAUTH_APPLE_SECRET=your-apple-client-secret
72
+ # OAUTH_MICROSOFT_KEY=your-microsoft-client-id
73
+ # OAUTH_MICROSOFT_SECRET=your-microsoft-client-secret
74
+ # OAUTH_TWITTER_KEY=your-twitter-api-key
75
+ # OAUTH_TWITTER_SECRET=your-twitter-api-secret
@@ -414,3 +414,55 @@ sentry:
414
414
  environment: production # Label sent to Sentry. Defaults to NODE_ENV.
415
415
  tracesSampleRate: 1.0 # Fraction of transactions to sample (0.0–1.0)
416
416
  debug: false # Enable Sentry SDK debug logging
417
+
418
+ # ── OAuth / Social Login ─────────────────────────────────────────────────────
419
+ # Powered by the "grant" library — supports 200+ OAuth providers.
420
+ # Secrets (client keys / secrets) MUST be set via environment variables:
421
+ # OAUTH_<PROVIDER>_KEY — client / app ID
422
+ # OAUTH_<PROVIDER>_SECRET — client / app secret
423
+ # See docs/oauth.md for the full provider list and setup guides.
424
+
425
+ oauth:
426
+ # Which authenticable entity to create/find users in (default: first authenticable entity).
427
+ entity: User
428
+
429
+ # Where to redirect after successful login. The JWT token is appended as ?token=...
430
+ # If omitted, the callback returns JSON instead.
431
+ successRedirect: /login?success=true
432
+
433
+ # Where to redirect on error. The error message is appended as ?error=...
434
+ errorRedirect: /login?error=true
435
+
436
+ # Default settings applied to all providers.
437
+ defaults:
438
+ transport: querystring
439
+
440
+ # Configure each provider you want to support.
441
+ # The provider names must match grant's provider names (lowercase).
442
+ # Full list: https://github.com/simov/grant#200-supported-providers
443
+ providers:
444
+ google:
445
+ scope:
446
+ - openid
447
+ - email
448
+ - profile
449
+ custom_params:
450
+ access_type: offline
451
+ # key and secret via: OAUTH_GOOGLE_KEY, OAUTH_GOOGLE_SECRET
452
+
453
+ github:
454
+ scope:
455
+ - user:email
456
+ # key and secret via: OAUTH_GITHUB_KEY, OAUTH_GITHUB_SECRET
457
+
458
+ # facebook:
459
+ # scope:
460
+ # - email
461
+ # - public_profile
462
+ # key and secret via: OAUTH_FACEBOOK_KEY, OAUTH_FACEBOOK_SECRET
463
+
464
+ # discord:
465
+ # scope:
466
+ # - identify
467
+ # - email
468
+ # key and secret via: OAUTH_DISCORD_KEY, OAUTH_DISCORD_SECRET
@@ -124,6 +124,43 @@
124
124
  "description": "Enable Sentry SDK debug logging."
125
125
  }
126
126
  }
127
+ },
128
+ "oauth": {
129
+ "type": "object",
130
+ "description": "OAuth / social login configuration powered by the grant library. Secrets (client keys and secrets) must be supplied via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "entity": {
134
+ "type": "string",
135
+ "description": "Name of the authenticable entity to use for OAuth users (e.g. 'User'). Defaults to the first authenticable entity."
136
+ },
137
+ "successRedirect": {
138
+ "type": "string",
139
+ "description": "URL to redirect to after successful OAuth login. The JWT token is appended as a ?token= query parameter. If omitted, returns JSON."
140
+ },
141
+ "errorRedirect": {
142
+ "type": "string",
143
+ "description": "URL to redirect to on OAuth error. The error message is appended as an ?error= query parameter. If omitted, returns JSON error."
144
+ },
145
+ "defaults": {
146
+ "type": "object",
147
+ "description": "Default settings applied to all providers (e.g. transport, scope).",
148
+ "properties": {
149
+ "transport": {
150
+ "type": "string",
151
+ "enum": ["querystring", "session"],
152
+ "default": "querystring"
153
+ }
154
+ }
155
+ },
156
+ "providers": {
157
+ "type": "object",
158
+ "description": "Map of OAuth provider names to their configuration. Provider names must match grant's supported provider list (e.g. google, github, facebook). See https://www.npmjs.com/package/grant for all 200+ supported providers.",
159
+ "additionalProperties": {
160
+ "$ref": "#/$defs/oauthProvider"
161
+ }
162
+ }
163
+ }
127
164
  }
128
165
  },
129
166
  "$defs": {
@@ -362,6 +399,31 @@
362
399
  "limit": { "type": "integer", "description": "Maximum number of requests allowed in the time window." },
363
400
  "ttl": { "type": "integer", "description": "Time window in milliseconds." }
364
401
  }
402
+ },
403
+ "oauthProvider": {
404
+ "type": "object",
405
+ "description": "Configuration for a single OAuth provider. The key and secret should be set via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
406
+ "properties": {
407
+ "key": { "type": "string", "description": "OAuth client/app ID. Prefer using OAUTH_<PROVIDER>_KEY env var instead." },
408
+ "secret": { "type": "string", "description": "OAuth client/app secret. Prefer using OAUTH_<PROVIDER>_SECRET env var instead." },
409
+ "scope": {
410
+ "oneOf": [
411
+ { "type": "string" },
412
+ { "type": "array", "items": { "type": "string" } }
413
+ ],
414
+ "description": "OAuth scopes to request (e.g. 'openid email profile')."
415
+ },
416
+ "callback": { "type": "string", "description": "Custom callback URL path. Defaults to /api/auth/oauth/callback." },
417
+ "custom_params": { "type": "object", "description": "Extra query parameters to send to the authorization URL." },
418
+ "subdomain": { "type": "string", "description": "Subdomain for providers that require one (e.g. Shopify)." },
419
+ "nonce": { "type": "boolean", "description": "Enable nonce generation (required by some OIDC providers)." },
420
+ "pkce": { "type": "boolean", "description": "Enable PKCE (Proof Key for Code Exchange) for enhanced security." },
421
+ "response": {
422
+ "type": "array",
423
+ "items": { "type": "string" },
424
+ "description": "Data to include in the callback (e.g. ['tokens', 'profile'])."
425
+ }
426
+ }
365
427
  }
366
428
  }
367
429
  }
package/cli/cli.js CHANGED
@@ -14,19 +14,26 @@ function printUsage() {
14
14
  ChadStart - YAML-first Backend as a Service
15
15
 
16
16
  Usage:
17
- npx chadstart dev Start server with hot-reload on YAML changes
18
- npx chadstart start Start server (production mode)
19
- npx chadstart build Validate YAML config and print schema summary
20
- npx chadstart seed Seed the database with dummy data
17
+ npx chadstart dev Start server with hot-reload on YAML changes
18
+ npx chadstart start Start server (production mode)
19
+ npx chadstart build Validate YAML config and print schema summary
20
+ npx chadstart seed Seed the database with dummy data
21
+ npx chadstart migrate Run pending database migrations
22
+ npx chadstart migrate:generate Generate migration from YAML diff (git-based)
23
+ npx chadstart migrate:status Show current migration status
21
24
 
22
25
  Options:
23
- --config <file> Path to YAML config (default: chadstart.yaml)
24
- --port <number> Override port from config
26
+ --config <file> Path to YAML config (default: chadstart.yaml)
27
+ --port <number> Override port from config
28
+ --migrations-dir <dir> Path to migrations directory (default: migrations)
29
+ --description <text> Description for generated migration
25
30
 
26
31
  Examples:
27
32
  npx chadstart dev
28
33
  npx chadstart dev --config my-backend.yaml
29
34
  npx chadstart start --port 8080
35
+ npx chadstart migrate:generate --description add-posts-table
36
+ npx chadstart migrate
30
37
  `);
31
38
  }
32
39
 
@@ -53,6 +60,12 @@ if (command === 'create') {
53
60
  runBuild();
54
61
  } else if (command === 'seed') {
55
62
  runSeed();
63
+ } else if (command === 'migrate') {
64
+ runMigrate();
65
+ } else if (command === 'migrate:generate') {
66
+ runMigrateGenerate();
67
+ } else if (command === 'migrate:status') {
68
+ runMigrateStatus();
56
69
  } else {
57
70
  console.error(`Unknown command: ${command}`);
58
71
  printUsage();
@@ -273,6 +286,122 @@ function runBuild() {
273
286
 
274
287
  // ─── Helpers ─────────────────────────────────────────────────────────────────
275
288
 
289
+ const migrationsDir = path.resolve(getOption('--migrations-dir') || 'migrations');
290
+ const migrationDescription = getOption('--description') || null;
291
+
292
+ async function runMigrate() {
293
+ if (!fs.existsSync(yamlPath)) {
294
+ console.error(`Config not found: ${yamlPath}`);
295
+ process.exit(1);
296
+ }
297
+
298
+ try {
299
+ const { loadYaml } = require('../core/yaml-loader');
300
+ const { validateSchema } = require('../core/schema-validator');
301
+ const { buildCore } = require('../core/entity-engine');
302
+ const { initDb, closeDb } = require('../core/db');
303
+ const { runMigrations, buildExecQueryFn } = require('../core/migrations');
304
+ const dbModule = require('../core/db');
305
+
306
+ const config = loadYaml(yamlPath);
307
+ validateSchema(config);
308
+ const core = buildCore(config);
309
+ await initDb(core);
310
+
311
+ console.log('\nšŸ”„ Running database migrations...\n');
312
+
313
+ const execQueryFn = buildExecQueryFn(dbModule);
314
+
315
+ const applied = await runMigrations(migrationsDir, execQueryFn);
316
+
317
+ if (applied.length === 0) {
318
+ console.log(' āœ… Database is up to date — no pending migrations.\n');
319
+ } else {
320
+ for (const m of applied) {
321
+ console.log(` āœ… Applied: ${m.version}.${m.action}${m.name ? '.' + m.name : ''}`);
322
+ }
323
+ console.log(`\n ${applied.length} migration${applied.length !== 1 ? 's' : ''} applied.\n`);
324
+ }
325
+
326
+ await closeDb();
327
+ } catch (err) {
328
+ console.error(`\nāŒ ${err.message}\n`);
329
+ process.exit(1);
330
+ }
331
+ }
332
+
333
+ async function runMigrateGenerate() {
334
+ if (!fs.existsSync(yamlPath)) {
335
+ console.error(`Config not found: ${yamlPath}`);
336
+ process.exit(1);
337
+ }
338
+
339
+ try {
340
+ const { generateMigration } = require('../core/migrations');
341
+
342
+ console.log('\nšŸ“ Generating migration from YAML diff...\n');
343
+
344
+ const result = generateMigration(yamlPath, migrationsDir, migrationDescription);
345
+
346
+ if (result.isEmpty) {
347
+ console.log(' ā„¹ļø No schema changes detected — nothing to generate.\n');
348
+ } else {
349
+ console.log(` āœ… Migration v${String(result.version).padStart(3, '0')} generated:`);
350
+ console.log(` DO: ${result.doPath}`);
351
+ console.log(` UNDO: ${result.undoPath}`);
352
+ console.log('\n Run `npx chadstart migrate` to apply.\n');
353
+ }
354
+ } catch (err) {
355
+ console.error(`\nāŒ ${err.message}\n`);
356
+ process.exit(1);
357
+ }
358
+ }
359
+
360
+ async function runMigrateStatus() {
361
+ if (!fs.existsSync(yamlPath)) {
362
+ console.error(`Config not found: ${yamlPath}`);
363
+ process.exit(1);
364
+ }
365
+
366
+ try {
367
+ const { loadYaml } = require('../core/yaml-loader');
368
+ const { validateSchema } = require('../core/schema-validator');
369
+ const { buildCore } = require('../core/entity-engine');
370
+ const { initDb, closeDb } = require('../core/db');
371
+ const { getMigrationStatus, buildExecQueryFn } = require('../core/migrations');
372
+ const dbModule = require('../core/db');
373
+
374
+ const config = loadYaml(yamlPath);
375
+ validateSchema(config);
376
+ const core = buildCore(config);
377
+ await initDb(core);
378
+
379
+ const execQueryFn = buildExecQueryFn(dbModule);
380
+
381
+ const status = await getMigrationStatus(migrationsDir, execQueryFn);
382
+
383
+ console.log(`\nšŸ“Š Migration Status\n`);
384
+ console.log(` Current version: ${status.currentVersion}`);
385
+ console.log(` Applied: ${status.applied.length}`);
386
+ console.log(` Pending: ${status.pending.length}`);
387
+
388
+ if (status.pending.length > 0) {
389
+ console.log('\n Pending migrations:');
390
+ for (const m of status.pending) {
391
+ console.log(` - ${m.version}.${m.action}${m.name ? '.' + m.name : ''}`);
392
+ }
393
+ }
394
+
395
+ console.log('');
396
+ await closeDb();
397
+ } catch (err) {
398
+ console.error(`\nāŒ ${err.message}\n`);
399
+ process.exit(1);
400
+ }
401
+ }
402
+
403
+ // ─── Other helpers ───────────────────────────────────────────────────────────
404
+
276
405
  function applyPortOverride() {
277
406
  if (portOverride) {
278
407
  process.env.CHADSTART_PORT = portOverride;
@@ -155,6 +155,7 @@ function buildCore(config) {
155
155
  port: parseInt(process.env.CHADSTART_PORT || process.env.PORT || config.port || 3000, 10),
156
156
  rateLimits,
157
157
  telemetry,
158
+ oauth: config.oauth || null,
158
159
  admin: {
159
160
  enable_app: adminCfg.enable_app !== false,
160
161
  enable_entity: adminCfg.enable_entity !== false,