Skip to content

Commit d14dfc2

Browse files
webstackdevPrincesseuhsarah11918
authored
Add a --db-app-token flag to "astro db" execute, push, and verify commands (#15069)
* Add --db-app-token CLI flag to astro db execute, push, and verify commands * Add changeset * Fix type errors in utils.js revealed on build * Bump timeout in unrelated test file to avoid flakiness in Windows-2025/Node 22 check, from 1000ms to 3000ms for two test cases * Add --db-app-token CLI parameter to query command * Update changeset * Update .changeset/happy-rooms-scream.md * Update .changeset/happy-rooms-scream.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
1 parent 9309916 commit d14dfc2

File tree

11 files changed

+134
-15
lines changed

11 files changed

+134
-15
lines changed

.changeset/happy-rooms-scream.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@astrojs/db': minor
3+
---
4+
5+
Adds a `--db-app-token` CLI flag to `astro db` commands `execute`, `push`, `query`, and `verify`
6+
7+
The new Astro DB CLI flags allow you to provide a remote database app token directly instead of `ASTRO_DB_APP_TOKEN`. This ensures that no untrusted code (e.g. CI / CD workflows) has access to the secret that is only needed by the `astro db` commands.
8+
9+
The following command can be used to safely push database configuration changes to your project database:
10+
11+
```
12+
astro db push --db-app-token <token>
13+
```
14+
15+
See the [Astro DB integration documentation](https://docs.astro.build/en/guides/integrations-guide/db/#astro-db-cli-reference) for more information.

packages/astro/test/custom-404-implicit-rerouting.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ for (const caseNumber of [1, 2, 3, 4, 5]) {
2727
});
2828

2929
// sanity check
30-
it('dev server handles normal requests', { timeout: 1000 }, async () => {
30+
it('dev server handles normal requests', { timeout: 3000 }, async () => {
3131
const response = await fixture.fetch('/');
3232
assert.equal(response.status, 200);
3333
});
3434

3535
// IMPORTANT: never skip
36-
it('dev server stays responsive', { timeout: 1000 }, async () => {
36+
it('dev server stays responsive', { timeout: 3000 }, async () => {
3737
const response = await fixture.fetch('/alvsibdlvjks');
3838
assert.equal(response.status, 404);
3939
});
@@ -52,15 +52,15 @@ for (const caseNumber of [1, 2, 3, 4, 5]) {
5252
});
5353

5454
// sanity check
55-
it('prod server handles normal requests', { timeout: 1000 }, async () => {
55+
it('prod server handles normal requests', { timeout: 3000 }, async () => {
5656
const response = await app.render(new Request('https://example.com/'));
5757
assert.equal(response.status, 200);
5858
});
5959

6060
// IMPORTANT: never skip
6161
it(
6262
'prod server stays responsive for case number ' + caseNumber,
63-
{ timeout: 1000 },
63+
{ timeout: 3000 },
6464
async () => {
6565
const response = await app.render(new Request('https://example.com/alvsibdlvjks'));
6666
assert.equal(response.status, 404);

packages/db/src/core/cli/commands/execute/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '../../../integration/vite-plugin-db.js';
1616
import { bundleFile, importBundledFile } from '../../../load-file.js';
1717
import type { DBConfig } from '../../../types.js';
18-
import { getRemoteDatabaseInfo } from '../../../utils.js';
18+
import { getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';
1919

2020
export async function cmd({
2121
astroConfig,
@@ -41,9 +41,10 @@ export async function cmd({
4141
let virtualModContents: string;
4242
if (flags.remote) {
4343
const dbInfo = getRemoteDatabaseInfo();
44+
const appToken = resolveDbAppToken(flags, dbInfo.token);
4445
virtualModContents = getRemoteVirtualModContents({
4546
tables: dbConfig.tables ?? {},
46-
appToken: flags.token ?? dbInfo.token,
47+
appToken,
4748
isBuild: false,
4849
output: 'server',
4950
localExecution: true,

packages/db/src/core/cli/commands/push/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { Arguments } from 'yargs-parser';
55
import { MIGRATION_VERSION } from '../../../consts.js';
66
import { createClient } from '../../../db-client/libsql-node.js';
77
import type { DBConfig, DBSnapshot } from '../../../types.js';
8-
import { getRemoteDatabaseInfo, type RemoteDatabaseInfo } from '../../../utils.js';
8+
import {
9+
getRemoteDatabaseInfo,
10+
type RemoteDatabaseInfo,
11+
resolveDbAppToken,
12+
} from '../../../utils.js';
913
import {
1014
createCurrentSnapshot,
1115
createEmptySnapshot,
@@ -25,7 +29,8 @@ export async function cmd({
2529
const isDryRun = flags.dryRun;
2630
const isForceReset = flags.forceReset;
2731
const dbInfo = getRemoteDatabaseInfo();
28-
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
32+
const appToken = resolveDbAppToken(flags, dbInfo.token);
33+
const productionSnapshot = await getProductionCurrentSnapshot({ ...dbInfo, token: appToken });
2934
const currentSnapshot = createCurrentSnapshot(dbConfig);
3035
const isFromScratch = !productionSnapshot;
3136
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
@@ -67,7 +72,7 @@ export async function cmd({
6772
await pushSchema({
6873
statements: migrationQueries,
6974
dbInfo,
70-
appToken: flags.token ?? dbInfo.token,
75+
appToken,
7176
isDryRun,
7277
currentSnapshot: currentSnapshot,
7378
});

packages/db/src/core/cli/commands/shell/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createClient as createLocalDatabaseClient } from '../../../db-client/li
77
import { createClient as createRemoteDatabaseClient } from '../../../db-client/libsql-node.js';
88
import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js';
99
import type { DBConfigInput } from '../../../types.js';
10-
import { getAstroEnv, getRemoteDatabaseInfo } from '../../../utils.js';
10+
import { getAstroEnv, getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';
1111

1212
export async function cmd({
1313
flags,
@@ -24,7 +24,8 @@ export async function cmd({
2424
}
2525
const dbInfo = getRemoteDatabaseInfo();
2626
if (flags.remote) {
27-
const db = createRemoteDatabaseClient(dbInfo);
27+
const appToken = resolveDbAppToken(flags, dbInfo.token);
28+
const db = createRemoteDatabaseClient({ ...dbInfo, token: appToken });
2829
const result = await db.run(sql.raw(query));
2930
console.log(result);
3031
} else {

packages/db/src/core/cli/commands/verify/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AstroConfig } from 'astro';
22
import type { Arguments } from 'yargs-parser';
33
import type { DBConfig } from '../../../types.js';
4-
import { getRemoteDatabaseInfo } from '../../../utils.js';
4+
import { getRemoteDatabaseInfo, resolveDbAppToken } from '../../../utils.js';
55
import {
66
createCurrentSnapshot,
77
createEmptySnapshot,
@@ -20,7 +20,8 @@ export async function cmd({
2020
}) {
2121
const isJson = flags.json;
2222
const dbInfo = getRemoteDatabaseInfo();
23-
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
23+
const appToken = resolveDbAppToken(flags, dbInfo.token);
24+
const productionSnapshot = await getProductionCurrentSnapshot({ ...dbInfo, token: appToken });
2425
const currentSnapshot = createCurrentSnapshot(dbConfig);
2526
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
2627
oldSnapshot: productionSnapshot || createEmptySnapshot(),

packages/db/src/core/cli/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export async function cli({
1414
// Most commands are `astro db foo`, but for now login/logout
1515
// are also handled by this package, so first check if this is a db command.
1616
const command = args[2] === 'db' ? args[3] : args[2];
17+
validateDbAppTokenFlag(command, flags);
1718
const { dbConfig } = await resolveDbConfig(astroConfig);
1819

1920
switch (command) {
@@ -68,3 +69,14 @@ export async function cli({
6869
}
6970
}
7071
}
72+
73+
function validateDbAppTokenFlag(command: string | undefined, flags: Arguments) {
74+
if (command !== 'execute' && command !== 'push' && command !== 'verify' && command !== 'shell') return;
75+
76+
const dbAppToken = (flags as Arguments & { dbAppToken?: unknown }).dbAppToken;
77+
if (dbAppToken == null) return;
78+
if (typeof dbAppToken !== 'string') {
79+
console.error(`Invalid value for --db-app-token; expected a string.`);
80+
process.exit(1);
81+
}
82+
}

packages/db/src/core/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AstroConfig, AstroIntegration } from 'astro';
22
import { loadEnv } from 'vite';
3+
import type { Arguments } from 'yargs-parser';
34
import './types.js';
45

56
export type VitePlugin = Required<AstroConfig['vite']>['plugins'][number];
@@ -23,6 +24,24 @@ export function getRemoteDatabaseInfo(): RemoteDatabaseInfo {
2324
};
2425
}
2526

27+
export function resolveDbAppToken(
28+
flags: Arguments,
29+
envToken: string,
30+
): string;
31+
export function resolveDbAppToken(
32+
flags: Arguments,
33+
envToken: string | undefined,
34+
): string | undefined;
35+
export function resolveDbAppToken(
36+
flags: Arguments,
37+
envToken: string | undefined,
38+
): string | undefined {
39+
const dbAppToken = (flags as Arguments & { dbAppToken?: unknown }).dbAppToken;
40+
if (typeof dbAppToken === 'string') return dbAppToken;
41+
42+
return envToken;
43+
}
44+
2645
export function getDbDirectoryUrl(root: URL | string) {
2746
return new URL('db/', root);
2847
}

packages/db/test/basics.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { after, before, describe, it } from 'node:test';
33
import { load as cheerioLoad } from 'cheerio';
44
import testAdapter from '../../astro/test/test-adapter.js';
55
import { loadFixture } from '../../astro/test/test-utils.js';
6+
import { resolveDbAppToken } from '../dist/core/utils.js';
67
import { clearEnvironment, setupRemoteDb } from './test-utils.js';
78

89
describe('astro:db', () => {
@@ -200,4 +201,31 @@ describe('astro:db', () => {
200201
assert.equal(ul.children().length, 5);
201202
});
202203
});
204+
205+
describe('cli --db-app-token', () => {
206+
it('Seeds remote database with --db-app-token flag set and without ASTRO_DB_APP_TOKEN env being set', async () => {
207+
clearEnvironment();
208+
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);
209+
210+
const remoteDbServer = await setupRemoteDb(fixture.config, { useDbAppTokenFlag: true });
211+
try {
212+
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);
213+
} finally {
214+
await remoteDbServer.stop();
215+
}
216+
assert.equal(process.env.ASTRO_DB_APP_TOKEN, undefined);
217+
});
218+
});
219+
220+
describe('Precedence for --db-app-token and ASTRO_DB_APP_TOKEN handled correctly', () => {
221+
it('prefers --db-app-token over `ASTRO_DB_APP_TOKEN`', () => {
222+
const flags = /** @type {any} */ ({ _: [], dbAppToken: 'from-flag' });
223+
assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-flag');
224+
});
225+
226+
it('falls back to ASTRO_DB_APP_TOKEN if no flags set', () => {
227+
const flags = /** @type {any} */ ({ _: [] });
228+
assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-env');
229+
});
230+
});
203231
});

packages/db/test/error-handling.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from 'node:assert/strict';
22
import { after, before, describe, it } from 'node:test';
33
import { loadFixture } from '../../astro/test/test-utils.js';
4+
import { cli } from '../dist/core/cli/index.js';
45
import { setupRemoteDb } from './test-utils.js';
56

67
const foreignKeyConstraintError = 'LibsqlError: SQLITE_CONSTRAINT: FOREIGN KEY constraint failed';
@@ -13,6 +14,39 @@ describe('astro:db - error handling', () => {
1314
});
1415
});
1516

17+
it('Errors on invalid --db-app-token input', async () => {
18+
const originalExit = process.exit;
19+
const originalError = console.error;
20+
/** @type {string[]} */
21+
const errorMessages = [];
22+
console.error = (...args) => {
23+
errorMessages.push(args.map(String).join(' '));
24+
};
25+
process.exit = (code) => {
26+
throw new Error(`EXIT_${code}`);
27+
};
28+
29+
try {
30+
await cli({
31+
config: fixture.config,
32+
flags: {
33+
_: [undefined, 'astro', 'db', 'verify'],
34+
dbAppToken: true,
35+
},
36+
});
37+
assert.fail('Expected command to exit');
38+
} catch (err) {
39+
assert.match(String(err), /EXIT_1/);
40+
assert.ok(
41+
errorMessages.some((m) => m.includes('Invalid value for --db-app-token')),
42+
`Expected error output to mention invalid --db-app-token, got: ${errorMessages.join('\n')}`,
43+
);
44+
} finally {
45+
process.exit = originalExit;
46+
console.error = originalError;
47+
}
48+
});
49+
1650
describe('development', () => {
1751
let devServer;
1852

0 commit comments

Comments
 (0)