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 +20 -0
- package/chadstart.example.yml +52 -0
- package/chadstart.schema.json +62 -0
- package/cli/cli.js +135 -6
- package/core/entity-engine.js +1 -0
- package/core/migrations.js +484 -0
- package/core/oauth.js +263 -0
- package/docs/auth.md +3 -0
- package/docs/cli.md +41 -0
- package/docs/migrations.md +260 -0
- package/docs/oauth.md +869 -0
- package/mkdocs.yml +2 -0
- package/package.json +3 -1
- package/server/express-server.js +2 -0
- package/test/migrations.test.js +498 -0
- package/test/oauth.test.js +259 -0
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
|
package/chadstart.example.yml
CHANGED
|
@@ -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
|
package/chadstart.schema.json
CHANGED
|
@@ -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
|
|
18
|
-
npx chadstart start
|
|
19
|
-
npx chadstart build
|
|
20
|
-
npx chadstart seed
|
|
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>
|
|
24
|
-
--port <number>
|
|
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;
|
package/core/entity-engine.js
CHANGED
|
@@ -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,
|