feat(apps): M3 groundwork — packager, GitHub device-flow auth, registry client

- env: GITHUB_OAUTH_CLIENT_ID (device flow enabled on the Rowboat OAuth app;
  overridable via ROWBOAT_GITHUB_CLIENT_ID)
- packager (§4.4): allowlist-only .rowboat-app ZIP (yazl), sorted entries,
  symlink skip, sha256
- github-auth (§10): device-code start/poll, identity fetch, token stored
  0600 with safeStorage encryption injected from main (core stays
  electron-free); githubAuth:* IPC + external open of the verification page
- registry client (§9.2): unauthenticated tarball index with 5-min cache and
  stale fallback, raw-record resolve, substring search, quota-free
  latestManifest via release-asset redirect with name-mismatch guard
- registry repo contents (docs/apps-registry): record JSON schema +
  validate-and-merge Action implementing §9.3 checks 1-7 with rejected:<code>
  comments and per-name concurrency
- fix: host-api copilot-run adapted to dev's ModelSelection {provider,model}
  (this is the same fix dev needs for Ramnique's packaging break)
- pnpm 11: blockExoticSubdeps=false (electron-forge has a git subdep)
This commit is contained in:
Gagan 2026-07-06 14:49:52 +05:30
parent b7d1019538
commit d7af51c10b
13 changed files with 739 additions and 13 deletions

View file

@ -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<IAgentScheduleRepo>('agentScheduleRepo');

View file

@ -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);
});