Add browser-based board CLI auth flow

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-23 07:48:03 -05:00
parent 1376fc8f44
commit 37c2c4acc4
31 changed files with 13299 additions and 19 deletions

View file

@ -3,7 +3,10 @@ import express from "express";
import request from "supertest";
import { boardMutationGuard } from "../middleware/board-mutation-guard.js";
function createApp(actorType: "board" | "agent", boardSource: "session" | "local_implicit" = "session") {
function createApp(
actorType: "board" | "agent",
boardSource: "session" | "local_implicit" | "board_key" = "session",
) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
@ -29,11 +32,26 @@ describe("boardMutationGuard", () => {
expect(res.status).toBe(204);
});
it("blocks board mutations without trusted origin", async () => {
const app = createApp("board");
const res = await request(app).post("/mutate").send({ ok: true });
expect(res.status).toBe(403);
expect(res.body).toEqual({ error: "Board mutation requires trusted browser origin" });
it("blocks board mutations without trusted origin", () => {
const middleware = boardMutationGuard();
const req = {
method: "POST",
actor: { type: "board", userId: "board", source: "session" },
header: () => undefined,
} as any;
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as any;
const next = vi.fn();
middleware(req, res, next);
expect(next).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: "Board mutation requires trusted browser origin",
});
});
it("allows local implicit board mutations without origin", async () => {
@ -42,6 +60,12 @@ describe("boardMutationGuard", () => {
expect(res.status).toBe(204);
});
it("allows board bearer-key mutations without origin", async () => {
const app = createApp("board", "board_key");
const res = await request(app).post("/mutate").send({ ok: true });
expect(res.status).toBe(204);
});
it("allows board mutations from trusted origin", async () => {
const app = createApp("board");
const res = await request(app)

View file

@ -0,0 +1,164 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockAccessService = vi.hoisted(() => ({
isInstanceAdmin: vi.fn(),
hasPermission: vi.fn(),
canUser: vi.fn(),
}));
const mockAgentService = vi.hoisted(() => ({
getById: vi.fn(),
}));
const mockBoardAuthService = vi.hoisted(() => ({
createCliAuthChallenge: vi.fn(),
describeCliAuthChallenge: vi.fn(),
approveCliAuthChallenge: vi.fn(),
cancelCliAuthChallenge: vi.fn(),
resolveBoardAccess: vi.fn(),
assertCurrentBoardKey: vi.fn(),
revokeBoardApiKey: vi.fn(),
}));
const mockLogActivity = vi.hoisted(() => vi.fn());
vi.mock("../services/index.js", () => ({
accessService: () => mockAccessService,
agentService: () => mockAgentService,
boardAuthService: () => mockBoardAuthService,
logActivity: mockLogActivity,
notifyHireApproved: vi.fn(),
deduplicateAgentName: vi.fn((name: string) => name),
}));
function createApp(actor: any) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = actor;
next();
});
return import("../routes/access.js").then(({ accessRoutes }) =>
import("../middleware/index.js").then(({ errorHandler }) => {
app.use(
"/api",
accessRoutes({} as any, {
deploymentMode: "authenticated",
deploymentExposure: "private",
bindHost: "127.0.0.1",
allowedHostnames: [],
}),
);
app.use(errorHandler);
return app;
})
);
}
describe("cli auth routes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("creates a CLI auth challenge with approval metadata", async () => {
mockBoardAuthService.createCliAuthChallenge.mockResolvedValue({
challenge: {
id: "challenge-1",
expiresAt: new Date("2026-03-23T13:00:00.000Z"),
},
challengeSecret: "pcp_cli_auth_secret",
pendingBoardToken: "pcp_board_token",
});
const app = await createApp({ type: "none", source: "none" });
const res = await request(app)
.post("/api/cli-auth/challenges")
.send({
command: "paperclipai company import",
clientName: "paperclipai cli",
requestedAccess: "board",
});
expect(res.status).toBe(201);
expect(res.body).toMatchObject({
id: "challenge-1",
token: "pcp_cli_auth_secret",
boardApiToken: "pcp_board_token",
approvalPath: "/cli-auth/challenge-1?token=pcp_cli_auth_secret",
pollPath: "/cli-auth/challenges/challenge-1",
expiresAt: "2026-03-23T13:00:00.000Z",
});
expect(res.body.approvalUrl).toContain("/cli-auth/challenge-1?token=pcp_cli_auth_secret");
});
it("marks challenge status as requiring sign-in for anonymous viewers", async () => {
mockBoardAuthService.describeCliAuthChallenge.mockResolvedValue({
id: "challenge-1",
status: "pending",
command: "paperclipai company import",
clientName: "paperclipai cli",
requestedAccess: "board",
requestedCompanyId: null,
requestedCompanyName: null,
approvedAt: null,
cancelledAt: null,
expiresAt: "2026-03-23T13:00:00.000Z",
approvedByUser: null,
});
const app = await createApp({ type: "none", source: "none" });
const res = await request(app).get("/api/cli-auth/challenges/challenge-1?token=pcp_cli_auth_secret");
expect(res.status).toBe(200);
expect(res.body.requiresSignIn).toBe(true);
expect(res.body.canApprove).toBe(false);
});
it("approves a CLI auth challenge for a signed-in board user", async () => {
mockBoardAuthService.approveCliAuthChallenge.mockResolvedValue({
status: "approved",
challenge: {
id: "challenge-1",
boardApiKeyId: "board-key-1",
requestedAccess: "board",
requestedCompanyId: "company-1",
expiresAt: new Date("2026-03-23T13:00:00.000Z"),
},
});
mockBoardAuthService.resolveBoardAccess.mockResolvedValue({
user: { id: "user-1", name: "User One", email: "user@example.com" },
companyIds: ["company-1"],
isInstanceAdmin: false,
});
const app = await createApp({
type: "board",
userId: "user-1",
source: "session",
isInstanceAdmin: false,
companyIds: ["company-1"],
});
const res = await request(app)
.post("/api/cli-auth/challenges/challenge-1/approve")
.send({ token: "pcp_cli_auth_secret" });
expect(res.status).toBe(200);
expect(res.body).toEqual({
approved: true,
status: "approved",
userId: "user-1",
keyId: "board-key-1",
expiresAt: "2026-03-23T13:00:00.000Z",
});
expect(mockLogActivity).toHaveBeenCalledTimes(1);
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
companyId: "company-1",
action: "board_api_key.created",
}),
);
});
});

