neonctl 2.22.2 → 2.23.1
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 +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/esbuild.js +147 -0
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
import { NeonAuthSupportedAuthProvider, NeonAuthOauthProviderId, NeonAuthOauthProviderType, NeonAuthEmailVerificationMethod, } from '@neondatabase/api-client';
|
|
2
|
+
import { isAxiosError } from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { retryOnLock } from '../api.js';
|
|
5
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
6
|
+
import { writer } from '../writer.js';
|
|
7
|
+
// Shared styled output helpers
|
|
8
|
+
const printKvBlock = (title, entries) => {
|
|
9
|
+
process.stdout.write(`\n${chalk.green(title)}\n`);
|
|
10
|
+
for (const [key, value] of entries) {
|
|
11
|
+
process.stdout.write(` ${chalk.green(key)} ${value ?? ''}\n`);
|
|
12
|
+
}
|
|
13
|
+
process.stdout.write('\n');
|
|
14
|
+
};
|
|
15
|
+
const printMessage = (message) => {
|
|
16
|
+
process.stdout.write(`\n${chalk.green(message)}\n\n`);
|
|
17
|
+
};
|
|
18
|
+
const INTEGRATION_RESPONSE_FIELDS = [
|
|
19
|
+
'auth_provider',
|
|
20
|
+
'db_name',
|
|
21
|
+
'base_url',
|
|
22
|
+
'schema_name',
|
|
23
|
+
'table_name',
|
|
24
|
+
'jwks_url',
|
|
25
|
+
];
|
|
26
|
+
const INTEGRATION_STATUS_FIELDS = [
|
|
27
|
+
'auth_provider',
|
|
28
|
+
'branch_id',
|
|
29
|
+
'db_name',
|
|
30
|
+
'base_url',
|
|
31
|
+
'created_at',
|
|
32
|
+
'jwks_url',
|
|
33
|
+
];
|
|
34
|
+
const OAUTH_PROVIDER_FIELDS = ['id', 'type', 'client_id'];
|
|
35
|
+
const ALLOW_LOCALHOST_FIELDS = ['allow_localhost'];
|
|
36
|
+
const SUPPORTED_OAUTH_PROVIDERS = [
|
|
37
|
+
NeonAuthOauthProviderId.Google,
|
|
38
|
+
NeonAuthOauthProviderId.Github,
|
|
39
|
+
NeonAuthOauthProviderId.Vercel,
|
|
40
|
+
];
|
|
41
|
+
const DOMAIN_FIELDS = ['domain'];
|
|
42
|
+
const EMAIL_PASSWORD_FIELDS = [
|
|
43
|
+
'enabled',
|
|
44
|
+
'email_verification_method',
|
|
45
|
+
'require_email_verification',
|
|
46
|
+
'auto_sign_in_after_verification',
|
|
47
|
+
'send_verification_email_on_sign_up',
|
|
48
|
+
'send_verification_email_on_sign_in',
|
|
49
|
+
'disable_sign_up',
|
|
50
|
+
];
|
|
51
|
+
const EMAIL_PROVIDER_FIELDS = [
|
|
52
|
+
'type',
|
|
53
|
+
'host',
|
|
54
|
+
'port',
|
|
55
|
+
'username',
|
|
56
|
+
'sender_email',
|
|
57
|
+
'sender_name',
|
|
58
|
+
];
|
|
59
|
+
const ORGANIZATION_FIELDS = [
|
|
60
|
+
'enabled',
|
|
61
|
+
'organization_limit',
|
|
62
|
+
'creator_role',
|
|
63
|
+
];
|
|
64
|
+
const WEBHOOK_FIELDS = [
|
|
65
|
+
'enabled',
|
|
66
|
+
'webhook_url',
|
|
67
|
+
'enabled_events',
|
|
68
|
+
'timeout_seconds',
|
|
69
|
+
];
|
|
70
|
+
const TEST_EMAIL_FIELDS = ['success', 'error_message'];
|
|
71
|
+
export const command = 'neon-auth';
|
|
72
|
+
export const describe = 'Manage Neon Auth';
|
|
73
|
+
export const builder = (argv) => {
|
|
74
|
+
return argv
|
|
75
|
+
.usage('$0 neon-auth <sub-command> [options]')
|
|
76
|
+
.options({
|
|
77
|
+
'project-id': {
|
|
78
|
+
describe: 'Project ID',
|
|
79
|
+
type: 'string',
|
|
80
|
+
},
|
|
81
|
+
branch: {
|
|
82
|
+
describe: 'Branch ID or name',
|
|
83
|
+
type: 'string',
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
.middleware(fillSingleProject)
|
|
87
|
+
.command('enable', 'Enable Neon Auth on a branch', (yargs) => yargs.options({
|
|
88
|
+
'database-name': {
|
|
89
|
+
describe: 'Database name to use for auth data',
|
|
90
|
+
type: 'string',
|
|
91
|
+
},
|
|
92
|
+
}), async (args) => {
|
|
93
|
+
await enable(args);
|
|
94
|
+
})
|
|
95
|
+
.command('status', 'Get Neon Auth status for a branch', (yargs) => yargs, async (args) => {
|
|
96
|
+
await status(args);
|
|
97
|
+
})
|
|
98
|
+
.command('disable', 'Disable Neon Auth on a branch', (yargs) => yargs.options({
|
|
99
|
+
'delete-data': {
|
|
100
|
+
describe: 'Permanently delete all Neon Auth data and schema from the database',
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
default: false,
|
|
103
|
+
},
|
|
104
|
+
}), async (args) => {
|
|
105
|
+
await disable(args);
|
|
106
|
+
})
|
|
107
|
+
.command('oauth-provider', 'Manage OAuth providers', (yargs) => {
|
|
108
|
+
return yargs
|
|
109
|
+
.usage('$0 neon-auth oauth-provider <sub-command> [options]')
|
|
110
|
+
.command('list', 'List OAuth providers', (yargs) => yargs, async (args) => {
|
|
111
|
+
await oauthProviderList(args);
|
|
112
|
+
})
|
|
113
|
+
.command('add', 'Add an OAuth provider', (yargs) => yargs.options({
|
|
114
|
+
'provider-id': {
|
|
115
|
+
describe: `OAuth provider ID. Supported values: ${SUPPORTED_OAUTH_PROVIDERS.join(', ')}`,
|
|
116
|
+
type: 'string',
|
|
117
|
+
choices: SUPPORTED_OAUTH_PROVIDERS,
|
|
118
|
+
demandOption: true,
|
|
119
|
+
},
|
|
120
|
+
'oauth-client-id': {
|
|
121
|
+
describe: "OAuth client ID from your provider app. Omit to use Neon's shared OAuth app.",
|
|
122
|
+
type: 'string',
|
|
123
|
+
},
|
|
124
|
+
'oauth-client-secret': {
|
|
125
|
+
describe: "OAuth client secret from your provider app. Omit to use Neon's shared OAuth app.",
|
|
126
|
+
type: 'string',
|
|
127
|
+
},
|
|
128
|
+
}), async (args) => {
|
|
129
|
+
await oauthProviderAdd(args);
|
|
130
|
+
})
|
|
131
|
+
.command('update', 'Update an OAuth provider', (yargs) => yargs.options({
|
|
132
|
+
'provider-id': {
|
|
133
|
+
describe: `OAuth provider ID. Supported values: ${SUPPORTED_OAUTH_PROVIDERS.join(', ')}`,
|
|
134
|
+
type: 'string',
|
|
135
|
+
choices: SUPPORTED_OAUTH_PROVIDERS,
|
|
136
|
+
demandOption: true,
|
|
137
|
+
},
|
|
138
|
+
'oauth-client-id': {
|
|
139
|
+
describe: "OAuth client ID from your provider app. Omit to use Neon's shared OAuth app.",
|
|
140
|
+
type: 'string',
|
|
141
|
+
},
|
|
142
|
+
'oauth-client-secret': {
|
|
143
|
+
describe: "OAuth client secret from your provider app. Omit to use Neon's shared OAuth app.",
|
|
144
|
+
type: 'string',
|
|
145
|
+
},
|
|
146
|
+
}), async (args) => {
|
|
147
|
+
await oauthProviderUpdate(args);
|
|
148
|
+
})
|
|
149
|
+
.command('delete', 'Delete an OAuth provider', (yargs) => yargs.options({
|
|
150
|
+
'provider-id': {
|
|
151
|
+
describe: `OAuth provider ID. Supported values: ${SUPPORTED_OAUTH_PROVIDERS.join(', ')}`,
|
|
152
|
+
type: 'string',
|
|
153
|
+
choices: SUPPORTED_OAUTH_PROVIDERS,
|
|
154
|
+
demandOption: true,
|
|
155
|
+
},
|
|
156
|
+
}), async (args) => {
|
|
157
|
+
await oauthProviderDelete(args);
|
|
158
|
+
});
|
|
159
|
+
})
|
|
160
|
+
.command('domain', 'Manage redirect URI trusted domains', (yargs) => {
|
|
161
|
+
return yargs
|
|
162
|
+
.usage('$0 neon-auth domain <sub-command> [options]')
|
|
163
|
+
.command('list', 'List trusted domains', (yargs) => yargs, async (args) => {
|
|
164
|
+
await domainList(args);
|
|
165
|
+
})
|
|
166
|
+
.command('add <domain>', 'Add a trusted domain', (yargs) => yargs
|
|
167
|
+
.usage('$0 neon-auth domain add <domain> [options]')
|
|
168
|
+
.positional('domain', {
|
|
169
|
+
describe: 'Domain to add',
|
|
170
|
+
type: 'string',
|
|
171
|
+
demandOption: true,
|
|
172
|
+
}), async (args) => {
|
|
173
|
+
await domainAdd(args);
|
|
174
|
+
})
|
|
175
|
+
.command('delete <domain>', 'Delete a trusted domain', (yargs) => yargs
|
|
176
|
+
.usage('$0 neon-auth domain delete <domain> [options]')
|
|
177
|
+
.positional('domain', {
|
|
178
|
+
describe: 'Domain to delete',
|
|
179
|
+
type: 'string',
|
|
180
|
+
demandOption: true,
|
|
181
|
+
}), async (args) => {
|
|
182
|
+
await domainDelete(args);
|
|
183
|
+
})
|
|
184
|
+
.command('allow-localhost', 'Manage localhost connection settings', (yargs) => yargs
|
|
185
|
+
.usage('$0 neon-auth domain allow-localhost <sub-command> [options]')
|
|
186
|
+
.command('get', 'Get localhost connection setting', (yargs) => yargs, async (args) => {
|
|
187
|
+
await allowLocalhostGet(args);
|
|
188
|
+
})
|
|
189
|
+
.command('enable', 'Allow localhost connections', (yargs) => yargs, async (args) => {
|
|
190
|
+
await allowLocalhostEnable(args);
|
|
191
|
+
})
|
|
192
|
+
.command('disable', 'Restrict localhost connections', (yargs) => yargs, async (args) => {
|
|
193
|
+
await allowLocalhostDisable(args);
|
|
194
|
+
}));
|
|
195
|
+
})
|
|
196
|
+
.command('config', 'Manage Neon Auth configuration', (yargs) => {
|
|
197
|
+
return yargs
|
|
198
|
+
.usage('$0 neon-auth config <sub-command> [options]')
|
|
199
|
+
.command('email-password', 'Manage email and password authentication settings', (yargs) => {
|
|
200
|
+
return yargs
|
|
201
|
+
.usage('$0 neon-auth config email-password <sub-command> [options]')
|
|
202
|
+
.command('get', 'Get email and password config', (yargs) => yargs, async (args) => {
|
|
203
|
+
await emailPasswordGet(args);
|
|
204
|
+
})
|
|
205
|
+
.command('update', 'Update email and password config', (yargs) => yargs.options({
|
|
206
|
+
enabled: {
|
|
207
|
+
describe: 'Enable email and password authentication',
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
},
|
|
210
|
+
'email-verification-method': {
|
|
211
|
+
describe: 'Email verification method',
|
|
212
|
+
type: 'string',
|
|
213
|
+
choices: Object.values(NeonAuthEmailVerificationMethod),
|
|
214
|
+
},
|
|
215
|
+
'require-email-verification': {
|
|
216
|
+
describe: 'Require email verification before users can sign in',
|
|
217
|
+
type: 'boolean',
|
|
218
|
+
},
|
|
219
|
+
'auto-sign-in-after-verification': {
|
|
220
|
+
describe: 'Auto sign in users after verifying their email',
|
|
221
|
+
type: 'boolean',
|
|
222
|
+
},
|
|
223
|
+
'send-verification-email-on-sign-up': {
|
|
224
|
+
describe: 'Send verification email on sign up',
|
|
225
|
+
type: 'boolean',
|
|
226
|
+
},
|
|
227
|
+
'send-verification-email-on-sign-in': {
|
|
228
|
+
describe: 'Send verification email on sign in',
|
|
229
|
+
type: 'boolean',
|
|
230
|
+
},
|
|
231
|
+
'disable-sign-up': {
|
|
232
|
+
describe: 'Disable new user sign ups',
|
|
233
|
+
type: 'boolean',
|
|
234
|
+
},
|
|
235
|
+
}), async (args) => {
|
|
236
|
+
await emailPasswordUpdate(args);
|
|
237
|
+
});
|
|
238
|
+
})
|
|
239
|
+
.command('email-provider', 'Manage email provider configuration', (yargs) => {
|
|
240
|
+
return yargs
|
|
241
|
+
.usage('$0 neon-auth config email-provider <sub-command> [options]')
|
|
242
|
+
.command('get', 'Get email provider config', (yargs) => yargs, async (args) => {
|
|
243
|
+
await emailProviderGet(args);
|
|
244
|
+
})
|
|
245
|
+
.command('update', 'Update email provider config', (yargs) => yargs.options({
|
|
246
|
+
type: {
|
|
247
|
+
describe: 'Email provider type',
|
|
248
|
+
type: 'string',
|
|
249
|
+
choices: ['standard', 'shared'],
|
|
250
|
+
demandOption: true,
|
|
251
|
+
},
|
|
252
|
+
host: {
|
|
253
|
+
describe: 'SMTP host (required for standard)',
|
|
254
|
+
type: 'string',
|
|
255
|
+
},
|
|
256
|
+
port: {
|
|
257
|
+
describe: 'SMTP port (required for standard)',
|
|
258
|
+
type: 'number',
|
|
259
|
+
},
|
|
260
|
+
username: {
|
|
261
|
+
describe: 'SMTP username (required for standard)',
|
|
262
|
+
type: 'string',
|
|
263
|
+
},
|
|
264
|
+
password: {
|
|
265
|
+
describe: 'SMTP password (required for standard)',
|
|
266
|
+
type: 'string',
|
|
267
|
+
},
|
|
268
|
+
'sender-email': {
|
|
269
|
+
describe: 'Sender email address',
|
|
270
|
+
type: 'string',
|
|
271
|
+
},
|
|
272
|
+
'sender-name': {
|
|
273
|
+
describe: 'Sender display name',
|
|
274
|
+
type: 'string',
|
|
275
|
+
},
|
|
276
|
+
}), async (args) => {
|
|
277
|
+
await emailProviderUpdate(args);
|
|
278
|
+
})
|
|
279
|
+
.command('test', 'Send a test email', (yargs) => yargs.options({
|
|
280
|
+
'recipient-email': {
|
|
281
|
+
describe: 'Email address to send test email to',
|
|
282
|
+
type: 'string',
|
|
283
|
+
demandOption: true,
|
|
284
|
+
},
|
|
285
|
+
host: {
|
|
286
|
+
describe: 'SMTP host',
|
|
287
|
+
type: 'string',
|
|
288
|
+
demandOption: true,
|
|
289
|
+
},
|
|
290
|
+
port: {
|
|
291
|
+
describe: 'SMTP port',
|
|
292
|
+
type: 'number',
|
|
293
|
+
demandOption: true,
|
|
294
|
+
},
|
|
295
|
+
username: {
|
|
296
|
+
describe: 'SMTP username',
|
|
297
|
+
type: 'string',
|
|
298
|
+
demandOption: true,
|
|
299
|
+
},
|
|
300
|
+
password: {
|
|
301
|
+
describe: 'SMTP password',
|
|
302
|
+
type: 'string',
|
|
303
|
+
demandOption: true,
|
|
304
|
+
},
|
|
305
|
+
'sender-email': {
|
|
306
|
+
describe: 'Sender email address',
|
|
307
|
+
type: 'string',
|
|
308
|
+
demandOption: true,
|
|
309
|
+
},
|
|
310
|
+
'sender-name': {
|
|
311
|
+
describe: 'Sender display name',
|
|
312
|
+
type: 'string',
|
|
313
|
+
demandOption: true,
|
|
314
|
+
},
|
|
315
|
+
}), async (args) => {
|
|
316
|
+
await emailProviderTest(args);
|
|
317
|
+
});
|
|
318
|
+
})
|
|
319
|
+
.command('organization', 'Manage organization plugin settings', (yargs) => {
|
|
320
|
+
return yargs
|
|
321
|
+
.usage('$0 neon-auth config organization <sub-command> [options]')
|
|
322
|
+
.command('get', 'Get organization plugin config', (yargs) => yargs, async (args) => {
|
|
323
|
+
await organizationGet(args);
|
|
324
|
+
})
|
|
325
|
+
.command('update', 'Update organization plugin config', (yargs) => yargs.options({
|
|
326
|
+
enabled: {
|
|
327
|
+
describe: 'Enable the organization plugin',
|
|
328
|
+
type: 'boolean',
|
|
329
|
+
},
|
|
330
|
+
limit: {
|
|
331
|
+
describe: 'Maximum number of organizations a user can create',
|
|
332
|
+
type: 'number',
|
|
333
|
+
},
|
|
334
|
+
'creator-role': {
|
|
335
|
+
describe: 'Role assigned to organization creator',
|
|
336
|
+
type: 'string',
|
|
337
|
+
choices: ['admin', 'owner'],
|
|
338
|
+
},
|
|
339
|
+
}), async (args) => {
|
|
340
|
+
await organizationUpdate(args);
|
|
341
|
+
});
|
|
342
|
+
})
|
|
343
|
+
.command('webhook', 'Manage webhook configuration', (yargs) => {
|
|
344
|
+
return yargs
|
|
345
|
+
.usage('$0 neon-auth config webhook <sub-command> [options]')
|
|
346
|
+
.command('get', 'Get webhook config', (yargs) => yargs, async (args) => {
|
|
347
|
+
await webhookGet(args);
|
|
348
|
+
})
|
|
349
|
+
.command('update', 'Update webhook config', (yargs) => yargs.options({
|
|
350
|
+
enabled: {
|
|
351
|
+
describe: 'Enable webhooks',
|
|
352
|
+
type: 'boolean',
|
|
353
|
+
demandOption: true,
|
|
354
|
+
},
|
|
355
|
+
url: {
|
|
356
|
+
describe: 'Webhook endpoint URL',
|
|
357
|
+
type: 'string',
|
|
358
|
+
},
|
|
359
|
+
'enabled-events': {
|
|
360
|
+
describe: 'Events to enable',
|
|
361
|
+
type: 'string',
|
|
362
|
+
choices: [
|
|
363
|
+
'user.before_create',
|
|
364
|
+
'user.created',
|
|
365
|
+
'send.otp',
|
|
366
|
+
'send.magic_link',
|
|
367
|
+
],
|
|
368
|
+
array: true,
|
|
369
|
+
},
|
|
370
|
+
timeout: {
|
|
371
|
+
describe: 'Webhook timeout in seconds (1-10)',
|
|
372
|
+
type: 'number',
|
|
373
|
+
},
|
|
374
|
+
}), async (args) => {
|
|
375
|
+
await webhookUpdate(args);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
})
|
|
379
|
+
.command('plugins', 'View Neon Auth plugin configurations', (yargs) => {
|
|
380
|
+
return yargs
|
|
381
|
+
.usage('$0 neon-auth plugins <sub-command> [options]')
|
|
382
|
+
.command('list', 'List all plugin configurations', (yargs) => yargs, async (args) => {
|
|
383
|
+
await pluginsList(args);
|
|
384
|
+
})
|
|
385
|
+
.command('get <plugin-name>', 'Get a specific plugin configuration', (yargs) => yargs
|
|
386
|
+
.usage('$0 neon-auth plugins get <plugin-name> [options]')
|
|
387
|
+
.positional('plugin-name', {
|
|
388
|
+
describe: 'Plugin name (e.g. organization, email_provider, email_and_password, oauth_providers, allow_localhost)',
|
|
389
|
+
type: 'string',
|
|
390
|
+
demandOption: true,
|
|
391
|
+
}), async (args) => {
|
|
392
|
+
await pluginsGet(args);
|
|
393
|
+
});
|
|
394
|
+
})
|
|
395
|
+
.command('user', 'Manage Neon Auth users', (yargs) => {
|
|
396
|
+
return yargs
|
|
397
|
+
.usage('$0 neon-auth user <sub-command> [options]')
|
|
398
|
+
.command('create', 'Create an auth user', (yargs) => yargs.options({
|
|
399
|
+
email: {
|
|
400
|
+
describe: 'User email address',
|
|
401
|
+
type: 'string',
|
|
402
|
+
demandOption: true,
|
|
403
|
+
},
|
|
404
|
+
name: {
|
|
405
|
+
describe: 'User display name (defaults to email if not provided)',
|
|
406
|
+
type: 'string',
|
|
407
|
+
},
|
|
408
|
+
}), async (args) => {
|
|
409
|
+
await userCreate(args);
|
|
410
|
+
})
|
|
411
|
+
.command('delete <user-id>', 'Delete an auth user', (yargs) => yargs
|
|
412
|
+
.usage('$0 neon-auth user delete <user-id> [options]')
|
|
413
|
+
.positional('user-id', {
|
|
414
|
+
describe: 'ID of the user to delete',
|
|
415
|
+
type: 'string',
|
|
416
|
+
demandOption: true,
|
|
417
|
+
}), async (args) => {
|
|
418
|
+
await userDelete(args);
|
|
419
|
+
})
|
|
420
|
+
.command('set-role <user-id>', 'Set roles for an auth user', (yargs) => yargs
|
|
421
|
+
.usage('$0 neon-auth user set-role <user-id> [options]')
|
|
422
|
+
.positional('user-id', {
|
|
423
|
+
describe: 'ID of the user to update',
|
|
424
|
+
type: 'string',
|
|
425
|
+
demandOption: true,
|
|
426
|
+
})
|
|
427
|
+
.options({
|
|
428
|
+
roles: {
|
|
429
|
+
describe: 'Roles to assign',
|
|
430
|
+
type: 'string',
|
|
431
|
+
array: true,
|
|
432
|
+
demandOption: true,
|
|
433
|
+
},
|
|
434
|
+
}), async (args) => {
|
|
435
|
+
await userSetRole(args);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
export const handler = (args) => {
|
|
440
|
+
return args;
|
|
441
|
+
};
|
|
442
|
+
const resolveBranch = async (props) => {
|
|
443
|
+
return props.branchId ?? (await branchIdFromProps(props));
|
|
444
|
+
};
|
|
445
|
+
const enable = async (props) => {
|
|
446
|
+
const branchId = await resolveBranch(props);
|
|
447
|
+
let data;
|
|
448
|
+
let alreadyEnabled = false;
|
|
449
|
+
try {
|
|
450
|
+
({ data } = await retryOnLock(() => props.apiClient.createNeonAuth(props.projectId, branchId, {
|
|
451
|
+
auth_provider: NeonAuthSupportedAuthProvider.BetterAuth,
|
|
452
|
+
database_name: props.databaseName,
|
|
453
|
+
})));
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
if (isAxiosError(err) && err.response?.status === 409) {
|
|
457
|
+
alreadyEnabled = true;
|
|
458
|
+
({ data } = await props.apiClient.getNeonAuth(props.projectId, branchId));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
465
|
+
writer(props).end(data, { fields: INTEGRATION_RESPONSE_FIELDS });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Access type-specific fields loosely — CREATE response has schema/table,
|
|
469
|
+
// GET response (already-enabled path) has db_name instead.
|
|
470
|
+
const d = data;
|
|
471
|
+
printKvBlock(alreadyEnabled ? 'Neon Auth is already enabled' : 'Neon Auth enabled', [
|
|
472
|
+
['Auth Provider:', data.auth_provider],
|
|
473
|
+
...(d.db_name ? [['Database: ', d.db_name]] : []),
|
|
474
|
+
['Base URL: ', data.base_url],
|
|
475
|
+
...(d.schema_name
|
|
476
|
+
? [['Schema Name: ', d.schema_name]]
|
|
477
|
+
: []),
|
|
478
|
+
...(d.table_name
|
|
479
|
+
? [['Table Name: ', d.table_name]]
|
|
480
|
+
: []),
|
|
481
|
+
['JWKS URL: ', data.jwks_url],
|
|
482
|
+
]);
|
|
483
|
+
if (data.base_url) {
|
|
484
|
+
process.stdout.write(` ${chalk.green('Set this environment variable in your application:')}\n`);
|
|
485
|
+
process.stdout.write(` NEON_AUTH_BASE_URL=${data.base_url}\n\n`);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const status = async (props) => {
|
|
489
|
+
const branchId = await resolveBranch(props);
|
|
490
|
+
let data;
|
|
491
|
+
try {
|
|
492
|
+
({ data } = await props.apiClient.getNeonAuth(props.projectId, branchId));
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
if (isAxiosError(err) && err.response?.status === 404) {
|
|
496
|
+
printMessage('Neon Auth is not configured for this branch');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
502
|
+
writer(props).end(data, { fields: INTEGRATION_STATUS_FIELDS });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
printKvBlock('Neon Auth status', [
|
|
506
|
+
['Auth Provider:', data.auth_provider],
|
|
507
|
+
['Branch ID: ', data.branch_id],
|
|
508
|
+
['Database: ', data.db_name],
|
|
509
|
+
['Base URL: ', data.base_url],
|
|
510
|
+
['Created At: ', data.created_at],
|
|
511
|
+
['JWKS URL: ', data.jwks_url],
|
|
512
|
+
]);
|
|
513
|
+
};
|
|
514
|
+
const disable = async (props) => {
|
|
515
|
+
const branchId = await resolveBranch(props);
|
|
516
|
+
await retryOnLock(() => props.apiClient.disableNeonAuth(props.projectId, branchId, {
|
|
517
|
+
delete_data: props.deleteData,
|
|
518
|
+
}));
|
|
519
|
+
printMessage('Neon Auth has been disabled');
|
|
520
|
+
};
|
|
521
|
+
// --- OAuth provider ---
|
|
522
|
+
const SHARED_PROVIDER_DISCLAIMER = 'Shared keys are created by the Neon team for development only ' +
|
|
523
|
+
'and should not be used for production apps. It helps you get started, ' +
|
|
524
|
+
'but will show Neon branding (logo and name) on the OAuth consent screen.';
|
|
525
|
+
const oauthProviderList = async (props) => {
|
|
526
|
+
const branchId = await resolveBranch(props);
|
|
527
|
+
const { data } = await props.apiClient.listBranchNeonAuthOauthProviders(props.projectId, branchId);
|
|
528
|
+
if (data.providers.length === 0 && props.output === 'table') {
|
|
529
|
+
printMessage('No OAuth providers are configured for this branch.');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
writer(props).end(data.providers, { fields: OAUTH_PROVIDER_FIELDS });
|
|
533
|
+
const hasShared = data.providers.some((p) => p.type === NeonAuthOauthProviderType.Shared);
|
|
534
|
+
if (hasShared && props.output === 'table') {
|
|
535
|
+
process.stdout.write(`\n${chalk.yellow('Caution:')} ${SHARED_PROVIDER_DISCLAIMER}\n\n`);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const oauthProviderAdd = async (props) => {
|
|
539
|
+
const branchId = await resolveBranch(props);
|
|
540
|
+
let data;
|
|
541
|
+
try {
|
|
542
|
+
({ data } = await props.apiClient.addBranchNeonAuthOauthProvider(props.projectId, branchId, {
|
|
543
|
+
id: props.providerId,
|
|
544
|
+
client_id: props.oauthClientId,
|
|
545
|
+
client_secret: props.oauthClientSecret,
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
if (isAxiosError(err) &&
|
|
550
|
+
err.response?.data?.code ===
|
|
551
|
+
'INVALID_SHARED_OAUTH_PROVIDER') {
|
|
552
|
+
throw new Error(`The "${props.providerId}" provider requires your own OAuth app credentials.\n` +
|
|
553
|
+
`Re-run with --oauth-client-id and --oauth-client-secret to provide them.\n` +
|
|
554
|
+
`Create an OAuth app at your provider and use those credentials.`);
|
|
555
|
+
}
|
|
556
|
+
throw err;
|
|
557
|
+
}
|
|
558
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
559
|
+
writer(props).end(data, { fields: OAUTH_PROVIDER_FIELDS });
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
printKvBlock('OAuth provider added', [
|
|
563
|
+
['ID: ', data.id],
|
|
564
|
+
['Type: ', data.type],
|
|
565
|
+
...(data.client_id
|
|
566
|
+
? [['Client ID: ', data.client_id]]
|
|
567
|
+
: []),
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
await printCallbackInstructions(props, branchId, props.providerId);
|
|
571
|
+
};
|
|
572
|
+
const oauthProviderUpdate = async (props) => {
|
|
573
|
+
const branchId = await resolveBranch(props);
|
|
574
|
+
let data;
|
|
575
|
+
try {
|
|
576
|
+
({ data } = await props.apiClient.updateBranchNeonAuthOauthProvider(props.projectId, branchId, props.providerId, {
|
|
577
|
+
client_id: props.oauthClientId,
|
|
578
|
+
client_secret: props.oauthClientSecret,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
if (isAxiosError(err) &&
|
|
583
|
+
err.response?.data?.code ===
|
|
584
|
+
'INVALID_SHARED_OAUTH_PROVIDER') {
|
|
585
|
+
throw new Error(`The "${props.providerId}" provider requires your own OAuth app credentials.\n` +
|
|
586
|
+
`Re-run with --oauth-client-id and --oauth-client-secret to provide them.\n` +
|
|
587
|
+
`Create an OAuth app at your provider and use those credentials.`);
|
|
588
|
+
}
|
|
589
|
+
throw err;
|
|
590
|
+
}
|
|
591
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
592
|
+
writer(props).end(data, { fields: OAUTH_PROVIDER_FIELDS });
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
printKvBlock('OAuth provider updated', [
|
|
596
|
+
['ID: ', data.id],
|
|
597
|
+
['Type: ', data.type],
|
|
598
|
+
...(data.client_id
|
|
599
|
+
? [['Client ID: ', data.client_id]]
|
|
600
|
+
: []),
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
await printCallbackInstructions(props, branchId, props.providerId);
|
|
604
|
+
};
|
|
605
|
+
const CALLBACK_INSTRUCTIONS = {
|
|
606
|
+
github: {
|
|
607
|
+
lead: 'Create an OAuth app in the GitHub Developer Portal and add the following authorization callback URL:',
|
|
608
|
+
urlLabel: 'callback/github',
|
|
609
|
+
},
|
|
610
|
+
vercel: {
|
|
611
|
+
lead: 'Create a Vercel App in your Vercel Dashboard and add the following authorization callback URL:',
|
|
612
|
+
urlLabel: 'callback/vercel',
|
|
613
|
+
},
|
|
614
|
+
google: {
|
|
615
|
+
lead: 'Get Google credentials by creating an OAuth client in Google Cloud Console > Credentials, and add the following authorized redirect URL:',
|
|
616
|
+
urlLabel: 'callback/google',
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
const printCallbackInstructions = async (props, branchId, providerId) => {
|
|
620
|
+
const instructions = CALLBACK_INSTRUCTIONS[providerId];
|
|
621
|
+
if (!instructions)
|
|
622
|
+
return;
|
|
623
|
+
if (props.output === 'json' || props.output === 'yaml')
|
|
624
|
+
return;
|
|
625
|
+
let baseUrl;
|
|
626
|
+
try {
|
|
627
|
+
const { data } = await props.apiClient.getNeonAuth(props.projectId, branchId);
|
|
628
|
+
baseUrl = data.base_url;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (!baseUrl)
|
|
634
|
+
return;
|
|
635
|
+
const callbackUrl = `${baseUrl.replace(/\/$/, '')}/${instructions.urlLabel}`;
|
|
636
|
+
printKvBlock(instructions.lead, [['URL: ', callbackUrl]]);
|
|
637
|
+
};
|
|
638
|
+
const oauthProviderDelete = async (props) => {
|
|
639
|
+
const branchId = await resolveBranch(props);
|
|
640
|
+
await props.apiClient.deleteBranchNeonAuthOauthProvider(props.projectId, branchId, props.providerId);
|
|
641
|
+
printMessage(`OAuth provider "${props.providerId}" deleted`);
|
|
642
|
+
};
|
|
643
|
+
// --- Domains ---
|
|
644
|
+
const domainList = async (props) => {
|
|
645
|
+
const branchId = await resolveBranch(props);
|
|
646
|
+
const { data } = await props.apiClient.listBranchNeonAuthTrustedDomains(props.projectId, branchId);
|
|
647
|
+
if (data.domains.length === 0 && props.output === 'table') {
|
|
648
|
+
printMessage('No trusted domains are configured for this branch.');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
writer(props).end(data.domains, { fields: DOMAIN_FIELDS });
|
|
652
|
+
};
|
|
653
|
+
const validateDomainUri = (domain) => {
|
|
654
|
+
let url;
|
|
655
|
+
try {
|
|
656
|
+
url = new URL(domain);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
throw new Error(`Invalid domain URI "${domain}". Must be a full URI including scheme, e.g. https://${domain}`);
|
|
660
|
+
}
|
|
661
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
662
|
+
throw new Error(`Invalid domain URI "${domain}". Must use http or https scheme, e.g. https://${url.host}`);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
const domainAdd = async (props) => {
|
|
666
|
+
validateDomainUri(props.domain);
|
|
667
|
+
const branchId = await resolveBranch(props);
|
|
668
|
+
await props.apiClient.addBranchNeonAuthTrustedDomain(props.projectId, branchId, {
|
|
669
|
+
domain: props.domain,
|
|
670
|
+
auth_provider: NeonAuthSupportedAuthProvider.BetterAuth,
|
|
671
|
+
});
|
|
672
|
+
printMessage(`Domain "${props.domain}" added`);
|
|
673
|
+
};
|
|
674
|
+
const domainDelete = async (props) => {
|
|
675
|
+
validateDomainUri(props.domain);
|
|
676
|
+
const branchId = await resolveBranch(props);
|
|
677
|
+
const { data: existing } = await props.apiClient.listBranchNeonAuthTrustedDomains(props.projectId, branchId);
|
|
678
|
+
if (!existing.domains.some((d) => d.domain === props.domain)) {
|
|
679
|
+
throw new Error(`Domain "${props.domain}" is not in the trusted domains list.`);
|
|
680
|
+
}
|
|
681
|
+
await props.apiClient.deleteBranchNeonAuthTrustedDomain(props.projectId, branchId, {
|
|
682
|
+
auth_provider: NeonAuthSupportedAuthProvider.BetterAuth,
|
|
683
|
+
domains: [{ domain: props.domain }],
|
|
684
|
+
});
|
|
685
|
+
printMessage(`Domain "${props.domain}" deleted`);
|
|
686
|
+
};
|
|
687
|
+
// --- Allow localhost ---
|
|
688
|
+
const allowLocalhostGet = async (props) => {
|
|
689
|
+
const branchId = await resolveBranch(props);
|
|
690
|
+
const { data } = await props.apiClient.getNeonAuthAllowLocalhost(props.projectId, branchId);
|
|
691
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
692
|
+
writer(props).end(data, { fields: ALLOW_LOCALHOST_FIELDS });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
printKvBlock('Localhost connection settings', [
|
|
696
|
+
['Allow localhost:', String(data.allow_localhost)],
|
|
697
|
+
]);
|
|
698
|
+
};
|
|
699
|
+
const allowLocalhostEnable = async (props) => {
|
|
700
|
+
const branchId = await resolveBranch(props);
|
|
701
|
+
await props.apiClient.updateNeonAuthAllowLocalhost(props.projectId, branchId, {
|
|
702
|
+
allow_localhost: true,
|
|
703
|
+
});
|
|
704
|
+
printMessage('Localhost connections allowed');
|
|
705
|
+
};
|
|
706
|
+
const allowLocalhostDisable = async (props) => {
|
|
707
|
+
const branchId = await resolveBranch(props);
|
|
708
|
+
await props.apiClient.updateNeonAuthAllowLocalhost(props.projectId, branchId, {
|
|
709
|
+
allow_localhost: false,
|
|
710
|
+
});
|
|
711
|
+
printMessage('Localhost connections restricted');
|
|
712
|
+
};
|
|
713
|
+
// --- Email and password ---
|
|
714
|
+
const printEmailPasswordEntries = (data) => [
|
|
715
|
+
['Enabled: ', String(data.enabled)],
|
|
716
|
+
['Verification Method: ', data.email_verification_method],
|
|
717
|
+
['Require Verification: ', String(data.require_email_verification)],
|
|
718
|
+
[
|
|
719
|
+
'Auto Sign In After Verify: ',
|
|
720
|
+
String(data.auto_sign_in_after_verification),
|
|
721
|
+
],
|
|
722
|
+
[
|
|
723
|
+
'Send Email On Sign Up: ',
|
|
724
|
+
String(data.send_verification_email_on_sign_up),
|
|
725
|
+
],
|
|
726
|
+
[
|
|
727
|
+
'Send Email On Sign In: ',
|
|
728
|
+
String(data.send_verification_email_on_sign_in),
|
|
729
|
+
],
|
|
730
|
+
['Disable Sign Up: ', String(data.disable_sign_up)],
|
|
731
|
+
];
|
|
732
|
+
const emailPasswordGet = async (props) => {
|
|
733
|
+
const branchId = await resolveBranch(props);
|
|
734
|
+
const { data } = await props.apiClient.getNeonAuthEmailAndPasswordConfig(props.projectId, branchId);
|
|
735
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
736
|
+
writer(props).end(data, { fields: EMAIL_PASSWORD_FIELDS });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
printKvBlock('Email & password auth configuration', printEmailPasswordEntries(data));
|
|
740
|
+
};
|
|
741
|
+
const emailPasswordUpdate = async (props) => {
|
|
742
|
+
const branchId = await resolveBranch(props);
|
|
743
|
+
const { data } = await props.apiClient.updateNeonAuthEmailAndPasswordConfig(props.projectId, branchId, {
|
|
744
|
+
enabled: props.enabled,
|
|
745
|
+
email_verification_method: props.emailVerificationMethod,
|
|
746
|
+
require_email_verification: props.requireEmailVerification,
|
|
747
|
+
auto_sign_in_after_verification: props.autoSignInAfterVerification,
|
|
748
|
+
send_verification_email_on_sign_up: props.sendVerificationEmailOnSignUp,
|
|
749
|
+
send_verification_email_on_sign_in: props.sendVerificationEmailOnSignIn,
|
|
750
|
+
disable_sign_up: props.disableSignUp,
|
|
751
|
+
});
|
|
752
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
753
|
+
writer(props).end(data, { fields: EMAIL_PASSWORD_FIELDS });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
printKvBlock('Email & password auth configuration updated', printEmailPasswordEntries(data));
|
|
757
|
+
};
|
|
758
|
+
// --- Email provider ---
|
|
759
|
+
const printEmailProviderEntries = (data) => [
|
|
760
|
+
['Type: ', data.type],
|
|
761
|
+
...(data.type === 'standard'
|
|
762
|
+
? [
|
|
763
|
+
['Host: ', data.host],
|
|
764
|
+
['Port: ', data.port != null ? String(data.port) : undefined],
|
|
765
|
+
['Username: ', data.username],
|
|
766
|
+
]
|
|
767
|
+
: []),
|
|
768
|
+
['Sender Email: ', data.sender_email],
|
|
769
|
+
['Sender Name: ', data.sender_name],
|
|
770
|
+
];
|
|
771
|
+
const emailProviderGet = async (props) => {
|
|
772
|
+
const branchId = await resolveBranch(props);
|
|
773
|
+
const { data } = await props.apiClient.getNeonAuthEmailProvider(props.projectId, branchId);
|
|
774
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
775
|
+
writer(props).end(data, { fields: EMAIL_PROVIDER_FIELDS });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
printKvBlock('Email provider configuration', printEmailProviderEntries(data));
|
|
779
|
+
};
|
|
780
|
+
const emailProviderUpdate = async (props) => {
|
|
781
|
+
if (props.type === 'standard' &&
|
|
782
|
+
(!props.host || !props.port || !props.username || !props.password)) {
|
|
783
|
+
throw new Error('--host, --port, --username, and --password are required for standard email provider');
|
|
784
|
+
}
|
|
785
|
+
const warnSharedSender = props.type === 'shared' && (props.senderEmail || props.senderName);
|
|
786
|
+
const branchId = await resolveBranch(props);
|
|
787
|
+
let config;
|
|
788
|
+
if (props.type === 'standard') {
|
|
789
|
+
config = {
|
|
790
|
+
type: 'standard',
|
|
791
|
+
host: props.host,
|
|
792
|
+
port: props.port,
|
|
793
|
+
username: props.username,
|
|
794
|
+
password: props.password,
|
|
795
|
+
sender_email: props.senderEmail,
|
|
796
|
+
sender_name: props.senderName,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
config = {
|
|
801
|
+
type: 'shared',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
const { data } = await props.apiClient.updateNeonAuthEmailProvider(props.projectId, branchId, config);
|
|
805
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
806
|
+
writer(props).end(data, { fields: EMAIL_PROVIDER_FIELDS });
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
printKvBlock('Email provider configuration updated', printEmailProviderEntries(data));
|
|
810
|
+
}
|
|
811
|
+
if (warnSharedSender) {
|
|
812
|
+
process.stderr.write(`${chalk.yellow('Warning:')} --sender-email and --sender-name are ignored for the shared email provider. ` +
|
|
813
|
+
`These values only take effect with --type standard.\n\n`);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const emailProviderTest = async (props) => {
|
|
817
|
+
const branchId = await resolveBranch(props);
|
|
818
|
+
const { data } = await props.apiClient.sendNeonAuthTestEmail(props.projectId, branchId, {
|
|
819
|
+
recipient_email: props.recipientEmail,
|
|
820
|
+
host: props.host,
|
|
821
|
+
port: props.port,
|
|
822
|
+
username: props.username,
|
|
823
|
+
password: props.password,
|
|
824
|
+
sender_email: props.senderEmail,
|
|
825
|
+
sender_name: props.senderName,
|
|
826
|
+
});
|
|
827
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
828
|
+
writer(props).end(data, { fields: TEST_EMAIL_FIELDS });
|
|
829
|
+
}
|
|
830
|
+
else if (data.success) {
|
|
831
|
+
printMessage('Test email sent successfully');
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
process.stdout.write(`\n${chalk.red('Test email failed')}\n ${data.error_message ?? 'Unknown error'}\n\n`);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
// --- Organization plugin ---
|
|
838
|
+
const printOrganizationEntries = (data) => [
|
|
839
|
+
['Enabled: ', String(data.enabled)],
|
|
840
|
+
['Org Limit: ', String(data.organization_limit)],
|
|
841
|
+
['Creator Role: ', data.creator_role],
|
|
842
|
+
];
|
|
843
|
+
const organizationGet = async (props) => {
|
|
844
|
+
const branchId = await resolveBranch(props);
|
|
845
|
+
const { data } = await props.apiClient.getNeonAuthPluginConfigs(props.projectId, branchId);
|
|
846
|
+
const org = data.organization;
|
|
847
|
+
if (!org) {
|
|
848
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
849
|
+
writer(props).end({}, { fields: ORGANIZATION_FIELDS });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
printMessage('No organization plugin config found.');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
856
|
+
writer(props).end(org, { fields: ORGANIZATION_FIELDS });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
printKvBlock('Organization configuration', printOrganizationEntries(org));
|
|
860
|
+
};
|
|
861
|
+
const organizationUpdate = async (props) => {
|
|
862
|
+
const branchId = await resolveBranch(props);
|
|
863
|
+
const { data } = await props.apiClient.updateNeonAuthOrganizationPlugin(props.projectId, branchId, {
|
|
864
|
+
enabled: props.enabled,
|
|
865
|
+
organization_limit: props.limit,
|
|
866
|
+
creator_role: props.creatorRole,
|
|
867
|
+
});
|
|
868
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
869
|
+
writer(props).end(data, { fields: ORGANIZATION_FIELDS });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
printKvBlock('Organization configuration updated', printOrganizationEntries(data));
|
|
873
|
+
};
|
|
874
|
+
// --- Webhook ---
|
|
875
|
+
const printWebhookEntries = (data) => [
|
|
876
|
+
['Enabled: ', String(data.enabled)],
|
|
877
|
+
['URL: ', data.webhook_url ?? ''],
|
|
878
|
+
['Events: ', (data.enabled_events ?? []).join(', ')],
|
|
879
|
+
[
|
|
880
|
+
'Timeout (sec): ',
|
|
881
|
+
data.timeout_seconds != null ? String(data.timeout_seconds) : '',
|
|
882
|
+
],
|
|
883
|
+
];
|
|
884
|
+
const webhookGet = async (props) => {
|
|
885
|
+
const branchId = await resolveBranch(props);
|
|
886
|
+
const { data } = await props.apiClient.getNeonAuthWebhookConfig(props.projectId, branchId);
|
|
887
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
888
|
+
writer(props).end(data, { fields: WEBHOOK_FIELDS });
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
printKvBlock('Webhook configuration', printWebhookEntries(data));
|
|
892
|
+
};
|
|
893
|
+
const webhookUpdate = async (props) => {
|
|
894
|
+
const branchId = await resolveBranch(props);
|
|
895
|
+
const { data } = await props.apiClient.updateNeonAuthWebhookConfig(props.projectId, branchId, {
|
|
896
|
+
enabled: props.enabled,
|
|
897
|
+
webhook_url: props.url,
|
|
898
|
+
enabled_events: props.enabledEvents,
|
|
899
|
+
timeout_seconds: props.timeout,
|
|
900
|
+
});
|
|
901
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
902
|
+
writer(props).end(data, { fields: WEBHOOK_FIELDS });
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
printKvBlock('Webhook configuration updated', printWebhookEntries(data));
|
|
906
|
+
};
|
|
907
|
+
// --- Plugins ---
|
|
908
|
+
const pluginTitle = (name) => name.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase()) +
|
|
909
|
+
' configuration';
|
|
910
|
+
const formatValue = (v) => {
|
|
911
|
+
if (v == null)
|
|
912
|
+
return '';
|
|
913
|
+
if (typeof v === 'string')
|
|
914
|
+
return v;
|
|
915
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
916
|
+
return String(v);
|
|
917
|
+
return JSON.stringify(v);
|
|
918
|
+
};
|
|
919
|
+
const pluginsList = async (props) => {
|
|
920
|
+
const branchId = await resolveBranch(props);
|
|
921
|
+
const { data } = await props.apiClient.getNeonAuthPluginConfigs(props.projectId, branchId);
|
|
922
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
923
|
+
writer(props).end(data, {
|
|
924
|
+
fields: Object.keys(data),
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const summarize = (value) => {
|
|
929
|
+
if (value == null)
|
|
930
|
+
return 'not configured';
|
|
931
|
+
if (typeof value === 'boolean')
|
|
932
|
+
return String(value);
|
|
933
|
+
if (typeof value === 'string')
|
|
934
|
+
return value;
|
|
935
|
+
if (typeof value === 'number')
|
|
936
|
+
return String(value);
|
|
937
|
+
if (Array.isArray(value))
|
|
938
|
+
return value.length === 1 ? '1 item' : `${String(value.length)} items`;
|
|
939
|
+
if (typeof value === 'object') {
|
|
940
|
+
const obj = value;
|
|
941
|
+
if ('enabled' in obj)
|
|
942
|
+
return obj.enabled ? 'enabled' : 'disabled';
|
|
943
|
+
if ('type' in obj)
|
|
944
|
+
return formatValue(obj.type);
|
|
945
|
+
}
|
|
946
|
+
return JSON.stringify(value);
|
|
947
|
+
};
|
|
948
|
+
const entries = Object.entries(data).map(([key, value]) => [key.padEnd(24), summarize(value)]);
|
|
949
|
+
printKvBlock('Neon Auth plugins', entries);
|
|
950
|
+
};
|
|
951
|
+
const pluginsGet = async (props) => {
|
|
952
|
+
const branchId = await resolveBranch(props);
|
|
953
|
+
const { data } = await props.apiClient.getNeonAuthPluginConfigs(props.projectId, branchId);
|
|
954
|
+
const plugin = data[props.pluginName];
|
|
955
|
+
if (plugin === undefined) {
|
|
956
|
+
const available = Object.keys(data).join(', ');
|
|
957
|
+
throw new Error(`Unknown plugin "${props.pluginName}". Available plugins: ${available}`);
|
|
958
|
+
}
|
|
959
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
960
|
+
const fields = typeof plugin === 'object' && !Array.isArray(plugin)
|
|
961
|
+
? Object.keys(plugin)
|
|
962
|
+
: [];
|
|
963
|
+
writer(props).end(plugin, { fields: fields });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (typeof plugin === 'object' && !Array.isArray(plugin) && plugin != null) {
|
|
967
|
+
const entries = Object.entries(plugin).map(([k, v]) => [
|
|
968
|
+
`${k}:`.padEnd(18),
|
|
969
|
+
formatValue(v),
|
|
970
|
+
]);
|
|
971
|
+
printKvBlock(pluginTitle(props.pluginName), entries);
|
|
972
|
+
}
|
|
973
|
+
else if (Array.isArray(plugin)) {
|
|
974
|
+
const entries = plugin.map((item, i) => [
|
|
975
|
+
`[${i}]:`.padEnd(18),
|
|
976
|
+
typeof item === 'object' ? JSON.stringify(item) : String(item),
|
|
977
|
+
]);
|
|
978
|
+
printKvBlock(pluginTitle(props.pluginName), entries);
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
printKvBlock(pluginTitle(props.pluginName), [
|
|
982
|
+
['Value:'.padEnd(18), String(plugin)],
|
|
983
|
+
]);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
// --- User ---
|
|
987
|
+
const userCreate = async (props) => {
|
|
988
|
+
const branchId = await resolveBranch(props);
|
|
989
|
+
const requestBody = {
|
|
990
|
+
email: props.email,
|
|
991
|
+
name: props.name ?? props.email,
|
|
992
|
+
};
|
|
993
|
+
const { data } = await props.apiClient.createBranchNeonAuthNewUser(props.projectId, branchId, requestBody);
|
|
994
|
+
const displayName = requestBody.name !== props.email ? requestBody.name : undefined;
|
|
995
|
+
printKvBlock('User created', [
|
|
996
|
+
['ID: ', data.id],
|
|
997
|
+
['Email: ', requestBody.email],
|
|
998
|
+
...(displayName ? [['Name: ', displayName]] : []),
|
|
999
|
+
]);
|
|
1000
|
+
};
|
|
1001
|
+
const userDelete = async (props) => {
|
|
1002
|
+
const branchId = await resolveBranch(props);
|
|
1003
|
+
await props.apiClient.deleteBranchNeonAuthUser(props.projectId, branchId, props.userId);
|
|
1004
|
+
printMessage(`User "${props.userId}" deleted`);
|
|
1005
|
+
};
|
|
1006
|
+
const userSetRole = async (props) => {
|
|
1007
|
+
const branchId = await resolveBranch(props);
|
|
1008
|
+
const { data } = await props.apiClient.updateNeonAuthUserRole(props.projectId, branchId, props.userId, { roles: props.roles });
|
|
1009
|
+
printKvBlock('Roles updated', [
|
|
1010
|
+
['User ID: ', data.id],
|
|
1011
|
+
['Roles: ', props.roles.join(', ')],
|
|
1012
|
+
]);
|
|
1013
|
+
};
|