diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b18ed37b..e0f05467 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -66,6 +66,7 @@ import * as appsIndexer from '@x/core/dist/apps/indexer.js'; import * as appsServer from '@x/core/dist/apps/server.js'; import * as appsAgents from '@x/core/dist/apps/agents.js'; import { capture } from '@x/core/dist/analytics/posthog.js'; +import * as githubAuth from '@x/core/dist/apps/github-auth.js'; import { consumePendingDeepLink } from './deeplink.js'; import { qualifyAndDisconnectComposioGoogle } from '@x/core/dist/migrations/composio-google-migration.js'; import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js'; @@ -1598,6 +1599,23 @@ export function setupIpcHandlers() { appsServer.setAppsTheme(args.theme); return { ok: true as const }; }, + // GitHub auth (device flow) — publishing only + 'githubAuth:start': async () => { + const result = await githubAuth.startDeviceFlow(); + // Surface the code and open GitHub's verification page externally (§10). + void shell.openExternal(result.verificationUri); + return result; + }, + 'githubAuth:poll': async () => { + return githubAuth.pollDeviceFlow(); + }, + 'githubAuth:status': async () => { + return githubAuth.getAuthStatus(); + }, + 'githubAuth:signOut': async () => { + await githubAuth.clearAuth(); + return { ok: true as const }; + }, // Agent schedule handlers 'agent-schedule:getConfig': async () => { const repo = container.resolve('agentScheduleRepo'); diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index c0cbb785..b7ac13ac 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, desktopCapturer, protocol, net, shell, session, type Session } from "electron"; +import { app, BrowserWindow, desktopCapturer, protocol, net, shell, session, safeStorage, type Session } from "electron"; import path from "node:path"; import { setupIpcHandlers, @@ -38,6 +38,7 @@ import { init as initBackgroundTaskScheduler } from "@x/core/dist/background-tas import { backgroundTaskEventConsumer } from "@x/core/dist/background-tasks/event-consumer.js"; import { init as initAppsServer, shutdown as shutdownAppsServer } from "@x/core/dist/apps/server.js"; import { registerAppsHostApi } from "@x/core/dist/apps/host-api.js"; +import { setTokenCipher as setGithubTokenCipher } from "@x/core/dist/apps/github-auth.js"; import { shutdown as shutdownAnalytics } from "@x/core/dist/analytics/posthog.js"; import { identifyIfSignedIn } from "@x/core/dist/analytics/identify.js"; import { migrateRuns } from "@x/core/dist/migrations/runs/migrate.js"; @@ -505,6 +506,14 @@ app.whenReady().then(async () => { // start the Rowboat Apps server (per-app origins on 127.0.0.1:3210) with the // full Host API (tools/fetch/llm/copilot behind the capability gate) registerAppsHostApi(); + + // GitHub publish token at rest: encrypt via the OS keychain when available + // (core stays electron-free; the cipher is injected here). + setGithubTokenCipher({ + isAvailable: () => safeStorage.isEncryptionAvailable(), + encrypt: (plain) => safeStorage.encryptString(plain).toString('base64'), + decrypt: (encrypted) => safeStorage.decryptString(Buffer.from(encrypted, 'base64')), + }); initAppsServer().catch((error) => { console.error('[Apps] Failed to start:', error); }); diff --git a/apps/x/docs/apps-registry/.github/workflows/validate-and-merge.yml b/apps/x/docs/apps-registry/.github/workflows/validate-and-merge.yml new file mode 100644 index 00000000..6e47051e --- /dev/null +++ b/apps/x/docs/apps-registry/.github/workflows/validate-and-merge.yml @@ -0,0 +1,102 @@ +# Validates publish PRs against the Rowboat Apps registry and auto-merges on +# success (spec §9.3). On any failure the PR is closed with a comment whose +# first line is machine-readable: `rejected: `. +name: validate-and-merge + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + validate: + runs-on: ubuntu-latest + # Per-name concurrency: two racing PRs for one name serialize; the loser + # fails the name-collision check. + concurrency: + group: publish-${{ github.event.pull_request.title }} + cancel-in-progress: false + steps: + - name: Checkout base (main) + uses: actions/checkout@v4 + with: + ref: main + + - name: Fetch PR diff and validate + id: validate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + reject() { echo "code=$1" >> "$GITHUB_OUTPUT"; echo "detail=$2" >> "$GITHUB_OUTPUT"; exit 1; } + + # 1. The diff adds exactly one file, under apps/, and changes nothing else. + files_json=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate) + count=$(echo "$files_json" | jq 'length') + [ "$count" = "1" ] || reject invalid_diff "PR must add exactly one file (found $count changes)" + status=$(echo "$files_json" | jq -r '.[0].status') + [ "$status" = "added" ] || reject invalid_diff "PR must ADD a record; modifications are not accepted in V1" + filepath=$(echo "$files_json" | jq -r '.[0].filename') + case "$filepath" in apps/*.json) ;; *) reject invalid_path "record must be apps/.json (got $filepath)";; esac + + # 2. Filename is .json with a valid name. + name=$(basename "$filepath" .json) + echo "$name" | grep -Eq '^[a-z0-9]+(-[a-z0-9]+)*$' || reject invalid_name "\"$name\" is not a valid package name" + len=${#name}; [ "$len" -ge 3 ] && [ "$len" -le 64 ] || reject invalid_name "name length must be 3-64" + + # 3. Content validates against the schema and name matches the filename stem. + gh api "repos/${GITHUB_REPOSITORY}/contents/${filepath}?ref=${HEAD_SHA}" --jq .content | base64 -d > /tmp/record.json + npx --yes ajv-cli@5 validate -s schema/registry-record.schema.json -d /tmp/record.json --strict=false \ + || reject invalid_record "record does not match schema/registry-record.schema.json" + rec_name=$(jq -r .name /tmp/record.json) + [ "$rec_name" = "$name" ] || reject name_mismatch "record.name \"$rec_name\" != filename stem \"$name\"" + + # 4. record.owner equals the PR author. + rec_owner=$(jq -r .owner /tmp/record.json) + [ "$rec_owner" = "$PR_AUTHOR" ] || reject owner_mismatch "record.owner \"$rec_owner\" != PR author \"$PR_AUTHOR\"" + + # 5. Name not taken and not retired. + [ ! -f "apps/${name}.json" ] || reject name_taken "apps/${name}.json already exists" + [ ! -f "removed/${name}.json" ] || reject name_retired "\"$name\" was removed and stays retired" + + # 6. record.repo is public and the author has push/admin on it. + repo=$(jq -r .repo /tmp/record.json) + visibility=$(gh api "repos/${repo}" --jq .visibility 2>/dev/null) || reject repo_unreachable "cannot read ${repo}" + [ "$visibility" = "public" ] || reject repo_not_public "${repo} is not public" + perm=$(gh api "repos/${repo}/collaborators/${PR_AUTHOR}/permission" --jq .permission 2>/dev/null) \ + || reject permission_check_failed "cannot check ${PR_AUTHOR}'s permission on ${repo}" + case "$perm" in admin|write) ;; *) reject not_repo_collaborator "PR author has \"$perm\" on ${repo}; needs push or admin";; esac + + # 7. Latest release carries the bundle asset (existence probe only — D14). + http=$(curl -s -o /dev/null -w "%{http_code}" -I -L "https://github.com/${repo}/releases/latest/download/${name}.rowboat-app") + case "$http" in 200|302) ;; *) reject missing_release_asset "releases/latest has no ${name}.rowboat-app (HTTP $http)";; esac + + echo "name=$name" >> "$GITHUB_OUTPUT" + + - name: Merge on success + if: success() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr comment "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" \ + --body "published: ${{ steps.validate.outputs.name }}" + gh pr merge "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" --squash --admin + + - name: Close on failure + if: failure() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + code="${{ steps.validate.outputs.code }}" + detail="${{ steps.validate.outputs.detail }}" + gh pr comment "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" \ + --body "rejected: ${code:-validation_error} + + ${detail:-Validation failed; see the Action log.}" + gh pr close "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" diff --git a/apps/x/docs/apps-registry/schema/registry-record.schema.json b/apps/x/docs/apps-registry/schema/registry-record.schema.json new file mode 100644 index 00000000..1973c1ef --- /dev/null +++ b/apps/x/docs/apps-registry/schema/registry-record.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Rowboat Apps registry record", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "name", "owner", "repo", "createdAt"], + "properties": { + "schemaVersion": { "const": 1 }, + "name": { + "type": "string", + "minLength": 3, + "maxLength": 64, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" + }, + "owner": { "type": "string", "minLength": 1 }, + "repo": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" + }, + "description": { "type": "string", "maxLength": 500 }, + "iconUrl": { "type": "string", "format": "uri", "pattern": "^https://" }, + "createdAt": { "type": "string" } + } +} diff --git a/apps/x/packages/core/package.json b/apps/x/packages/core/package.json index 14d35e81..85d7495d 100644 --- a/apps/x/packages/core/package.json +++ b/apps/x/packages/core/package.json @@ -50,6 +50,8 @@ "react": "^19.2.3", "xlsx": "^0.18.5", "yaml": "^2.8.2", + "yauzl": "^3.4.0", + "yazl": "^3.3.1", "zod": "^4.2.1" }, "devDependencies": { @@ -59,6 +61,8 @@ "@types/papaparse": "^5.5.2", "@types/pdf-parse": "^1.1.5", "@types/qrcode": "^1.5.6", + "@types/yauzl": "^3.4.0", + "@types/yazl": "^3.3.1", "vitest": "catalog:" } } diff --git a/apps/x/packages/core/src/apps/github-auth.ts b/apps/x/packages/core/src/apps/github-auth.ts new file mode 100644 index 00000000..d67ad394 --- /dev/null +++ b/apps/x/packages/core/src/apps/github-auth.ts @@ -0,0 +1,158 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { WorkDir } from '../config/config.js'; +import { GITHUB_OAUTH_CLIENT_ID } from '../config/env.js'; + +// GitHub authentication via the OAuth device flow (spec §10). Sign-in is +// required ONLY for publishing; create/run/install/update never touch it. +// The token doubles as publisher identity (D6). + +const AUTH_FILE = path.join(WorkDir, 'config', 'github-auth.json'); + +// Token-at-rest encryption is provided by the Electron main process +// (safeStorage) — core stays electron-free. When no cipher is wired (or the OS +// keychain is unavailable) the token is stored plaintext with a marker, +// matching the existing token-storage approach in auth/. +export interface TokenCipher { + isAvailable(): boolean; + encrypt(plain: string): string; // returns base64 + decrypt(encrypted: string): string; +} +let cipher: TokenCipher | null = null; +export function setTokenCipher(c: TokenCipher): void { + cipher = c; +} + +type StoredAuth = { + login: string; + createdAt: string; + token?: string; // plaintext fallback + tokenEncrypted?: string; // base64 via cipher + plaintext?: boolean; +}; + +type PendingFlow = { + deviceCode: string; + intervalMs: number; + expiresAt: number; +}; +let pending: PendingFlow | null = null; + +async function readAuth(): Promise { + try { + return JSON.parse(await fs.readFile(AUTH_FILE, 'utf-8')) as StoredAuth; + } catch { + return null; + } +} + +async function writeAuth(auth: StoredAuth): Promise { + await fs.mkdir(path.dirname(AUTH_FILE), { recursive: true }); + await fs.writeFile(AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 }); +} + +/** Start the device flow. Returns the code the user enters on github.com. */ +export async function startDeviceFlow(): Promise<{ userCode: string; verificationUri: string; expiresIn: number }> { + const res = await fetch('https://github.com/login/device/code', { + method: 'POST', + headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ client_id: GITHUB_OAUTH_CLIENT_ID, scope: 'public_repo' }), + }); + if (!res.ok) throw new Error(`device_code_failed: HTTP ${res.status}`); + const body = await res.json() as { + device_code: string; user_code: string; verification_uri: string; + expires_in: number; interval: number; + }; + pending = { + deviceCode: body.device_code, + intervalMs: (body.interval || 5) * 1000, + expiresAt: Date.now() + body.expires_in * 1000, + }; + return { userCode: body.user_code, verificationUri: body.verification_uri, expiresIn: body.expires_in }; +} + +export type PollResult = + | { status: 'pending' } + | { status: 'authorized'; login: string } + | { status: 'expired' } + | { status: 'denied' }; + +/** Poll once for device-flow completion (renderer drives the cadence). */ +export async function pollDeviceFlow(): Promise { + if (!pending) return { status: 'expired' }; + if (Date.now() > pending.expiresAt) { + pending = null; + return { status: 'expired' }; + } + + const res = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: GITHUB_OAUTH_CLIENT_ID, + device_code: pending.deviceCode, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }), + }); + const body = await res.json() as { access_token?: string; error?: string }; + + if (body.error === 'authorization_pending') return { status: 'pending' }; + if (body.error === 'slow_down') { + pending.intervalMs += 5000; + return { status: 'pending' }; + } + if (body.error === 'expired_token') { + pending = null; + return { status: 'expired' }; + } + if (body.error === 'access_denied') { + pending = null; + return { status: 'denied' }; + } + if (!body.access_token) return { status: 'pending' }; + + // Identity: cache login alongside the token. + const userRes = await fetch('https://api.github.com/user', { + headers: { 'Authorization': `Bearer ${body.access_token}`, 'Accept': 'application/vnd.github+json' }, + }); + if (!userRes.ok) throw new Error(`identity_failed: HTTP ${userRes.status}`); + const user = await userRes.json() as { login: string }; + + const auth: StoredAuth = { login: user.login, createdAt: new Date().toISOString() }; + if (cipher?.isAvailable()) { + auth.tokenEncrypted = cipher.encrypt(body.access_token); + } else { + auth.token = body.access_token; + auth.plaintext = true; + } + await writeAuth(auth); + pending = null; + return { status: 'authorized', login: user.login }; +} + +export async function getAuthStatus(): Promise<{ signedIn: boolean; login?: string }> { + const auth = await readAuth(); + return auth ? { signedIn: true, login: auth.login } : { signedIn: false }; +} + +/** The stored token, or null. Callers hitting a 401 MUST call clearAuth(). */ +export async function getGithubToken(): Promise<{ token: string; login: string } | null> { + const auth = await readAuth(); + if (!auth) return null; + if (auth.tokenEncrypted && cipher?.isAvailable()) { + try { + return { token: cipher.decrypt(auth.tokenEncrypted), login: auth.login }; + } catch { + await clearAuth(); + return null; + } + } + if (auth.token) return { token: auth.token, login: auth.login }; + return null; +} + +/** Sign out / expire (a 401 from any GitHub call surfaces as github_auth_expired). */ +export async function clearAuth(): Promise { + pending = null; + await fs.rm(AUTH_FILE, { force: true }); +} diff --git a/apps/x/packages/core/src/apps/host-api.ts b/apps/x/packages/core/src/apps/host-api.ts index 7fce4129..9b246a4d 100644 --- a/apps/x/packages/core/src/apps/host-api.ts +++ b/apps/x/packages/core/src/apps/host-api.ts @@ -354,10 +354,11 @@ async function handleCopilotRun( // Headless tool profile: the background-task agent (no shell, no // ask-human/interactive tools) — the same runtime scheduled agents use. // The run is recorded as a normal attributed turn (visible in history). - const model = await getBackgroundTaskAgentModel(); + const selection = await getBackgroundTaskAgentModel(); const run = await createRun({ agentId: 'background-task-agent', - model, + model: selection.model, + provider: selection.provider, useCase: 'app_copilot_run', subUseCase: slug, }); diff --git a/apps/x/packages/core/src/apps/packager.ts b/apps/x/packages/core/src/apps/packager.ts new file mode 100644 index 00000000..47ecee8e --- /dev/null +++ b/apps/x/packages/core/src/apps/packager.ts @@ -0,0 +1,120 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import yazl from 'yazl'; +import { RowboatAppManifestSchema, type RowboatAppManifest } from '@x/shared/dist/rowboat-app.js'; +import { appDir } from './indexer.js'; + +// Packager (spec §4.4): builds the `.rowboat-app` ZIP from the ALLOWLIST +// only — rowboat-app.json, dist/**, agents/, defaults/** — in +// sorted path order (determinism). Personal data cannot leak by omission (D15): +// src/, package.json, node_modules/, data/, dotfiles, and .rowboat-*.json are +// simply never on the list. Symlinks are skipped with a warning, never followed. + +export class PackageError extends Error { + readonly code: string; + constructor(code: string, message: string) { + super(message); + this.code = code; + } +} + +/** Recursively collect files under root; returns package-relative POSIX paths. */ +async function collectFiles(absRoot: string, relPrefix: string, warnings: string[]): Promise { + const out: string[] = []; + let entries; + try { + entries = await fsp.readdir(absRoot, { withFileTypes: true }); + } catch { + return out; + } + for (const entry of entries) { + const abs = path.join(absRoot, entry.name); + const rel = `${relPrefix}/${entry.name}`; + if (entry.isSymbolicLink()) { + warnings.push(`skipped symlink: ${rel}`); + continue; + } + if (entry.isDirectory()) { + out.push(...await collectFiles(abs, rel, warnings)); + } else if (entry.isFile()) { + out.push(rel); + } + } + return out; +} + +export interface PackageResult { + /** Absolute path of the written bundle. */ + bundlePath: string; + /** Lowercase-hex SHA-256 of the finished ZIP bytes. */ + sha256: string; + manifest: RowboatAppManifest; + /** Package-relative paths included, in the order written. */ + files: string[]; + warnings: string[]; +} + +/** + * Build `.rowboat-app` for the app at `folder`, writing the bundle to + * `outDir` (created if needed). + */ +export async function packageApp(folder: string, outDir: string): Promise { + const dir = appDir(folder); + + // 1. Manifest must parse and dist/ must exist. + let manifest: RowboatAppManifest; + try { + manifest = RowboatAppManifestSchema.parse( + JSON.parse(await fsp.readFile(path.join(dir, 'rowboat-app.json'), 'utf-8')), + ); + } catch (e) { + throw new PackageError('invalid_manifest', `rowboat-app.json is missing or invalid: ${e instanceof Error ? e.message : String(e)}`); + } + const entryAbs = path.join(dir, 'dist', manifest.entry); + if (!fs.existsSync(entryAbs) || !fs.statSync(entryAbs).isFile()) { + throw new PackageError('missing_entry', `dist/${manifest.entry} does not exist`); + } + + // 2. Assemble the allowlist, sorted for determinism. + const warnings: string[] = []; + const files: string[] = ['rowboat-app.json']; + files.push(...(await collectFiles(path.join(dir, 'dist'), 'dist', warnings)).sort()); + // agents/: ONLY files listed in manifest.agents (and they must exist). + for (const agentFile of [...manifest.agents].sort()) { + const abs = path.join(dir, 'agents', agentFile); + if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) { + throw new PackageError('missing_agent', `agents/${agentFile} is listed in the manifest but missing`); + } + files.push(`agents/${agentFile}`); + } + files.push(...(await collectFiles(path.join(dir, 'defaults'), 'defaults', warnings)).sort()); + + // 3. Write the ZIP. + await fsp.mkdir(outDir, { recursive: true }); + const bundlePath = path.join(outDir, `${manifest.name}.rowboat-app`); + const zip = new yazl.ZipFile(); + for (const rel of files) { + zip.addFile(path.join(dir, ...rel.split('/')), rel); + } + zip.end(); + + const hash = crypto.createHash('sha256'); + await new Promise((resolve, reject) => { + const out = fs.createWriteStream(bundlePath); + zip.outputStream.on('data', (chunk: Buffer) => hash.update(chunk)); + zip.outputStream.pipe(out); + out.on('close', () => resolve()); + out.on('error', reject); + zip.outputStream.on('error', reject); + }); + + return { + bundlePath, + sha256: hash.digest('hex'), + manifest, + files, + warnings, + }; +} diff --git a/apps/x/packages/core/src/apps/registry.ts b/apps/x/packages/core/src/apps/registry.ts new file mode 100644 index 00000000..29f96b8a --- /dev/null +++ b/apps/x/packages/core/src/apps/registry.ts @@ -0,0 +1,146 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import zlib from 'node:zlib'; +import { + RegistryRecordSchema, + RowboatAppManifestSchema, + type RegistryRecord, + type RowboatAppManifest, +} from '@x/shared/dist/rowboat-app.js'; +import { REGISTRY_REPO, REGISTRY_BRANCH, CATALOG_CACHE_PATH, CATALOG_TTL_MS } from './constants.js'; + +// Registry client (spec §9.2). All registry access goes through RegistryClient — +// this is the backend swap seam (D5). The GitHub implementation reads the +// registry repo as one unauthenticated tarball and resolves versions via +// release-asset URLs (plain HTTPS redirects, no API quota). + +export interface RegisterResult { + status: 'published' | 'pending' | 'rejected'; + prUrl?: string; + rejectionCode?: string; +} + +export interface RegistryClient { + refreshIndex(force?: boolean): Promise<{ records: RegistryRecord[]; stale: boolean; fetchedAt: string }>; + resolve(name: string): Promise; + search(query: string): Promise; + latestManifest(record: RegistryRecord): Promise; +} + +type CatalogCache = { fetchedAt: string; records: RegistryRecord[] }; + +// --------------------------------------------------------------------------- +// Minimal tar reader — enough to pull apps/*.json out of a codeload tarball. +// --------------------------------------------------------------------------- + +function* tarEntries(tarBuf: Buffer): Generator<{ name: string; body: Buffer }> { + let offset = 0; + while (offset + 512 <= tarBuf.length) { + const header = tarBuf.subarray(offset, offset + 512); + if (header.every((b) => b === 0)) break; // end-of-archive + const name = header.subarray(0, 100).toString('utf-8').replace(/\0.*$/, ''); + const sizeOctal = header.subarray(124, 136).toString('utf-8').replace(/\0.*$/, '').trim(); + const size = parseInt(sizeOctal, 8) || 0; + const body = tarBuf.subarray(offset + 512, offset + 512 + size); + yield { name, body: Buffer.from(body) }; + offset += 512 + Math.ceil(size / 512) * 512; + } +} + +async function readCache(): Promise { + try { + return JSON.parse(await fs.readFile(CATALOG_CACHE_PATH, 'utf-8')) as CatalogCache; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// GitHub implementation +// --------------------------------------------------------------------------- + +export class GitHubRegistryClient implements RegistryClient { + async refreshIndex(force = false): Promise<{ records: RegistryRecord[]; stale: boolean; fetchedAt: string }> { + const cache = await readCache(); + if (!force && cache && Date.now() - new Date(cache.fetchedAt).getTime() < CATALOG_TTL_MS) { + return { records: cache.records, stale: false, fetchedAt: cache.fetchedAt }; + } + + try { + const res = await fetch(`https://codeload.github.com/${REGISTRY_REPO}/tar.gz/${REGISTRY_BRANCH}`); + if (!res.ok) throw new Error(`registry tarball: HTTP ${res.status}`); + const gz = Buffer.from(await res.arrayBuffer()); + const tar = zlib.gunzipSync(gz); + + const records: RegistryRecord[] = []; + for (const entry of tarEntries(tar)) { + // Entries look like "-/apps/.json". + const m = /^[^/]+\/apps\/([^/]+)\.json$/.exec(entry.name); + if (!m) continue; + try { + const parsed = RegistryRecordSchema.safeParse(JSON.parse(entry.body.toString('utf-8'))); + if (parsed.success) { + records.push(parsed.data); + } else { + console.warn(`[Apps] registry record ${m[1]} failed schema; skipping`); + } + } catch { + console.warn(`[Apps] registry record ${m[1]} is invalid JSON; skipping`); + } + } + records.sort((a, b) => a.name.localeCompare(b.name)); + + const fetchedAt = new Date().toISOString(); + await fs.mkdir(path.dirname(CATALOG_CACHE_PATH), { recursive: true }); + await fs.writeFile(CATALOG_CACHE_PATH, JSON.stringify({ fetchedAt, records } satisfies CatalogCache, null, 2)); + return { records, stale: false, fetchedAt }; + } catch (err) { + // Network failure with a cache present → serve cache, marked stale. + if (cache) return { records: cache.records, stale: true, fetchedAt: cache.fetchedAt }; + throw err; + } + } + + async resolve(name: string): Promise { + const cache = await readCache(); + if (cache && Date.now() - new Date(cache.fetchedAt).getTime() < CATALOG_TTL_MS) { + return cache.records.find((r) => r.name === name) ?? null; + } + try { + const res = await fetch(`https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_BRANCH}/apps/${name}.json`); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`registry record: HTTP ${res.status}`); + const parsed = RegistryRecordSchema.safeParse(await res.json()); + return parsed.success ? parsed.data : null; + } catch { + // Fall back to any cache we have, however old. + return cache?.records.find((r) => r.name === name) ?? null; + } + } + + async search(query: string): Promise { + const { records } = await this.refreshIndex(); + const q = query.trim().toLowerCase(); + if (!q) return records; + return records.filter((r) => + r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q)); + } + + /** + * Latest version's manifest via the release-asset URL — a plain HTTPS + * redirect, NOT the REST API, so it costs no unauthenticated API quota. + */ + async latestManifest(record: RegistryRecord): Promise { + const res = await fetch(`https://github.com/${record.repo}/releases/latest/download/rowboat-app.json`, { + redirect: 'follow', + }); + if (!res.ok) throw new Error(`latest_manifest_unavailable: HTTP ${res.status}`); + const manifest = RowboatAppManifestSchema.parse(await res.json()); + if (manifest.name !== record.name) { + throw new Error(`name_mismatch: release manifest says "${manifest.name}" but the registry record is "${record.name}"`); + } + return manifest; + } +} + +export const registryClient: RegistryClient = new GitHubRegistryClient(); diff --git a/apps/x/packages/core/src/config/env.ts b/apps/x/packages/core/src/config/env.ts index 0f4026f5..b276d12b 100644 --- a/apps/x/packages/core/src/config/env.ts +++ b/apps/x/packages/core/src/config/env.ts @@ -1,2 +1,7 @@ export const API_URL = process.env.API_URL || 'https://api.x.rowboatlabs.com'; + +// GitHub OAuth app used for Apps publishing (device flow, public_repo scope). +// Client IDs are public identifiers, not secrets (spec §3). +export const GITHUB_OAUTH_CLIENT_ID = + process.env.ROWBOAT_GITHUB_CLIENT_ID || 'Ov23liAka106zKEovj4B'; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index f6cf8825..4eda18f5 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -1256,6 +1256,26 @@ const ipcSchemas = { req: z.object({ theme: z.enum(['light', 'dark']) }), res: z.object({ ok: z.literal(true) }), }, + // GitHub auth (device flow) — required only for publishing apps (spec §10). + 'githubAuth:start': { + req: z.object({}), + res: z.object({ userCode: z.string(), verificationUri: z.string(), expiresIn: z.number() }), + }, + 'githubAuth:poll': { + req: z.object({}), + res: z.object({ + status: z.enum(['pending', 'authorized', 'expired', 'denied']), + login: z.string().optional(), + }), + }, + 'githubAuth:status': { + req: z.object({}), + res: z.object({ signedIn: z.boolean(), login: z.string().optional() }), + }, + 'githubAuth:signOut': { + req: z.object({}), + res: z.object({ ok: z.literal(true) }), + }, 'composio:didConnect': { req: z.object({ toolkitSlug: z.string(), diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index ed73f72a..8299f5bd 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -231,16 +231,16 @@ importers: version: 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-placeholder': specifier: 3.22.4 - version: 3.22.4(@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + version: 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) '@tiptap/extension-table': specifier: 3.22.4 version: 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-task-item': specifier: 3.22.4 - version: 3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + version: 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) '@tiptap/extension-task-list': specifier: 3.22.4 - version: 3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + version: 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) '@tiptap/pm': specifier: 3.22.4 version: 3.22.4 @@ -533,6 +533,12 @@ importers: yaml: specifier: ^2.8.2 version: 2.8.2 + yauzl: + specifier: ^3.4.0 + version: 3.4.0 + yazl: + specifier: ^3.3.1 + version: 3.3.1 zod: specifier: ^4.2.1 version: 4.2.1 @@ -555,6 +561,12 @@ importers: '@types/qrcode': specifier: ^1.5.6 version: 1.5.6 + '@types/yauzl': + specifier: ^3.4.0 + version: 3.4.0 + '@types/yazl': + specifier: ^3.3.1 + version: 3.3.1 vitest: specifier: 'catalog:' version: 4.1.7(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(jsdom@29.1.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) @@ -657,21 +669,25 @@ packages: resolution: {integrity: sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==} cpu: [arm64] os: [linux] + libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': resolution: {integrity: sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==} cpu: [arm64] os: [linux] + libc: [glibc] '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': resolution: {integrity: sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==} cpu: [x64] os: [linux] + libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': resolution: {integrity: sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==} cpu: [x64] os: [linux] + libc: [glibc] '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': resolution: {integrity: sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==} @@ -1150,6 +1166,7 @@ packages: '@eigenpal/docx-editor-agents@1.0.3': resolution: {integrity: sha512-Bk/J9/PBnMCOxb6w4cHQiCTuN/1C4FtZM9evC9EXXcLP13yFMdqoEqsYs+Lh3HyaRRAaCZTrkfgOZyTqqyjtwQ==} + deprecated: deprecated peerDependencies: '@ai-sdk/vue': ^2.0.0 ai: ^5.0.0 || ^6.0.0 @@ -1167,6 +1184,7 @@ packages: '@eigenpal/docx-editor-core@1.0.3': resolution: {integrity: sha512-etpupuln9ZlHLW4DgS7877WBdMEChsAG0D1bEZLjF70isYbyxrd2ARWax745P7XMm4GqqkAfByzxE2GGWQmJaA==} + deprecated: deprecated hasBin: true peerDependencies: prosemirror-commands: ^1.5.2 @@ -1181,6 +1199,7 @@ packages: '@eigenpal/docx-editor-i18n@1.0.3': resolution: {integrity: sha512-zwz/S+duPOnzg/kh4bs28T3UqI8mKMzHdmFgbWgMxwtTfUkAxaUAnAVbuZgrysl1aD2scv4Hfy4EgOZcFy9NnA==} + deprecated: deprecated '@eigenpal/docx-editor-react@1.0.3': resolution: {integrity: sha512-KupDVHo6KC4KUs48bM1pMYFFbDJqkW8XyIhgsnLx+BWk2yOPU4bx2HfWB6H+JEVROA1h1AmhTAyE39gk75wg5w==} @@ -1302,7 +1321,7 @@ packages: engines: {node: '>=14'} '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': - resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} + resolution: {gitHosted: true, tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} version: 10.2.0-electron.1 engines: {node: '>=12.13.0'} hasBin: true @@ -1802,89 +1821,105 @@ packages: resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.3.2': resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.3.2': resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.3.2': resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.3.2': resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.3.2': resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.3.2': resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.3.2': resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.35.3': resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.35.3': resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.35.3': resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.35.3': resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.35.3': resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.35.3': resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.35.3': resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.35.3': resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.35.3': resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} @@ -2126,30 +2161,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.80': resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.80': resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.80': resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.80': resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==} @@ -3328,56 +3368,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -3711,24 +3762,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -3887,6 +3942,12 @@ packages: peerDependencies: '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list@3.22.4': + resolution: {integrity: sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + '@tiptap/extension-list@3.22.5': resolution: {integrity: sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==} peerDependencies: @@ -3939,6 +4000,12 @@ packages: peerDependencies: '@tiptap/core': 3.22.5 + '@tiptap/extensions@3.22.4': + resolution: {integrity: sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + '@tiptap/extensions@3.22.5': resolution: {integrity: sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==} peerDependencies: @@ -4250,6 +4317,12 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@types/yauzl@3.4.0': + resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==} + + '@types/yazl@3.3.1': + resolution: {integrity: sha512-DIWfCKpsTp6hE5BDBHV3+fIL/bLUF9Bv13iDrWnMlmhQpH67buNvI291ZauQ1xcccxK3FqQ9honnXpq4R8NMuQ==} + '@typescript-eslint/eslint-plugin@8.50.1': resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4311,6 +4384,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} @@ -4719,6 +4793,10 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -6630,24 +6708,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -9142,6 +9224,13 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + + yazl@3.3.1: + resolution: {integrity: sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -13334,6 +13423,11 @@ snapshots: dependencies: '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + '@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': dependencies: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) @@ -13347,9 +13441,9 @@ snapshots: dependencies: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': dependencies: - '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-strike@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': dependencies: @@ -13360,13 +13454,13 @@ snapshots: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) '@tiptap/pm': 3.22.4 - '@tiptap/extension-task-item@3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-task-item@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-task-list@3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-task-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-text@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': dependencies: @@ -13376,6 +13470,11 @@ snapshots: dependencies: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + '@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': dependencies: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) @@ -13791,6 +13890,14 @@ snapshots: '@types/node': 25.0.3 optional: true + '@types/yauzl@3.4.0': + dependencies: + '@types/node': 25.0.3 + + '@types/yazl@3.3.1': + dependencies: + '@types/node': 25.0.3 + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -14354,6 +14461,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -19637,6 +19746,14 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + + yazl@3.3.1: + dependencies: + buffer-crc32: 1.0.0 + yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} diff --git a/apps/x/pnpm-workspace.yaml b/apps/x/pnpm-workspace.yaml index 5024847a..1b34ff4c 100644 --- a/apps/x/pnpm-workspace.yaml +++ b/apps/x/pnpm-workspace.yaml @@ -13,6 +13,8 @@ allowBuilds: node-pty: true protobufjs: true +blockExoticSubdeps: false + overrides: vscode-jsonrpc: 8.2.0