mirror of
https://github.com/kennethnym/aris.git
synced 2026-04-15 06:11:17 +01:00
Compare commits
3 Commits
feat/cloud
...
feat/db-tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
d46e6a9c5d
|
|||
| e54c5d5462 | |||
| b5236e0e52 |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"js/ts.experimental.useTsgo": true
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "^6",
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,7 @@ import {
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import {
|
import { fetchSourceConfig, pushLocation, replaceSource, updateProviderConfig } from "@/lib/api"
|
||||||
fetchSourceConfig,
|
|
||||||
pushLocation,
|
|
||||||
replaceSource,
|
|
||||||
updateProviderConfig,
|
|
||||||
updateSourceCredentials,
|
|
||||||
} from "@/lib/api"
|
|
||||||
|
|
||||||
interface SourceConfigPanelProps {
|
interface SourceConfigPanelProps {
|
||||||
source: SourceDefinition
|
source: SourceDefinition
|
||||||
@@ -80,23 +74,24 @@ export function SourceConfigPanel({ source, onUpdate }: SourceConfigPanelProps)
|
|||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const promises: Promise<void>[] = [
|
|
||||||
replaceSource(source.id, { enabled, config: getUserConfig() }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const credentialFields = getCredentialFields()
|
const credentialFields = getCredentialFields()
|
||||||
const hasCredentials = Object.values(credentialFields).some(
|
const hasCredentials = Object.values(credentialFields).some(
|
||||||
(v) => typeof v === "string" && v.length > 0,
|
(v) => typeof v === "string" && v.length > 0,
|
||||||
)
|
)
|
||||||
if (hasCredentials) {
|
|
||||||
if (source.perUserCredentials) {
|
|
||||||
promises.push(updateSourceCredentials(source.id, credentialFields))
|
|
||||||
} else {
|
|
||||||
promises.push(updateProviderConfig(source.id, { credentials: credentialFields }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
const body: Parameters<typeof replaceSource>[1] = {
|
||||||
|
enabled,
|
||||||
|
config: getUserConfig(),
|
||||||
|
}
|
||||||
|
if (hasCredentials && source.perUserCredentials) {
|
||||||
|
body.credentials = credentialFields
|
||||||
|
}
|
||||||
|
await replaceSource(source.id, body)
|
||||||
|
|
||||||
|
// For non-per-user credentials (provider-level), still use the admin endpoint.
|
||||||
|
if (hasCredentials && !source.perUserCredentials) {
|
||||||
|
await updateProviderConfig(source.id, { credentials: credentialFields })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
setDirty({})
|
setDirty({})
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const sourceDefinitions: SourceDefinition[] = [
|
|||||||
timeZone: {
|
timeZone: {
|
||||||
type: "string",
|
type: "string",
|
||||||
label: "Timezone",
|
label: "Timezone",
|
||||||
description: "IANA timezone for determining \"today\" (e.g. Europe/London). Defaults to UTC.",
|
description: 'IANA timezone for determining "today" (e.g. Europe/London). Defaults to UTC.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -174,7 +174,7 @@ export async function fetchConfigs(): Promise<SourceConfig[]> {
|
|||||||
|
|
||||||
export async function replaceSource(
|
export async function replaceSource(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
body: { enabled: boolean; config: unknown },
|
body: { enabled: boolean; config: unknown; credentials?: Record<string, unknown> },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await fetch(`${serverBase()}/sources/${sourceId}`, {
|
const res = await fetch(`${serverBase()}/sources/${sourceId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
@@ -16,14 +15,12 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"files": [],
|
"files": [],
|
||||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,12 @@
|
|||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ CREDENTIAL_ENCRYPTION_KEY=
|
|||||||
# Base URL of the backend
|
# Base URL of the backend
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# Cloudflare Workers AI (LLM feed enhancement)
|
# OpenRouter (LLM feed enhancement)
|
||||||
CF_ACCOUNT_ID=
|
OPENROUTER_API_KEY=
|
||||||
WORKERS_AI_API_KEY=
|
# Optional: override the default model (default: openai/gpt-4.1-mini)
|
||||||
# Optional: override the default model (default: @cf/zai-org/glm-4.7-flash)
|
# OPENROUTER_MODEL=openai/gpt-4.1-mini
|
||||||
# WORKERS_AI_MODEL=@cf/zai-org/glm-4.7-flash
|
|
||||||
|
|
||||||
# Apple WeatherKit credentials
|
# Apple WeatherKit credentials
|
||||||
WEATHERKIT_PRIVATE_KEY=
|
WEATHERKIT_PRIVATE_KEY=
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@aelis/source-location": "workspace:*",
|
"@aelis/source-location": "workspace:*",
|
||||||
"@aelis/source-tfl": "workspace:*",
|
"@aelis/source-tfl": "workspace:*",
|
||||||
"@aelis/source-weatherkit": "workspace:*",
|
"@aelis/source-weatherkit": "workspace:*",
|
||||||
|
"@openrouter/sdk": "^0.9.11",
|
||||||
"arktype": "^2.1.29",
|
"arktype": "^2.1.29",
|
||||||
"better-auth": "^1",
|
"better-auth": "^1",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import type { PgDatabase } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
import { SQL } from "bun"
|
import { SQL } from "bun"
|
||||||
import { drizzle, type BunSQLDatabase } from "drizzle-orm/bun-sql"
|
import { drizzle, type BunSQLQueryResultHKT } from "drizzle-orm/bun-sql"
|
||||||
|
|
||||||
import * as schema from "./schema.ts"
|
import * as schema from "./schema.ts"
|
||||||
|
|
||||||
export type Database = BunSQLDatabase<typeof schema>
|
/** Covers both the top-level drizzle instance and transaction handles. */
|
||||||
|
export type Database = PgDatabase<BunSQLQueryResultHKT, typeof schema>
|
||||||
|
|
||||||
export interface DatabaseConnection {
|
export interface DatabaseConnection {
|
||||||
db: Database
|
db: Database
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { type } from "arktype"
|
import { OpenRouter } from "@openrouter/sdk"
|
||||||
|
|
||||||
import type { EnhancementResult } from "./schema.ts"
|
import type { EnhancementResult } from "./schema.ts"
|
||||||
|
|
||||||
import { enhancementResultJsonSchema, parseEnhancementResult } from "./schema.ts"
|
import { enhancementResultJsonSchema, parseEnhancementResult } from "./schema.ts"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "@cf/zai-org/glm-4.7-flash"
|
const DEFAULT_MODEL = "z-ai/glm-4.7-flash"
|
||||||
const DEFAULT_TIMEOUT_MS = 30_000
|
const DEFAULT_TIMEOUT_MS = 30_000
|
||||||
|
|
||||||
export interface LlmClientConfig {
|
export interface LlmClientConfig {
|
||||||
accountId: string
|
|
||||||
apiKey: string
|
apiKey: string
|
||||||
model?: string
|
model?: string
|
||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
@@ -23,94 +22,52 @@ export interface LlmClient {
|
|||||||
enhance(request: LlmClientRequest): Promise<EnhancementResult | null>
|
enhance(request: LlmClientRequest): Promise<EnhancementResult | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudflareApiResponse = type({
|
|
||||||
result: {
|
|
||||||
choices: type({
|
|
||||||
message: {
|
|
||||||
content: "string",
|
|
||||||
"role?": "string",
|
|
||||||
},
|
|
||||||
}).array(),
|
|
||||||
},
|
|
||||||
success: "boolean",
|
|
||||||
"errors?": type({ message: "string" }).array(),
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a reusable LLM client backed by Cloudflare Workers AI.
|
* Creates a reusable LLM client backed by OpenRouter.
|
||||||
* Uses the REST API with structured JSON output.
|
* The OpenRouter SDK instance is created once and reused across calls.
|
||||||
*/
|
*/
|
||||||
export function createLlmClient(config: LlmClientConfig): LlmClient {
|
export function createLlmClient(config: LlmClientConfig): LlmClient {
|
||||||
|
const client = new OpenRouter({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||||
|
})
|
||||||
const model = config.model ?? DEFAULT_MODEL
|
const model = config.model ?? DEFAULT_MODEL
|
||||||
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
||||||
const baseUrl = `https://api.cloudflare.com/client/v4/accounts/${config.accountId}/ai/run/${model}`
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async enhance(request) {
|
async enhance(request) {
|
||||||
try {
|
const response = await client.chat.send({
|
||||||
const res = await fetch(baseUrl, {
|
chatGenerationParams: {
|
||||||
method: "POST",
|
model,
|
||||||
headers: {
|
messages: [
|
||||||
Authorization: `Bearer ${config.apiKey}`,
|
{ role: "system" as const, content: request.systemPrompt },
|
||||||
"Content-Type": "application/json",
|
{ role: "user" as const, content: request.userMessage },
|
||||||
},
|
],
|
||||||
body: JSON.stringify({
|
responseFormat: {
|
||||||
messages: [
|
type: "json_schema" as const,
|
||||||
{ role: "system", content: request.systemPrompt },
|
jsonSchema: {
|
||||||
{ role: "user", content: request.userMessage },
|
name: "enhancement_result",
|
||||||
],
|
strict: false,
|
||||||
response_format: {
|
schema: enhancementResultJsonSchema,
|
||||||
type: "json_schema",
|
|
||||||
json_schema: {
|
|
||||||
name: "enhancement_result",
|
|
||||||
strict: false,
|
|
||||||
schema: enhancementResultJsonSchema,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
stream: false,
|
},
|
||||||
}),
|
reasoning: { effort: "none" },
|
||||||
// @ts-expect-error — bun-types AbortSignal conflicts with ESNext lib in tsc; works at runtime and in VSCode
|
stream: false,
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
const message = response.choices?.[0]?.message
|
||||||
const body = await res.text()
|
const content = message?.content ?? message?.reasoning
|
||||||
console.warn(`[enhancement] Cloudflare API error ${res.status}: ${body}`)
|
if (typeof content !== "string") {
|
||||||
return null
|
console.warn("[enhancement] LLM returned no content in response")
|
||||||
}
|
|
||||||
|
|
||||||
const json: unknown = await res.json()
|
|
||||||
const parsed = CloudflareApiResponse(json)
|
|
||||||
if (parsed instanceof type.errors) {
|
|
||||||
console.warn("[enhancement] Unexpected API response shape:", parsed.summary)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
console.warn("[enhancement] Cloudflare API errors:", parsed.errors)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = parsed.result.choices[0]?.message.content
|
|
||||||
if (content === undefined) {
|
|
||||||
console.warn("[enhancement] LLM returned no choices in response")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = parseEnhancementResult(content)
|
|
||||||
if (!result) {
|
|
||||||
console.warn("[enhancement] Failed to parse LLM response:", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
||||||
console.warn("[enhancement] LLM request timed out")
|
|
||||||
} else {
|
|
||||||
console.warn("[enhancement] LLM request failed:", error)
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = parseEnhancementResult(content)
|
||||||
|
if (!result) {
|
||||||
|
console.warn("[enhancement] Failed to parse LLM response:", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export type SyntheticItem = typeof SyntheticItem.infer
|
|||||||
export type EnhancementResult = typeof EnhancementResult.infer
|
export type EnhancementResult = typeof EnhancementResult.infer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Schema passed to Cloudflare Workers AI for structured output.
|
* JSON Schema passed to OpenRouter's structured output.
|
||||||
|
* OpenRouter doesn't support arktype, so this is maintained separately.
|
||||||
*
|
*
|
||||||
* ⚠️ Must stay in sync with EnhancementResult above.
|
* ⚠️ Must stay in sync with EnhancementResult above.
|
||||||
* If you add/remove fields, update both schemas.
|
* If you add/remove fields, update both schemas.
|
||||||
|
|||||||
@@ -23,22 +23,17 @@ function main() {
|
|||||||
const { db, close: closeDb } = createDatabase(process.env.DATABASE_URL!)
|
const { db, close: closeDb } = createDatabase(process.env.DATABASE_URL!)
|
||||||
const auth = createAuth(db)
|
const auth = createAuth(db)
|
||||||
|
|
||||||
const cfAccountId = process.env.CF_ACCOUNT_ID
|
const openrouterApiKey = process.env.OPENROUTER_API_KEY
|
||||||
const workersAiApiKey = process.env.WORKERS_AI_API_KEY
|
const feedEnhancer = openrouterApiKey
|
||||||
const feedEnhancer =
|
? createFeedEnhancer({
|
||||||
cfAccountId && workersAiApiKey
|
client: createLlmClient({
|
||||||
? createFeedEnhancer({
|
apiKey: openrouterApiKey,
|
||||||
client: createLlmClient({
|
model: process.env.OPENROUTER_MODEL || undefined,
|
||||||
accountId: cfAccountId,
|
}),
|
||||||
apiKey: workersAiApiKey,
|
})
|
||||||
model: process.env.WORKERS_AI_MODEL || undefined,
|
: null
|
||||||
}),
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
if (!feedEnhancer) {
|
if (!feedEnhancer) {
|
||||||
console.warn(
|
console.warn("[enhancement] OPENROUTER_API_KEY not set — feed enhancement disabled")
|
||||||
"[enhancement] CF_ACCOUNT_ID/WORKERS_AI_API_KEY not set — feed enhancement disabled",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentialEncryptionKey = process.env.CREDENTIAL_ENCRYPTION_KEY
|
const credentialEncryptionKey = process.env.CREDENTIAL_ENCRYPTION_KEY
|
||||||
|
|||||||
@@ -81,6 +81,27 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
// Delegates to find — row locking is a no-op in tests.
|
||||||
|
if (mockFindResult !== undefined) return mockFindResult
|
||||||
|
const now = new Date()
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
userId,
|
||||||
|
sourceId,
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
credentials: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateConfig(_sourceId: string, _update: { enabled?: boolean; config?: unknown }) {
|
||||||
|
// no-op for tests
|
||||||
|
},
|
||||||
|
async upsertConfig(_sourceId: string, _data: { enabled: boolean; config: unknown }) {
|
||||||
|
// no-op for tests
|
||||||
|
},
|
||||||
async updateCredentials(sourceId: string, credentials: Buffer) {
|
async updateCredentials(sourceId: string, credentials: Buffer) {
|
||||||
if (mockUpdateCredentialsError) {
|
if (mockUpdateCredentialsError) {
|
||||||
throw mockUpdateCredentialsError
|
throw mockUpdateCredentialsError
|
||||||
@@ -90,7 +111,9 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fakeDb = {} as Database
|
const fakeDb = {
|
||||||
|
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
|
||||||
|
} as unknown as Database
|
||||||
|
|
||||||
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
|
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
|
||||||
return {
|
return {
|
||||||
@@ -824,3 +847,121 @@ describe("UserSessionManager.updateSourceCredentials", () => {
|
|||||||
expect(factory).not.toHaveBeenCalled()
|
expect(factory).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("UserSessionManager.saveSourceConfig", () => {
|
||||||
|
test("upserts config without credentials (existing behavior)", async () => {
|
||||||
|
setEnabledSources(["test"])
|
||||||
|
const factory = mock(async () => createStubSource("test"))
|
||||||
|
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
|
||||||
|
const manager = new UserSessionManager({
|
||||||
|
db: fakeDb,
|
||||||
|
providers: [provider],
|
||||||
|
credentialEncryptor: testEncryptor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a session first so we can verify the source is refreshed
|
||||||
|
await manager.getOrCreate("user-1")
|
||||||
|
|
||||||
|
await manager.saveSourceConfig("user-1", "test", {
|
||||||
|
enabled: true,
|
||||||
|
config: { key: "value" },
|
||||||
|
})
|
||||||
|
|
||||||
|
// feedSourceForUser called once for session creation, once for upsert refresh
|
||||||
|
expect(factory).toHaveBeenCalledTimes(2)
|
||||||
|
// No credentials should have been persisted
|
||||||
|
expect(mockUpdateCredentialsCalls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("upserts config with credentials — persists both and passes credentials to source", async () => {
|
||||||
|
setEnabledSources(["test"])
|
||||||
|
let receivedCredentials: unknown = null
|
||||||
|
const factory = mock(async (_userId: string, _config: unknown, creds: unknown) => {
|
||||||
|
receivedCredentials = creds
|
||||||
|
return createStubSource("test")
|
||||||
|
})
|
||||||
|
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
|
||||||
|
const manager = new UserSessionManager({
|
||||||
|
db: fakeDb,
|
||||||
|
providers: [provider],
|
||||||
|
credentialEncryptor: testEncryptor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a session so the source refresh path runs
|
||||||
|
await manager.getOrCreate("user-1")
|
||||||
|
|
||||||
|
const creds = { username: "alice", password: "s3cret" }
|
||||||
|
await manager.saveSourceConfig("user-1", "test", {
|
||||||
|
enabled: true,
|
||||||
|
config: { serverUrl: "https://example.com" },
|
||||||
|
credentials: creds,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Credentials were encrypted and persisted
|
||||||
|
expect(mockUpdateCredentialsCalls).toHaveLength(1)
|
||||||
|
const decrypted = JSON.parse(testEncryptor.decrypt(mockUpdateCredentialsCalls[0]!.credentials))
|
||||||
|
expect(decrypted).toEqual(creds)
|
||||||
|
|
||||||
|
// feedSourceForUser received the provided credentials (not null)
|
||||||
|
expect(receivedCredentials).toEqual(creds)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("upserts config with credentials adds source to session when not already present", async () => {
|
||||||
|
// Start with no enabled sources so the session is empty
|
||||||
|
setEnabledSources([])
|
||||||
|
const factory = mock(async () => createStubSource("test"))
|
||||||
|
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
|
||||||
|
const manager = new UserSessionManager({
|
||||||
|
db: fakeDb,
|
||||||
|
providers: [provider],
|
||||||
|
credentialEncryptor: testEncryptor,
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = await manager.getOrCreate("user-1")
|
||||||
|
expect(session.hasSource("test")).toBe(false)
|
||||||
|
|
||||||
|
// Set mockFindResult to undefined so find() returns a row (simulating the row was just created by upsertConfig)
|
||||||
|
await manager.saveSourceConfig("user-1", "test", {
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
credentials: { token: "abc" },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Source should now be in the session
|
||||||
|
expect(session.hasSource("test")).toBe(true)
|
||||||
|
expect(mockUpdateCredentialsCalls).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws CredentialStorageUnavailableError when credentials provided without encryptor", async () => {
|
||||||
|
setEnabledSources(["test"])
|
||||||
|
const provider = createStubProvider("test")
|
||||||
|
const manager = new UserSessionManager({
|
||||||
|
db: fakeDb,
|
||||||
|
providers: [provider],
|
||||||
|
// No credentialEncryptor
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
manager.saveSourceConfig("user-1", "test", {
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
credentials: { token: "abc" },
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(CredentialStorageUnavailableError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws SourceNotFoundError for unknown provider", async () => {
|
||||||
|
const manager = new UserSessionManager({
|
||||||
|
db: fakeDb,
|
||||||
|
providers: [],
|
||||||
|
credentialEncryptor: testEncryptor,
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
manager.saveSourceConfig("user-1", "unknown", {
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(SourceNotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -126,27 +126,29 @@ export class UserSessionManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing row for config merging and credential access.
|
// Use a transaction with SELECT FOR UPDATE to prevent lost updates
|
||||||
// NOTE: find + updateConfig is not atomic. A concurrent update could
|
// when concurrent PATCH requests merge config against the same base.
|
||||||
// read stale config. Use SELECT FOR UPDATE or atomic jsonb merge if
|
const { existingRow, mergedConfig } = await this.db.transaction(async (tx) => {
|
||||||
// this becomes a problem.
|
const existingRow = await sources(tx, userId).findForUpdate(sourceId)
|
||||||
const existingRow = await sources(this.db, userId).find(sourceId)
|
|
||||||
|
|
||||||
let mergedConfig: Record<string, unknown> | undefined
|
let mergedConfig: Record<string, unknown> | undefined
|
||||||
if (update.config !== undefined && provider.configSchema) {
|
if (update.config !== undefined && provider.configSchema) {
|
||||||
const existingConfig = (existingRow?.config ?? {}) as Record<string, unknown>
|
const existingConfig = (existingRow?.config ?? {}) as Record<string, unknown>
|
||||||
mergedConfig = merge({}, existingConfig, update.config)
|
mergedConfig = merge({}, existingConfig, update.config)
|
||||||
|
|
||||||
const validated = provider.configSchema(mergedConfig)
|
const validated = provider.configSchema(mergedConfig)
|
||||||
if (validated instanceof type.errors) {
|
if (validated instanceof type.errors) {
|
||||||
throw new InvalidSourceConfigError(sourceId, validated.summary)
|
throw new InvalidSourceConfigError(sourceId, validated.summary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Throws SourceNotFoundError if the row doesn't exist
|
// Throws SourceNotFoundError if the row doesn't exist
|
||||||
await sources(this.db, userId).updateConfig(sourceId, {
|
await sources(tx, userId).updateConfig(sourceId, {
|
||||||
enabled: update.enabled,
|
enabled: update.enabled,
|
||||||
config: mergedConfig,
|
config: mergedConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { existingRow, mergedConfig }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Refresh the specific source in the active session instead of
|
// Refresh the specific source in the active session instead of
|
||||||
@@ -171,13 +173,18 @@ export class UserSessionManager {
|
|||||||
* inserts a new row if one doesn't exist and fully replaces config
|
* inserts a new row if one doesn't exist and fully replaces config
|
||||||
* (no merge).
|
* (no merge).
|
||||||
*
|
*
|
||||||
|
* When `credentials` is provided, they are encrypted and persisted
|
||||||
|
* alongside the config in the same flow, avoiding the race condition
|
||||||
|
* of separate config + credential requests.
|
||||||
|
*
|
||||||
* @throws {SourceNotFoundError} if the sourceId has no registered provider
|
* @throws {SourceNotFoundError} if the sourceId has no registered provider
|
||||||
* @throws {InvalidSourceConfigError} if config fails schema validation
|
* @throws {InvalidSourceConfigError} if config fails schema validation
|
||||||
|
* @throws {CredentialStorageUnavailableError} if credentials are provided but no encryptor is configured
|
||||||
*/
|
*/
|
||||||
async upsertSourceConfig(
|
async saveSourceConfig(
|
||||||
userId: string,
|
userId: string,
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
data: { enabled: boolean; config?: unknown },
|
data: { enabled: boolean; config?: unknown; credentials?: unknown },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const provider = this.providers.get(sourceId)
|
const provider = this.providers.get(sourceId)
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -191,15 +198,28 @@ export class UserSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.credentials !== undefined && !this.encryptor) {
|
||||||
|
throw new CredentialStorageUnavailableError()
|
||||||
|
}
|
||||||
|
|
||||||
const config = data.config ?? {}
|
const config = data.config ?? {}
|
||||||
|
|
||||||
// Fetch existing row before upsert to capture credentials for session refresh.
|
// Run the upsert + credential update atomically so a failure in
|
||||||
// For new rows this will be undefined — credentials will be null.
|
// either step doesn't leave the row in an inconsistent state.
|
||||||
const existingRow = await sources(this.db, userId).find(sourceId)
|
const existingRow = await this.db.transaction(async (tx) => {
|
||||||
|
const existing = await sources(tx, userId).find(sourceId)
|
||||||
|
|
||||||
await sources(this.db, userId).upsertConfig(sourceId, {
|
await sources(tx, userId).upsertConfig(sourceId, {
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
config,
|
config,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.credentials !== undefined && this.encryptor) {
|
||||||
|
const encrypted = this.encryptor.encrypt(JSON.stringify(data.credentials))
|
||||||
|
await sources(tx, userId).updateCredentials(sourceId, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
})
|
})
|
||||||
|
|
||||||
const session = this.sessions.get(userId)
|
const session = this.sessions.get(userId)
|
||||||
@@ -207,9 +227,13 @@ export class UserSessionManager {
|
|||||||
if (!data.enabled) {
|
if (!data.enabled) {
|
||||||
session.removeSource(sourceId)
|
session.removeSource(sourceId)
|
||||||
} else {
|
} else {
|
||||||
const credentials = existingRow?.credentials
|
// Prefer the just-provided credentials over what was in the DB.
|
||||||
? this.decryptCredentials(existingRow.credentials)
|
let credentials: unknown = null
|
||||||
: null
|
if (data.credentials !== undefined) {
|
||||||
|
credentials = data.credentials
|
||||||
|
} else if (existingRow?.credentials) {
|
||||||
|
credentials = this.decryptCredentials(existingRow.credentials)
|
||||||
|
}
|
||||||
const source = await provider.feedSourceForUser(userId, config, credentials)
|
const source = await provider.feedSourceForUser(userId, config, credentials)
|
||||||
if (session.hasSource(sourceId)) {
|
if (session.hasSource(sourceId)) {
|
||||||
session.replaceSource(sourceId, source)
|
session.replaceSource(sourceId, source)
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ function createInMemoryStore() {
|
|||||||
async find(sourceId: string) {
|
async find(sourceId: string) {
|
||||||
return rows.get(key(userId, sourceId))
|
return rows.get(key(userId, sourceId))
|
||||||
},
|
},
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
return rows.get(key(userId, sourceId))
|
||||||
|
},
|
||||||
async updateConfig(sourceId: string, update: { enabled?: boolean; config?: unknown }) {
|
async updateConfig(sourceId: string, update: { enabled?: boolean; config?: unknown }) {
|
||||||
const existing = rows.get(key(userId, sourceId))
|
const existing = rows.get(key(userId, sourceId))
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -125,7 +128,9 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fakeDb = {} as Database
|
const fakeDb = {
|
||||||
|
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
|
||||||
|
} as unknown as Database
|
||||||
|
|
||||||
function createApp(providers: FeedSourceProvider[], userId?: string) {
|
function createApp(providers: FeedSourceProvider[], userId?: string) {
|
||||||
const sessionManager = new UserSessionManager({ providers, db: fakeDb })
|
const sessionManager = new UserSessionManager({ providers, db: fakeDb })
|
||||||
@@ -738,6 +743,42 @@ describe("PUT /api/sources/:sourceId", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(204)
|
expect(res.status).toBe(204)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("returns 204 when credentials are included alongside config", async () => {
|
||||||
|
activeStore = createInMemoryStore()
|
||||||
|
const { app } = createAppWithEncryptor(
|
||||||
|
[createStubProvider("aelis.weather", weatherConfig)],
|
||||||
|
MOCK_USER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await put(app, "aelis.weather", {
|
||||||
|
enabled: true,
|
||||||
|
config: { units: "metric" },
|
||||||
|
credentials: { apiKey: "secret123" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(204)
|
||||||
|
const row = activeStore.rows.get(`${MOCK_USER_ID}:aelis.weather`)
|
||||||
|
expect(row).toBeDefined()
|
||||||
|
expect(row!.enabled).toBe(true)
|
||||||
|
expect(row!.config).toEqual({ units: "metric" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns 503 when credentials are provided but no encryptor is configured", async () => {
|
||||||
|
activeStore = createInMemoryStore()
|
||||||
|
// createApp does NOT configure an encryptor
|
||||||
|
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
|
||||||
|
|
||||||
|
const res = await put(app, "aelis.weather", {
|
||||||
|
enabled: true,
|
||||||
|
config: { units: "metric" },
|
||||||
|
credentials: { apiKey: "secret123" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toBe(503)
|
||||||
|
const body = (await res.json()) as { error: string }
|
||||||
|
expect(body.error).toContain("not configured")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("PUT /api/sources/:sourceId/credentials", () => {
|
describe("PUT /api/sources/:sourceId/credentials", () => {
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ const ReplaceSourceConfigRequestBody = type({
|
|||||||
"+": "reject",
|
"+": "reject",
|
||||||
enabled: "boolean",
|
enabled: "boolean",
|
||||||
config: "unknown",
|
config: "unknown",
|
||||||
|
"credentials?": "unknown",
|
||||||
})
|
})
|
||||||
|
|
||||||
const ReplaceSourceConfigNoConfigRequestBody = type({
|
const ReplaceSourceConfigNoConfigRequestBody = type({
|
||||||
"+": "reject",
|
"+": "reject",
|
||||||
enabled: "boolean",
|
enabled: "boolean",
|
||||||
|
"credentials?": "unknown",
|
||||||
})
|
})
|
||||||
|
|
||||||
export function registerSourcesHttpHandlers(
|
export function registerSourcesHttpHandlers(
|
||||||
@@ -161,14 +163,15 @@ async function handleReplaceSource(c: Context<Env>) {
|
|||||||
return c.json({ error: parsed.summary }, 400)
|
return c.json({ error: parsed.summary }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { enabled } = parsed
|
const { enabled, credentials } = parsed
|
||||||
const config = "config" in parsed ? parsed.config : undefined
|
const config = "config" in parsed ? parsed.config : undefined
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sessionManager.upsertSourceConfig(user.id, sourceId, {
|
await sessionManager.saveSourceConfig(user.id, sourceId, {
|
||||||
enabled,
|
enabled,
|
||||||
config,
|
config,
|
||||||
|
credentials,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof SourceNotFoundError) {
|
if (err instanceof SourceNotFoundError) {
|
||||||
@@ -177,6 +180,9 @@ async function handleReplaceSource(c: Context<Env>) {
|
|||||||
if (err instanceof InvalidSourceConfigError) {
|
if (err instanceof InvalidSourceConfigError) {
|
||||||
return c.json({ error: err.message }, 400)
|
return c.json({ error: err.message }, 400)
|
||||||
}
|
}
|
||||||
|
if (err instanceof CredentialStorageUnavailableError) {
|
||||||
|
return c.json({ error: err.message }, 503)
|
||||||
|
}
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export function sources(db: Database, userId: string) {
|
|||||||
return rows[0]
|
return rows[0]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Like find(), but acquires a row lock to prevent concurrent modifications. Must be called inside a transaction. */
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(userSources)
|
||||||
|
.where(and(eq(userSources.userId, userId), eq(userSources.sourceId, sourceId)))
|
||||||
|
.limit(1)
|
||||||
|
.for("update")
|
||||||
|
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
|
||||||
/** Enables a source for the user. Throws if the source row doesn't exist. */
|
/** Enables a source for the user. Throws if the source row doesn't exist. */
|
||||||
async enableSource(sourceId: string) {
|
async enableSource(sourceId: string) {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
|
|||||||
@@ -55,6 +55,6 @@
|
|||||||
"eas-cli": "^18.0.1",
|
"eas-cli": "^18.0.1",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.0",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "^6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^6",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
{
|
{
|
||||||
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
"lib": ["DOM", "ES2022"],
|
||||||
"types": ["node", "vite/client"],
|
"types": ["node", "vite/client"],
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"rootDirs": [".", "./.react-router/types"],
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./app/*"]
|
"~/*": ["./app/*"]
|
||||||
},
|
},
|
||||||
"esModuleInterop": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
|||||||
44
bun.lock
44
bun.lock
@@ -7,12 +7,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@json-render/core": "^0.12.1",
|
"@json-render/core": "^0.12.1",
|
||||||
"@nym.sh/jrx": "^0.2.0",
|
"@nym.sh/jrx": "^0.2.0",
|
||||||
"@types/bun": "^1.3.12",
|
"@types/bun": "latest",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260412.1",
|
||||||
"oxfmt": "^0.24.0",
|
"oxfmt": "^0.24.0",
|
||||||
"oxlint": "^1.39.0",
|
"oxlint": "^1.39.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/admin-dashboard": {
|
"apps/admin-dashboard": {
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "^6",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"@aelis/source-location": "workspace:*",
|
"@aelis/source-location": "workspace:*",
|
||||||
"@aelis/source-tfl": "workspace:*",
|
"@aelis/source-tfl": "workspace:*",
|
||||||
"@aelis/source-weatherkit": "workspace:*",
|
"@aelis/source-weatherkit": "workspace:*",
|
||||||
|
"@openrouter/sdk": "^0.9.11",
|
||||||
"arktype": "^2.1.29",
|
"arktype": "^2.1.29",
|
||||||
"better-auth": "^1",
|
"better-auth": "^1",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
@@ -110,7 +112,7 @@
|
|||||||
"eas-cli": "^18.0.1",
|
"eas-cli": "^18.0.1",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.0",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "^6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/waitlist-website": {
|
"apps/waitlist-website": {
|
||||||
@@ -137,7 +139,7 @@
|
|||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^6",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
},
|
},
|
||||||
@@ -767,6 +769,8 @@
|
|||||||
|
|
||||||
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
||||||
|
|
||||||
|
"@openrouter/sdk": ["@openrouter/sdk@0.9.11", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-BgFu6NcIJO4a9aVjr04y3kZ8pyM71j15I+bzfVAGEvxnj+KQNIkBYQGgwrG3D+aT1QpDKLki8btcQmpaxUas6A=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
||||||
@@ -1369,7 +1373,7 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||||
|
|
||||||
"@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="],
|
"@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="],
|
||||||
|
|
||||||
@@ -1449,6 +1453,22 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260412.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260412.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260412.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-tDw3XZt2BkjAlt/MJmnFGmbe9lgKmc5wezmrMoBIEvJcqz+/KVpVBVvjbkZoaiABnJmuG3G3b6IUFrEveTw6UQ=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260412.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sSkFG+hjtRWffg6FddF3dEkk7N3TRMEqfiUpixwcWhXgyocMdPw8wutTvQRBxQdgxeL9y01M2SO8A8YPPiEgVg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260412.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2BTeaLkrHEEDg0D9snigddy01qTY+wgx+W+GpXAfx36PPvW4xWuGXNVWfSaB8bqAC9C8NeLnT/C9/G/rJ5v2w=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wDLekbfsfmKMWORg7CTnEnpKj8oXpU/6AEBrtVN9CEUCiQAe6yH878nZHhJNzWQXHtrtFf3lY49Yplqmdxja3w=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JAdsG6MlVV1hoAUKPy8zxAL7xLeNxz8JgCbLCJVqW8EyH29R9FD4cFTqr7CSIRTNUEDzDTrgnXUyoRtDe1gr+w=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gYgppiQIqid3jZ7D8THh4k3Q+4bwidrQH6SL9Xgbk1qfP6/jwv8twuPqDOfZ+cK2OD55lQHp15fOh2lMNAC40Q=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260412.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-TOh7rH5H3jisHJqRXJSjmUGMzcbNBocS/hufhXPQIv+g3pdG5IKZoSnv3SV62I5d12FFDSS5KQon5MQPnOKAHg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260412.1", "", { "os": "win32", "cpu": "x64" }, "sha512-u+70wL89wspN1wKoX6FVNUATRGCG3BpleByP3H/UqOZvlwuMm8N7Gy8hEbM0U8bDyAuyP/daUfTBVkqXjjv9mA=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||||
@@ -1661,7 +1681,7 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -3425,7 +3445,7 @@
|
|||||||
|
|
||||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||||
|
|
||||||
"ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="],
|
"ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="],
|
||||||
|
|
||||||
@@ -3901,8 +3921,6 @@
|
|||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"aelis-client/@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
|
||||||
|
|
||||||
"aelis-client/@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
|
"aelis-client/@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
|
||||||
|
|
||||||
"aelis-client/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
"aelis-client/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
@@ -3929,6 +3947,8 @@
|
|||||||
|
|
||||||
"body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
"body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
||||||
|
|
||||||
"c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
"c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
@@ -4539,8 +4559,6 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||||
|
|
||||||
"aelis-client/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
|
||||||
|
|
||||||
"aelis-client/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
"aelis-client/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
"better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
"better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||||
@@ -4549,6 +4567,8 @@
|
|||||||
|
|
||||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"chrome-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"chrome-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"chromium-edge-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"chromium-edge-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@json-render/core": "^0.12.1",
|
"@json-render/core": "^0.12.1",
|
||||||
"@nym.sh/jrx": "^0.2.0",
|
"@nym.sh/jrx": "^0.2.0",
|
||||||
"@types/bun": "^1.3.12",
|
"@types/bun": "latest",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260412.1",
|
||||||
"oxfmt": "^0.24.0",
|
"oxfmt": "^0.24.0",
|
||||||
"oxlint": "^1.39.0"
|
"oxlint": "^1.39.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
@@ -8,20 +7,19 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
// Best practices
|
"types": ["bun"],
|
||||||
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
|||||||
Reference in New Issue
Block a user