chadstart 1.0.1 → 1.0.2

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/mkdocs.yml CHANGED
@@ -42,6 +42,7 @@ nav:
42
42
  - Entities: entities.md
43
43
  - CRUD: crud.md
44
44
  - Authentication: auth.md
45
+ - OAuth / Social Login: oauth.md
45
46
  - Access Policies: access-policies.md
46
47
  - Validation: validation.md
47
48
  - Configuration: config.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chadstart",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "YAML-first Backend as a Service — define your entire backend in one YAML file",
5
5
  "main": "server/express-server.js",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "execa": "^9.6.1",
48
48
  "express": "^5.2.1",
49
49
  "express-rate-limit": "^8.3.1",
50
+ "grant": "^5.4.24",
50
51
  "htmx.org": "2.0.4",
51
52
  "jsonwebtoken": "^9.0.3",
52
53
  "mysql2": "^3.20.0",
@@ -23,6 +23,7 @@ const { loadPlugins } = require('../core/plugin-loader');
23
23
  const { initErrorReporter, getRequestHandler, attachErrorHandler } = require('../core/error-reporter');
24
24
  const { getTelemetryConfig, initTelemetry } = require('../core/telemetry');
25
25
  const { setupFunctions, cleanup: cleanupFunctions } = require('../core/functions-engine');
26
+ const { registerOAuthRoutes } = require('../core/oauth');
26
27
  const logger = require('../utils/logger');
27
28
 
