diff --git a/Dockerfile.onboard-smoke b/Dockerfile.onboard-smoke new file mode 100644 index 00000000..2639d834 --- /dev/null +++ b/Dockerfile.onboard-smoke @@ -0,0 +1,29 @@ +FROM ubuntu:24.04 + +ARG NODE_MAJOR=20 +ARG PAPERCLIPAI_VERSION=latest + +ENV DEBIAN_FRONTEND=noninteractive \ + PAPERCLIP_HOME=/paperclip \ + PAPERCLIP_OPEN_ON_LISTEN=false \ + HOST=0.0.0.0 \ + PORT=3100 \ + NODE_MAJOR=${NODE_MAJOR} \ + PAPERCLIPAI_VERSION=${PAPERCLIPAI_VERSION} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +VOLUME ["/paperclip"] +WORKDIR /workspace +EXPOSE 3100 + +CMD ["bash", "-lc", "set -euo pipefail; mkdir -p \"$PAPERCLIP_HOME\"; npx --yes \"paperclipai@${PAPERCLIPAI_VERSION}\" onboard --yes --data-dir \"$PAPERCLIP_HOME\""] diff --git a/doc/DOCKER.md b/doc/DOCKER.md index 404a72e1..3fc9e037 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -66,3 +66,29 @@ Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. + +## Onboard Smoke Test (Ubuntu + npm only) + +Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify: + +- `npx paperclipai onboard --yes` completes +- the server binds to `0.0.0.0:3100` so host access works + +Build + run: + +```sh +./scripts/docker-onboard-smoke.sh +``` + +Open: `http://localhost:3100` + +Useful overrides: + +```sh +HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh +``` + +Notes: + +- Persistent data is mounted at `./data/docker-onboard-smoke` by default. +- The image definition is in `Dockerfile.onboard-smoke`. diff --git a/scripts/docker-onboard-smoke.sh b/scripts/docker-onboard-smoke.sh new file mode 100755 index 00000000..afd9d2f5 --- /dev/null +++ b/scripts/docker-onboard-smoke.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE_NAME="${IMAGE_NAME:-paperclip-onboard-smoke}" +HOST_PORT="${HOST_PORT:-3100}" +PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}" +DATA_DIR="${DATA_DIR:-$REPO_ROOT/data/docker-onboard-smoke}" + +mkdir -p "$DATA_DIR" + +echo "==> Building onboard smoke image" +docker build \ + --build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \ + -f "$REPO_ROOT/Dockerfile.onboard-smoke" \ + -t "$IMAGE_NAME" \ + "$REPO_ROOT" + +echo "==> Running onboard smoke container" +echo " UI should be reachable at: http://localhost:$HOST_PORT" +echo " Data dir: $DATA_DIR" +docker run --rm \ + --name "${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}" \ + -p "$HOST_PORT:3100" \ + -e HOST=0.0.0.0 \ + -e PORT=3100 \ + -v "$DATA_DIR:/paperclip" \ + "$IMAGE_NAME" diff --git a/server/src/index.ts b/server/src/index.ts index 0d09fa74..b5c16645 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4,7 +4,7 @@ import { createServer } from "node:http"; import { resolve } from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; -import type { Request as ExpressRequest } from "express"; +import type { Request as ExpressRequest, RequestHandler } from "express"; import { and, eq } from "drizzle-orm"; import { createDb, @@ -26,12 +26,17 @@ import { heartbeatService } from "./services/index.js"; import { createStorageServiceFromConfig } from "./storage/index.js"; import { printStartupBanner } from "./startup-banner.js"; import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js"; -import { - createBetterAuthHandler, - createBetterAuthInstance, - resolveBetterAuthSession, - resolveBetterAuthSessionFromHeaders, -} from "./auth/better-auth.js"; + +type BetterAuthSessionUser = { + id: string; + email?: string | null; + name?: string | null; +}; + +type BetterAuthSessionResult = { + session: { id: string; userId: string } | null; + user: BetterAuthSessionUser | null; +}; type EmbeddedPostgresInstance = { initialise(): Promise; @@ -388,17 +393,23 @@ if (config.deploymentMode === "authenticated") { } let authReady = config.deploymentMode === "local_trusted"; -let betterAuthHandler: ReturnType | undefined; +let betterAuthHandler: RequestHandler | undefined; let resolveSession: - | ((req: ExpressRequest) => Promise>>) + | ((req: ExpressRequest) => Promise) | undefined; let resolveSessionFromHeaders: - | ((headers: Headers) => Promise>>) + | ((headers: Headers) => Promise) | undefined; if (config.deploymentMode === "local_trusted") { await ensureLocalTrustedBoardPrincipal(db as any); } if (config.deploymentMode === "authenticated") { + const { + createBetterAuthHandler, + createBetterAuthInstance, + resolveBetterAuthSession, + resolveBetterAuthSessionFromHeaders, + } = await import("./auth/better-auth.js"); const betterAuthSecret = process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim(); if (!betterAuthSecret) {