View file

@ -23,11 +23,22 @@ const mockAgentService = vi.hoisted(() => ({
getById: vi.fn(),
}));
const mockBoardAuthService = vi.hoisted(() => ({
createCliAuthChallenge: vi.fn(),
describeCliAuthChallenge: vi.fn(),
approveCliAuthChallenge: vi.fn(),
cancelCliAuthChallenge: vi.fn(),
resolveBoardAccess: vi.fn(),
assertCurrentBoardKey: vi.fn(),
revokeBoardApiKey: vi.fn(),
}));
const mockLogActivity = vi.hoisted(() => vi.fn());
vi.mock("../services/index.js", () => ({
accessService: () => mockAccessService,
agentService: () => mockAgentService,
boardAuthService: () => mockBoardAuthService,
deduplicateAgentName: vi.fn(),
logActivity: mockLogActivity,
notifyHireApproved: vi.fn(),

View file

@ -7,6 +7,7 @@ import { verifyLocalAgentJwt } from "../agent-auth-jwt.js";
import type { DeploymentMode } from "@paperclipai/shared";
import type { BetterAuthSessionResult } from "../auth/better-auth.js";
import { logger } from "./logger.js";
import { boardAuthService } from "../services/board-auth.js";
function hashToken(token: string) {
return createHash("sha256").update(token).digest("hex");
@ -18,6 +19,7 @@ interface ActorMiddlewareOptions {
}
export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHandler {
const boardAuth = boardAuthService(db);
return async (req, _res, next) => {
req.actor =
opts.deploymentMode === "local_trusted"
@ -80,6 +82,25 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
return;
}
const boardKey = await boardAuth.findBoardApiKeyByToken(token);
if (boardKey) {
const access = await boardAuth.resolveBoardAccess(boardKey.userId);
if (access.user) {
await boardAuth.touchBoardApiKey(boardKey.id);
req.actor = {
type: "board",
userId: boardKey.userId,
companyIds: access.companyIds,
isInstanceAdmin: access.isInstanceAdmin,
keyId: boardKey.id,
runId: runIdHeader || undefined,
source: "board_key",
};
next();
return;
}
}
const tokenHash = hashToken(token);
const key = await db
.select()

View file

@ -49,10 +49,9 @@ export function boardMutationGuard(): RequestHandler {
return;
}
// Local-trusted mode uses an implicit board actor for localhost-only development.
// In this mode, origin/referer headers can be omitted by some clients for multipart
// uploads; do not block those mutations.
if (req.actor.source === "local_implicit") {
// Local-trusted mode and board bearer keys are not browser-session requests.
// In these modes, origin/referer headers can be absent; do not block those mutations.
if (req.actor.source === "local_implicit" || req.actor.source === "board_key") {
next();
return;
}

View file

@ -19,10 +19,12 @@ import {
} from "@paperclipai/db";
import {
acceptInviteSchema,
createCliAuthChallengeSchema,
claimJoinRequestApiKeySchema,
createCompanyInviteSchema,
createOpenClawInvitePromptSchema,
listJoinRequestsQuerySchema,
resolveCliAuthChallengeSchema,
updateMemberPermissionsSchema,
updateUserCompanyAccessSchema,
PERMISSION_KEYS
@ -40,6 +42,7 @@ import { validate } from "../middleware/validate.js";
import {
accessService,
agentService,
boardAuthService,
deduplicateAgentName,
logActivity,
notifyHireApproved
@ -95,6 +98,10 @@ function requestBaseUrl(req: Request) {
return `${proto}://${host}`;
}
function buildCliAuthApprovalPath(challengeId: string, token: string) {
return `/cli-auth/${challengeId}?token=${encodeURIComponent(token)}`;
}
function readSkillMarkdown(skillName: string): string | null {
const normalized = skillName.trim().toLowerCase();
if (
@ -1537,6 +1544,7 @@ export function accessRoutes(
) {
const router = Router();
const access = accessService(db);
const boardAuth = boardAuthService(db);
const agents = agentService(db);
async function assertInstanceAdmin(req: Request) {
@ -1594,6 +1602,159 @@ export function accessRoutes(
throw conflict("Board claim challenge is no longer available");
});
router.post(
"/cli-auth/challenges",
validate(createCliAuthChallengeSchema),
async (req, res) => {
const created = await boardAuth.createCliAuthChallenge(req.body);
const approvalPath = buildCliAuthApprovalPath(
created.challenge.id,
created.challengeSecret,
);
const baseUrl = requestBaseUrl(req);
res.status(201).json({
id: created.challenge.id,
token: created.challengeSecret,
boardApiToken: created.pendingBoardToken,
approvalPath,
approvalUrl: baseUrl ? `${baseUrl}${approvalPath}` : null,
pollPath: `/cli-auth/challenges/${created.challenge.id}`,
expiresAt: created.challenge.expiresAt.toISOString(),
suggestedPollIntervalMs: 1000,
});
},
);
router.get("/cli-auth/challenges/:id", async (req, res) => {
const id = (req.params.id as string).trim();
const token =
typeof req.query.token === "string" ? req.query.token.trim() : "";
if (!id || !token) throw notFound("CLI auth challenge not found");
const challenge = await boardAuth.describeCliAuthChallenge(id, token);
if (!challenge) throw notFound("CLI auth challenge not found");
const isSignedInBoardUser =
req.actor.type === "board" &&
(req.actor.source === "session" || isLocalImplicit(req)) &&
Boolean(req.actor.userId);
const canApprove =
isSignedInBoardUser &&
(challenge.requestedAccess !== "instance_admin_required" ||
isLocalImplicit(req) ||
Boolean(req.actor.isInstanceAdmin));
res.json({
...challenge,
requiresSignIn: !isSignedInBoardUser,
canApprove,
currentUserId: req.actor.type === "board" ? req.actor.userId ?? null : null,
});
});
router.post(
"/cli-auth/challenges/:id/approve",
validate(resolveCliAuthChallengeSchema),
async (req, res) => {
const id = (req.params.id as string).trim();
if (
req.actor.type !== "board" ||
(!req.actor.userId && !isLocalImplicit(req))
) {
throw unauthorized("Sign in before approving CLI access");
}
const userId = req.actor.userId ?? "local-board";
const approved = await boardAuth.approveCliAuthChallenge(
id,
req.body.token,
userId,
);
if (approved.status === "approved") {
const accessSnapshot = await boardAuth.resolveBoardAccess(userId);
for (const companyId of accessSnapshot.companyIds) {
await logActivity(db, {
companyId,
actorType: "user",
actorId: userId,
action: "board_api_key.created",
entityType: "user",
entityId: userId,
details: {
boardApiKeyId: approved.challenge.boardApiKeyId,
requestedAccess: approved.challenge.requestedAccess,
requestedCompanyId: approved.challenge.requestedCompanyId,
challengeId: approved.challenge.id,
},
});
}
}
res.json({
approved: approved.status === "approved",
status: approved.status,
userId,
keyId: approved.challenge.boardApiKeyId ?? null,
expiresAt: approved.challenge.expiresAt.toISOString(),
});
},
);
router.post(
"/cli-auth/challenges/:id/cancel",
validate(resolveCliAuthChallengeSchema),
async (req, res) => {
const id = (req.params.id as string).trim();
const cancelled = await boardAuth.cancelCliAuthChallenge(id, req.body.token);
res.json({
status: cancelled.status,
cancelled: cancelled.status === "cancelled",
});
},
);
router.get("/cli-auth/me", async (req, res) => {
if (req.actor.type !== "board" || !req.actor.userId) {
throw unauthorized("Board authentication required");
}
const accessSnapshot = await boardAuth.resolveBoardAccess(req.actor.userId);
res.json({
user: accessSnapshot.user,
userId: req.actor.userId,
isInstanceAdmin: accessSnapshot.isInstanceAdmin,
companyIds: accessSnapshot.companyIds,
source: req.actor.source ?? "none",
keyId: req.actor.source === "board_key" ? req.actor.keyId ?? null : null,
});
});
router.post("/cli-auth/revoke-current", async (req, res) => {
if (req.actor.type !== "board" || req.actor.source !== "board_key") {
throw badRequest("Current board API key context is required");
}
const key = await boardAuth.assertCurrentBoardKey(
req.actor.keyId,
req.actor.userId,
);
await boardAuth.revokeBoardApiKey(key.id);
const accessSnapshot = await boardAuth.resolveBoardAccess(key.userId);
for (const companyId of accessSnapshot.companyIds) {
await logActivity(db, {
companyId,
actorType: "user",
actorId: key.userId,
action: "board_api_key.revoked",
entityType: "user",
entityId: key.userId,
details: {
boardApiKeyId: key.id,
revokedVia: "cli_auth_logout",
},
});
}
res.json({ revoked: true, keyId: key.id });
});
async function assertCompanyPermission(
req: Request,
companyId: string,

View file

@ -0,0 +1,301 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { and, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
authUsers,
boardApiKeys,
cliAuthChallenges,
companies,
companyMemberships,
instanceUserRoles,
} from "@paperclipai/db";
import { conflict, forbidden, notFound } from "../errors.js";
export const BOARD_API_KEY_TTL_MS = 30 * 24 * 60 * 60 * 1000;
export const CLI_AUTH_CHALLENGE_TTL_MS = 10 * 60 * 1000;
export type CliAuthChallengeStatus = "pending" | "approved" | "cancelled" | "expired";
export function hashBearerToken(token: string) {
return createHash("sha256").update(token).digest("hex");
}
export function tokenHashesMatch(left: string, right: string) {
const leftBytes = Buffer.from(left, "utf8");
const rightBytes = Buffer.from(right, "utf8");
return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
}
export function createBoardApiToken() {
return `pcp_board_${randomBytes(24).toString("hex")}`;
}
export function createCliAuthSecret() {
return `pcp_cli_auth_${randomBytes(24).toString("hex")}`;
}
export function boardApiKeyExpiresAt(nowMs: number = Date.now()) {
return new Date(nowMs + BOARD_API_KEY_TTL_MS);
}
export function cliAuthChallengeExpiresAt(nowMs: number = Date.now()) {
return new Date(nowMs + CLI_AUTH_CHALLENGE_TTL_MS);
}
function challengeStatusForRow(row: typeof cliAuthChallenges.$inferSelect): CliAuthChallengeStatus {
if (row.cancelledAt) return "cancelled";
if (row.expiresAt.getTime() <= Date.now()) return "expired";
if (row.approvedAt && row.boardApiKeyId) return "approved";
return "pending";
}
export function boardAuthService(db: Db) {
async function resolveBoardAccess(userId: string) {
const [user, memberships, adminRole] = await Promise.all([
db
.select({
id: authUsers.id,
name: authUsers.name,
email: authUsers.email,
})
.from(authUsers)
.where(eq(authUsers.id, userId))
.then((rows) => rows[0] ?? null),
db
.select({ companyId: companyMemberships.companyId })
.from(companyMemberships)
.where(
and(
eq(companyMemberships.principalType, "user"),
eq(companyMemberships.principalId, userId),
eq(companyMemberships.status, "active"),
),
)
.then((rows) => rows.map((row) => row.companyId)),
db
.select({ id: instanceUserRoles.id })
.from(instanceUserRoles)
.where(and(eq(instanceUserRoles.userId, userId), eq(instanceUserRoles.role, "instance_admin")))
.then((rows) => rows[0] ?? null),
]);
return {
user,
companyIds: memberships,
isInstanceAdmin: Boolean(adminRole),
};
}
async function findBoardApiKeyByToken(token: string) {
const tokenHash = hashBearerToken(token);
const now = new Date();
return db
.select()
.from(boardApiKeys)
.where(
and(
eq(boardApiKeys.keyHash, tokenHash),
isNull(boardApiKeys.revokedAt),
),
)
.then((rows) => rows.find((row) => !row.expiresAt || row.expiresAt.getTime() > now.getTime()) ?? null);
}
async function touchBoardApiKey(id: string) {
await db.update(boardApiKeys).set({ lastUsedAt: new Date() }).where(eq(boardApiKeys.id, id));
}
async function revokeBoardApiKey(id: string) {
const now = new Date();
return db
.update(boardApiKeys)
.set({ revokedAt: now, lastUsedAt: now })
.where(and(eq(boardApiKeys.id, id), isNull(boardApiKeys.revokedAt)))
.returning()
.then((rows) => rows[0] ?? null);
}
async function createCliAuthChallenge(input: {
command: string;
clientName?: string | null;
requestedAccess: "board" | "instance_admin_required";
requestedCompanyId?: string | null;
}) {
const challengeSecret = createCliAuthSecret();
const pendingBoardToken = createBoardApiToken();
const expiresAt = cliAuthChallengeExpiresAt();
const labelBase = input.clientName?.trim() || "paperclipai cli";
const pendingKeyName =
input.requestedAccess === "instance_admin_required"
? `${labelBase} (instance admin)`
: `${labelBase} (board)`;
const created = await db
.insert(cliAuthChallenges)
.values({
secretHash: hashBearerToken(challengeSecret),
command: input.command.trim(),
clientName: input.clientName?.trim() || null,
requestedAccess: input.requestedAccess,
requestedCompanyId: input.requestedCompanyId?.trim() || null,
pendingKeyHash: hashBearerToken(pendingBoardToken),
pendingKeyName,
expiresAt,
})
.returning()
.then((rows) => rows[0]);
return {
challenge: created,
challengeSecret,
pendingBoardToken,
};
}
async function getCliAuthChallenge(id: string) {
return db
.select()
.from(cliAuthChallenges)
.where(eq(cliAuthChallenges.id, id))
.then((rows) => rows[0] ?? null);
}
async function getCliAuthChallengeBySecret(id: string, token: string) {
const challenge = await getCliAuthChallenge(id);
if (!challenge) return null;
if (!tokenHashesMatch(challenge.secretHash, hashBearerToken(token))) return null;
return challenge;
}
async function describeCliAuthChallenge(id: string, token: string) {
const challenge = await getCliAuthChallengeBySecret(id, token);
if (!challenge) return null;
const [company, approvedBy] = await Promise.all([
challenge.requestedCompanyId
? db
.select({ id: companies.id, name: companies.name })
.from(companies)
.where(eq(companies.id, challenge.requestedCompanyId))
.then((rows) => rows[0] ?? null)
: Promise.resolve(null),
challenge.approvedByUserId
? db
.select({ id: authUsers.id, name: authUsers.name, email: authUsers.email })
.from(authUsers)
.where(eq(authUsers.id, challenge.approvedByUserId))
.then((rows) => rows[0] ?? null)
: Promise.resolve(null),
]);
return {
id: challenge.id,
status: challengeStatusForRow(challenge),
command: challenge.command,
clientName: challenge.clientName ?? null,
requestedAccess: challenge.requestedAccess as "board" | "instance_admin_required",
requestedCompanyId: challenge.requestedCompanyId ?? null,
requestedCompanyName: company?.name ?? null,
approvedAt: challenge.approvedAt?.toISOString() ?? null,
cancelledAt: challenge.cancelledAt?.toISOString() ?? null,
expiresAt: challenge.expiresAt.toISOString(),
approvedByUser: approvedBy
? {
id: approvedBy.id,
name: approvedBy.name,
email: approvedBy.email,
}
: null,
};
}
async function approveCliAuthChallenge(id: string, token: string, userId: string) {
const challenge = await getCliAuthChallengeBySecret(id, token);
if (!challenge) throw notFound("CLI auth challenge not found");
const status = challengeStatusForRow(challenge);
if (status === "expired") return { status, challenge };
if (status === "cancelled") return { status, challenge };
const access = await resolveBoardAccess(userId);
if (challenge.requestedAccess === "instance_admin_required" && !access.isInstanceAdmin) {
throw forbidden("Instance admin required");
}
let boardKeyId = challenge.boardApiKeyId;
if (!boardKeyId) {
const createdKey = await db
.insert(boardApiKeys)
.values({
userId,
name: challenge.pendingKeyName,
keyHash: challenge.pendingKeyHash,
expiresAt: boardApiKeyExpiresAt(),
})
.returning()
.then((rows) => rows[0]);
boardKeyId = createdKey.id;
}
const approvedAt = challenge.approvedAt ?? new Date();
const updated = await db
.update(cliAuthChallenges)
.set({
approvedByUserId: userId,
boardApiKeyId: boardKeyId,
approvedAt,
updatedAt: new Date(),
})
.where(eq(cliAuthChallenges.id, challenge.id))
.returning()
.then((rows) => rows[0] ?? challenge);
return { status: "approved" as const, challenge: updated };
}
async function cancelCliAuthChallenge(id: string, token: string) {
const challenge = await getCliAuthChallengeBySecret(id, token);
if (!challenge) throw notFound("CLI auth challenge not found");
const status = challengeStatusForRow(challenge);
if (status === "approved") return { status, challenge };
if (status === "expired") return { status, challenge };
if (status === "cancelled") return { status, challenge };
const updated = await db
.update(cliAuthChallenges)
.set({
cancelledAt: new Date(),
updatedAt: new Date(),
})
.where(eq(cliAuthChallenges.id, challenge.id))
.returning()
.then((rows) => rows[0] ?? challenge);
return { status: "cancelled" as const, challenge: updated };
}
async function assertCurrentBoardKey(keyId: string | undefined, userId: string | undefined) {
if (!keyId || !userId) throw conflict("Board API key context is required");
const key = await db
.select()
.from(boardApiKeys)
.where(and(eq(boardApiKeys.id, keyId), eq(boardApiKeys.userId, userId)))
.then((rows) => rows[0] ?? null);
if (!key || key.revokedAt) throw notFound("Board API key not found");
return key;
}
return {
resolveBoardAccess,
findBoardApiKeyByToken,
touchBoardApiKey,
revokeBoardApiKey,
createCliAuthChallenge,
getCliAuthChallengeBySecret,
describeCliAuthChallenge,
approveCliAuthChallenge,
cancelCliAuthChallenge,
assertCurrentBoardKey,
};
}

View file

@ -19,6 +19,7 @@ export { heartbeatService } from "./heartbeat.js";
export { dashboardService } from "./dashboard.js";
export { sidebarBadgeService } from "./sidebar-badges.js";
export { accessService } from "./access.js";
export { boardAuthService } from "./board-auth.js";
export { instanceSettingsService } from "./instance-settings.js";
export { companyPortabilityService } from "./company-portability.js";
export { executionWorkspaceService } from "./execution-workspaces.js";

View file

@ -12,7 +12,7 @@ declare global {
isInstanceAdmin?: boolean;
keyId?: string;
runId?: string;
source?: "local_implicit" | "session" | "agent_key" | "agent_jwt" | "none";
source?: "local_implicit" | "session" | "board_key" | "agent_key" | "agent_jwt" | "none";
};
}
}