28
29
  function limiter(windowMs, max) {
@@ -133,6 +134,7 @@ async function buildApp(yamlPath, reloadFn) {
133
134
  app.use('/api/auth', authLimiter);
134
135
  registerAuthRoutes(app, core, emit);
135
136
  registerApiKeyRoutes(app, core);
137
+ registerOAuthRoutes(app, core, emit);
136
138
 
137
139
  const apiLimiters = buildApiLimiters(core);
138
140
  app.use('/api', ...apiLimiters);
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const { buildGrantConfig, normalizeProfile } = require('../core/oauth');
5
+ const { buildCore } = require('../core/entity-engine');
6
+
7
+ describe('OAuth – normalizeProfile', () => {
8
+ it('extracts email/name/providerId from standard profile', () => {
9
+ const result = normalizeProfile('google', {
10
+ email: 'user@example.com',
11
+ name: 'John Doe',
12
+ sub: '123456',
13
+ });
14
+ assert.strictEqual(result.email, 'user@example.com');
15
+ assert.strictEqual(result.name, 'John Doe');
16
+ assert.strictEqual(result.providerId, '123456');
17
+ });
18
+
19
+ it('extracts email from emails array', () => {
20
+ const result = normalizeProfile('github', {
21
+ emails: [{ value: 'gh@example.com' }],
22
+ login: 'octocat',
23
+ id: 42,
24
+ });
25
+ assert.strictEqual(result.email, 'gh@example.com');
26
+ assert.strictEqual(result.name, 'octocat');
27
+ assert.strictEqual(result.providerId, '42');
28
+ });
29
+
30
+ it('handles displayName fallback', () => {
31
+ const result = normalizeProfile('discord', {
32
+ email: 'dc@example.com',
33
+ displayName: 'DiscordUser',
34
+ id: '999',
35
+ });
36
+ assert.strictEqual(result.name, 'DiscordUser');
37
+ });
38
+
39
+ it('handles first_name + last_name fallback', () => {
40
+ const result = normalizeProfile('facebook', {
41
+ email: 'fb@example.com',
42
+ first_name: 'Jane',
43
+ last_name: 'Smith',
44
+ id: '555',
45
+ });
46
+ assert.strictEqual(result.name, 'Jane Smith');
47
+ });
48
+
49
+ it('handles missing profile', () => {
50
+ const result = normalizeProfile('google', null);
51
+ assert.strictEqual(result.email, null);
52
+ assert.strictEqual(result.name, null);
53
+ assert.strictEqual(result.providerId, null);
54
+ });
55
+
56
+ it('handles empty profile', () => {
57
+ const result = normalizeProfile('google', {});
58
+ assert.strictEqual(result.email, null);
59
+ assert.strictEqual(result.name, null);
60
+ assert.strictEqual(result.providerId, null);
61
+ });
62
+
63
+ it('handles mail field (used by some providers)', () => {
64
+ const result = normalizeProfile('microsoft', {
65
+ mail: 'ms@example.com',
66
+ name: 'MS User',
67
+ user_id: 'abc123',
68
+ });
69
+ assert.strictEqual(result.email, 'ms@example.com');
70
+ assert.strictEqual(result.providerId, 'abc123');
71
+ });
72
+
73
+ it('uses username as name fallback', () => {
74
+ const result = normalizeProfile('gitlab', {
75
+ email: 'gl@example.com',
76
+ username: 'gluser',
77
+ id: 789,
78
+ });
79
+ assert.strictEqual(result.name, 'gluser');
80
+ });
81
+
82
+ it('uses account_id as providerId fallback', () => {
83
+ const result = normalizeProfile('dropbox', {
84
+ email: 'db@example.com',
85
+ account_id: 'dbid:AAA',
86
+ });
87
+ assert.strictEqual(result.providerId, 'dbid:AAA');
88
+ });
89
+
90
+ it('handles emails array with plain strings', () => {
91
+ const result = normalizeProfile('other', {
92
+ emails: ['plain@example.com'],
93
+ id: '10',
94
+ });
95
+ assert.strictEqual(result.email, 'plain@example.com');
96
+ });
97
+ });
98
+
99
+ describe('OAuth – buildGrantConfig', () => {
100
+ const baseUrl = 'http://localhost:3000';
101
+
102
+ afterEach(() => {
103
+ // Clean up env vars set during tests
104
+ delete process.env.OAUTH_GOOGLE_KEY;
105
+ delete process.env.OAUTH_GOOGLE_SECRET;
106
+ delete process.env.OAUTH_GITHUB_KEY;
107
+ delete process.env.OAUTH_GITHUB_SECRET;
108
+ });
109
+
110
+ it('builds config with defaults', () => {
111
+ const oauthConfig = {
112
+ providers: {
113
+ google: {
114
+ scope: ['openid', 'email'],
115
+ },
116
+ },
117
+ };
118
+ const config = buildGrantConfig(oauthConfig, baseUrl);
119
+
120
+ assert.strictEqual(config.defaults.origin, 'http://localhost:3000');
121
+ assert.strictEqual(config.defaults.transport, 'querystring');
122
+ assert.ok(config.google);
123
+ assert.deepStrictEqual(config.google.scope, ['openid', 'email']);
124
+ assert.strictEqual(config.google.callback, '/api/auth/oauth/callback');
125
+ });
126
+
127
+ it('uses env vars for key/secret', () => {
128
+ process.env.OAUTH_GOOGLE_KEY = 'env-key';
129
+ process.env.OAUTH_GOOGLE_SECRET = 'env-secret';
130
+
131
+ const oauthConfig = {
132
+ providers: {
133
+ google: {
134
+ scope: ['openid'],
135
+ key: 'yaml-key', // Should be overridden by env
136
+ secret: 'yaml-secret',
137
+ },
138
+ },
139
+ };
140
+ const config = buildGrantConfig(oauthConfig, baseUrl);
141
+
142
+ assert.strictEqual(config.google.key, 'env-key');
143
+ assert.strictEqual(config.google.secret, 'env-secret');
144
+ });
145
+
146
+ it('falls back to YAML key/secret when env not set', () => {
147
+ const oauthConfig = {
148
+ providers: {
149
+ github: {
150
+ key: 'yaml-key',
151
+ secret: 'yaml-secret',
152
+ scope: ['user:email'],
153
+ },
154
+ },
155
+ };
156
+ const config = buildGrantConfig(oauthConfig, baseUrl);
157
+
158
+ assert.strictEqual(config.github.key, 'yaml-key');
159
+ assert.strictEqual(config.github.secret, 'yaml-secret');
160
+ });
161
+
162
+ it('supports multiple providers', () => {
163
+ const oauthConfig = {
164
+ providers: {
165
+ google: { scope: ['email'] },
166
+ github: { scope: ['user:email'] },
167
+ discord: { scope: ['identify'] },
168
+ },
169
+ };
170
+ const config = buildGrantConfig(oauthConfig, baseUrl);
171
+
172
+ assert.ok(config.google);
173
+ assert.ok(config.github);
174
+ assert.ok(config.discord);
175
+ });
176
+
177
+ it('preserves custom callback', () => {
178
+ const oauthConfig = {
179
+ providers: {
180
+ google: {
181
+ callback: '/my-custom-callback',
182
+ scope: ['email'],
183
+ },
184
+ },
185
+ };
186
+ const config = buildGrantConfig(oauthConfig, baseUrl);
187
+
188
+ assert.strictEqual(config.google.callback, '/my-custom-callback');
189
+ });
190
+
191
+ it('strips trailing slash from origin', () => {
192
+ const config = buildGrantConfig({ providers: { google: {} } }, 'http://localhost:3000/');
193
+ assert.strictEqual(config.defaults.origin, 'http://localhost:3000');
194
+ });
195
+
196
+ it('merges custom defaults', () => {
197
+ const oauthConfig = {
198
+ defaults: { transport: 'session' },
199
+ providers: { google: {} },
200
+ };
201
+ const config = buildGrantConfig(oauthConfig, baseUrl);
202
+ assert.strictEqual(config.defaults.transport, 'session');
203
+ });
204
+
205
+ it('handles custom_params', () => {
206
+ const oauthConfig = {
207
+ providers: {
208
+ google: {
209
+ scope: ['openid'],
210
+ custom_params: { access_type: 'offline' },
211
+ },
212
+ },
213
+ };
214
+ const config = buildGrantConfig(oauthConfig, baseUrl);
215
+ assert.deepStrictEqual(config.google.custom_params, { access_type: 'offline' });
216
+ });
217
+ });
218
+
219
+ describe('OAuth – buildCore integration', () => {
220
+ it('buildCore includes oauth config when present', () => {
221
+ const core = buildCore({
222
+ name: 'TestApp',
223
+ entities: { User: { authenticable: true, properties: ['name'] } },
224
+ oauth: {
225
+ entity: 'User',
226
+ providers: { google: { scope: ['email'] } },
227
+ },
228
+ });
229
+
230
+ assert.ok(core.oauth);
231
+ assert.strictEqual(core.oauth.entity, 'User');
232
+ assert.ok(core.oauth.providers.google);
233
+ });
234
+
235
+ it('buildCore sets oauth to null when not configured', () => {
236
+ const core = buildCore({
237
+ name: 'TestApp',
238
+ entities: { User: { authenticable: true, properties: ['name'] } },
239
+ });
240
+
241
+ assert.strictEqual(core.oauth, null);
242
+ });
243
+
244
+ it('buildCore preserves successRedirect and errorRedirect', () => {
245
+ const core = buildCore({
246
+ name: 'TestApp',
247
+ entities: { User: { authenticable: true, properties: ['name'] } },
248
+ oauth: {
249
+ entity: 'User',
250
+ successRedirect: '/dashboard',
251
+ errorRedirect: '/login?error=true',
252
+ providers: { github: { scope: ['user:email'] } },
253
+ },
254
+ });
255
+
256
+ assert.strictEqual(core.oauth.successRedirect, '/dashboard');
257
+ assert.strictEqual(core.oauth.errorRedirect, '/login?error=true');
258
+ });
259
+ });