mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-11 22:40:32 +01:00
Add API key management endpoints and integrate API keys section in settings panel
This commit is contained in:
62
flow.toml
62
flow.toml
@@ -2346,6 +2346,68 @@ run().catch(console.error);
|
|||||||
dependencies = ["node", "pnpm"]
|
dependencies = ["node", "pnpm"]
|
||||||
shortcuts = ["user"]
|
shortcuts = ["user"]
|
||||||
|
|
||||||
|
[[tasks]]
|
||||||
|
name = "save-tabs"
|
||||||
|
description = "Save all Safari tabs to Linsa as bookmarks"
|
||||||
|
command = '''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd packages/web
|
||||||
|
|
||||||
|
# Load env
|
||||||
|
if [ -f .env ]; then
|
||||||
|
set -a
|
||||||
|
. .env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${LINSA_API_KEY:-}" ]; then
|
||||||
|
echo "❌ LINSA_API_KEY not set"
|
||||||
|
echo ""
|
||||||
|
echo "Generate one with: f gen-api-key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Saving Safari tabs to Linsa..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
LINSA_API_KEY="$LINSA_API_KEY" LINSA_API_URL="${LINSA_API_URL:-http://localhost:5613}" pnpm tsx tests/bookmarks-save.ts
|
||||||
|
'''
|
||||||
|
dependencies = ["node", "pnpm"]
|
||||||
|
shortcuts = ["tabs", "safari"]
|
||||||
|
|
||||||
|
[[tasks]]
|
||||||
|
name = "gen-api-key"
|
||||||
|
interactive = true
|
||||||
|
description = "Generate a Linsa API key for current user"
|
||||||
|
command = '''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd packages/web
|
||||||
|
|
||||||
|
# Load env
|
||||||
|
if [ -f .env ]; then
|
||||||
|
set -a
|
||||||
|
. .env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${PROD_DATABASE_URL:-}" ] && [ -z "${DATABASE_URL:-}" ]; then
|
||||||
|
echo "❌ No database URL found"
|
||||||
|
echo "Set DATABASE_URL or PROD_DATABASE_URL in packages/web/.env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_URL="${PROD_DATABASE_URL:-$DATABASE_URL}"
|
||||||
|
|
||||||
|
read -p "Enter user ID to generate key for [nikiv]: " USER_ID
|
||||||
|
USER_ID="${USER_ID:-nikiv}"
|
||||||
|
|
||||||
|
DATABASE_URL="$DB_URL" pnpm tsx tests/generate-api-key.ts "$USER_ID"
|
||||||
|
'''
|
||||||
|
dependencies = ["node", "pnpm"]
|
||||||
|
shortcuts = ["apikey", "genkey"]
|
||||||
|
|
||||||
[[tasks]]
|
[[tasks]]
|
||||||
name = "test-jazz-stream"
|
name = "test-jazz-stream"
|
||||||
description = "Test Jazz live stream recording flow (API → Jazz FileStream → Timeline)"
|
description = "Test Jazz live stream recording flow (API → Jazz FileStream → Timeline)"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nikiv/ts-utils": "^0.1.7",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useMemo } from "react"
|
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
@@ -6,9 +5,10 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Video,
|
Video,
|
||||||
|
Key,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
type SettingsSection = "preferences" | "profile" | "streaming" | "billing"
|
type SettingsSection = "preferences" | "profile" | "streaming" | "api" | "billing"
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
name?: string | null
|
name?: string | null
|
||||||
@@ -33,35 +33,10 @@ const navItems: NavItem[] = [
|
|||||||
{ id: "preferences", label: "Preferences", icon: SlidersHorizontal },
|
{ id: "preferences", label: "Preferences", icon: SlidersHorizontal },
|
||||||
{ id: "profile", label: "Profile", icon: UserRound },
|
{ id: "profile", label: "Profile", icon: UserRound },
|
||||||
{ id: "streaming", label: "Streaming", icon: Video },
|
{ id: "streaming", label: "Streaming", icon: Video },
|
||||||
|
{ id: "api", label: "API Keys", icon: Key },
|
||||||
{ id: "billing", label: "Manage Billing", icon: CreditCard },
|
{ id: "billing", label: "Manage Billing", icon: CreditCard },
|
||||||
]
|
]
|
||||||
|
|
||||||
function Avatar({ profile }: { profile?: UserProfile | null }) {
|
|
||||||
const initial = useMemo(() => {
|
|
||||||
if (!profile) return "G"
|
|
||||||
return (
|
|
||||||
profile.name?.slice(0, 1) ??
|
|
||||||
profile.email?.slice(0, 1)?.toUpperCase() ??
|
|
||||||
"G"
|
|
||||||
)
|
|
||||||
}, [profile])
|
|
||||||
|
|
||||||
if (profile?.image) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={profile.image}
|
|
||||||
alt={profile.name ?? profile.email}
|
|
||||||
className="w-9 h-9 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-9 h-9 rounded-full bg-teal-600 text-white text-sm font-semibold grid place-items-center">
|
|
||||||
{initial}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPanel({
|
export default function SettingsPanel({
|
||||||
activeSection,
|
activeSection,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { Route as ApiChatMessagesRouteImport } from './routes/api/chat-messages'
|
|||||||
import { Route as ApiCanvasRouteImport } from './routes/api/canvas'
|
import { Route as ApiCanvasRouteImport } from './routes/api/canvas'
|
||||||
import { Route as ApiBrowserSessionsRouteImport } from './routes/api/browser-sessions'
|
import { Route as ApiBrowserSessionsRouteImport } from './routes/api/browser-sessions'
|
||||||
import { Route as ApiArchivesRouteImport } from './routes/api/archives'
|
import { Route as ApiArchivesRouteImport } from './routes/api/archives'
|
||||||
|
import { Route as ApiApiKeysRouteImport } from './routes/api/api-keys'
|
||||||
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
|
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
|
||||||
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request'
|
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request'
|
||||||
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names'
|
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names'
|
||||||
@@ -260,6 +261,11 @@ const ApiArchivesRoute = ApiArchivesRouteImport.update({
|
|||||||
path: '/api/archives',
|
path: '/api/archives',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiApiKeysRoute = ApiApiKeysRouteImport.update({
|
||||||
|
id: '/api/api-keys',
|
||||||
|
path: '/api/api-keys',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
|
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
|
||||||
id: '/demo/start/server-funcs',
|
id: '/demo/start/server-funcs',
|
||||||
path: '/demo/start/server-funcs',
|
path: '/demo/start/server-funcs',
|
||||||
@@ -454,6 +460,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/streams': typeof StreamsRoute
|
'/streams': typeof StreamsRoute
|
||||||
'/urls': typeof UrlsRoute
|
'/urls': typeof UrlsRoute
|
||||||
'/users': typeof UsersRoute
|
'/users': typeof UsersRoute
|
||||||
|
'/api/api-keys': typeof ApiApiKeysRoute
|
||||||
'/api/archives': typeof ApiArchivesRouteWithChildren
|
'/api/archives': typeof ApiArchivesRouteWithChildren
|
||||||
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
||||||
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
||||||
@@ -525,6 +532,7 @@ export interface FileRoutesByTo {
|
|||||||
'/streams': typeof StreamsRoute
|
'/streams': typeof StreamsRoute
|
||||||
'/urls': typeof UrlsRoute
|
'/urls': typeof UrlsRoute
|
||||||
'/users': typeof UsersRoute
|
'/users': typeof UsersRoute
|
||||||
|
'/api/api-keys': typeof ApiApiKeysRoute
|
||||||
'/api/archives': typeof ApiArchivesRouteWithChildren
|
'/api/archives': typeof ApiArchivesRouteWithChildren
|
||||||
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
||||||
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
||||||
@@ -598,6 +606,7 @@ export interface FileRoutesById {
|
|||||||
'/streams': typeof StreamsRoute
|
'/streams': typeof StreamsRoute
|
||||||
'/urls': typeof UrlsRoute
|
'/urls': typeof UrlsRoute
|
||||||
'/users': typeof UsersRoute
|
'/users': typeof UsersRoute
|
||||||
|
'/api/api-keys': typeof ApiApiKeysRoute
|
||||||
'/api/archives': typeof ApiArchivesRouteWithChildren
|
'/api/archives': typeof ApiArchivesRouteWithChildren
|
||||||
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
|
||||||
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
'/api/canvas': typeof ApiCanvasRouteWithChildren
|
||||||
@@ -672,6 +681,7 @@ export interface FileRouteTypes {
|
|||||||
| '/streams'
|
| '/streams'
|
||||||
| '/urls'
|
| '/urls'
|
||||||
| '/users'
|
| '/users'
|
||||||
|
| '/api/api-keys'
|
||||||
| '/api/archives'
|
| '/api/archives'
|
||||||
| '/api/browser-sessions'
|
| '/api/browser-sessions'
|
||||||
| '/api/canvas'
|
| '/api/canvas'
|
||||||
@@ -743,6 +753,7 @@ export interface FileRouteTypes {
|
|||||||
| '/streams'
|
| '/streams'
|
||||||
| '/urls'
|
| '/urls'
|
||||||
| '/users'
|
| '/users'
|
||||||
|
| '/api/api-keys'
|
||||||
| '/api/archives'
|
| '/api/archives'
|
||||||
| '/api/browser-sessions'
|
| '/api/browser-sessions'
|
||||||
| '/api/canvas'
|
| '/api/canvas'
|
||||||
@@ -815,6 +826,7 @@ export interface FileRouteTypes {
|
|||||||
| '/streams'
|
| '/streams'
|
||||||
| '/urls'
|
| '/urls'
|
||||||
| '/users'
|
| '/users'
|
||||||
|
| '/api/api-keys'
|
||||||
| '/api/archives'
|
| '/api/archives'
|
||||||
| '/api/browser-sessions'
|
| '/api/browser-sessions'
|
||||||
| '/api/canvas'
|
| '/api/canvas'
|
||||||
@@ -888,6 +900,7 @@ export interface RootRouteChildren {
|
|||||||
StreamsRoute: typeof StreamsRoute
|
StreamsRoute: typeof StreamsRoute
|
||||||
UrlsRoute: typeof UrlsRoute
|
UrlsRoute: typeof UrlsRoute
|
||||||
UsersRoute: typeof UsersRoute
|
UsersRoute: typeof UsersRoute
|
||||||
|
ApiApiKeysRoute: typeof ApiApiKeysRoute
|
||||||
ApiArchivesRoute: typeof ApiArchivesRouteWithChildren
|
ApiArchivesRoute: typeof ApiArchivesRouteWithChildren
|
||||||
ApiBrowserSessionsRoute: typeof ApiBrowserSessionsRouteWithChildren
|
ApiBrowserSessionsRoute: typeof ApiBrowserSessionsRouteWithChildren
|
||||||
ApiCanvasRoute: typeof ApiCanvasRouteWithChildren
|
ApiCanvasRoute: typeof ApiCanvasRouteWithChildren
|
||||||
@@ -1183,6 +1196,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ApiArchivesRouteImport
|
preLoaderRoute: typeof ApiArchivesRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/api-keys': {
|
||||||
|
id: '/api/api-keys'
|
||||||
|
path: '/api/api-keys'
|
||||||
|
fullPath: '/api/api-keys'
|
||||||
|
preLoaderRoute: typeof ApiApiKeysRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/demo/start/server-funcs': {
|
'/demo/start/server-funcs': {
|
||||||
id: '/demo/start/server-funcs'
|
id: '/demo/start/server-funcs'
|
||||||
path: '/demo/start/server-funcs'
|
path: '/demo/start/server-funcs'
|
||||||
@@ -1589,6 +1609,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
StreamsRoute: StreamsRoute,
|
StreamsRoute: StreamsRoute,
|
||||||
UrlsRoute: UrlsRoute,
|
UrlsRoute: UrlsRoute,
|
||||||
UsersRoute: UsersRoute,
|
UsersRoute: UsersRoute,
|
||||||
|
ApiApiKeysRoute: ApiApiKeysRoute,
|
||||||
ApiArchivesRoute: ApiArchivesRouteWithChildren,
|
ApiArchivesRoute: ApiArchivesRouteWithChildren,
|
||||||
ApiBrowserSessionsRoute: ApiBrowserSessionsRouteWithChildren,
|
ApiBrowserSessionsRoute: ApiBrowserSessionsRouteWithChildren,
|
||||||
ApiCanvasRoute: ApiCanvasRouteWithChildren,
|
ApiCanvasRoute: ApiCanvasRouteWithChildren,
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { createAPIFileRoute } from "@tanstack/react-start/api"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { getDb } from "@/db/connection"
|
import { getDb } from "@/db/connection"
|
||||||
import { api_keys } from "@/db/schema"
|
import { api_keys } from "@/db/schema"
|
||||||
import { auth } from "@/lib/auth"
|
import { getAuth } from "@/lib/auth"
|
||||||
import { headers } from "@tanstack/react-start/server"
|
|
||||||
|
const json = (data: unknown, status = 200) =>
|
||||||
|
new Response(JSON.stringify(data), {
|
||||||
|
status,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
})
|
||||||
|
|
||||||
// Generate a random API key
|
// Generate a random API key
|
||||||
function generateApiKey(): string {
|
function generateApiKey(): string {
|
||||||
@@ -24,110 +29,117 @@ async function hashApiKey(key: string): Promise<string> {
|
|||||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APIRoute = createAPIFileRoute("/api/api-keys")({
|
export const Route = createFileRoute("/api/api-keys")({
|
||||||
// GET - List user's API keys (without the actual key, just metadata)
|
server: {
|
||||||
GET: async () => {
|
handlers: {
|
||||||
try {
|
// GET - List user's API keys (without the actual key, just metadata)
|
||||||
const session = await auth.api.getSession({ headers: await headers() })
|
GET: async ({ request }) => {
|
||||||
if (!session?.user?.id) {
|
try {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
const auth = getAuth()
|
||||||
}
|
const session = await auth.api.getSession({ headers: request.headers })
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return json({ error: "Unauthorized" }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
const db = getDb(process.env.DATABASE_URL!)
|
const db = getDb(process.env.DATABASE_URL!)
|
||||||
|
|
||||||
const keys = await db
|
const keys = await db
|
||||||
.select({
|
.select({
|
||||||
id: api_keys.id,
|
id: api_keys.id,
|
||||||
name: api_keys.name,
|
name: api_keys.name,
|
||||||
last_used_at: api_keys.last_used_at,
|
last_used_at: api_keys.last_used_at,
|
||||||
created_at: api_keys.created_at,
|
created_at: api_keys.created_at,
|
||||||
})
|
})
|
||||||
.from(api_keys)
|
.from(api_keys)
|
||||||
.where(eq(api_keys.user_id, session.user.id))
|
.where(eq(api_keys.user_id, session.user.id))
|
||||||
.orderBy(api_keys.created_at)
|
.orderBy(api_keys.created_at)
|
||||||
|
|
||||||
return Response.json({ keys })
|
return json({ keys })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching API keys:", error)
|
console.error("Error fetching API keys:", error)
|
||||||
return Response.json({ error: "Failed to fetch API keys" }, { status: 500 })
|
return json({ error: "Failed to fetch API keys" }, 500)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// POST - Create a new API key
|
// POST - Create a new API key
|
||||||
POST: async ({ request }) => {
|
POST: async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const session = await auth.api.getSession({ headers: await headers() })
|
const auth = getAuth()
|
||||||
if (!session?.user?.id) {
|
const session = await auth.api.getSession({ headers: request.headers })
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
if (!session?.user?.id) {
|
||||||
}
|
return json({ error: "Unauthorized" }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json().catch(() => ({}))
|
const body = (await request.json().catch(() => ({}))) as { name?: string }
|
||||||
const name = body.name || "Default"
|
const name = body.name || "Default"
|
||||||
|
|
||||||
const db = getDb(process.env.DATABASE_URL!)
|
const db = getDb(process.env.DATABASE_URL!)
|
||||||
|
|
||||||
// Generate new key
|
// Generate new key
|
||||||
const plainKey = generateApiKey()
|
const plainKey = generateApiKey()
|
||||||
const keyHash = await hashApiKey(plainKey)
|
const keyHash = await hashApiKey(plainKey)
|
||||||
|
|
||||||
// Insert key record
|
// Insert key record
|
||||||
const [keyRecord] = await db
|
const [keyRecord] = await db
|
||||||
.insert(api_keys)
|
.insert(api_keys)
|
||||||
.values({
|
.values({
|
||||||
user_id: session.user.id,
|
user_id: session.user.id,
|
||||||
key_hash: keyHash,
|
key_hash: keyHash,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
id: api_keys.id,
|
id: api_keys.id,
|
||||||
name: api_keys.name,
|
name: api_keys.name,
|
||||||
created_at: api_keys.created_at,
|
created_at: api_keys.created_at,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return the plain key ONLY on creation (it won't be retrievable later)
|
// Return the plain key ONLY on creation (it won't be retrievable later)
|
||||||
return Response.json({
|
return json({
|
||||||
key: plainKey,
|
key: plainKey,
|
||||||
id: keyRecord.id,
|
id: keyRecord.id,
|
||||||
name: keyRecord.name,
|
name: keyRecord.name,
|
||||||
created_at: keyRecord.created_at,
|
created_at: keyRecord.created_at,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating API key:", error)
|
console.error("Error creating API key:", error)
|
||||||
return Response.json({ error: "Failed to create API key" }, { status: 500 })
|
return json({ error: "Failed to create API key" }, 500)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// DELETE - Revoke an API key
|
// DELETE - Revoke an API key
|
||||||
DELETE: async ({ request }) => {
|
DELETE: async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const session = await auth.api.getSession({ headers: await headers() })
|
const auth = getAuth()
|
||||||
if (!session?.user?.id) {
|
const session = await auth.api.getSession({ headers: request.headers })
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
if (!session?.user?.id) {
|
||||||
}
|
return json({ error: "Unauthorized" }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const keyId = url.searchParams.get("id")
|
const keyId = url.searchParams.get("id")
|
||||||
|
|
||||||
if (!keyId) {
|
if (!keyId) {
|
||||||
return Response.json({ error: "Key ID is required" }, { status: 400 })
|
return json({ error: "Key ID is required" }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getDb(process.env.DATABASE_URL!)
|
const db = getDb(process.env.DATABASE_URL!)
|
||||||
|
|
||||||
// Delete key (only if it belongs to the user)
|
// Delete key (only if it belongs to the user)
|
||||||
const [deleted] = await db
|
const [deleted] = await db
|
||||||
.delete(api_keys)
|
.delete(api_keys)
|
||||||
.where(eq(api_keys.id, keyId))
|
.where(eq(api_keys.id, keyId))
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
return Response.json({ error: "Key not found" }, { status: 404 })
|
return json({ error: "Key not found" }, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ success: true })
|
return json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting API key:", error)
|
console.error("Error deleting API key:", error)
|
||||||
return Response.json({ error: "Failed to delete API key" }, { status: 500 })
|
return json({ error: "Failed to delete API key" }, 500)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import {
|
|||||||
HelpCircle,
|
HelpCircle,
|
||||||
Copy,
|
Copy,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Key,
|
||||||
|
Trash2,
|
||||||
|
Plus,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
type SectionId = "preferences" | "profile" | "streaming" | "billing"
|
type SectionId = "preferences" | "profile" | "streaming" | "api" | "billing"
|
||||||
|
|
||||||
const PLAN_CARD_NOISE =
|
const PLAN_CARD_NOISE =
|
||||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.18'/%3E%3C/svg%3E"
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.18'/%3E%3C/svg%3E"
|
||||||
@@ -842,6 +847,232 @@ function StreamingSection({ username }: { username: string | null | undefined })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiKeyData {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
last_used_at: string | null
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiKeysSection() {
|
||||||
|
const [keys, setKeys] = useState<ApiKeyData[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [newKeyName, setNewKeyName] = useState("")
|
||||||
|
const [newKey, setNewKey] = useState<string | null>(null)
|
||||||
|
const [showNewKey, setShowNewKey] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchKeys = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/api-keys", { credentials: "include" })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setKeys(data.keys || [])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchKeys()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCreateKey = async () => {
|
||||||
|
setCreating(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/api-keys", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ name: newKeyName || "Default" }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(data.error || "Failed to create key")
|
||||||
|
} else {
|
||||||
|
setNewKey(data.key)
|
||||||
|
setShowNewKey(true)
|
||||||
|
setNewKeyName("")
|
||||||
|
fetchKeys()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Network error")
|
||||||
|
} finally {
|
||||||
|
setCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteKey = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/api-keys?id=${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setKeys(keys.filter((k) => k.id !== id))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyKey = () => {
|
||||||
|
if (newKey) {
|
||||||
|
navigator.clipboard.writeText(newKey)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string | null) => {
|
||||||
|
if (!dateStr) return "Never"
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="api" className="scroll-mt-24">
|
||||||
|
<SectionHeader
|
||||||
|
title="API Keys"
|
||||||
|
description="Manage your API keys for programmatic access."
|
||||||
|
/>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{/* Create new key */}
|
||||||
|
<SettingCard title="Create API Key">
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div className="p-3 bg-teal-500/10 border border-teal-500/20 rounded-lg">
|
||||||
|
<p className="text-sm text-teal-300">
|
||||||
|
API keys allow you to access Linsa programmatically. Use them to save bookmarks, sync data, and integrate with other tools.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newKeyName}
|
||||||
|
onChange={(e) => setNewKeyName(e.target.value)}
|
||||||
|
placeholder="Key name (optional)"
|
||||||
|
className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateKey}
|
||||||
|
disabled={creating}
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold text-white bg-teal-600 hover:bg-teal-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
{creating ? "Creating..." : "Create Key"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-sm text-rose-400">{error}</p>}
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
|
||||||
|
{/* New key display */}
|
||||||
|
{newKey && (
|
||||||
|
<SettingCard title="New API Key">
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div className="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||||
|
<p className="text-sm text-yellow-300">
|
||||||
|
Copy this key now. You won't be able to see it again!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<code className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-teal-400 text-sm font-mono overflow-x-auto">
|
||||||
|
{showNewKey ? newKey : "•".repeat(40)}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowNewKey(!showNewKey)}
|
||||||
|
className="p-2 bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{showNewKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copyKey}
|
||||||
|
className="p-2 bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4 text-teal-400" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewKey(null)}
|
||||||
|
className="text-sm text-white/50 hover:text-white"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Existing keys */}
|
||||||
|
<SettingCard title="Your API Keys">
|
||||||
|
<div className="py-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="h-20 bg-white/5 rounded-lg animate-pulse" />
|
||||||
|
) : keys.length === 0 ? (
|
||||||
|
<p className="text-sm text-white/50 py-4 text-center">
|
||||||
|
No API keys yet. Create one above.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{keys.map((key) => (
|
||||||
|
<div
|
||||||
|
key={key.id}
|
||||||
|
className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/10"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Key className="w-4 h-4 text-white/50" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-white">{key.name}</p>
|
||||||
|
<p className="text-xs text-white/50">
|
||||||
|
Created {formatDate(key.created_at)} • Last used {formatDate(key.last_used_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteKey(key.id)}
|
||||||
|
className="p-2 text-rose-400 hover:bg-rose-400/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
|
||||||
|
{/* Usage example */}
|
||||||
|
<SettingCard title="Usage">
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<p className="text-sm text-white/70">
|
||||||
|
Use your API key to save bookmarks:
|
||||||
|
</p>
|
||||||
|
<pre className="bg-white/5 border border-white/10 rounded-lg p-3 text-sm text-white/80 overflow-x-auto">
|
||||||
|
{`curl -X POST https://linsa.io/api/bookmarks \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"url": "https://example.com",
|
||||||
|
"title": "Example",
|
||||||
|
"api_key": "lk_your_key_here"
|
||||||
|
}'`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function BillingSection() {
|
function BillingSection() {
|
||||||
const [isSubscribed, setIsSubscribed] = useState(false)
|
const [isSubscribed, setIsSubscribed] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -1031,6 +1262,8 @@ function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
) : activeSection === "streaming" ? (
|
) : activeSection === "streaming" ? (
|
||||||
<StreamingSection username={session?.user?.username} />
|
<StreamingSection username={session?.user?.username} />
|
||||||
|
) : activeSection === "api" ? (
|
||||||
|
<ApiKeysSection />
|
||||||
) : activeSection === "billing" && BILLING_ENABLED ? (
|
) : activeSection === "billing" && BILLING_ENABLED ? (
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,127 +1,57 @@
|
|||||||
/**
|
/**
|
||||||
* Test script to save Safari tabs as bookmarks to Linsa
|
* Save Safari tabs as bookmarks to Linsa
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* pnpm tsx tests/bookmarks-save.ts
|
* LINSA_API_KEY=lk_xxx pnpm tsx tests/bookmarks-save.ts
|
||||||
*
|
*
|
||||||
* Requires:
|
* Or via flow:
|
||||||
* - LINSA_API_KEY environment variable (or create one at /settings)
|
* f save-tabs
|
||||||
* - Safari running with tabs open
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "node:child_process"
|
import { executeJxa } from "@nikiv/ts-utils"
|
||||||
|
|
||||||
const API_URL = process.env.LINSA_API_URL || "http://localhost:5613"
|
const API_URL = process.env.LINSA_API_URL || "http://localhost:5613"
|
||||||
const API_KEY = process.env.LINSA_API_KEY
|
const API_KEY = process.env.LINSA_API_KEY
|
||||||
|
|
||||||
if (!API_KEY) {
|
if (!API_KEY) {
|
||||||
console.error("Error: LINSA_API_KEY environment variable is required")
|
console.error("Error: LINSA_API_KEY environment variable is required")
|
||||||
console.error("Generate one at /settings or via POST /api/api-keys")
|
console.error("Generate one at /settings or via: f gen-api-key")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SafariTab {
|
type LocalTab = {
|
||||||
|
uuid: string
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
windowIndex: number
|
window_id: number
|
||||||
|
index: number
|
||||||
|
is_local: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Safari tabs using AppleScript
|
async function fetchSafariTabs(): Promise<LocalTab[]> {
|
||||||
function getSafariTabs(): SafariTab[] {
|
return executeJxa(`
|
||||||
const script = `
|
const safari = Application("com.apple.Safari");
|
||||||
tell application "Safari"
|
const tabs = [];
|
||||||
set tabList to {}
|
safari.windows().map(window => {
|
||||||
set windowCount to count of windows
|
const windowTabs = window.tabs();
|
||||||
repeat with w from 1 to windowCount
|
if (windowTabs) {
|
||||||
set tabCount to count of tabs of window w
|
return windowTabs.map(tab => {
|
||||||
repeat with t from 1 to tabCount
|
tabs.push({
|
||||||
set tabTitle to name of tab t of window w
|
uuid: window.id() + '-' + tab.index(),
|
||||||
set tabURL to URL of tab t of window w
|
title: tab.name(),
|
||||||
set end of tabList to {windowIndex:w, title:tabTitle, url:tabURL}
|
url: tab.url() || '',
|
||||||
end repeat
|
window_id: window.id(),
|
||||||
end repeat
|
index: tab.index(),
|
||||||
return tabList
|
is_local: true
|
||||||
end tell
|
});
|
||||||
`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 10000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Parse AppleScript output: {{windowIndex:1, title:"...", url:"..."}, ...}
|
|
||||||
const tabs: SafariTab[] = []
|
|
||||||
|
|
||||||
// AppleScript returns records in format: window index:1, title:..., url:...
|
|
||||||
const matches = result.matchAll(
|
|
||||||
/window ?[iI]ndex:(\d+),\s*title:(.*?),\s*url:(.*?)(?=,\s*window|$)/g
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
tabs.push({
|
|
||||||
windowIndex: parseInt(match[1]),
|
|
||||||
title: match[2].trim(),
|
|
||||||
url: match[3].trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If regex didn't work, try simpler line-by-line parsing
|
|
||||||
if (tabs.length === 0) {
|
|
||||||
// Alternative: get just URLs and titles separately
|
|
||||||
const urlScript = `
|
|
||||||
tell application "Safari"
|
|
||||||
set urls to {}
|
|
||||||
repeat with w in windows
|
|
||||||
repeat with t in tabs of w
|
|
||||||
set end of urls to URL of t
|
|
||||||
end repeat
|
|
||||||
end repeat
|
|
||||||
return urls
|
|
||||||
end tell
|
|
||||||
`
|
|
||||||
const titleScript = `
|
|
||||||
tell application "Safari"
|
|
||||||
set titles to {}
|
|
||||||
repeat with w in windows
|
|
||||||
repeat with t in tabs of w
|
|
||||||
set end of titles to name of t
|
|
||||||
end repeat
|
|
||||||
end repeat
|
|
||||||
return titles
|
|
||||||
end tell
|
|
||||||
`
|
|
||||||
|
|
||||||
const urlsRaw = execSync(`osascript -e '${urlScript.replace(/'/g, "'\\''")}'`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim()
|
|
||||||
|
|
||||||
const titlesRaw = execSync(`osascript -e '${titleScript.replace(/'/g, "'\\''")}'`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim()
|
|
||||||
|
|
||||||
// Parse comma-separated lists
|
|
||||||
const urls = urlsRaw.split(", ").filter(Boolean)
|
|
||||||
const titles = titlesRaw.split(", ").filter(Boolean)
|
|
||||||
|
|
||||||
for (let i = 0; i < urls.length; i++) {
|
|
||||||
tabs.push({
|
|
||||||
windowIndex: 1,
|
|
||||||
title: titles[i] || "",
|
|
||||||
url: urls[i],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
return tabs;
|
||||||
return tabs
|
`)
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get Safari tabs:", error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a bookmark to Linsa
|
async function saveBookmark(tab: LocalTab, sessionTag: string): Promise<boolean> {
|
||||||
async function saveBookmark(tab: SafariTab, sessionTag: string): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/api/bookmarks`, {
|
const response = await fetch(`${API_URL}/api/bookmarks`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -138,59 +68,63 @@ async function saveBookmark(tab: SafariTab, sessionTag: string): Promise<boolean
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json()
|
const error = await response.json()
|
||||||
console.error(`Failed to save ${tab.url}:`, error)
|
console.error(` ✗ Failed: ${error.error || response.status}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to save ${tab.url}:`, error)
|
console.error(` ✗ Error:`, error instanceof Error ? error.message : error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log("Getting Safari tabs...")
|
console.log("Getting Safari tabs...")
|
||||||
const tabs = getSafariTabs()
|
const tabs = await fetchSafariTabs()
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
console.log("No Safari tabs found. Make sure Safari is running with tabs open.")
|
console.log("No Safari tabs found. Make sure Safari is running with tabs open.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${tabs.length} tabs`)
|
console.log(`Found ${tabs.length} tabs across ${new Set(tabs.map((t) => t.window_id)).size} windows`)
|
||||||
|
|
||||||
// Create session tag with timestamp
|
// Create session tag with timestamp
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
|
const date = new Date()
|
||||||
|
const timestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${String(date.getHours()).padStart(2, "0")}${String(date.getMinutes()).padStart(2, "0")}`
|
||||||
const sessionTag = `session-${timestamp}`
|
const sessionTag = `session-${timestamp}`
|
||||||
|
|
||||||
console.log(`Saving to Linsa with tag: ${sessionTag}`)
|
console.log(`\nSaving to Linsa with tag: ${sessionTag}`)
|
||||||
console.log(`API URL: ${API_URL}`)
|
console.log(`API URL: ${API_URL}`)
|
||||||
console.log("")
|
console.log("")
|
||||||
|
|
||||||
let saved = 0
|
let saved = 0
|
||||||
|
let skipped = 0
|
||||||
let failed = 0
|
let failed = 0
|
||||||
|
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
// Skip empty URLs or about: pages
|
// Skip empty URLs, about: pages, favorites
|
||||||
if (!tab.url || tab.url.startsWith("about:") || tab.url === "favorites://") {
|
if (!tab.url || tab.url.startsWith("about:") || tab.url === "favorites://") {
|
||||||
|
skipped++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(` Saving: ${tab.title.slice(0, 50)}... `)
|
const shortTitle = tab.title.length > 50 ? tab.title.slice(0, 47) + "..." : tab.title
|
||||||
|
process.stdout.write(` ${shortTitle} `)
|
||||||
|
|
||||||
const success = await saveBookmark(tab, sessionTag)
|
const success = await saveBookmark(tab, sessionTag)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log("✓")
|
console.log("✓")
|
||||||
saved++
|
saved++
|
||||||
} else {
|
} else {
|
||||||
console.log("✗")
|
|
||||||
failed++
|
failed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("")
|
console.log("")
|
||||||
console.log(`Done! Saved ${saved} bookmarks, ${failed} failed`)
|
console.log(`Done! Saved: ${saved}, Skipped: ${skipped}, Failed: ${failed}`)
|
||||||
console.log(`Session tag: ${sessionTag}`)
|
console.log(`Session tag: ${sessionTag}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
256
pnpm-lock.yaml
generated
256
pnpm-lock.yaml
generated
@@ -200,6 +200,9 @@ importers:
|
|||||||
specifier: ^4.1.13
|
specifier: ^4.1.13
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@nikiv/ts-utils':
|
||||||
|
specifier: ^0.1.7
|
||||||
|
version: 0.1.7
|
||||||
'@testing-library/dom':
|
'@testing-library/dom':
|
||||||
specifier: ^10.4.1
|
specifier: ^10.4.1
|
||||||
version: 10.4.1
|
version: 10.4.1
|
||||||
@@ -248,6 +251,9 @@ importers:
|
|||||||
|
|
||||||
packages/worker:
|
packages/worker:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@1focus/logs':
|
||||||
|
specifier: file:/Users/nikiv/lang/ts/lib/1focus/packages/logs
|
||||||
|
version: file:../../../lang/ts/lib/1focus/packages/logs
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.0
|
specifier: ^0.45.0
|
||||||
version: 0.45.0(@cloudflare/workers-types@4.20251205.0)(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.15.6)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
version: 0.45.0(@cloudflare/workers-types@4.20251205.0)(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.15.6)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
||||||
@@ -282,6 +288,9 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@1focus/logs@file:../../../lang/ts/lib/1focus/packages/logs':
|
||||||
|
resolution: {directory: ../../../lang/ts/lib/1focus/packages/logs, type: directory}
|
||||||
|
|
||||||
'@acemir/cssom@0.9.28':
|
'@acemir/cssom@0.9.28':
|
||||||
resolution: {integrity: sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==}
|
resolution: {integrity: sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==}
|
||||||
|
|
||||||
@@ -1874,6 +1883,9 @@ packages:
|
|||||||
'@neondatabase/serverless@0.10.4':
|
'@neondatabase/serverless@0.10.4':
|
||||||
resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==}
|
resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==}
|
||||||
|
|
||||||
|
'@nikiv/ts-utils@0.1.7':
|
||||||
|
resolution: {integrity: sha512-hWYla5Xm08xzsac5az32AK++rK76qK6ZndkO0DAAYiIovcv15rhbRtaQPGIXFUCzBM1JgqzZJdyuFJZrxr5cIg==}
|
||||||
|
|
||||||
'@noble/ciphers@1.3.0':
|
'@noble/ciphers@1.3.0':
|
||||||
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
@@ -1936,6 +1948,61 @@ packages:
|
|||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@oven/bun-darwin-aarch64@1.3.5':
|
||||||
|
resolution: {integrity: sha512-8GvNtMo0NINM7Emk9cNAviCG3teEgr3BUX9be0+GD029zIagx2Sf54jMui1Eu1IpFm7nWHODuLEefGOQNaJ0gQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oven/bun-darwin-x64-baseline@1.3.5':
|
||||||
|
resolution: {integrity: sha512-p5q3rJk48qhLuLBOFehVc+kqCE03YrswTc6NCxbwsxiwfySXwcAvpF2KWKF/ZZObvvR8hCCvqe1F81b2p5r2dg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oven/bun-darwin-x64@1.3.5':
|
||||||
|
resolution: {integrity: sha512-r33eHQOHAwkuiBJIwmkXIyqONQOQMnd1GMTpDzaxx9vf9+svby80LZO9Hcm1ns6KT/TBRFyODC/0loA7FAaffg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oven/bun-linux-aarch64-musl@1.3.5':
|
||||||
|
resolution: {integrity: sha512-HKBeUlJdNduRkzJKZ5DXM+pPqntfC50/Hu2X65jVX0Y7hu/6IC8RaUTqpr8FtCZqqmc9wDK0OTL+Mbi9UQIKYQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-linux-aarch64@1.3.5':
|
||||||
|
resolution: {integrity: sha512-zkcHPI23QxJ1TdqafhgkXt1NOEN8o5C460sVeNnrhfJ43LwZgtfcvcQE39x/pBedu67fatY8CU0iY00nOh46ZQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-baseline@1.3.5':
|
||||||
|
resolution: {integrity: sha512-FeCQyBU62DMuB0nn01vPnf3McXrKOsrK9p7sHaBFYycw0mmoU8kCq/WkBkGMnLuvQljJSyen8QBTx+fXdNupWg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-musl-baseline@1.3.5':
|
||||||
|
resolution: {integrity: sha512-TJiYC7KCr0XxFTsxgwQOeE7dncrEL/RSyL0EzSL3xRkrxJMWBCvCSjQn7LV1i6T7hFst0+3KoN3VWvD5BinqHA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-musl@1.3.5':
|
||||||
|
resolution: {integrity: sha512-XkCCHkByYn8BIDvoxnny898znju4xnW2kvFE8FT5+0Y62cWdcBGMZ9RdsEUTeRz16k8hHtJpaSfLcEmNTFIwRQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64@1.3.5':
|
||||||
|
resolution: {integrity: sha512-n7zhKTSDZS0yOYg5Rq8easZu5Y/o47sv0c7yGr2ciFdcie9uYV55fZ7QMqhWMGK33ezCSikh5EDkUMCIvfWpjA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oven/bun-windows-x64-baseline@1.3.5':
|
||||||
|
resolution: {integrity: sha512-rtVQB9/1XK8FWJgFtsOthbPifRMYypgJwxu+pK3NHx8WvFKmq7HcPDqNr8xLzGULjQEO7eAo2aOZfONOwYz+5g==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@oven/bun-windows-x64@1.3.5':
|
||||||
|
resolution: {integrity: sha512-T3xkODItb/0ftQPFsZDc7EAX2D6A4TEazQ2YZyofZToO8Q7y8YT8ooWdhd0BQiTCd66uEvgE1DCZetynwg2IoA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@planetscale/database@1.19.0':
|
'@planetscale/database@1.19.0':
|
||||||
resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==}
|
resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -3030,6 +3097,12 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
bun@1.3.5:
|
||||||
|
resolution: {integrity: sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw==}
|
||||||
|
cpu: [arm64, x64]
|
||||||
|
os: [darwin, linux, win32]
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
cac@6.7.14:
|
cac@6.7.14:
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3125,6 +3198,10 @@ packages:
|
|||||||
cjs-module-lexer@1.4.3:
|
cjs-module-lexer@1.4.3:
|
||||||
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
|
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
|
||||||
|
|
||||||
|
clipboardy@4.0.0:
|
||||||
|
resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3696,6 +3773,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
execa@8.0.1:
|
||||||
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
|
engines: {node: '>=16.17'}
|
||||||
|
|
||||||
exit-hook@2.2.1:
|
exit-hook@2.2.1:
|
||||||
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3861,6 +3942,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
get-stream@8.0.1:
|
||||||
|
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
get-tsconfig@4.13.0:
|
get-tsconfig@4.13.0:
|
||||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
@@ -3995,6 +4080,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
human-signals@5.0.0:
|
||||||
|
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||||
|
engines: {node: '>=16.17.0'}
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4060,6 +4149,11 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
is-docker@3.0.0:
|
||||||
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4075,6 +4169,11 @@ packages:
|
|||||||
is-hexadecimal@2.0.1:
|
is-hexadecimal@2.0.1:
|
||||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-number@7.0.0:
|
is-number@7.0.0:
|
||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
@@ -4086,10 +4185,22 @@ packages:
|
|||||||
is-potential-custom-element-name@1.0.1:
|
is-potential-custom-element-name@1.0.1:
|
||||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
|
||||||
|
is-stream@3.0.0:
|
||||||
|
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
is-wsl@2.2.0:
|
is-wsl@2.2.0:
|
||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-wsl@3.1.0:
|
||||||
|
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
is64bit@2.0.0:
|
||||||
|
resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
isbot@5.1.32:
|
isbot@5.1.32:
|
||||||
resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==}
|
resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4666,6 +4777,10 @@ packages:
|
|||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mimic-fn@4.0.0:
|
||||||
|
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
mimic-response@1.0.1:
|
mimic-response@1.0.1:
|
||||||
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4749,6 +4864,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
npm-run-path@5.3.0:
|
||||||
|
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
nth-check@2.1.1:
|
nth-check@2.1.1:
|
||||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||||
|
|
||||||
@@ -4788,6 +4907,10 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
onetime@6.0.0:
|
||||||
|
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
open@7.4.2:
|
open@7.4.2:
|
||||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4799,6 +4922,9 @@ packages:
|
|||||||
orderedmap@2.1.1:
|
orderedmap@2.1.1:
|
||||||
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
||||||
|
|
||||||
|
osascript-tag@0.1.2:
|
||||||
|
resolution: {integrity: sha512-aAp0P3/liaNfWGVkwWYlUv8z/kB3+fsdhTHFdsenMOqsS4DayJlUeoc4Rw7Zgqh18jRSAZzQAsODS7Qku3weUg==}
|
||||||
|
|
||||||
p-cancelable@2.1.1:
|
p-cancelable@2.1.1:
|
||||||
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
|
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4858,6 +4984,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-key@4.0.0:
|
||||||
|
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
path-to-regexp@6.3.0:
|
path-to-regexp@6.3.0:
|
||||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||||
|
|
||||||
@@ -5341,6 +5471,10 @@ packages:
|
|||||||
signal-exit@3.0.7:
|
signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
|
signal-exit@4.1.0:
|
||||||
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
simple-swizzle@0.2.4:
|
||||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||||
|
|
||||||
@@ -5431,6 +5565,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-final-newline@3.0.0:
|
||||||
|
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
strip-json-comments@3.1.1:
|
strip-json-comments@3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -5477,6 +5615,10 @@ packages:
|
|||||||
symbol-tree@3.2.4:
|
symbol-tree@3.2.4:
|
||||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
|
system-architecture@0.1.0:
|
||||||
|
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tailwind-merge@3.0.2:
|
tailwind-merge@3.0.2:
|
||||||
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
||||||
|
|
||||||
@@ -6067,6 +6209,8 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@1focus/logs@file:../../../lang/ts/lib/1focus/packages/logs': {}
|
||||||
|
|
||||||
'@acemir/cssom@0.9.28': {}
|
'@acemir/cssom@0.9.28': {}
|
||||||
|
|
||||||
'@ai-sdk/gateway@2.0.18(zod@4.1.13)':
|
'@ai-sdk/gateway@2.0.18(zod@4.1.13)':
|
||||||
@@ -7300,6 +7444,13 @@ snapshots:
|
|||||||
'@types/pg': 8.11.6
|
'@types/pg': 8.11.6
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nikiv/ts-utils@0.1.7':
|
||||||
|
dependencies:
|
||||||
|
bun: 1.3.5
|
||||||
|
clipboardy: 4.0.0
|
||||||
|
osascript-tag: 0.1.2
|
||||||
|
ufo: 1.6.1
|
||||||
|
|
||||||
'@noble/ciphers@1.3.0': {}
|
'@noble/ciphers@1.3.0': {}
|
||||||
|
|
||||||
'@noble/ciphers@2.1.1': {}
|
'@noble/ciphers@2.1.1': {}
|
||||||
@@ -7353,6 +7504,39 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
|
'@oven/bun-darwin-aarch64@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-darwin-x64-baseline@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-darwin-x64@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-aarch64-musl@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-aarch64@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-baseline@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-musl-baseline@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64-musl@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-linux-x64@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-windows-x64-baseline@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oven/bun-windows-x64@1.3.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@planetscale/database@1.19.0':
|
'@planetscale/database@1.19.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -8776,6 +8960,20 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
bun@1.3.5:
|
||||||
|
optionalDependencies:
|
||||||
|
'@oven/bun-darwin-aarch64': 1.3.5
|
||||||
|
'@oven/bun-darwin-x64': 1.3.5
|
||||||
|
'@oven/bun-darwin-x64-baseline': 1.3.5
|
||||||
|
'@oven/bun-linux-aarch64': 1.3.5
|
||||||
|
'@oven/bun-linux-aarch64-musl': 1.3.5
|
||||||
|
'@oven/bun-linux-x64': 1.3.5
|
||||||
|
'@oven/bun-linux-x64-baseline': 1.3.5
|
||||||
|
'@oven/bun-linux-x64-musl': 1.3.5
|
||||||
|
'@oven/bun-linux-x64-musl-baseline': 1.3.5
|
||||||
|
'@oven/bun-windows-x64': 1.3.5
|
||||||
|
'@oven/bun-windows-x64-baseline': 1.3.5
|
||||||
|
|
||||||
cac@6.7.14: {}
|
cac@6.7.14: {}
|
||||||
|
|
||||||
cacheable-lookup@5.0.4: {}
|
cacheable-lookup@5.0.4: {}
|
||||||
@@ -8896,6 +9094,12 @@ snapshots:
|
|||||||
|
|
||||||
cjs-module-lexer@1.4.3: {}
|
cjs-module-lexer@1.4.3: {}
|
||||||
|
|
||||||
|
clipboardy@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
execa: 8.0.1
|
||||||
|
is-wsl: 3.1.0
|
||||||
|
is64bit: 2.0.0
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -9524,6 +9728,18 @@ snapshots:
|
|||||||
|
|
||||||
eventsource-parser@3.0.6: {}
|
eventsource-parser@3.0.6: {}
|
||||||
|
|
||||||
|
execa@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
get-stream: 8.0.1
|
||||||
|
human-signals: 5.0.0
|
||||||
|
is-stream: 3.0.0
|
||||||
|
merge-stream: 2.0.0
|
||||||
|
npm-run-path: 5.3.0
|
||||||
|
onetime: 6.0.0
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
strip-final-newline: 3.0.0
|
||||||
|
|
||||||
exit-hook@2.2.1: {}
|
exit-hook@2.2.1: {}
|
||||||
|
|
||||||
expect-type@1.2.2: {}
|
expect-type@1.2.2: {}
|
||||||
@@ -9679,6 +9895,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
|
|
||||||
|
get-stream@8.0.1: {}
|
||||||
|
|
||||||
get-tsconfig@4.13.0:
|
get-tsconfig@4.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
@@ -9848,6 +10066,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
human-signals@5.0.0: {}
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
@@ -9904,6 +10124,8 @@ snapshots:
|
|||||||
|
|
||||||
is-docker@2.2.1: {}
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-fullwidth-code-point@3.0.0: {}
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
@@ -9914,16 +10136,30 @@ snapshots:
|
|||||||
|
|
||||||
is-hexadecimal@2.0.1: {}
|
is-hexadecimal@2.0.1: {}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 3.0.0
|
||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
is-plain-obj@4.1.0: {}
|
is-plain-obj@4.1.0: {}
|
||||||
|
|
||||||
is-potential-custom-element-name@1.0.1: {}
|
is-potential-custom-element-name@1.0.1: {}
|
||||||
|
|
||||||
|
is-stream@3.0.0: {}
|
||||||
|
|
||||||
is-wsl@2.2.0:
|
is-wsl@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
|
||||||
|
is-wsl@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
is-inside-container: 1.0.0
|
||||||
|
|
||||||
|
is64bit@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
system-architecture: 0.1.0
|
||||||
|
|
||||||
isbot@5.1.32: {}
|
isbot@5.1.32: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
@@ -10860,6 +11096,8 @@ snapshots:
|
|||||||
|
|
||||||
mime@3.0.0: {}
|
mime@3.0.0: {}
|
||||||
|
|
||||||
|
mimic-fn@4.0.0: {}
|
||||||
|
|
||||||
mimic-response@1.0.1: {}
|
mimic-response@1.0.1: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
mimic-response@3.1.0: {}
|
||||||
@@ -10945,6 +11183,10 @@ snapshots:
|
|||||||
|
|
||||||
normalize-url@6.1.0: {}
|
normalize-url@6.1.0: {}
|
||||||
|
|
||||||
|
npm-run-path@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
path-key: 4.0.0
|
||||||
|
|
||||||
nth-check@2.1.1:
|
nth-check@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
@@ -10979,6 +11221,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
onetime@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
mimic-fn: 4.0.0
|
||||||
|
|
||||||
open@7.4.2:
|
open@7.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
@@ -10995,6 +11241,8 @@ snapshots:
|
|||||||
|
|
||||||
orderedmap@2.1.1: {}
|
orderedmap@2.1.1: {}
|
||||||
|
|
||||||
|
osascript-tag@0.1.2: {}
|
||||||
|
|
||||||
p-cancelable@2.1.1: {}
|
p-cancelable@2.1.1: {}
|
||||||
|
|
||||||
p-limit@2.3.0:
|
p-limit@2.3.0:
|
||||||
@@ -11054,6 +11302,8 @@ snapshots:
|
|||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-key@4.0.0: {}
|
||||||
|
|
||||||
path-to-regexp@6.3.0: {}
|
path-to-regexp@6.3.0: {}
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
@@ -11679,6 +11929,8 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@3.0.7: {}
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
simple-swizzle@0.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.3.4
|
is-arrayish: 0.3.4
|
||||||
@@ -11752,6 +12004,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
strip-final-newline@3.0.0: {}
|
||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
stripe@20.1.0(@types/node@24.10.1):
|
stripe@20.1.0(@types/node@24.10.1):
|
||||||
@@ -11801,6 +12055,8 @@ snapshots:
|
|||||||
|
|
||||||
symbol-tree@3.2.4: {}
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
|
system-architecture@0.1.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.0.2: {}
|
tailwind-merge@3.0.2: {}
|
||||||
|
|
||||||
tailwindcss@4.1.17: {}
|
tailwindcss@4.1.17: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user