diff --git a/.env.example b/.env.example index d3ba41f..7459089 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,18 @@ MAILCHIMP_TAG=v2 Website Signups # The Graph (optional — falls back to hardcoded values) THEGRAPH_API_KEY= + +# PymtHouse integration (required for Studio auth/device flow) +# Issuer must be the full OIDC issuer URL, e.g. http://localhost:3001/api/v1/oidc +# (site origin is derived from this URL in code — no PMTHOUSE_BASE_URL) +PYMTHOUSE_ISSUER_URL= +# Public OIDC client id (app_...) +PYMTHOUSE_PUBLIC_CLIENT_ID= +# Confidential helper client id (m2m_...) +PYMTHOUSE_M2M_CLIENT_ID= +# Confidential helper secret (pmth_cs_...) +PYMTHOUSE_M2M_CLIENT_SECRET= + +# Website session signing secret +# Generate with: openssl rand -base64 32 +LP_SESSION_SECRET= diff --git a/.gitignore b/.gitignore index 219d8bd..41ec2d2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ img/ .vscode/ *.code-workspace .playwright-mcp/ + +certificates \ No newline at end of file diff --git a/app/(studio)/studio/header-qa/page.tsx b/app/(studio)/studio/header-qa/page.tsx index cda6f97..cfa92b5 100644 --- a/app/(studio)/studio/header-qa/page.tsx +++ b/app/(studio)/studio/header-qa/page.tsx @@ -34,7 +34,7 @@ export default function HeaderQaPage() { ))} diff --git a/components/studio/StudioHeader.tsx b/components/studio/StudioHeader.tsx index 5b6d457..46812d9 100644 --- a/components/studio/StudioHeader.tsx +++ b/components/studio/StudioHeader.tsx @@ -180,7 +180,10 @@ function AvatarMenu({ user, disconnect, compact = false, mobileMinimal = false } + {error &&

{error}

} + {/* Token list */}
- {keys.map((key) => ( -
- {/* Name + prefix + status */} -
-
-

{key.name}

- {key.status === "revoked" && ( - - Revoked - - )} + {isLoading ? ( +
Loading tokens...
+ ) : sortedKeys.length === 0 ? ( +
+ No tokens yet. Create your first `pmth_` token above. +
+ ) : ( + sortedKeys.map((key) => ( +
+ {/* Name + prefix + status */} +
+
+

{key.name}

+ {key.status === "revoked" && ( + + Revoked + + )} +
+

+ {key.prefix} +

+

+ Session token + · + + {formatRequests(key.calls7d)} + {" "} + requests this week +

-

- {key.prefix} - {"•".repeat(24)} -

-

- Free tier - · - - {formatRequests(key.calls7d)} - {" "} - requests this week -

-
- {/* Right rail: badge + actions */} -
- {key.isDefault && ( + {/* Right rail: badge + actions */} +
- Free tier + pmth_ - )} - handleCopy(key.prefix), - }, - { - label: "Revoke", - icon: Trash2, - destructive: true, - disabled: key.isDefault || key.status === "revoked", - onClick: () => handleRevoke(key.id), - }, - ]} - /> + void handleCopy(key.prefix), + }, + { + label: "Revoke", + icon: Trash2, + destructive: true, + disabled: key.status === "revoked", + onClick: () => void handleRevoke(key.id), + }, + ]} + /> +
-
- ))} + )) + )}
+ + setCreatedToken(null)} + maxWidth="max-w-[560px]" + > +
+

Token created

+

+ Copy this token now. You will not be able to view it again. +

+
+
+

+ {createdToken?.name} +

