mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add GitHub App manifest registration and OAuth login
Adds one-click GitHub App setup via the manifest flow, OAuth login via Arctic, and cookie-based sessions so the app shell shows the real authenticated user instead of a hardcoded placeholder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c560c608e2
commit
743a4fc677
13 changed files with 386 additions and 32 deletions
10
.env.example
10
.env.example
|
|
@ -7,7 +7,15 @@ export KIMI_API_KEY=
|
|||
export MINIMAX_API_KEY=
|
||||
export OPENAI_API_KEY=
|
||||
export ZAI_API_KEY=
|
||||
|
||||
export ARC_JWT_PRIVATE_KEY=
|
||||
export ARC_JWT_PUBLIC_KEY=
|
||||
export ARC_API_BASE_URL=
|
||||
export ARC_INSECURE_DISABLE_AUTHENTICATION=
|
||||
export ARC_INSECURE_DISABLE_AUTHENTICATION=
|
||||
|
||||
export SESSION_SECRET=
|
||||
export GITHUB_APP_ID=
|
||||
export GITHUB_APP_CLIENT_ID=
|
||||
export GITHUB_APP_CLIENT_SECRET=
|
||||
export GITHUB_APP_WEBHOOK_SECRET=
|
||||
export GITHUB_APP_PRIVATE_KEY=
|
||||
|
|
@ -20,15 +20,19 @@ import {
|
|||
SunIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Link, Outlet, useLocation, useMatches } from "react-router";
|
||||
import { Form, Link, Outlet, redirect, useLocation, useMatches } from "react-router";
|
||||
import { useTheme } from "../lib/theme";
|
||||
import { isGitHubAppConfigured } from "../lib/github.server";
|
||||
import { requireUser } from "../lib/session.server";
|
||||
import type { Route } from "./+types/app-shell";
|
||||
|
||||
const user = {
|
||||
name: "Tom Cook",
|
||||
email: "tom@example.com",
|
||||
imageUrl:
|
||||
"https://avatars.githubusercontent.com/u/19?v=4",
|
||||
};
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
if (!isGitHubAppConfigured()) {
|
||||
throw redirect("/setup");
|
||||
}
|
||||
const user = await requireUser(request);
|
||||
return { user };
|
||||
}
|
||||
|
||||
const navigation = [
|
||||
{ name: "Start", href: "/start", icon: SparklesIcon },
|
||||
|
|
@ -40,13 +44,12 @@ const navigation = [
|
|||
{ name: "Settings", href: "/settings", icon: Cog6ToothIcon },
|
||||
];
|
||||
|
||||
const userNavigation = [{ name: "Sign out", href: "#" }];
|
||||
|
||||
function classNames(...classes: Array<string | false | null | undefined>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function AppShell() {
|
||||
export default function AppShell({ loaderData }: Route.ComponentProps) {
|
||||
const { user } = loaderData;
|
||||
const { pathname } = useLocation();
|
||||
const matches = useMatches();
|
||||
const { theme, toggle } = useTheme();
|
||||
|
|
@ -113,7 +116,7 @@ export default function AppShell() {
|
|||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
alt=""
|
||||
src={user.imageUrl}
|
||||
src={user.avatarUrl}
|
||||
className="size-8 rounded-full outline -outline-offset-1 outline-line-strong"
|
||||
/>
|
||||
</MenuButton>
|
||||
|
|
@ -122,16 +125,16 @@ export default function AppShell() {
|
|||
transition
|
||||
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
|
||||
>
|
||||
{userNavigation.map((item) => (
|
||||
<MenuItem key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="block px-4 py-2 text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
|
||||
<MenuItem>
|
||||
<Form method="POST" action="/auth/logout">
|
||||
<button
|
||||
type="submit"
|
||||
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</MenuItem>
|
||||
))}
|
||||
Sign out
|
||||
</button>
|
||||
</Form>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
|
@ -181,7 +184,7 @@ export default function AppShell() {
|
|||
<div className="shrink-0">
|
||||
<img
|
||||
alt=""
|
||||
src={user.imageUrl}
|
||||
src={user.avatarUrl}
|
||||
className="size-10 rounded-full outline -outline-offset-1 outline-line-strong"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -204,16 +207,15 @@ export default function AppShell() {
|
|||
</button>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1 px-2">
|
||||
{userNavigation.map((item) => (
|
||||
<Form method="POST" action="/auth/logout">
|
||||
<DisclosureButton
|
||||
key={item.name}
|
||||
as="a"
|
||||
href={item.href}
|
||||
className="block rounded-md px-3 py-2 text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
|
||||
as="button"
|
||||
type="submit"
|
||||
className="block w-full rounded-md px-3 py-2 text-left text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
|
||||
>
|
||||
{item.name}
|
||||
Sign out
|
||||
</DisclosureButton>
|
||||
))}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
|
|
|
|||
16
apps/arc-web/app/lib/github.server.ts
Normal file
16
apps/arc-web/app/lib/github.server.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { GitHub, generateState } from "arctic";
|
||||
|
||||
export { generateState };
|
||||
|
||||
export function getGitHubOAuth() {
|
||||
const clientId = process.env.GITHUB_APP_CLIENT_ID;
|
||||
const clientSecret = process.env.GITHUB_APP_CLIENT_SECRET;
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error("GitHub App is not configured");
|
||||
}
|
||||
return new GitHub(clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
export function isGitHubAppConfigured(): boolean {
|
||||
return !!process.env.GITHUB_APP_CLIENT_ID;
|
||||
}
|
||||
59
apps/arc-web/app/lib/session.server.ts
Normal file
59
apps/arc-web/app/lib/session.server.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { createCookieSessionStorage, redirect } from "react-router";
|
||||
|
||||
interface SessionData {
|
||||
githubLogin: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
function getSessionStorage() {
|
||||
const secret = process.env.SESSION_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("SESSION_SECRET is not set");
|
||||
}
|
||||
return createCookieSessionStorage<SessionData>({
|
||||
cookie: {
|
||||
name: "__arc_session",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secrets: [secret],
|
||||
path: "/",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSession(request: Request) {
|
||||
const storage = getSessionStorage();
|
||||
return storage.getSession(request.headers.get("Cookie"));
|
||||
}
|
||||
|
||||
export async function commitSession(session: Awaited<ReturnType<typeof getSession>>) {
|
||||
const storage = getSessionStorage();
|
||||
return storage.commitSession(session);
|
||||
}
|
||||
|
||||
export async function destroySession(session: Awaited<ReturnType<typeof getSession>>) {
|
||||
const storage = getSessionStorage();
|
||||
return storage.destroySession(session);
|
||||
}
|
||||
|
||||
export async function getUser(request: Request) {
|
||||
const session = await getSession(request);
|
||||
const githubLogin = session.get("githubLogin");
|
||||
if (!githubLogin) return null;
|
||||
return {
|
||||
githubLogin,
|
||||
name: session.get("name") ?? githubLogin,
|
||||
email: session.get("email") ?? "",
|
||||
avatarUrl: session.get("avatarUrl") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export async function requireUser(request: Request) {
|
||||
const user = await getUser(request);
|
||||
if (!user) throw redirect("/auth/login");
|
||||
return user;
|
||||
}
|
||||
|
|
@ -7,6 +7,11 @@ import {
|
|||
|
||||
export default [
|
||||
index("routes/redirect-home.tsx"),
|
||||
route("setup", "routes/setup.tsx"),
|
||||
route("setup/callback", "routes/setup-callback.tsx"),
|
||||
route("auth/login", "routes/auth-login.tsx"),
|
||||
route("auth/callback", "routes/auth-callback.tsx"),
|
||||
route("auth/logout", "routes/auth-logout.tsx"),
|
||||
layout("layouts/app-shell.tsx", [
|
||||
route("start", "routes/start.tsx"),
|
||||
route("sessions/:sessionId", "routes/session-detail.tsx"),
|
||||
|
|
|
|||
45
apps/arc-web/app/routes/auth-callback.tsx
Normal file
45
apps/arc-web/app/routes/auth-callback.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { redirect } from "react-router";
|
||||
import { getGitHubOAuth } from "../lib/github.server";
|
||||
import { getSession, commitSession } from "../lib/session.server";
|
||||
import type { Route } from "./+types/auth-callback";
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
|
||||
const cookies = request.headers.get("Cookie") ?? "";
|
||||
const stateMatch = cookies.match(/arc_oauth_state=([^;]+)/);
|
||||
const storedState = stateMatch?.[1];
|
||||
|
||||
if (!code || !state || state !== storedState) {
|
||||
throw redirect("/auth/login");
|
||||
}
|
||||
|
||||
const github = getGitHubOAuth();
|
||||
const tokens = await github.validateAuthorizationCode(code);
|
||||
const accessToken = tokens.accessToken();
|
||||
|
||||
const userResponse = await fetch("https://api.github.com/user", {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const profile = (await userResponse.json()) as {
|
||||
login: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
const session = await getSession(request);
|
||||
session.set("githubLogin", profile.login);
|
||||
session.set("name", profile.name ?? profile.login);
|
||||
session.set("email", profile.email ?? "");
|
||||
session.set("avatarUrl", profile.avatar_url);
|
||||
session.set("accessToken", accessToken);
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("Set-Cookie", await commitSession(session));
|
||||
headers.append("Set-Cookie", "arc_oauth_state=; HttpOnly; Path=/; Max-Age=0");
|
||||
|
||||
return redirect("/start", { headers });
|
||||
}
|
||||
36
apps/arc-web/app/routes/auth-login.tsx
Normal file
36
apps/arc-web/app/routes/auth-login.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { redirect } from "react-router";
|
||||
import { getGitHubOAuth, generateState } from "../lib/github.server";
|
||||
import type { Route } from "./+types/auth-login";
|
||||
|
||||
export function action({ request }: Route.ActionArgs) {
|
||||
const github = getGitHubOAuth();
|
||||
const state = generateState();
|
||||
const authUrl = github.createAuthorizationURL(state, ["read:user", "user:email"]);
|
||||
|
||||
return redirect(authUrl.toString(), {
|
||||
headers: {
|
||||
"Set-Cookie": `arc_oauth_state=${state}; HttpOnly; Path=/; Max-Age=600; SameSite=Lax`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function AuthLogin() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-page">
|
||||
<div className="w-full max-w-md rounded-lg border border-line-strong bg-panel p-8 shadow-sm">
|
||||
<h1 className="text-xl font-semibold text-fg">Sign in to Arc</h1>
|
||||
<p className="mt-2 text-sm text-fg-muted">
|
||||
Authenticate with your GitHub account to continue.
|
||||
</p>
|
||||
<form method="POST" className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-md bg-teal-600 px-4 py-2 text-sm font-medium text-white hover:bg-teal-500 focus:outline-2 focus:outline-offset-2 focus:outline-teal-500"
|
||||
>
|
||||
Sign in with GitHub
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/arc-web/app/routes/auth-logout.tsx
Normal file
10
apps/arc-web/app/routes/auth-logout.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { redirect } from "react-router";
|
||||
import { getSession, destroySession } from "../lib/session.server";
|
||||
import type { Route } from "./+types/auth-logout";
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
const session = await getSession(request);
|
||||
return redirect("/auth/login", {
|
||||
headers: { "Set-Cookie": await destroySession(session) },
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,15 @@
|
|||
import { redirect } from "react-router";
|
||||
import { isGitHubAppConfigured } from "../lib/github.server";
|
||||
import { getUser } from "../lib/session.server";
|
||||
import type { Route } from "./+types/redirect-home";
|
||||
|
||||
export function loader() {
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
if (!isGitHubAppConfigured()) {
|
||||
return redirect("/setup");
|
||||
}
|
||||
const user = await getUser(request);
|
||||
if (!user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
return redirect("/start");
|
||||
}
|
||||
|
|
|
|||
87
apps/arc-web/app/routes/setup-callback.tsx
Normal file
87
apps/arc-web/app/routes/setup-callback.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { Route } from "./+types/setup-callback";
|
||||
|
||||
const ENV_PATH = resolve(import.meta.dirname, "../../../../.env");
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
if (!code) {
|
||||
return { success: false, error: "Missing code parameter" };
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/app-manifests/${code}/conversions`,
|
||||
{ method: "POST", headers: { Accept: "application/vnd.github+json" } }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `GitHub API error: ${response.status} ${body}`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
id: number;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
webhook_secret: string;
|
||||
pem: string;
|
||||
};
|
||||
|
||||
const sessionSecret = randomBytes(32).toString("hex");
|
||||
|
||||
let existing = "";
|
||||
try {
|
||||
existing = await readFile(ENV_PATH, "utf-8");
|
||||
} catch {
|
||||
// file doesn't exist yet
|
||||
}
|
||||
|
||||
const newVars = [
|
||||
`export SESSION_SECRET=${sessionSecret}`,
|
||||
`export GITHUB_APP_ID=${data.id}`,
|
||||
`export GITHUB_APP_CLIENT_ID=${data.client_id}`,
|
||||
`export GITHUB_APP_CLIENT_SECRET=${data.client_secret}`,
|
||||
`export GITHUB_APP_WEBHOOK_SECRET=${data.webhook_secret}`,
|
||||
`export GITHUB_APP_PRIVATE_KEY="${data.pem}"`,
|
||||
].join("\n");
|
||||
|
||||
const envContent = existing ? `${existing.trimEnd()}\n\n${newVars}\n` : `${newVars}\n`;
|
||||
await writeFile(ENV_PATH, envContent, "utf-8");
|
||||
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
export default function SetupCallback({ loaderData }: Route.ComponentProps) {
|
||||
const { success, error } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-page">
|
||||
<div className="w-full max-w-md rounded-lg border border-line-strong bg-panel p-8 shadow-sm">
|
||||
{success ? (
|
||||
<>
|
||||
<h1 className="text-xl font-semibold text-fg">
|
||||
GitHub App registered
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-fg-muted">
|
||||
Credentials have been written to <code>.env</code>. Restart the
|
||||
app to pick up the new configuration.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-xl font-semibold text-red-500">
|
||||
Setup failed
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-fg-muted">{error}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/arc-web/app/routes/setup.tsx
Normal file
60
apps/arc-web/app/routes/setup.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { redirect } from "react-router";
|
||||
import { isGitHubAppConfigured } from "../lib/github.server";
|
||||
import type { Route } from "./+types/setup";
|
||||
|
||||
export function loader({ request }: Route.LoaderArgs) {
|
||||
if (isGitHubAppConfigured()) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
const suffix = Math.random().toString(16).slice(2, 8);
|
||||
|
||||
const manifest = {
|
||||
name: `Arc-${suffix}`,
|
||||
url: baseUrl,
|
||||
redirect_url: `${baseUrl}/setup/callback`,
|
||||
callback_urls: [`${baseUrl}/auth/callback`],
|
||||
setup_url: `${baseUrl}/setup/callback`,
|
||||
public: false,
|
||||
default_permissions: {
|
||||
contents: "write",
|
||||
metadata: "read",
|
||||
pull_requests: "write",
|
||||
checks: "write",
|
||||
issues: "write",
|
||||
},
|
||||
default_events: [] as string[],
|
||||
};
|
||||
|
||||
return { manifest: JSON.stringify(manifest), baseUrl };
|
||||
}
|
||||
|
||||
export default function Setup({ loaderData }: Route.ComponentProps) {
|
||||
const { manifest } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-page">
|
||||
<div className="w-full max-w-md rounded-lg border border-line-strong bg-panel p-8 shadow-sm">
|
||||
<h1 className="text-xl font-semibold text-fg">Set up Arc</h1>
|
||||
<p className="mt-2 text-sm text-fg-muted">
|
||||
Register a GitHub App to enable OAuth login and repository access.
|
||||
</p>
|
||||
<form
|
||||
method="POST"
|
||||
action="https://github.com/settings/apps/new"
|
||||
className="mt-6"
|
||||
>
|
||||
<input type="hidden" name="manifest" value={manifest} />
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-md bg-teal-600 px-4 py-2 text-sm font-medium text-white hover:bg-teal-500 focus:outline-2 focus:outline-offset-2 focus:outline-teal-500"
|
||||
>
|
||||
Register GitHub App
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,15 +16,16 @@
|
|||
"@headlessui/react": "^2.2.9",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@pierre/diffs": "^1.0.11",
|
||||
"@qltysh/arc-api-client": "workspace:*",
|
||||
"@react-router/node": "7.12.0",
|
||||
"@react-router/serve": "7.12.0",
|
||||
"@viz-js/viz": "^3.24.0",
|
||||
"arctic": "^3.7.0",
|
||||
"isbot": "^5.1.31",
|
||||
"jose": "^6.1.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "7.12.0",
|
||||
"@qltysh/arc-api-client": "workspace:*"
|
||||
"react-router": "7.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "7.12.0",
|
||||
|
|
|
|||
15
bun.lock
15
bun.lock
|
|
@ -16,6 +16,7 @@
|
|||
"@react-router/node": "7.12.0",
|
||||
"@react-router/serve": "7.12.0",
|
||||
"@viz-js/viz": "^3.24.0",
|
||||
"arctic": "^3.7.0",
|
||||
"isbot": "^5.1.31",
|
||||
"jose": "^6.1.3",
|
||||
"react": "^19.2.4",
|
||||
|
|
@ -185,6 +186,16 @@
|
|||
|
||||
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
"@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
|
||||
|
||||
"@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
|
||||
|
||||
"@pierre/diffs": ["@pierre/diffs@1.0.11", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw=="],
|
||||
|
||||
"@qltysh/arc-api-client": ["@qltysh/arc-api-client@workspace:packages/arc-api-client"],
|
||||
|
|
@ -337,6 +348,8 @@
|
|||
|
||||
"arc-web": ["arc-web@workspace:apps/arc-web"],
|
||||
|
||||
"arctic": ["arctic@3.7.0", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
|
@ -741,6 +754,8 @@
|
|||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue