Compare commits
3 Commits
@paperclip
...
@paperclip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c672b71f7f | ||
|
|
01c5a6f198 | ||
|
|
64f5c3f837 |
@@ -75,6 +75,11 @@ export async function bootstrapCeoInvite(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = createDb(dbUrl);
|
const db = createDb(dbUrl);
|
||||||
|
const closableDb = db as typeof db & {
|
||||||
|
$client?: {
|
||||||
|
end?: (options?: { timeout?: number }) => Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const existingAdminCount = await db
|
const existingAdminCount = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -122,5 +127,7 @@ export async function bootstrapCeoInvite(opts: {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
p.log.error(`Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`);
|
p.log.error(`Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
p.log.info("If using embedded-postgres, start the Paperclip server and run this command again.");
|
p.log.info("If using embedded-postgres, start the Paperclip server and run this command again.");
|
||||||
|
} finally {
|
||||||
|
await closableDb.$client?.end?.({ timeout: 5 }).catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,5 +123,6 @@ Notes:
|
|||||||
- Smoke script defaults to `authenticated/private` mode so `HOST=0.0.0.0` can be exposed to the host.
|
- Smoke script defaults to `authenticated/private` mode so `HOST=0.0.0.0` can be exposed to the host.
|
||||||
- Smoke script defaults host port to `3131` to avoid conflicts with local Paperclip on `3100`.
|
- Smoke script defaults host port to `3131` to avoid conflicts with local Paperclip on `3100`.
|
||||||
- Smoke script also defaults `PAPERCLIP_PUBLIC_URL` to `http://localhost:<HOST_PORT>` so bootstrap invite URLs and auth callbacks use the reachable host port instead of the container's internal `3100`.
|
- Smoke script also defaults `PAPERCLIP_PUBLIC_URL` to `http://localhost:<HOST_PORT>` so bootstrap invite URLs and auth callbacks use the reachable host port instead of the container's internal `3100`.
|
||||||
|
- In authenticated mode, the smoke script defaults `SMOKE_AUTO_BOOTSTRAP=true` and drives the real bootstrap path automatically: it signs up a real user, runs `paperclipai auth bootstrap-ceo` inside the container to mint a real bootstrap invite, accepts that invite over HTTP, and verifies board session access.
|
||||||
- Run the script in the foreground to watch the onboarding flow; stop with `Ctrl+C` after validation.
|
- Run the script in the foreground to watch the onboarding flow; stop with `Ctrl+C` after validation.
|
||||||
- The image definition is in `Dockerfile.onboard-smoke`.
|
- The image definition is in `Dockerfile.onboard-smoke`.
|
||||||
|
|||||||
@@ -10,14 +10,198 @@ HOST_UID="${HOST_UID:-$(id -u)}"
|
|||||||
PAPERCLIP_DEPLOYMENT_MODE="${PAPERCLIP_DEPLOYMENT_MODE:-authenticated}"
|
PAPERCLIP_DEPLOYMENT_MODE="${PAPERCLIP_DEPLOYMENT_MODE:-authenticated}"
|
||||||
PAPERCLIP_DEPLOYMENT_EXPOSURE="${PAPERCLIP_DEPLOYMENT_EXPOSURE:-private}"
|
PAPERCLIP_DEPLOYMENT_EXPOSURE="${PAPERCLIP_DEPLOYMENT_EXPOSURE:-private}"
|
||||||
PAPERCLIP_PUBLIC_URL="${PAPERCLIP_PUBLIC_URL:-http://localhost:${HOST_PORT}}"
|
PAPERCLIP_PUBLIC_URL="${PAPERCLIP_PUBLIC_URL:-http://localhost:${HOST_PORT}}"
|
||||||
DOCKER_TTY_ARGS=()
|
SMOKE_AUTO_BOOTSTRAP="${SMOKE_AUTO_BOOTSTRAP:-true}"
|
||||||
|
SMOKE_ADMIN_NAME="${SMOKE_ADMIN_NAME:-Smoke Admin}"
|
||||||
if [[ -t 0 && -t 1 ]]; then
|
SMOKE_ADMIN_EMAIL="${SMOKE_ADMIN_EMAIL:-smoke-admin@paperclip.local}"
|
||||||
DOCKER_TTY_ARGS=(-it)
|
SMOKE_ADMIN_PASSWORD="${SMOKE_ADMIN_PASSWORD:-paperclip-smoke-password}"
|
||||||
fi
|
CONTAINER_NAME="${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}"
|
||||||
|
LOG_PID=""
|
||||||
|
COOKIE_JAR=""
|
||||||
|
TMP_DIR=""
|
||||||
|
|
||||||
mkdir -p "$DATA_DIR"
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$LOG_PID" ]]; then
|
||||||
|
kill "$LOG_PID" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
|
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
wait_for_http() {
|
||||||
|
local url="$1"
|
||||||
|
local attempts="${2:-60}"
|
||||||
|
local sleep_seconds="${3:-1}"
|
||||||
|
local i
|
||||||
|
for ((i = 1; i <= attempts; i += 1)); do
|
||||||
|
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep "$sleep_seconds"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_bootstrap_invite_url() {
|
||||||
|
local bootstrap_output
|
||||||
|
local bootstrap_status
|
||||||
|
if bootstrap_output="$(
|
||||||
|
docker exec \
|
||||||
|
-e PAPERCLIP_DEPLOYMENT_MODE="$PAPERCLIP_DEPLOYMENT_MODE" \
|
||||||
|
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
|
||||||
|
-e PAPERCLIP_PUBLIC_URL="$PAPERCLIP_PUBLIC_URL" \
|
||||||
|
-e PAPERCLIP_HOME="/paperclip" \
|
||||||
|
"$CONTAINER_NAME" bash -lc \
|
||||||
|
'timeout 20s npx --yes "paperclipai@${PAPERCLIPAI_VERSION}" auth bootstrap-ceo --data-dir "$PAPERCLIP_HOME" --base-url "$PAPERCLIP_PUBLIC_URL"' \
|
||||||
|
2>&1
|
||||||
|
)"; then
|
||||||
|
bootstrap_status=0
|
||||||
|
else
|
||||||
|
bootstrap_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $bootstrap_status -ne 0 && $bootstrap_status -ne 124 ]]; then
|
||||||
|
echo "Smoke bootstrap failed: could not run bootstrap-ceo inside container" >&2
|
||||||
|
printf '%s\n' "$bootstrap_output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local invite_url
|
||||||
|
invite_url="$(
|
||||||
|
printf '%s\n' "$bootstrap_output" \
|
||||||
|
| grep -o 'https\?://[^[:space:]]*/invite/pcp_bootstrap_[[:alnum:]]*' \
|
||||||
|
| tail -n 1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$invite_url" ]]; then
|
||||||
|
echo "Smoke bootstrap failed: bootstrap-ceo did not print an invite URL" >&2
|
||||||
|
printf '%s\n' "$bootstrap_output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $bootstrap_status -eq 124 ]]; then
|
||||||
|
echo " Smoke bootstrap: bootstrap-ceo timed out after printing invite URL; continuing" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$invite_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
post_json_with_cookies() {
|
||||||
|
local url="$1"
|
||||||
|
local body="$2"
|
||||||
|
local output_file="$3"
|
||||||
|
curl -sS \
|
||||||
|
-o "$output_file" \
|
||||||
|
-w "%{http_code}" \
|
||||||
|
-c "$COOKIE_JAR" \
|
||||||
|
-b "$COOKIE_JAR" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Origin: $PAPERCLIP_PUBLIC_URL" \
|
||||||
|
-X POST \
|
||||||
|
"$url" \
|
||||||
|
--data "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_with_cookies() {
|
||||||
|
local url="$1"
|
||||||
|
curl -fsS \
|
||||||
|
-c "$COOKIE_JAR" \
|
||||||
|
-b "$COOKIE_JAR" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_up_or_sign_in() {
|
||||||
|
local signup_response="$TMP_DIR/signup.json"
|
||||||
|
local signup_status
|
||||||
|
signup_status="$(post_json_with_cookies \
|
||||||
|
"$PAPERCLIP_PUBLIC_URL/api/auth/sign-up/email" \
|
||||||
|
"{\"name\":\"$SMOKE_ADMIN_NAME\",\"email\":\"$SMOKE_ADMIN_EMAIL\",\"password\":\"$SMOKE_ADMIN_PASSWORD\"}" \
|
||||||
|
"$signup_response")"
|
||||||
|
if [[ "$signup_status" =~ ^2 ]]; then
|
||||||
|
echo " Smoke bootstrap: created admin user $SMOKE_ADMIN_EMAIL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local signin_response="$TMP_DIR/signin.json"
|
||||||
|
local signin_status
|
||||||
|
signin_status="$(post_json_with_cookies \
|
||||||
|
"$PAPERCLIP_PUBLIC_URL/api/auth/sign-in/email" \
|
||||||
|
"{\"email\":\"$SMOKE_ADMIN_EMAIL\",\"password\":\"$SMOKE_ADMIN_PASSWORD\"}" \
|
||||||
|
"$signin_response")"
|
||||||
|
if [[ "$signin_status" =~ ^2 ]]; then
|
||||||
|
echo " Smoke bootstrap: signed in existing admin user $SMOKE_ADMIN_EMAIL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Smoke bootstrap failed: could not sign up or sign in admin user" >&2
|
||||||
|
echo "Sign-up response:" >&2
|
||||||
|
cat "$signup_response" >&2 || true
|
||||||
|
echo >&2
|
||||||
|
echo "Sign-in response:" >&2
|
||||||
|
cat "$signin_response" >&2 || true
|
||||||
|
echo >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_bootstrap_authenticated_smoke() {
|
||||||
|
local health_url="$PAPERCLIP_PUBLIC_URL/api/health"
|
||||||
|
local health_json
|
||||||
|
health_json="$(curl -fsS "$health_url")"
|
||||||
|
if [[ "$health_json" != *'"deploymentMode":"authenticated"'* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sign_up_or_sign_in
|
||||||
|
|
||||||
|
if [[ "$health_json" == *'"bootstrapStatus":"ready"'* ]]; then
|
||||||
|
echo " Smoke bootstrap: instance already ready"
|
||||||
|
else
|
||||||
|
local invite_url
|
||||||
|
invite_url="$(generate_bootstrap_invite_url)"
|
||||||
|
echo " Smoke bootstrap: generated bootstrap invite via auth bootstrap-ceo"
|
||||||
|
|
||||||
|
local invite_token="${invite_url##*/}"
|
||||||
|
local accept_response="$TMP_DIR/accept.json"
|
||||||
|
local accept_status
|
||||||
|
accept_status="$(post_json_with_cookies \
|
||||||
|
"$PAPERCLIP_PUBLIC_URL/api/invites/$invite_token/accept" \
|
||||||
|
'{"requestType":"human"}' \
|
||||||
|
"$accept_response")"
|
||||||
|
if [[ ! "$accept_status" =~ ^2 ]]; then
|
||||||
|
echo "Smoke bootstrap failed: bootstrap invite acceptance returned HTTP $accept_status" >&2
|
||||||
|
cat "$accept_response" >&2 || true
|
||||||
|
echo >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " Smoke bootstrap: accepted bootstrap invite"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local session_json
|
||||||
|
session_json="$(get_with_cookies "$PAPERCLIP_PUBLIC_URL/api/auth/get-session")"
|
||||||
|
if [[ "$session_json" != *'"userId"'* ]]; then
|
||||||
|
echo "Smoke bootstrap failed: no authenticated session after bootstrap" >&2
|
||||||
|
echo "$session_json" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local companies_json
|
||||||
|
companies_json="$(get_with_cookies "$PAPERCLIP_PUBLIC_URL/api/companies")"
|
||||||
|
if [[ "${companies_json:0:1}" != "[" ]]; then
|
||||||
|
echo "Smoke bootstrap failed: board companies endpoint did not return JSON array" >&2
|
||||||
|
echo "$companies_json" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Smoke bootstrap: board session verified"
|
||||||
|
echo " Smoke admin credentials: $SMOKE_ADMIN_EMAIL / $SMOKE_ADMIN_PASSWORD"
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> Building onboard smoke image"
|
echo "==> Building onboard smoke image"
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
|
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
|
||||||
@@ -29,12 +213,15 @@ docker build \
|
|||||||
echo "==> Running onboard smoke container"
|
echo "==> Running onboard smoke container"
|
||||||
echo " UI should be reachable at: http://localhost:$HOST_PORT"
|
echo " UI should be reachable at: http://localhost:$HOST_PORT"
|
||||||
echo " Public URL: $PAPERCLIP_PUBLIC_URL"
|
echo " Public URL: $PAPERCLIP_PUBLIC_URL"
|
||||||
|
echo " Smoke auto-bootstrap: $SMOKE_AUTO_BOOTSTRAP"
|
||||||
echo " Data dir: $DATA_DIR"
|
echo " Data dir: $DATA_DIR"
|
||||||
echo " Deployment: $PAPERCLIP_DEPLOYMENT_MODE/$PAPERCLIP_DEPLOYMENT_EXPOSURE"
|
echo " Deployment: $PAPERCLIP_DEPLOYMENT_MODE/$PAPERCLIP_DEPLOYMENT_EXPOSURE"
|
||||||
echo " Live output: onboard banner and server logs stream in this terminal (Ctrl+C to stop)"
|
echo " Live output: onboard banner and server logs stream in this terminal (Ctrl+C to stop)"
|
||||||
docker run --rm \
|
|
||||||
"${DOCKER_TTY_ARGS[@]}" \
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
--name "${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}" \
|
|
||||||
|
docker run -d --rm \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
-p "$HOST_PORT:3100" \
|
-p "$HOST_PORT:3100" \
|
||||||
-e HOST=0.0.0.0 \
|
-e HOST=0.0.0.0 \
|
||||||
-e PORT=3100 \
|
-e PORT=3100 \
|
||||||
@@ -42,4 +229,21 @@ docker run --rm \
|
|||||||
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
|
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
|
||||||
-e PAPERCLIP_PUBLIC_URL="$PAPERCLIP_PUBLIC_URL" \
|
-e PAPERCLIP_PUBLIC_URL="$PAPERCLIP_PUBLIC_URL" \
|
||||||
-v "$DATA_DIR:/paperclip" \
|
-v "$DATA_DIR:/paperclip" \
|
||||||
"$IMAGE_NAME"
|
"$IMAGE_NAME" >/dev/null
|
||||||
|
|
||||||
|
docker logs -f "$CONTAINER_NAME" &
|
||||||
|
LOG_PID=$!
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/paperclip-onboard-smoke.XXXXXX")"
|
||||||
|
COOKIE_JAR="$TMP_DIR/cookies.txt"
|
||||||
|
|
||||||
|
if ! wait_for_http "$PAPERCLIP_PUBLIC_URL/api/health" 90 1; then
|
||||||
|
echo "Smoke bootstrap failed: server did not become ready at $PAPERCLIP_PUBLIC_URL/api/health" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SMOKE_AUTO_BOOTSTRAP" == "true" && "$PAPERCLIP_DEPLOYMENT_MODE" == "authenticated" ]]; then
|
||||||
|
auto_bootstrap_authenticated_smoke
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait "$LOG_PID"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import type { Db } from "@paperclipai/db";
|
import type { Db } from "@paperclipai/db";
|
||||||
import { count, sql } from "drizzle-orm";
|
import { and, count, eq, gt, isNull, sql } from "drizzle-orm";
|
||||||
import { instanceUserRoles } from "@paperclipai/db";
|
import { instanceUserRoles, invites } from "@paperclipai/db";
|
||||||
import type { DeploymentExposure, DeploymentMode } from "@paperclipai/shared";
|
import type { DeploymentExposure, DeploymentMode } from "@paperclipai/shared";
|
||||||
|
|
||||||
export function healthRoutes(
|
export function healthRoutes(
|
||||||
@@ -27,6 +27,7 @@ export function healthRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bootstrapStatus: "ready" | "bootstrap_pending" = "ready";
|
let bootstrapStatus: "ready" | "bootstrap_pending" = "ready";
|
||||||
|
let bootstrapInviteActive = false;
|
||||||
if (opts.deploymentMode === "authenticated") {
|
if (opts.deploymentMode === "authenticated") {
|
||||||
const roleCount = await db
|
const roleCount = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
@@ -34,6 +35,23 @@ export function healthRoutes(
|
|||||||
.where(sql`${instanceUserRoles.role} = 'instance_admin'`)
|
.where(sql`${instanceUserRoles.role} = 'instance_admin'`)
|
||||||
.then((rows) => Number(rows[0]?.count ?? 0));
|
.then((rows) => Number(rows[0]?.count ?? 0));
|
||||||
bootstrapStatus = roleCount > 0 ? "ready" : "bootstrap_pending";
|
bootstrapStatus = roleCount > 0 ? "ready" : "bootstrap_pending";
|
||||||
|
|
||||||
|
if (bootstrapStatus === "bootstrap_pending") {
|
||||||
|
const now = new Date();
|
||||||
|
const inviteCount = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(invites)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(invites.inviteType, "bootstrap_ceo"),
|
||||||
|
isNull(invites.revokedAt),
|
||||||
|
isNull(invites.acceptedAt),
|
||||||
|
gt(invites.expiresAt, now),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((rows) => Number(rows[0]?.count ?? 0));
|
||||||
|
bootstrapInviteActive = inviteCount > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -42,6 +60,7 @@ export function healthRoutes(
|
|||||||
deploymentExposure: opts.deploymentExposure,
|
deploymentExposure: opts.deploymentExposure,
|
||||||
authReady: opts.authReady,
|
authReady: opts.authReady,
|
||||||
bootstrapStatus,
|
bootstrapStatus,
|
||||||
|
bootstrapInviteActive,
|
||||||
features: {
|
features: {
|
||||||
companyDeletionEnabled: opts.companyDeletionEnabled,
|
companyDeletionEnabled: opts.companyDeletionEnabled,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,14 +32,15 @@ import { queryKeys } from "./lib/queryKeys";
|
|||||||
import { useCompany } from "./context/CompanyContext";
|
import { useCompany } from "./context/CompanyContext";
|
||||||
import { useDialog } from "./context/DialogContext";
|
import { useDialog } from "./context/DialogContext";
|
||||||
|
|
||||||
function BootstrapPendingPage() {
|
function BootstrapPendingPage({ hasActiveInvite = false }: { hasActiveInvite?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-xl py-10">
|
<div className="mx-auto max-w-xl py-10">
|
||||||
<div className="rounded-lg border border-border bg-card p-6">
|
<div className="rounded-lg border border-border bg-card p-6">
|
||||||
<h1 className="text-xl font-semibold">Instance setup required</h1>
|
<h1 className="text-xl font-semibold">Instance setup required</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
No instance admin exists yet. Run this command in your Paperclip environment to generate
|
{hasActiveInvite
|
||||||
the first admin invite URL:
|
? "No instance admin exists yet. A bootstrap invite is already active. Check your Paperclip startup logs for the first admin invite URL, or run this command to rotate it:"
|
||||||
|
: "No instance admin exists yet. Run this command in your Paperclip environment to generate the first admin invite URL:"}
|
||||||
</p>
|
</p>
|
||||||
<pre className="mt-4 overflow-x-auto rounded-md border border-border bg-muted/30 p-3 text-xs">
|
<pre className="mt-4 overflow-x-auto rounded-md border border-border bg-muted/30 p-3 text-xs">
|
||||||
{`pnpm paperclipai auth bootstrap-ceo`}
|
{`pnpm paperclipai auth bootstrap-ceo`}
|
||||||
@@ -55,6 +56,15 @@ function CloudAccessGate() {
|
|||||||
queryKey: queryKeys.health,
|
queryKey: queryKeys.health,
|
||||||
queryFn: () => healthApi.get(),
|
queryFn: () => healthApi.get(),
|
||||||
retry: false,
|
retry: false,
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
const data = query.state.data as
|
||||||
|
| { deploymentMode?: "local_trusted" | "authenticated"; bootstrapStatus?: "ready" | "bootstrap_pending" }
|
||||||
|
| undefined;
|
||||||
|
return data?.deploymentMode === "authenticated" && data.bootstrapStatus === "bootstrap_pending"
|
||||||
|
? 2000
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAuthenticatedMode = healthQuery.data?.deploymentMode === "authenticated";
|
const isAuthenticatedMode = healthQuery.data?.deploymentMode === "authenticated";
|
||||||
@@ -78,7 +88,7 @@ function CloudAccessGate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthenticatedMode && healthQuery.data?.bootstrapStatus === "bootstrap_pending") {
|
if (isAuthenticatedMode && healthQuery.data?.bootstrapStatus === "bootstrap_pending") {
|
||||||
return <BootstrapPendingPage />;
|
return <BootstrapPendingPage hasActiveInvite={healthQuery.data.bootstrapInviteActive} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthenticatedMode && !sessionQuery.data) {
|
if (isAuthenticatedMode && !sessionQuery.data) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type HealthStatus = {
|
|||||||
deploymentExposure?: "private" | "public";
|
deploymentExposure?: "private" | "public";
|
||||||
authReady?: boolean;
|
authReady?: boolean;
|
||||||
bootstrapStatus?: "ready" | "bootstrap_pending";
|
bootstrapStatus?: "ready" | "bootstrap_pending";
|
||||||
|
bootstrapInviteActive?: boolean;
|
||||||
features?: {
|
features?: {
|
||||||
companyDeletionEnabled?: boolean;
|
companyDeletionEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user