+
+ {createdToken?.token} +
+
+
+ + +
+
); } diff --git a/components/studio/settings/UsageTab.tsx b/components/studio/settings/UsageTab.tsx index db58ac8..4591219 100644 --- a/components/studio/settings/UsageTab.tsx +++ b/components/studio/settings/UsageTab.tsx @@ -14,21 +14,43 @@ import { ChevronDown } from "lucide-react"; import StatCard from "@/components/studio/statistics/StatCard"; import PeriodToggle from "@/components/studio/statistics/PeriodToggle"; import { StackedChartTooltip } from "@/components/studio/statistics/ChartTooltip"; -import { - ACCOUNT_USAGE_SUMMARY, - ACCOUNT_USAGE_BY_SIGNER, - ACCOUNT_USAGE_BY_TOKEN, - ACCOUNT_USAGE_DAILY, - MOCK_RECENT_REQUESTS, - SIGNER_COLORS, -} from "@/lib/studio/mock-data"; +import { SIGNER_COLORS } from "@/lib/studio/mock-data"; import type { NetworkStat, AccountActivityRow, + AccountUsageBySigner, + AccountUsageByToken, AccountUsageDailyPoint, + AccountUsageSummary, SignerKey, } from "@/lib/studio/types"; +type UsagePayload = { + summary: AccountUsageSummary; + bySigner: AccountUsageBySigner[]; + byToken: AccountUsageByToken[]; + daily: AccountUsageDailyPoint[]; + recentRequests: AccountActivityRow[]; +}; + +function normalizeUsagePayload( + payload: Partial & { recentRequests?: AccountActivityRow[] }, +): UsagePayload { + return { + summary: payload.summary ?? { + requests: 0, + spendDisplay: "$0.00", + freeTierUsed: 0, + freeTierLimit: 10_000, + freeTierResetIn: "—", + }, + bySigner: payload.bySigner ?? [], + byToken: payload.byToken ?? [], + daily: payload.daily ?? [], + recentRequests: payload.recentRequests ?? [], + }; +} + // ─── Period filter ─── type Period = "24h" | "7d" | "30d" | "3m"; @@ -128,24 +150,74 @@ export default function UsageTab() { const [period, setPeriod] = useState("30d"); const [signerFilter, setSignerFilter] = useState("all"); const [tokenFilter, setTokenFilter] = useState("all"); + const [usageLoading, setUsageLoading] = useState(true); + const [usageError, setUsageError] = useState(null); + const [usageData, setUsageData] = useState(null); const [highlightedRequestId, setHighlightedRequestId] = useState< string | null >(null); const searchParams = useSearchParams(); const targetRequestId = searchParams.get("request"); + useEffect(() => { + let cancelled = false; + const fetchUsage = async () => { + setUsageLoading(true); + setUsageError(null); + try { + const response = await fetch(`/api/usage?period=${period}`, { + method: "GET", + cache: "no-store", + }); + const payload = (await response.json().catch(() => ({}))) as Partial & { + error_description?: string; + }; + if (!response.ok) { + throw new Error(payload.error_description || "Failed to load usage data."); + } + if (!cancelled) { + setUsageData(normalizeUsagePayload(payload)); + } + } catch (error) { + if (!cancelled) { + setUsageError( + error instanceof Error ? error.message : "Failed to load usage data.", + ); + setUsageData(null); + } + } finally { + if (!cancelled) { + setUsageLoading(false); + } + } + }; + + void fetchUsage(); + return () => { + cancelled = true; + }; + }, [period]); + + const summary = usageData?.summary; + const signerRows = usageData?.bySigner ?? []; + const tokenRows = usageData?.byToken ?? []; + const dailyRows = usageData?.daily ?? []; + const chartData = useMemo( - () => filterByPeriod(ACCOUNT_USAGE_DAILY, period), - [period], + () => filterByPeriod(dailyRows, period), + [dailyRows, period], ); const totalRequests = useMemo(() => { + if (usageData?.summary.requests != null) { + return usageData.summary.requests; + } return chartData.reduce( (sum, d) => sum + d.freeTier + d.paymthouse + d.livepeerCloud + d.ethWallet, 0, ); - }, [chartData]); + }, [chartData, usageData]); const visibleSigners: SignerKey[] = useMemo( () => (signerFilter === "all" ? SIGNER_KEYS : [signerFilter]), @@ -154,18 +226,18 @@ export default function UsageTab() { const filteredSignerRows = useMemo( () => - ACCOUNT_USAGE_BY_SIGNER.filter( + signerRows.filter( (row) => signerFilter === "all" || row.signer === signerFilter, ), - [signerFilter], + [signerFilter, signerRows], ); const filteredTokenRows = useMemo( () => - ACCOUNT_USAGE_BY_TOKEN.filter( + tokenRows.filter( (row) => tokenFilter === "all" || row.tokenId === tokenFilter, ), - [tokenFilter], + [tokenFilter, tokenRows], ); const periodCutoffMs = useMemo(() => { @@ -175,14 +247,15 @@ export default function UsageTab() { }, [period]); const filteredActivity = useMemo(() => { - return MOCK_RECENT_REQUESTS.filter((row) => { + const rows = usageData?.recentRequests ?? []; + return rows.filter((row) => { const ts = new Date(row.timestamp).getTime(); if (Number.isNaN(ts) || ts < periodCutoffMs) return false; if (signerFilter !== "all" && row.signer !== signerFilter) return false; if (tokenFilter !== "all" && row.tokenId !== tokenFilter) return false; return true; }); - }, [periodCutoffMs, signerFilter, tokenFilter]); + }, [usageData?.recentRequests, periodCutoffMs, signerFilter, tokenFilter]); const clearAllFilters = () => { setPeriod("30d"); @@ -223,32 +296,29 @@ export default function UsageTab() { }, [targetRequestId, filteredActivity]); const headerStats: NetworkStat[] = useMemo(() => { + if (!summary) return []; const freePct = Math.round( - (ACCOUNT_USAGE_SUMMARY.freeTierUsed / - ACCOUNT_USAGE_SUMMARY.freeTierLimit) * - 100, + (summary.freeTierUsed / summary.freeTierLimit) * 100, ); return [ { label: "Requests this period", - value: ACCOUNT_USAGE_SUMMARY.requests.toLocaleString(), - delta: "+12.4% vs last period", - trend: "up", + value: summary.requests.toLocaleString(), + trend: "flat", }, { label: "Spend this period", - value: ACCOUNT_USAGE_SUMMARY.spendDisplay, - delta: "+8.1% vs last period", - trend: "up", + value: summary.spendDisplay, + trend: "flat", }, { label: `Free tier (${freePct}% used)`, - value: `${ACCOUNT_USAGE_SUMMARY.freeTierUsed.toLocaleString()} / ${ACCOUNT_USAGE_SUMMARY.freeTierLimit.toLocaleString()}`, - delta: `Resets in ${ACCOUNT_USAGE_SUMMARY.freeTierResetIn}`, + value: `${summary.freeTierUsed.toLocaleString()} / ${summary.freeTierLimit.toLocaleString()}`, + delta: `Resets ${summary.freeTierResetIn}`, trend: "flat", }, ]; - }, []); + }, [summary]); const signerSelectOptions = useMemo<{ key: SignerFilter; label: string }[]>( () => [ @@ -261,16 +331,50 @@ export default function UsageTab() { const tokenSelectOptions = useMemo( () => [ { key: "all", label: "All tokens" }, - ...ACCOUNT_USAGE_BY_TOKEN.map((t) => ({ + ...tokenRows.map((t) => ({ key: t.tokenId, label: t.tokenName, })), ], - [], + [tokenRows], ); + const showSyncBanner = usageLoading && usageData; + return (
+ {usageError && ( +
+ {usageError} +
+ )} + + {usageLoading && !usageData && ( +
+

Loading usage…

+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+
+
+
+
+ )} + + {showSyncBanner && ( +
+ Syncing usage from pymthouse... +
+ )} + + {usageData && ( + <> {/* KPI cards */}
{headerStats.map((stat) => ( @@ -667,6 +771,8 @@ export default function UsageTab() {
)}
+ + )}
); } diff --git a/lib/pmth-studio-device-approval.ts b/lib/pmth-studio-device-approval.ts new file mode 100644 index 0000000..67c9e23 --- /dev/null +++ b/lib/pmth-studio-device-approval.ts @@ -0,0 +1,44 @@ +import { PmtHouseError } from "@pymthouse/builder-api"; +import { createPmtHouseClientFromEnv } from "@pymthouse/builder-api/env"; +import type { SessionPayload } from "@/lib/session"; + +/** + * Studio device flow only: upsert app user, mint user JWT, RFC 8693 device approval. + * Not used for normal `/api/auth/login` (website stub session). + */ +export async function completeStudioDeviceApprovalWithPymthouse(params: { + session: SessionPayload; + userCode: string; +}): Promise { + if ( + !process.env.PYMTHOUSE_ISSUER_URL?.trim() || + !process.env.PYMTHOUSE_PUBLIC_CLIENT_ID?.trim() || + !process.env.PYMTHOUSE_M2M_CLIENT_ID?.trim() || + !process.env.PYMTHOUSE_M2M_CLIENT_SECRET?.trim() + ) { + throw new PmtHouseError( + "Pymthouse is not configured. Set PYMTHOUSE_* environment variables.", + { status: 503, code: "pymthouse_required" }, + ); + } + + const client = createPmtHouseClientFromEnv(); + + await client.upsertAppUser({ + externalUserId: params.session.externalUserId, + email: params.session.email, + status: "active", + }); + + const userToken = await client.mintUserAccessToken({ + externalUserId: params.session.externalUserId, + scope: "sign:job", + }); + + await client.completeDeviceApproval({ + userJwt: userToken.access_token, + userCode: params.userCode, + }); + + return userToken.access_token; +} diff --git a/lib/session.ts b/lib/session.ts new file mode 100644 index 0000000..e001b61 --- /dev/null +++ b/lib/session.ts @@ -0,0 +1,326 @@ +import { SignJWT, jwtVerify, type JWTPayload } from "jose"; +import type { NextRequest, NextResponse } from "next/server"; + +export const SESSION_COOKIE_NAME = "lp_session"; +export const SESSION_USER_COOKIE_NAME = "lp_session_user"; +export const BROWSER_JWT_COOKIE_NAME = "lp_browser_jwt"; +export const DEVICE_FLOW_COOKIE_NAME = "lp_device_flow"; + +const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7; +const BROWSER_JWT_TTL_SECONDS = 60 * 15; +const DEVICE_FLOW_TTL_SECONDS = 60 * 10; + +type StudioAuthProvider = "github" | "google" | "email"; + +export type StoredApiTokenMeta = { + id: string; + name: string; + prefix: string; + createdAt: string; + lastUsedAt: string | null; + status: "active" | "revoked"; +}; + +export interface SessionPayload extends JWTPayload { + sub: string; + externalUserId: string; + email: string; + name: string; + initials: string; + provider: StudioAuthProvider; + pmthUserJwt: string; + apiTokens: StoredApiTokenMeta[]; +} + +export interface SessionPublicUser { + externalUserId: string; + email: string; + name: string; + initials: string; + provider: StudioAuthProvider; + hasPmthouseBinding: boolean; +} + +export interface IssueSessionInput { + externalUserId: string; + email: string; + name: string; + initials: string; + provider: StudioAuthProvider; + pmthUserJwt: string; + apiTokens?: StoredApiTokenMeta[]; +} + +export interface DeviceFlowPayload extends JWTPayload { + iss: string; + targetLinkUri: string; + userCode: string; + clientId: string; +} + +function getSessionSecret(): Uint8Array { + const raw = process.env.LP_SESSION_SECRET; + if (raw && raw.trim()) { + return new TextEncoder().encode(raw.trim()); + } + + if (process.env.NODE_ENV === "production") { + throw new Error("LP_SESSION_SECRET is required in production"); + } + + return new TextEncoder().encode("dev-only-unsafe-session-secret"); +} + +async function signJwt( + payload: JWTPayload, + expiresInSeconds: number, +): Promise { + const now = Math.floor(Date.now() / 1000); + return new SignJWT(payload) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(now) + .setExpirationTime(now + expiresInSeconds) + .sign(getSessionSecret()); +} + +export async function createSessionToken( + input: IssueSessionInput, +): Promise { + const payload: SessionPayload = { + sub: input.externalUserId, + externalUserId: input.externalUserId, + email: input.email, + name: input.name, + initials: input.initials, + provider: input.provider, + pmthUserJwt: input.pmthUserJwt, + apiTokens: input.apiTokens ?? [], + }; + return signJwt(payload, SESSION_TTL_SECONDS); +} + +export async function createBrowserJwt( + payload: SessionPublicUser, +): Promise { + return signJwt( + { + sub: payload.externalUserId, + email: payload.email, + name: payload.name, + initials: payload.initials, + provider: payload.provider, + hasPmthouseBinding: payload.hasPmthouseBinding, + aud: "studio-browser", + }, + BROWSER_JWT_TTL_SECONDS, + ); +} + +export async function verifySessionToken( + token: string, +): Promise { + try { + const { payload } = await jwtVerify(token, getSessionSecret(), { + algorithms: ["HS256"], + }); + if ( + typeof payload.sub !== "string" || + typeof payload.externalUserId !== "string" || + typeof payload.pmthUserJwt !== "string" + ) { + return null; + } + + return { + ...payload, + sub: payload.sub, + externalUserId: payload.externalUserId, + email: String(payload.email ?? ""), + name: String(payload.name ?? ""), + initials: String(payload.initials ?? ""), + provider: (payload.provider as StudioAuthProvider) ?? "email", + pmthUserJwt: payload.pmthUserJwt, + apiTokens: Array.isArray(payload.apiTokens) + ? (payload.apiTokens as StoredApiTokenMeta[]) + : [], + }; + } catch { + return null; + } +} + +export async function createDeviceFlowToken( + payload: Omit, +): Promise { + return signJwt(payload, DEVICE_FLOW_TTL_SECONDS); +} + +export async function verifyDeviceFlowToken( + token: string, +): Promise { + try { + const { payload } = await jwtVerify(token, getSessionSecret(), { + algorithms: ["HS256"], + }); + if ( + typeof payload.iss !== "string" || + typeof payload.targetLinkUri !== "string" || + typeof payload.userCode !== "string" || + typeof payload.clientId !== "string" + ) { + return null; + } + + return { + ...payload, + iss: payload.iss, + targetLinkUri: payload.targetLinkUri, + userCode: payload.userCode, + clientId: payload.clientId, + }; + } catch { + return null; + } +} + +export async function readSessionFromRequest( + request: NextRequest, +): Promise { + const token = request.cookies.get(SESSION_COOKIE_NAME)?.value; + if (!token) { + return null; + } + return verifySessionToken(token); +} + +export async function readDeviceFlowFromRequest( + request: NextRequest, +): Promise { + const token = request.cookies.get(DEVICE_FLOW_COOKIE_NAME)?.value; + if (!token) { + return null; + } + return verifyDeviceFlowToken(token); +} + +export function toSessionPublicUser(payload: SessionPayload): SessionPublicUser { + return { + externalUserId: payload.externalUserId, + email: payload.email, + name: payload.name, + initials: payload.initials, + provider: payload.provider, + hasPmthouseBinding: Boolean(payload.pmthUserJwt), + }; +} + +export async function applySessionCookies( + response: NextResponse, + input: IssueSessionInput, +): Promise { + const sessionToken = await createSessionToken(input); + const publicUser = toSessionPublicUser({ + ...input, + sub: input.externalUserId, + apiTokens: input.apiTokens ?? [], + }); + const browserJwt = await createBrowserJwt(publicUser); + + const secure = process.env.NODE_ENV === "production"; + response.cookies.set(SESSION_COOKIE_NAME, sessionToken, { + httpOnly: true, + sameSite: "lax", + secure, + path: "/", + maxAge: SESSION_TTL_SECONDS, + }); + response.cookies.set( + SESSION_USER_COOKIE_NAME, + Buffer.from(JSON.stringify(publicUser), "utf-8").toString("base64"), + { + httpOnly: false, + sameSite: "lax", + secure, + path: "/", + maxAge: SESSION_TTL_SECONDS, + }, + ); + response.cookies.set(BROWSER_JWT_COOKIE_NAME, browserJwt, { + httpOnly: false, + sameSite: "lax", + secure, + path: "/", + maxAge: BROWSER_JWT_TTL_SECONDS, + }); +} + +export function clearSessionCookies(response: NextResponse): void { + response.cookies.set(SESSION_COOKIE_NAME, "", { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 0, + }); + response.cookies.set(SESSION_USER_COOKIE_NAME, "", { + httpOnly: false, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 0, + }); + response.cookies.set(BROWSER_JWT_COOKIE_NAME, "", { + httpOnly: false, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 0, + }); + clearDeviceFlowCookie(response); +} + +export async function setDeviceFlowCookie( + response: NextResponse, + payload: Omit, +): Promise { + const token = await createDeviceFlowToken(payload); + response.cookies.set(DEVICE_FLOW_COOKIE_NAME, token, { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: DEVICE_FLOW_TTL_SECONDS, + }); +} + +export function clearDeviceFlowCookie(response: NextResponse): void { + response.cookies.set(DEVICE_FLOW_COOKIE_NAME, "", { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 0, + }); +} + +export function upsertApiTokenMeta( + payload: SessionPayload, + token: StoredApiTokenMeta, +): StoredApiTokenMeta[] { + const filtered = payload.apiTokens.filter((item) => item.id !== token.id); + return [...filtered, token]; +} + +export function revokeApiTokenMeta( + payload: SessionPayload, + tokenId: string, +): StoredApiTokenMeta[] { + return payload.apiTokens.map((item) => + item.id === tokenId + ? { + ...item, + status: "revoked", + } + : item, + ); +} diff --git a/lib/studio-auth.ts b/lib/studio-auth.ts new file mode 100644 index 0000000..1417df9 --- /dev/null +++ b/lib/studio-auth.ts @@ -0,0 +1,63 @@ +import { createHash } from "crypto"; + +export type StudioAuthProvider = "github" | "google" | "email"; + +export interface StudioLoginProfile { + provider: StudioAuthProvider; + name: string; + email: string; + initials: string; +} + +const MOCK_PROVIDER_PROFILES: Record< + Exclude, + { name: string; email: string } +> = { + github: { + name: "Studio Developer", + email: "studio.github@example.com", + }, + google: { + name: "Studio Developer", + email: "studio.google@example.com", + }, +}; + +export function toInitials(name: string): string { + return name + .split(" ") + .map((word) => word.trim()) + .filter(Boolean) + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2) || "SU"; +} + +export function deriveExternalUserId(email: string): string { + const digest = createHash("sha256") + .update(email.trim().toLowerCase()) + .digest("hex") + .slice(0, 24); + return `ext_${digest}`; +} + +export function resolveLoginProfile(input: { + provider: StudioAuthProvider; + email?: string; + name?: string; +}): StudioLoginProfile { + const provider = input.provider; + const fallback = provider === "email" ? null : MOCK_PROVIDER_PROFILES[provider]; + + const email = input.email?.trim() || fallback?.email || "studio.user@example.com"; + const displayName = + input.name?.trim() || fallback?.name || email.split("@")[0] || "Studio User"; + + return { + provider, + name: displayName, + email, + initials: toInitials(displayName), + }; +} diff --git a/package.json b/package.json index f80defd..56fdfb0 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "format:check": "prettier . --check" }, "dependencies": { + "@pymthouse/builder-api": "^0.0.4", "framer-motion": "^11.15.0", "gray-matter": "^4.0.3", + "jose": "^6.2.2", "lucide-react": "^1.6.0", "next": "^15.1.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13cfc55..0bc8581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: dependencies: + '@pymthouse/builder-api': + specifier: ^0.0.4 + version: 0.0.4 framer-motion: specifier: ^11.15.0 version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) gray-matter: specifier: ^4.0.3 version: 4.0.3 + jose: + specifier: ^6.2.2 + version: 6.2.2 lucide-react: specifier: ^1.6.0 version: 1.7.0(react@19.2.4) @@ -400,6 +406,10 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@pymthouse/builder-api@0.0.4': + resolution: {integrity: sha512-24ZDRtIcs2QryJndQW8Y2ELlgAI3wCtSdHyv/d3jk+FBIUil9WozeX6gwOp6Od+pm7u4gT2OF/RqhMDUlnyYHw==} + engines: {node: '>=20'} + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -1079,19 +1089,14 @@ packages: typescript: optional: true -<<<<<<< Updated upstream eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} -======= eslint-import-resolver-node@0.3.10: resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} ->>>>>>> Stashed changes eslint-import-resolver-typescript@3.10.1: resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} @@ -1535,6 +1540,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1818,6 +1826,9 @@ packages: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2540,6 +2551,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@pymthouse/builder-api@0.0.4': + dependencies: + oauth4webapi: 3.8.5 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 @@ -3266,15 +3281,11 @@ snapshots: - eslint-plugin-import-x - supports-color -<<<<<<< Updated upstream eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node@0.3.9: -======= eslint-import-resolver-node@0.3.10: ->>>>>>> Stashed changes dependencies: debug: 3.2.7 is-core-module: 2.16.1 @@ -3795,6 +3806,8 @@ snapshots: jiti@2.6.1: {} + jose@6.2.2: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -4142,6 +4155,8 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + oauth4webapi@3.8.5: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {}