From 4cf861c4a24080c71803c2fe4ced53c9331770dc Mon Sep 17 00:00:00 2001 From: Wes Mason Date: Fri, 3 Jul 2026 11:59:07 +0100 Subject: [PATCH 1/2] 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. --- .server-changes/seed-local-cli-pat.md | 10 ++++++ apps/webapp/seed.ts | 45 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .server-changes/seed-local-cli-pat.md diff --git a/.server-changes/seed-local-cli-pat.md b/.server-changes/seed-local-cli-pat.md new file mode 100644 index 0000000000..f9d39fef69 --- /dev/null +++ b/.server-changes/seed-local-cli-pat.md @@ -0,0 +1,10 @@ +--- +area: webapp +type: improvement +--- + +The db seed now mints (and prints) a Personal Access Token for the seeded +`local@trigger.dev` user. This lets the CLI authenticate against a local +instance via `TRIGGER_ACCESS_TOKEN` without the browser magic-link flow, which +matters for headless/agent onboarding. Idempotent: re-seeding decrypts and +reprints the existing `local-dev-cli` token instead of creating new ones. diff --git a/apps/webapp/seed.ts b/apps/webapp/seed.ts index 47a3202e49..dcec94fbc2 100644 --- a/apps/webapp/seed.ts +++ b/apps/webapp/seed.ts @@ -3,6 +3,9 @@ import { createOrganization } from "./app/models/organization.server"; import { createProject } from "./app/models/project.server"; import type { Organization, Prisma, User } from "@trigger.dev/database"; import { AuthenticationMethod } from "@trigger.dev/database"; +import { encryptToken, decryptToken, hashToken } from "./app/utils/tokens.server"; +import { env } from "./app/env.server"; +import { randomBytes } from "node:crypto"; async function seed() { console.log("🌱 Starting seed..."); @@ -86,11 +89,53 @@ async function seed() { console.log(`User: ${user.email}`); console.log(`Organization: ${organization.title} (${organization.slug})`); console.log(`Projects: ${referenceProjects.map((p) => p.name).join(", ")}`); + + // The PAT is an admin credential. Only mint and print it when seeding a local + // instance, so a stray non-local `db:seed` can't leak it to stdout/logs. + const isLocalInstance = + env.NODE_ENV !== "production" && /localhost|127\.0\.0\.1/.test(env.APP_ORIGIN); + if (isLocalInstance) { + const localPat = await ensureLocalCliPat(user); + console.log(`\nšŸ”‘ CLI access token for ${user.email} (name: ${localPat.name}):`); + console.log(` ${localPat.token}`); + console.log(` Point the CLI at this local instance without a browser login:`); + console.log(` export TRIGGER_ACCESS_TOKEN=${localPat.token}`); + console.log(` export TRIGGER_API_URL=${env.APP_ORIGIN}`); + } console.log("\nāš ļø Note: in your triggerdotdev/references clone, set TRIGGER_PROJECT_REF in:"); console.log(` - projects/d3-chat/.env: TRIGGER_PROJECT_REF=proj_cdmymsrobxmcgjqzhdkq`); console.log(` - projects/realtime-streams/.env: TRIGGER_PROJECT_REF=proj_klxlzjnzxmbgiwuuwhvb`); } +// Mints (or reuses) a Personal Access Token for the seeded local user so the +// CLI can authenticate against this instance without the browser magic-link +// flow. Idempotent: on re-seed we decrypt and reprint the existing token +// rather than piling up new ones. The token is created inline (rather than via +// personalAccessToken.server) so the seed doesn't pull the RBAC/service module +// graph into its import chain. +async function ensureLocalCliPat(user: User) { + const name = "local-dev-cli"; + const existing = await prisma.personalAccessToken.findFirst({ + where: { userId: user.id, name, revokedAt: null }, + }); + if (existing) { + const enc = existing.encryptedToken as { nonce: string; ciphertext: string; tag: string }; + return { name, token: decryptToken(enc.nonce, enc.ciphertext, enc.tag, env.ENCRYPTION_KEY) }; + } + const token = `tr_pat_${randomBytes(20).toString("hex")}`; + const body = token.slice("tr_pat_".length); + await prisma.personalAccessToken.create({ + data: { + name, + userId: user.id, + encryptedToken: encryptToken(token, env.ENCRYPTION_KEY), + hashedToken: hashToken(token), + obfuscatedToken: `tr_pat_${body.slice(0, 4)}${"•".repeat(18)}${body.slice(-4)}`, + }, + }); + return { name, token }; +} + async function createBatchLimitOrgs(user: User) { const org1 = await findOrCreateOrganization("batch-limit-org-1", user, { batchQueueConcurrencyConfig: { processingConcurrency: 1 }, From 159841429c1ecbb73e1a52b6cc0e730d320d7664 Mon Sep 17 00:00:00 2001 From: Wes Mason Date: Fri, 3 Jul 2026 12:53:43 +0100 Subject: [PATCH 2/2] fix: compare env.APP_ORIGIN parsed hostname rathern than substring match Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/webapp/seed.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/seed.ts b/apps/webapp/seed.ts index dcec94fbc2..5f70c37685 100644 --- a/apps/webapp/seed.ts +++ b/apps/webapp/seed.ts @@ -92,8 +92,9 @@ async function seed() { // The PAT is an admin credential. Only mint and print it when seeding a local // instance, so a stray non-local `db:seed` can't leak it to stdout/logs. + const localHostnames = new Set(["localhost", "127.0.0.1", "[::1]"]); const isLocalInstance = - env.NODE_ENV !== "production" && /localhost|127\.0\.0\.1/.test(env.APP_ORIGIN); + env.NODE_ENV !== "production" && localHostnames.has(new URL(env.APP_ORIGIN).hostname); if (isLocalInstance) { const localPat = await ensureLocalCliPat(user); console.log(`\nšŸ”‘ CLI access token for ${user.email} (name: ${localPat.name}):`);