Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_DISCOVERY_URL=https://auth.bosslevel.dev/.well-known/openid-configuration

# Ory Network project slug or UUID (`ory list projects`). When set with
# OIDC_CLIENT_ID, `pnpm dev` registers the portless callback via the ory CLI.
# ORY_PROJECT_ID=confident-sinoussi-hk8lp01xr9

# Optional: refuse script runs without a session once login works
# AUTH_REQUIRED=true

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"packageManager": "pnpm@11.10.0",
"scripts": {
"dev": "node scripts/dev.mjs",
"dev": "node --env-file-if-exists=.env scripts/dev.mjs",
"dev:ports": "turbo run dev",
"build": "turbo run build",
"start": "turbo run start",
Expand Down
12 changes: 12 additions & 0 deletions scripts/dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
//
// The API is not in this list: the frontend serves @repo/api at /api/* from its
// own process. `pnpm --filter @repo/backend serve:portless` runs it standalone.
//
// When ORY_PROJECT_ID + OIDC_CLIENT_ID are set, registers this worktree's OIDC
// callback on the Ory client before servers start (see ory-redirect.mjs).
import { spawn } from "node:child_process"
import { ensureOryRedirectUri } from "./ory-redirect.mjs"
import { portlessName } from "./portless-name.mjs"

const APPS = [["frontend", "returntypes"]]
Expand All @@ -14,6 +18,14 @@ for (const [label, app] of APPS) {
console.log(` ${label.padEnd(9)} ${url}`)
console.log(` ${"api".padEnd(9)} ${url}/api (${url}/api/scalar)`)
}

if (process.env.ORY_PROJECT_ID && process.env.OIDC_CLIENT_ID) {
ensureOryRedirectUri()
} else if (process.env.OIDC_CLIENT_ID && !process.env.ORY_PROJECT_ID) {
console.log(
` ory skip redirect sync (set ORY_PROJECT_ID to register the portless callback)`
)
}
console.log()

const child = spawn("pnpm", ["exec", "turbo", "run", "dev:portless"], {
Expand Down
148 changes: 148 additions & 0 deletions scripts/ory-redirect.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Registers this worktree's portless OIDC callback on the Ory OAuth2 client so
// login works without hand-editing redirect URIs for every branch.
//
// Requires ORY_PROJECT_ID (Ory Network project slug or UUID) and OIDC_CLIENT_ID.
// No-ops when either is unset, or when the `ory` CLI is missing / unauthenticated.
import { spawnSync } from "node:child_process"
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { portlessName } from "./portless-name.mjs"

const CALLBACK_PATH = "/api/auth/oauth2/callback/oidc"

/** Fields Hydra rejects or ignores on replace — strip before --file update. */
const READ_ONLY = new Set([
"AdditionalProperties",
"created_at",
"updated_at",
"client_secret_expires_at",
])

function warn(msg) {
console.warn(` ory: ${msg}`)
}

function ory(args, { quiet = false } = {}) {
const result = spawnSync("ory", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
})
if (result.error) {
if (result.error.code === "ENOENT") {
return {
ok: false,
error: "ory CLI not found — install it or skip ORY_PROJECT_ID",
}
}
return { ok: false, error: result.error.message }
}
if (result.status !== 0) {
const err = (
result.stderr ||
result.stdout ||
`ory exited ${result.status}`
).trim()
return { ok: false, error: quiet ? err.split("\n")[0] : err }
}
return { ok: true, stdout: result.stdout }
}

export function redirectUriForFrontend() {
return `https://${portlessName("returntypes")}.localhost${CALLBACK_PATH}`
}

/**
* Ensure `redirectUri` is on the OAuth2 client. Returns true when the client
* already had it or the update succeeded.
*/
export function ensureOryRedirectUri({
projectId = process.env.ORY_PROJECT_ID,
clientId = process.env.OIDC_CLIENT_ID,
redirectUri = redirectUriForFrontend(),
} = {}) {
if (!(projectId && clientId)) return false

const got = ory(
[
"get",
"oauth2-client",
clientId,
"--project",
projectId,
"--format",
"json",
],
{ quiet: true }
)
if (!got.ok) {
warn(`could not read client: ${got.error}`)
return false
}

let client
try {
client = JSON.parse(got.stdout)
} catch {
warn("could not parse ory get oauth2-client output")
return false
}

const existing = Array.isArray(client.redirect_uris)
? client.redirect_uris
: []
if (existing.includes(redirectUri)) {
console.log(` ory redirect URI already registered`)
console.log(` ${redirectUri}`)
return true
}

const next = {
...Object.fromEntries(
Object.entries(client).filter(([k]) => !READ_ONLY.has(k))
),
redirect_uris: [...existing, redirectUri],
}
// Prefer env secret so a replace cannot blank the stored one.
if (process.env.OIDC_CLIENT_SECRET) {
next.client_secret = process.env.OIDC_CLIENT_SECRET
}

const dir = mkdtempSync(join(tmpdir(), "ory-redirect-"))
const file = join(dir, "client.json")
try {
writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`)
const updated = ory(
[
"update",
"oauth2-client",
clientId,
"--project",
projectId,
"--file",
file,
"--format",
"json",
"-y",
],
{ quiet: true }
)
if (!updated.ok) {
warn(`could not update client: ${updated.error}`)
return false
}
} finally {
rmSync(dir, { recursive: true, force: true })
}

console.log(` ory registered redirect URI`)
console.log(` ${redirectUri}`)
return true
}

if (import.meta.filename === process.argv[1]) {
const ok = ensureOryRedirectUri()
process.exit(
ok || !(process.env.ORY_PROJECT_ID && process.env.OIDC_CLIENT_ID) ? 0 : 1
)
}
5 changes: 5 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"globalPassThroughEnv": [
"ORY_PROJECT_ID",
"OIDC_CLIENT_ID",
"OIDC_CLIENT_SECRET"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
Expand Down