@newly/cli 0.1.0 → 0.1.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.
@@ -0,0 +1,201 @@
1
+ import { stdin as input, stdout as output } from 'node:process';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { ApiClient, friendlyApiError } from '../api.js';
4
+ import { requireCredentials, resolveProject } from '../config.js';
5
+ function makeClient(project) {
6
+ const creds = requireCredentials();
7
+ return {
8
+ client: new ApiClient(creds.apiUrl, creds.token),
9
+ projectId: resolveProject(process.cwd(), project),
10
+ appUrl: creds.appUrl,
11
+ };
12
+ }
13
+ export function summarizeDomain(candidate) {
14
+ const name = String(candidate.name ?? candidate.domain ?? candidate.domain_name ?? '(unknown)');
15
+ const state = candidate.registrable === true || candidate.available === true
16
+ ? 'available'
17
+ : candidate.registrable === false || candidate.available === false
18
+ ? 'unavailable'
19
+ : 'unknown';
20
+ const pricing = candidate.pricing ? ` ${JSON.stringify(candidate.pricing)}` : '';
21
+ const reason = candidate.reason ? ` ${String(candidate.reason)}` : '';
22
+ return `${name} ${state}${pricing}${reason}`;
23
+ }
24
+ export function renderProjectDomain(domain) {
25
+ return `${domain.id} ${domain.hostname} ${domain.status} ${domain.ownership_mode}`;
26
+ }
27
+ async function confirm(prompt) {
28
+ const rl = createInterface({ input, output });
29
+ try {
30
+ const answer = (await rl.question(`${prompt} Type "yes" to continue: `)).trim().toLowerCase();
31
+ return answer === 'yes';
32
+ }
33
+ finally {
34
+ rl.close();
35
+ }
36
+ }
37
+ export async function runDomainSearch(client, query, limit = '10', log = console.log) {
38
+ const params = new URLSearchParams({ q: query, limit });
39
+ const result = (await client.get(`/domains/search?${params.toString()}`));
40
+ const candidates = result.domains ?? [];
41
+ log(candidates.length ? candidates.map(summarizeDomain).join('\n') : 'No suggestions.');
42
+ }
43
+ export async function runDomainCheck(client, domainNames, log = console.log) {
44
+ const result = (await client.post('/domains/check', { domains: domainNames }));
45
+ const candidates = result.domains ?? [];
46
+ log(candidates.length ? candidates.map(summarizeDomain).join('\n') : 'No results.');
47
+ }
48
+ export async function runDomainBuy(client, projectId, domainName, opts, log = console.log, confirmPurchase = confirm) {
49
+ const quoteResult = (await client.post(`/projects/${projectId}/domains/registrations/quote`, {
50
+ domain: domainName,
51
+ years: opts.years,
52
+ }));
53
+ const quote = quoteResult.quote;
54
+ log(JSON.stringify(quote, null, 2));
55
+ const ok = opts.yes ||
56
+ (await confirmPurchase(`Domain registrations are not refundable once complete. Buy ${domainName}?`));
57
+ if (!ok) {
58
+ log('Cancelled.');
59
+ return;
60
+ }
61
+ const appUrl = (opts.appUrl ?? 'https://newly.app').replace(/\/+$/, '');
62
+ const result = (await client.post(`/projects/${projectId}/domains/registrations/checkout`, {
63
+ domain: domainName,
64
+ years: opts.years,
65
+ success_url: `${appUrl}/project/${projectId}/website?domain_checkout=success`,
66
+ cancel_url: `${appUrl}/project/${projectId}/website?domain_checkout=cancel`,
67
+ }));
68
+ if (!result.checkout_url)
69
+ throw new Error('Backend did not return a checkout URL.');
70
+ log(`Checkout: ${result.checkout_url}`);
71
+ if (result.order?.id)
72
+ log(`Order: ${result.order.id} (${result.order.status ?? 'checkout_created'})`);
73
+ }
74
+ export async function runDomainList(client, projectId, log = console.log) {
75
+ const result = (await client.get(`/projects/${projectId}/domains`));
76
+ const projectDomains = result.domains ?? [];
77
+ log(projectDomains.length ? projectDomains.map(renderProjectDomain).join('\n') : 'No domains.');
78
+ }
79
+ export async function runDomainConnect(client, projectId, hostname, opts, log = console.log) {
80
+ const result = await client.post(`/projects/${projectId}/domains/connect`, {
81
+ hostname,
82
+ deployment_id: opts.deployment,
83
+ ownership_mode: opts.managed ? 'managed_cloudflare' : 'byo_dns',
84
+ });
85
+ log(renderProjectDomain(result.domain));
86
+ log(JSON.stringify(result.dns_instructions, null, 2));
87
+ }
88
+ export async function runDomainStatus(client, projectId, hostnameOrId, log = console.log) {
89
+ const list = (await client.get(`/projects/${projectId}/domains`));
90
+ const projectDomains = list.domains ?? [];
91
+ const domain = hostnameOrId
92
+ ? projectDomains.find((item) => item.id === hostnameOrId || item.hostname === hostnameOrId)
93
+ : projectDomains[0];
94
+ if (!domain)
95
+ throw new Error('Domain not found.');
96
+ const refreshed = await client.post(`/projects/${projectId}/domains/${domain.id}/refresh`, {});
97
+ log(renderProjectDomain(refreshed.domain));
98
+ if (refreshed.dns_instructions)
99
+ log(JSON.stringify(refreshed.dns_instructions, null, 2));
100
+ }
101
+ export function registerDomains(program) {
102
+ const domains = program.command('domains').description('Search, buy, and connect domains');
103
+ domains
104
+ .command('search')
105
+ .argument('<query>')
106
+ .option('--limit <n>', 'number of suggestions', '10')
107
+ .description('Search available domains through Cloudflare Registrar')
108
+ .action(async (query, opts) => {
109
+ try {
110
+ const creds = requireCredentials();
111
+ const client = new ApiClient(creds.apiUrl, creds.token);
112
+ await runDomainSearch(client, query, opts.limit);
113
+ }
114
+ catch (e) {
115
+ console.error(friendlyApiError(e));
116
+ process.exitCode = 1;
117
+ }
118
+ });
119
+ domains
120
+ .command('check')
121
+ .argument('<domains...>')
122
+ .description('Check domain availability through Cloudflare Registrar')
123
+ .action(async (domainNames) => {
124
+ try {
125
+ const creds = requireCredentials();
126
+ const client = new ApiClient(creds.apiUrl, creds.token);
127
+ await runDomainCheck(client, domainNames);
128
+ }
129
+ catch (e) {
130
+ console.error(friendlyApiError(e));
131
+ process.exitCode = 1;
132
+ }
133
+ });
134
+ domains
135
+ .command('buy')
136
+ .argument('<domain>')
137
+ .option('--years <n>', 'registration term in years', '1')
138
+ .option('--project <id>', 'project id (defaults to the linked directory)')
139
+ .option('--yes', 'confirm purchase without an interactive prompt', false)
140
+ .description('Buy a domain through Cloudflare Registrar')
141
+ .action(async (domainName, opts) => {
142
+ try {
143
+ const { client, projectId, appUrl } = makeClient(opts.project);
144
+ await runDomainBuy(client, projectId, domainName, {
145
+ years: Number(opts.years),
146
+ yes: opts.yes,
147
+ appUrl,
148
+ });
149
+ }
150
+ catch (e) {
151
+ console.error(friendlyApiError(e));
152
+ process.exitCode = 1;
153
+ }
154
+ });
155
+ domains
156
+ .command('list')
157
+ .option('--project <id>', 'project id (defaults to the linked directory)')
158
+ .description('List domains connected to a project')
159
+ .action(async (opts) => {
160
+ try {
161
+ const { client, projectId } = makeClient(opts.project);
162
+ await runDomainList(client, projectId);
163
+ }
164
+ catch (e) {
165
+ console.error(friendlyApiError(e));
166
+ process.exitCode = 1;
167
+ }
168
+ });
169
+ domains
170
+ .command('connect')
171
+ .argument('<hostname>')
172
+ .option('--project <id>', 'project id (defaults to the linked directory)')
173
+ .option('--deployment <id>', 'deployment id to serve')
174
+ .option('--managed', 'domain DNS is managed by Newly/Cloudflare', false)
175
+ .description('Connect an existing domain to the published website')
176
+ .action(async (hostname, opts) => {
177
+ try {
178
+ const { client, projectId } = makeClient(opts.project);
179
+ await runDomainConnect(client, projectId, hostname, opts);
180
+ }
181
+ catch (e) {
182
+ console.error(friendlyApiError(e));
183
+ process.exitCode = 1;
184
+ }
185
+ });
186
+ domains
187
+ .command('status')
188
+ .argument('[hostname-or-id]')
189
+ .option('--project <id>', 'project id (defaults to the linked directory)')
190
+ .description('Show or refresh connected domain status')
191
+ .action(async (hostnameOrId, opts) => {
192
+ try {
193
+ const { client, projectId } = makeClient(opts.project);
194
+ await runDomainStatus(client, projectId, hostnameOrId);
195
+ }
196
+ catch (e) {
197
+ console.error(friendlyApiError(e));
198
+ process.exitCode = 1;
199
+ }
200
+ });
201
+ }
@@ -0,0 +1,86 @@
1
+ import { ApiClient, friendlyApiError } from '../api.js';
2
+ import { requireCredentials, resolveProject } from '../config.js';
3
+ import { assertDevOnly } from './misc.js';
4
+ function makeClient(project) {
5
+ const creds = requireCredentials();
6
+ return {
7
+ client: new ApiClient(creds.apiUrl, creds.token),
8
+ projectId: resolveProject(process.cwd(), project),
9
+ };
10
+ }
11
+ export async function getEmailStatus(client, projectId, env) {
12
+ return await client.get(`/newly_backend/email/status?project_id=${encodeURIComponent(projectId)}&env=${env}`);
13
+ }
14
+ export function formatEmailStatus(status) {
15
+ const lines = [
16
+ `email: ${status.configured ? 'configured' : 'not configured'}`,
17
+ `from: ${status.from ?? '(not set)'}`,
18
+ ];
19
+ if (status.requires_deploy)
20
+ lines.push('requires deploy: yes');
21
+ if (status.notes)
22
+ lines.push(`notes: ${status.notes}`);
23
+ return lines.join('\n');
24
+ }
25
+ export async function sendTestEmail(client, opts) {
26
+ return await client.post('/newly_backend/email/test', {
27
+ project_id: opts.projectId,
28
+ env: opts.env,
29
+ to: opts.to,
30
+ from: opts.from,
31
+ subject: opts.subject,
32
+ text: opts.text,
33
+ html: opts.html,
34
+ });
35
+ }
36
+ export function registerEmail(program) {
37
+ const email = program.command('email').description('Manage Newly Email');
38
+ email
39
+ .command('status')
40
+ .description('Show Newly Email configuration for this backend')
41
+ .option('--env <env>', 'dev or prod', 'dev')
42
+ .option('--project <id>', 'project id')
43
+ .action(async (opts) => {
44
+ try {
45
+ assertDevOnly(opts.env, 'email status');
46
+ const { client, projectId } = makeClient(opts.project);
47
+ console.log(formatEmailStatus(await getEmailStatus(client, projectId, opts.env)));
48
+ }
49
+ catch (e) {
50
+ console.error(friendlyApiError(e));
51
+ process.exitCode = 1;
52
+ }
53
+ });
54
+ email
55
+ .command('test')
56
+ .description('Send a test email through the deployed API service')
57
+ .requiredOption('--to <email>', 'recipient email address')
58
+ .option('--from <email>', 'sender email address; defaults to the backend sender setting')
59
+ .option('--subject <subject>', 'email subject', 'Newly email test')
60
+ .option('--text <text>', 'plain-text body', 'This is a test email from your Newly backend.')
61
+ .option('--html <html>', 'optional HTML body')
62
+ .option('--env <env>', 'dev or prod', 'dev')
63
+ .option('--project <id>', 'project id')
64
+ .action(async (opts) => {
65
+ try {
66
+ assertDevOnly(opts.env, 'email test');
67
+ const { client, projectId } = makeClient(opts.project);
68
+ const result = await sendTestEmail(client, {
69
+ projectId,
70
+ env: opts.env,
71
+ to: opts.to,
72
+ from: opts.from,
73
+ subject: opts.subject,
74
+ text: opts.text,
75
+ html: opts.html,
76
+ });
77
+ console.log(result.message_id
78
+ ? `Email sent. message_id=${result.message_id}`
79
+ : 'Email sent.');
80
+ }
81
+ catch (e) {
82
+ console.error(friendlyApiError(e));
83
+ process.exitCode = 1;
84
+ }
85
+ });
86
+ }
@@ -2,7 +2,7 @@ import http from 'node:http';
2
2
  import crypto from 'node:crypto';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { clearCredentials, saveCredentials } from '../config.js';
5
- const DEFAULT_APP_URL = 'https://newly.app';
5
+ const DEFAULT_APP_URL = process.env.NEWLY_APP_URL ?? 'https://newly.app';
6
6
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
7
7
  export async function startCallbackServer(state, appOrigin, timeoutMs) {
8
8
  let resolvePayload;
@@ -96,8 +96,32 @@ export function registerLogin(program) {
96
96
  .description('Log in with your Newly account via the browser')
97
97
  .option('--app-url <url>', 'Newly web app URL', DEFAULT_APP_URL)
98
98
  .option('--manual', 'paste the credential from the browser instead of using the local callback', false)
99
+ .option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
100
+ .option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
101
+ .option('--expires-in <seconds>', 'session lifetime when using --token (default 7 days)', (v) => Number(v), 604800)
99
102
  .action(async (opts) => {
100
103
  try {
104
+ const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
105
+ const hasToken = typeof opts.token === 'string' && opts.token.length > 0;
106
+ if (hasApiUrl !== hasToken) {
107
+ throw new Error('--api-url and --token must be provided together for headless login.');
108
+ }
109
+ if (hasApiUrl && hasToken) {
110
+ try {
111
+ new URL(opts.apiUrl);
112
+ }
113
+ catch {
114
+ throw new Error(`--api-url is not a valid URL: ${opts.apiUrl}`);
115
+ }
116
+ saveCredentials({
117
+ token: opts.token,
118
+ apiUrl: opts.apiUrl,
119
+ appUrl: opts.appUrl,
120
+ expiresAt: Math.floor(Date.now() / 1000) + opts.expiresIn,
121
+ });
122
+ console.log(`Logged in headlessly. Credentials written for API ${opts.apiUrl}.`);
123
+ return;
124
+ }
101
125
  if (opts.manual) {
102
126
  const { createInterface } = await import('node:readline/promises');
103
127
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -30,13 +30,34 @@ export function resolveLogWindow(opts, now = Date.now()) {
30
30
  validateIso(opts.end);
31
31
  return { start, end: opts.end };
32
32
  }
33
- export async function fetchLogs(client, projectId, env, service, n, bounds = {}) {
34
- const body = { project_id: projectId, env, service, n };
33
+ export function formatLogEntry(e) {
34
+ if (!e || typeof e !== 'object')
35
+ return String(e);
36
+ const ts = e.ts ?? '-';
37
+ const sev = (e.severity ?? 'OK').padEnd(5);
38
+ const trace = e.trace_id ? ` trace=${e.trace_id.slice(0, 16)}` : '';
39
+ const name = e.name ?? '';
40
+ return `${ts} ${sev} ${name}${trace}`.trimEnd();
41
+ }
42
+ export async function fetchLogs(client, projectId, env, service, pageSize, bounds = {}, filters = {}) {
43
+ const params = new URLSearchParams();
44
+ params.set('env', env);
45
+ params.set('service', service);
46
+ params.set('page_size', String(pageSize));
35
47
  if (bounds.start)
36
- body.start = bounds.start;
48
+ params.set('start', bounds.start);
37
49
  if (bounds.end)
38
- body.end = bounds.end;
39
- const out = await client.post('/newly_backend/tail_logs', body);
50
+ params.set('end', bounds.end);
51
+ if (filters.severityMin)
52
+ params.set('severity_min', filters.severityMin);
53
+ if (filters.traceId)
54
+ params.set('trace_id', filters.traceId);
55
+ if (filters.textSearch)
56
+ params.set('text_search', filters.textSearch);
57
+ const out = await client.get(`/newly_backend/logs/${encodeURIComponent(projectId)}?${params.toString()}`);
58
+ if (Array.isArray(out.entries)) {
59
+ return out.entries.map((e) => formatLogEntry(e)).join('\n');
60
+ }
40
61
  if (typeof out.logs === 'string')
41
62
  return out.logs;
42
63
  if (Array.isArray(out.lines))
@@ -75,18 +96,24 @@ export function registerMisc(program) {
75
96
  .command('logs')
76
97
  .description('Tail backend service logs')
77
98
  .option('--env <env>', 'dev or prod', 'dev')
78
- .option('--service <service>', 'api or auth', 'api')
79
- .option('-n, --lines <n>', 'number of lines', '200')
99
+ .option('--service <service>', 'api | auth', 'api')
100
+ .option('-n, --lines <n>', 'page size (number of log entries to fetch)', '200')
80
101
  .option('--since <duration>', 'relative window, e.g. 5m, 1h, 30s')
81
102
  .option('--start <iso>', 'window start (ISO 8601 timestamp)')
82
103
  .option('--end <iso>', 'window end (ISO 8601 timestamp)')
104
+ .option('--severity <min>', 'min severity: ok | warn | error')
105
+ .option('--trace <id>', 'restrict to a single trace_id')
106
+ .option('--search <text>', 'substring filter on the entry name')
83
107
  .option('--project <id>', 'project id')
84
108
  .action(async (opts) => {
85
109
  try {
86
110
  assertDevOnly(opts.env, 'logs');
111
+ if (opts.service !== 'api' && opts.service !== 'auth') {
112
+ throw new Error(`--service must be 'api' or 'auth' (got ${JSON.stringify(opts.service)})`);
113
+ }
87
114
  const bounds = resolveLogWindow(opts);
88
115
  const { client, projectId } = makeClient(opts.project);
89
- console.log(await fetchLogs(client, projectId, opts.env, opts.service, Number(opts.lines), bounds));
116
+ console.log(await fetchLogs(client, projectId, opts.env, opts.service, Number(opts.lines), bounds, { severityMin: opts.severity, traceId: opts.trace, textSearch: opts.search }));
90
117
  }
91
118
  catch (e) {
92
119
  console.error(friendlyApiError(e));
@@ -97,7 +124,7 @@ export function registerMisc(program) {
97
124
  secrets
98
125
  .command('list')
99
126
  .option('--env <env>', 'dev or prod', 'dev')
100
- .option('--service <service>', 'api or auth', 'api')
127
+ .option('--service <service>', 'backend service name', 'worker')
101
128
  .option('--project <id>', 'project id')
102
129
  .action(async (opts) => {
103
130
  try {
@@ -115,7 +142,7 @@ export function registerMisc(program) {
115
142
  .argument('<key>')
116
143
  .argument('<value>')
117
144
  .option('--env <env>', 'dev or prod', 'dev')
118
- .option('--service <service>', 'api or auth', 'api')
145
+ .option('--service <service>', 'backend service name', 'worker')
119
146
  .option('--project <id>', 'project id')
120
147
  .action(async (key, value, opts) => {
121
148
  try {
@@ -131,7 +158,7 @@ export function registerMisc(program) {
131
158
  });
132
159
  const db = program.command('db').description('Database helpers');
133
160
  db.command('url')
134
- .description('Print a readonly Postgres URL for the backend database')
161
+ .description('Print a readonly URL for the Newly DB')
135
162
  .option('--env <env>', 'dev or prod', 'dev')
136
163
  .option('--project <id>', 'project id')
137
164
  .action(async (opts) => {
@@ -0,0 +1,39 @@
1
+ import { friendlyApiError } from '../api.js';
2
+ import { readProjectLink, resolveProject } from '../config.js';
3
+ import { readBackendServices, serviceUrl } from '../services-manifest.js';
4
+ export function renderServices(services = readBackendServices(process.cwd()), opts = {}) {
5
+ const lines = ['name type path public url'];
6
+ for (const service of services) {
7
+ const isPublic = service.public !== false;
8
+ const url = opts.projectId && isPublic ? serviceUrl(opts.projectId, opts.env ?? 'dev', service.name) : '-';
9
+ lines.push([
10
+ service.name.padEnd(5),
11
+ service.type.padEnd(7),
12
+ service.path.padEnd(13),
13
+ String(isPublic).padEnd(7),
14
+ url,
15
+ ].join(' '));
16
+ }
17
+ return lines.join('\n');
18
+ }
19
+ export function registerServices(program) {
20
+ const services = program.command('services').description('Inspect backend services');
21
+ services
22
+ .command('list')
23
+ .description('List backend services from backend/newly.services.json')
24
+ .option('--env <env>', 'dev or prod', 'dev')
25
+ .option('--project <id>', 'project id (defaults to the linked directory when present)')
26
+ .action(async (opts) => {
27
+ try {
28
+ if (opts.env !== 'dev' && opts.env !== 'prod')
29
+ throw new Error('--env must be dev or prod');
30
+ const projectId = opts.project ?? readProjectLink(process.cwd())?.projectId;
31
+ const resolved = projectId ? resolveProject(process.cwd(), projectId) : undefined;
32
+ console.log(renderServices(readBackendServices(process.cwd()), { projectId: resolved, env: opts.env }));
33
+ }
34
+ catch (e) {
35
+ console.error(friendlyApiError(e));
36
+ process.exitCode = 1;
37
+ }
38
+ });
39
+ }
@@ -1,25 +1,27 @@
1
1
  import { ApiClient, friendlyApiError } from '../api.js';
2
2
  import { requireCredentials, resolveProject } from '../config.js';
3
+ export async function getStack(client, projectId) {
4
+ return (await client.get(`/newly_backend/stack/${projectId}`));
5
+ }
6
+ export function formatStack(out) {
7
+ const stack = out.stack ?? 'unknown';
8
+ const cli = out.cli_deployable
9
+ ? '✓ CLI-deployable — Newly backend'
10
+ : '✗ not CLI-deployable — use the Newly web app to deploy this project.';
11
+ return `stack: ${stack}\n${cli}`;
12
+ }
3
13
  export function registerStack(program) {
4
14
  program
5
15
  .command('stack')
6
- .description('Show which backend stack a project uses (newly-backend or specular)')
16
+ .description('Show which backend type a project uses and whether it supports CLI deploys')
7
17
  .option('--project <id>', 'project id (defaults to the linked directory)')
8
18
  .action(async (opts) => {
9
19
  try {
10
20
  const creds = requireCredentials();
11
21
  const projectId = resolveProject(process.cwd(), opts.project);
12
22
  const client = new ApiClient(creds.apiUrl, creds.token);
13
- const out = await client.get(`/newly_backend/stack/${projectId}`);
14
- const stack = out.stack ?? 'unknown';
15
- console.log(`stack: ${stack}`);
16
- if (out.cli_deployable) {
17
- console.log('✓ CLI-deployable — Python api-service + Node auth-service');
18
- }
19
- else {
20
- console.log('✗ not CLI-deployable — this project is on the Specular stack ' +
21
- '(a single Node/drizzle backend). `newly deploy` supports only the newly-backend stack.');
22
- }
23
+ const out = await getStack(client, projectId);
24
+ console.log(formatStack(out));
23
25
  }
24
26
  catch (e) {
25
27
  console.error(friendlyApiError(e));
@@ -22,23 +22,23 @@ function renderProvisioningStages(s) {
22
22
  complete: true,
23
23
  },
24
24
  {
25
- label: 'Cloud environment assigned',
26
- note: 'dedicated cloud environment allocated for your backend',
25
+ label: 'Newly server assigned',
26
+ note: 'Newly server workspace allocated for your backend',
27
27
  complete: s.cloud_env_assigned,
28
28
  },
29
29
  {
30
- label: 'Infrastructure provisioned',
31
- note: 'container registry, storage buckets, IAM roles set up',
32
- complete: s.neon_ready,
30
+ label: 'Newly resources provisioned',
31
+ note: 'Newly storage and backend services provisioned',
32
+ complete: s.database_ready,
33
33
  },
34
34
  {
35
- label: 'Database created',
36
- note: 'Postgres database ready for your API service',
37
- complete: s.neon_ready,
35
+ label: 'Newly DB created',
36
+ note: 'Newly DB ready for your backend',
37
+ complete: s.database_ready,
38
38
  },
39
39
  {
40
- label: 'Environment secrets wired',
41
- note: 'DB credentials, API keys and auth tokens encrypted and stored',
40
+ label: 'Backend configuration stored',
41
+ note: 'Newly DB credentials, API keys, and auth settings encrypted and stored',
42
42
  complete: s.secrets_wired,
43
43
  },
44
44
  {