Skip to content

Commit 7feda32

Browse files
committed
feat(webapp): seed a local CLI personal access token
Getting the CLI talking to a local instance meant the browser magic-link login, which is no good when you're driving things headlessly (an agent, a container, or just no browser to hand). The seed already prints dev secret keys for the batch-limit orgs, so it now also mints a personal access token for the seeded local@trigger.dev user and prints a ready-to-run TRIGGER_ACCESS_TOKEN export next to them. Re-seeding stays idempotent: it decrypts and reprints the existing local-dev-cli token rather than piling up a new one on every run.
1 parent ea9d6f7 commit 7feda32

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
The db seed now mints (and prints) a Personal Access Token for the seeded
7+
`local@trigger.dev` user. This lets the CLI authenticate against a local
8+
instance via `TRIGGER_ACCESS_TOKEN` without the browser magic-link flow, which
9+
matters for headless/agent onboarding. Idempotent: re-seeding decrypts and
10+
reprints the existing `local-dev-cli` token instead of creating new ones.

apps/webapp/seed.mts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { createOrganization } from "./app/models/organization.server";
33
import { createProject } from "./app/models/project.server";
44
import type { Organization, Prisma, User } from "@trigger.dev/database";
55
import { AuthenticationMethod } from "@trigger.dev/database";
6+
import { encryptToken, decryptToken, hashToken } from "./app/utils/tokens.server";
7+
import { env } from "./app/env.server";
8+
import { randomBytes } from "node:crypto";
69

710
async function seed() {
811
console.log("🌱 Starting seed...");
@@ -81,16 +84,52 @@ async function seed() {
8184
await createBatchLimitOrgs(user);
8285
await ensureDefaultWorkerGroup();
8386

87+
const localPat = await ensureLocalCliPat(user);
88+
8489
console.log("\n🎉 Seed complete!\n");
8590
console.log("Summary:");
8691
console.log(`User: ${user.email}`);
8792
console.log(`Organization: ${organization.title} (${organization.slug})`);
8893
console.log(`Projects: ${referenceProjects.map((p) => p.name).join(", ")}`);
94+
console.log(`\n🔑 CLI access token for ${user.email} (name: ${localPat.name}):`);
95+
console.log(` ${localPat.token}`);
96+
console.log(` Point the CLI at this local instance without a browser login:`);
97+
console.log(` export TRIGGER_ACCESS_TOKEN=${localPat.token}`);
98+
console.log(` export TRIGGER_API_URL=http://localhost:3030`);
8999
console.log("\n⚠️ Note: in your triggerdotdev/references clone, set TRIGGER_PROJECT_REF in:");
90100
console.log(` - projects/d3-chat/.env: TRIGGER_PROJECT_REF=proj_cdmymsrobxmcgjqzhdkq`);
91101
console.log(` - projects/realtime-streams/.env: TRIGGER_PROJECT_REF=proj_klxlzjnzxmbgiwuuwhvb`);
92102
}
93103

104+
// Mints (or reuses) a Personal Access Token for the seeded local user so the
105+
// CLI can authenticate against this instance without the browser magic-link
106+
// flow. Idempotent: on re-seed we decrypt and reprint the existing token
107+
// rather than piling up new ones. The token is created inline (rather than via
108+
// personalAccessToken.server) so the seed doesn't pull the RBAC/service module
109+
// graph into its import chain.
110+
async function ensureLocalCliPat(user: User) {
111+
const name = "local-dev-cli";
112+
const existing = await prisma.personalAccessToken.findFirst({
113+
where: { userId: user.id, name, revokedAt: null },
114+
});
115+
if (existing) {
116+
const enc = existing.encryptedToken as { nonce: string; ciphertext: string; tag: string };
117+
return { name, token: decryptToken(enc.nonce, enc.ciphertext, enc.tag, env.ENCRYPTION_KEY) };
118+
}
119+
const token = `tr_pat_${randomBytes(20).toString("hex")}`;
120+
const body = token.slice("tr_pat_".length);
121+
await prisma.personalAccessToken.create({
122+
data: {
123+
name,
124+
userId: user.id,
125+
encryptedToken: encryptToken(token, env.ENCRYPTION_KEY),
126+
hashedToken: hashToken(token),
127+
obfuscatedToken: `tr_pat_${body.slice(0, 4)}${"•".repeat(18)}${body.slice(-4)}`,
128+
},
129+
});
130+
return { name, token };
131+
}
132+
94133
async function createBatchLimitOrgs(user: User) {
95134
const org1 = await findOrCreateOrganization("batch-limit-org-1", user, {
96135
batchQueueConcurrencyConfig: { processingConcurrency: 1 },

0 commit comments

Comments
 (0)