@flame0510/project-aether 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { getSecret, listProfiles } from './vault';
|
|
3
|
+
import { getProvider } from './providers';
|
|
4
|
+
import { listAgentContainers as listAgentContainerInfo } from './detect';
|
|
5
|
+
|
|
6
|
+
export interface SyncResult {
|
|
7
|
+
container: string;
|
|
8
|
+
status: 'ok' | 'error';
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Container discovery ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
// Container names only, tolerant of docker failures (shared discovery lives in detect.ts).
|
|
15
|
+
function listAgentContainers(): string[] {
|
|
16
|
+
try {
|
|
17
|
+
return listAgentContainerInfo().map((c) => c.name).filter(Boolean);
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Exec helpers ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function containerExec(container: string, script: string, timeoutMs = 30000): void {
|
|
26
|
+
execSync(`docker exec ${container} sh -c ${JSON.stringify(script)}`, {
|
|
27
|
+
timeout: timeoutMs,
|
|
28
|
+
stdio: 'pipe',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function containerRead(container: string, path: string): string {
|
|
33
|
+
try {
|
|
34
|
+
return execSync(
|
|
35
|
+
`docker exec ${container} sh -c ${JSON.stringify(`cat ${path} 2>/dev/null || true`)}`,
|
|
36
|
+
{ timeout: 5000, encoding: 'utf-8', maxBuffer: 512 * 1024 },
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function b64(s: string): string {
|
|
44
|
+
return Buffer.from(s, 'utf-8').toString('base64');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// ── Per-provider delivery adapters ────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
interface VercelUserResponse { user?: { id?: string; defaultTeamId?: string } }
|
|
51
|
+
interface VercelTeam { id: string; slug: string }
|
|
52
|
+
interface VercelTeamsResponse { teams?: VercelTeam[] }
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate a Vercel token against the API and discover its accessible team scope.
|
|
56
|
+
* Returns the token and the team ID (not slug) to use in config.json.
|
|
57
|
+
* Uses Node.js fetch() instead of curl — token never appears in command line.
|
|
58
|
+
*/
|
|
59
|
+
async function discoverVercelScope(token: string): Promise<{ token: string; teamId: string | null }> {
|
|
60
|
+
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
61
|
+
|
|
62
|
+
// Validate token + get user info
|
|
63
|
+
const userRes = await fetch('https://api.vercel.com/v2/user', { headers, signal: AbortSignal.timeout(10000) });
|
|
64
|
+
if (!userRes.ok) throw new Error(`Vercel API returned ${userRes.status} for /v2/user`);
|
|
65
|
+
const userData: VercelUserResponse = await userRes.json() as VercelUserResponse;
|
|
66
|
+
if (!userData?.user?.id) throw new Error('Invalid token response from Vercel API');
|
|
67
|
+
|
|
68
|
+
let teamId: string | null = null;
|
|
69
|
+
try {
|
|
70
|
+
const teamsRes = await fetch('https://api.vercel.com/v2/teams', { headers, signal: AbortSignal.timeout(10000) });
|
|
71
|
+
if (teamsRes.ok) {
|
|
72
|
+
const teamsData: VercelTeamsResponse = await teamsRes.json() as VercelTeamsResponse;
|
|
73
|
+
const teams = teamsData?.teams;
|
|
74
|
+
if (teams && teams.length > 0) {
|
|
75
|
+
const defaultTeamId = userData.user.defaultTeamId;
|
|
76
|
+
const defaultTeam = defaultTeamId
|
|
77
|
+
? teams.find((t) => t.id === defaultTeamId)
|
|
78
|
+
: undefined;
|
|
79
|
+
teamId = (defaultTeam ?? teams[0]).id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Personal token, no team scope needed
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { token, teamId };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deliverGithub(container: string, secret: Record<string, string>): void {
|
|
90
|
+
const { token } = secret;
|
|
91
|
+
if (!token) throw new Error('Missing token');
|
|
92
|
+
|
|
93
|
+
// Require gh CLI preinstalled in the agent image
|
|
94
|
+
const hasGh = execSync(
|
|
95
|
+
`docker exec ${container} sh -c ${JSON.stringify('command -v gh >/dev/null 2>&1 && echo ok || echo missing')}`,
|
|
96
|
+
{ timeout: 5000, encoding: 'utf-8' },
|
|
97
|
+
).trim();
|
|
98
|
+
if (hasGh !== 'ok') throw new Error('gh CLI not installed in this container. Rebuild the base image: docker build -t openclaw-agent-base:latest agent-templates/base-image/');
|
|
99
|
+
|
|
100
|
+
containerExec(
|
|
101
|
+
container,
|
|
102
|
+
`echo ${JSON.stringify(b64(token))} | base64 -d | gh auth login --with-token`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function deliverVercel(container: string, secret: Record<string, string>): Promise<void> {
|
|
107
|
+
const { token } = secret;
|
|
108
|
+
if (!token) throw new Error('Missing token');
|
|
109
|
+
|
|
110
|
+
// Require vercel CLI preinstalled in the agent image
|
|
111
|
+
const hasVercel = execSync(
|
|
112
|
+
`docker exec ${container} sh -c ${JSON.stringify('command -v vercel >/dev/null 2>&1 && echo ok || echo missing')}`,
|
|
113
|
+
{ timeout: 5000, encoding: 'utf-8' },
|
|
114
|
+
).trim();
|
|
115
|
+
if (hasVercel !== 'ok') throw new Error('vercel CLI not installed in this container. Rebuild the base image: docker build -t openclaw-agent-base:latest agent-templates/base-image/');
|
|
116
|
+
|
|
117
|
+
// Validate token and discover team scope via Node fetch (no curl/exec)
|
|
118
|
+
const scope = await discoverVercelScope(token);
|
|
119
|
+
|
|
120
|
+
// Write auth.json for Vercel CLI auth — same directory as config.json
|
|
121
|
+
const authJson = JSON.stringify({ token });
|
|
122
|
+
containerExec(
|
|
123
|
+
container,
|
|
124
|
+
`mkdir -p /root/.local/share/com.vercel.cli && echo ${JSON.stringify(b64(authJson))} | base64 -d > /root/.local/share/com.vercel.cli/auth.json && chmod 600 /root/.local/share/com.vercel.cli/auth.json`,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Write config.json with currentTeam = team ID (prevents scope errors like "cashbee")
|
|
128
|
+
// Both auth.json and config.json MUST be in ~/.local/share/com.vercel.cli/
|
|
129
|
+
if (scope.teamId) {
|
|
130
|
+
const vercelConfig = JSON.stringify({ currentTeam: scope.teamId });
|
|
131
|
+
containerExec(
|
|
132
|
+
container,
|
|
133
|
+
`echo ${JSON.stringify(b64(vercelConfig))} | base64 -d > /root/.local/share/com.vercel.cli/config.json && chmod 600 /root/.local/share/com.vercel.cli/config.json`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deliverSupabase(container: string, secret: Record<string, string>): void {
|
|
140
|
+
const { accessToken } = secret;
|
|
141
|
+
if (!accessToken || !accessToken.trim()) throw new Error('Missing Supabase access token');
|
|
142
|
+
|
|
143
|
+
// Check supabase CLI is available (installed globally in the image)
|
|
144
|
+
const hasSupabase = execSync(
|
|
145
|
+
`docker exec ${container} sh -c ${JSON.stringify('command -v supabase >/dev/null 2>&1 && echo ok || echo missing')}`,
|
|
146
|
+
{ timeout: 5000, encoding: 'utf-8' },
|
|
147
|
+
).trim();
|
|
148
|
+
if (hasSupabase !== 'ok') throw new Error('supabase CLI not installed in this container. Rebuild the base image: docker build -t openclaw-agent-base:latest agent-templates/base-image/');
|
|
149
|
+
|
|
150
|
+
// Login with access token — grants access to all projects on the account
|
|
151
|
+
// supabase login --token supports non-interactive auth
|
|
152
|
+
containerExec(
|
|
153
|
+
container,
|
|
154
|
+
`supabase login --token ${JSON.stringify(accessToken.trim())} --no-browser 2>&1`,
|
|
155
|
+
30000,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function deliverTrello(container: string, secret: Record<string, string>): void {
|
|
160
|
+
const { apiKey, token } = secret;
|
|
161
|
+
if (!apiKey || !token) throw new Error('Missing Trello API key or token');
|
|
162
|
+
|
|
163
|
+
// Require trello CLI preinstalled in the agent image
|
|
164
|
+
const hasTrello = execSync(
|
|
165
|
+
`docker exec ${container} sh -c ${JSON.stringify('command -v trello >/dev/null 2>&1 && echo ok || echo missing')}`,
|
|
166
|
+
{ timeout: 5000, encoding: 'utf-8' },
|
|
167
|
+
).trim();
|
|
168
|
+
if (hasTrello !== 'ok') throw new Error('trello CLI not installed in this container. Rebuild the base image: docker build -t openclaw-agent-base:latest agent-templates/base-image/');
|
|
169
|
+
|
|
170
|
+
// Write trello-cli config file (same format trello auth would create)
|
|
171
|
+
const config = JSON.stringify({ apiKey, token });
|
|
172
|
+
containerExec(
|
|
173
|
+
container,
|
|
174
|
+
`mkdir -p /root/.trello-cli/default && echo ${JSON.stringify(b64(config))} | base64 -d > /root/.trello-cli/default/config.json && chmod 600 /root/.trello-cli/default/config.json`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Notion: no CLI, just write a config file (same pattern as Trello). */
|
|
179
|
+
function deliverNotion(container: string, secret: Record<string, string>): void {
|
|
180
|
+
const { token } = secret;
|
|
181
|
+
if (!token) throw new Error('Missing Notion API token');
|
|
182
|
+
|
|
183
|
+
const config = JSON.stringify({ token });
|
|
184
|
+
containerExec(
|
|
185
|
+
container,
|
|
186
|
+
`mkdir -p /root/.config/notion && echo ${JSON.stringify(b64(config))} | base64 -d > /root/.config/notion/config.json && chmod 600 /root/.config/notion/config.json`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── TOOLS.md managed section (per-container) ──────────────────────────────
|
|
191
|
+
|
|
192
|
+
const MARKER_START = '<!-- REV4A:INTEGRATIONS:START -->';
|
|
193
|
+
const MARKER_END = '<!-- REV4A:INTEGRATIONS:END -->';
|
|
194
|
+
const TOOLS_MD_PATH = '/root/.openclaw/workspace/TOOLS.md';
|
|
195
|
+
|
|
196
|
+
interface IntegrationEntry {
|
|
197
|
+
tool: string;
|
|
198
|
+
status: string;
|
|
199
|
+
note: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const PROVIDER_ENTRIES: Record<string, IntegrationEntry> = {
|
|
203
|
+
'github-pat': {
|
|
204
|
+
tool: 'gh',
|
|
205
|
+
status: '✓ ready',
|
|
206
|
+
note: 'GitHub CLI — `gh api`, `gh repo clone`, `gh pr create`, `gh issue list`',
|
|
207
|
+
},
|
|
208
|
+
vercel: {
|
|
209
|
+
tool: 'vercel',
|
|
210
|
+
status: '✓ ready',
|
|
211
|
+
note: 'Vercel CLI — `vercel list`, `vercel deploy`, `vercel env pull`',
|
|
212
|
+
},
|
|
213
|
+
supabase: {
|
|
214
|
+
tool: 'supabase',
|
|
215
|
+
status: '✓ ready',
|
|
216
|
+
note: 'Supabase CLI — `supabase login`, `supabase db push/pull`, `supabase functions deploy`, `supabase projects list`',
|
|
217
|
+
},
|
|
218
|
+
trello: {
|
|
219
|
+
tool: 'trello',
|
|
220
|
+
status: '✓ ready',
|
|
221
|
+
note: 'Trello CLI — `trello card:list`, `trello card:create`, `trello interactive`',
|
|
222
|
+
},
|
|
223
|
+
notion: {
|
|
224
|
+
tool: 'notion',
|
|
225
|
+
status: '✓ ready',
|
|
226
|
+
note: 'Notion API — read/write pages, databases, and blocks via REST API',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function entryRow(entry: IntegrationEntry): string {
|
|
231
|
+
return `| \`${entry.tool}\` | ${entry.status} | ${entry.note} |`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildBlock(integrations: IntegrationEntry[]): string {
|
|
235
|
+
const ts = new Date().toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
|
|
236
|
+
|
|
237
|
+
if (integrations.length === 0) {
|
|
238
|
+
return `${MARKER_START}\n_No integrations configured via Rev4a._\n${MARKER_END}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const rows = integrations.map((e) => entryRow(e)).join('\n');
|
|
242
|
+
return `${MARKER_START}
|
|
243
|
+
## Active Integrations
|
|
244
|
+
|
|
245
|
+
> Managed by Rev4a — do not edit this section manually. Last synced: ${ts}
|
|
246
|
+
|
|
247
|
+
| Tool | Status | Notes |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
${rows}
|
|
250
|
+
|
|
251
|
+
${MARKER_END}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function collectIntegrations(): IntegrationEntry[] {
|
|
255
|
+
const profiles = listProfiles();
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
const entries: IntegrationEntry[] = [];
|
|
258
|
+
|
|
259
|
+
for (const profile of profiles) {
|
|
260
|
+
if (profile.status !== 'active') continue;
|
|
261
|
+
if (seen.has(profile.providerId)) continue;
|
|
262
|
+
seen.add(profile.providerId);
|
|
263
|
+
|
|
264
|
+
const entry = PROVIDER_ENTRIES[profile.providerId];
|
|
265
|
+
if (entry) entries.push(entry);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return entries;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Helper: extract rows as pure string lines from the block content (between markers).
|
|
273
|
+
*/
|
|
274
|
+
function extractRows(existing: string, si: number, ei: number): string[] {
|
|
275
|
+
const between = existing.slice(si + MARKER_START.length, ei);
|
|
276
|
+
const content = between.trim();
|
|
277
|
+
return content.split('\n').filter((l) => l.trim().startsWith('|'));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Helper: convert raw row strings back to IntegrationEntry for buildBlock.
|
|
282
|
+
*/
|
|
283
|
+
function rowsToEntries(rows: string[]): IntegrationEntry[] {
|
|
284
|
+
return rows
|
|
285
|
+
.map((r) => {
|
|
286
|
+
const parts = r.split('|').map((s) => s.trim());
|
|
287
|
+
const toolPart = parts[1] ?? '';
|
|
288
|
+
const toolName = toolPart.replace(/^`|`$/g, '');
|
|
289
|
+
return Object.values(PROVIDER_ENTRIES).find((e) => e.tool === toolName);
|
|
290
|
+
})
|
|
291
|
+
.filter(Boolean) as IntegrationEntry[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Add or remove a single provider's row from the TOOLS.md integration block
|
|
296
|
+
* inside a single agent container (docker exec only, no host filesystem access).
|
|
297
|
+
*/
|
|
298
|
+
function toggleProviderRow(
|
|
299
|
+
container: string,
|
|
300
|
+
providerId: string,
|
|
301
|
+
action: 'add' | 'remove',
|
|
302
|
+
): void {
|
|
303
|
+
const safe = container.replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
304
|
+
if (!safe) return;
|
|
305
|
+
|
|
306
|
+
const entry = PROVIDER_ENTRIES[providerId];
|
|
307
|
+
if (!entry) return;
|
|
308
|
+
|
|
309
|
+
const existing = containerRead(safe, TOOLS_MD_PATH);
|
|
310
|
+
const row = entryRow(entry);
|
|
311
|
+
const si = existing.indexOf(MARKER_START);
|
|
312
|
+
const ei = existing.indexOf(MARKER_END);
|
|
313
|
+
|
|
314
|
+
let rows: string[];
|
|
315
|
+
|
|
316
|
+
if (action === 'add') {
|
|
317
|
+
if (si !== -1 && ei !== -1 && ei > si) {
|
|
318
|
+
rows = extractRows(existing, si, ei);
|
|
319
|
+
if (!rows.some((r) => r.includes(`\`${entry.tool}\``))) {
|
|
320
|
+
rows.push(row);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// No block yet — create one with just this row
|
|
324
|
+
const newBlock = buildBlock([entry]);
|
|
325
|
+
containerExec(
|
|
326
|
+
safe,
|
|
327
|
+
`echo ${JSON.stringify(b64(newBlock + '\n' + existing))} | base64 -d > ${TOOLS_MD_PATH}`,
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// action === 'remove'
|
|
333
|
+
if (si === -1 || ei === -1 || ei <= si) return; // No block to edit
|
|
334
|
+
rows = extractRows(existing, si, ei).filter((r) => !r.includes(`\`${entry.tool}\``));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const newBlock = buildBlock(rowsToEntries(rows));
|
|
338
|
+
const patched = existing.slice(0, si) + newBlock + existing.slice(ei + MARKER_END.length);
|
|
339
|
+
containerExec(
|
|
340
|
+
safe,
|
|
341
|
+
`echo ${JSON.stringify(b64(patched))} | base64 -d > ${TOOLS_MD_PATH}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Agent primitives — operate on ONE container ──────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Remove credentials for a specific provider from a single agent container.
|
|
349
|
+
*/
|
|
350
|
+
function cleanProvider(container: string, providerId: string): void {
|
|
351
|
+
const safe = container.replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
352
|
+
if (!safe) return;
|
|
353
|
+
|
|
354
|
+
switch (providerId) {
|
|
355
|
+
case 'github-pat':
|
|
356
|
+
try { containerExec(safe, 'rm -rf /root/.config/gh /root/.local/share/gh 2>/dev/null; exit 0', 10000); } catch {}
|
|
357
|
+
break;
|
|
358
|
+
case 'vercel':
|
|
359
|
+
try { containerExec(safe, 'rm -rf /root/.local/share/com.vercel.cli', 10000); } catch {}
|
|
360
|
+
break;
|
|
361
|
+
case 'supabase':
|
|
362
|
+
try { containerExec(safe, 'rm -f /root/.supabase/access-token 2>/dev/null; exit 0', 10000); } catch {}
|
|
363
|
+
break;
|
|
364
|
+
case 'trello':
|
|
365
|
+
try { containerExec(safe, 'rm -rf /root/.trello-cli', 10000); } catch {}
|
|
366
|
+
break;
|
|
367
|
+
case 'notion':
|
|
368
|
+
try { containerExec(safe, 'rm -rf /root/.config/notion', 10000); } catch {}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Inject a credential for a given provider into a single agent container.
|
|
375
|
+
*/
|
|
376
|
+
async function deliverToAgent(
|
|
377
|
+
container: string,
|
|
378
|
+
providerId: string,
|
|
379
|
+
secret: Record<string, string>,
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
switch (providerId) {
|
|
382
|
+
case 'github-pat': deliverGithub(container, secret); break;
|
|
383
|
+
case 'vercel': await deliverVercel(container, secret); break;
|
|
384
|
+
case 'supabase': deliverSupabase(container, secret); break;
|
|
385
|
+
case 'trello': deliverTrello(container, secret); break;
|
|
386
|
+
case 'notion': deliverNotion(container, secret); break;
|
|
387
|
+
default: throw new Error(`No delivery adapter for: ${providerId}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Sync a credential to specific containers.
|
|
393
|
+
*
|
|
394
|
+
* - Cleans credentials from selected containers, then delivers new ones.
|
|
395
|
+
* - Manages the TOOLS.md integration block row-by-row for the given provider only.
|
|
396
|
+
* - Unselected containers are NOT touched.
|
|
397
|
+
*/
|
|
398
|
+
export async function syncProfileToAgents(
|
|
399
|
+
profileId: string,
|
|
400
|
+
providerId: string,
|
|
401
|
+
containers?: string[],
|
|
402
|
+
): Promise<SyncResult[]> {
|
|
403
|
+
if (!getProvider(providerId)) throw new Error(`Unknown provider: ${providerId}`);
|
|
404
|
+
|
|
405
|
+
const secret = getSecret(profileId);
|
|
406
|
+
const selectedTargets = containers ?? listAgentContainers();
|
|
407
|
+
|
|
408
|
+
// Clean the specific provider from selected targets (don't touch other providers)
|
|
409
|
+
for (const container of selectedTargets) {
|
|
410
|
+
cleanProvider(container, providerId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Deliver credentials to selected targets
|
|
414
|
+
const results: SyncResult[] = [];
|
|
415
|
+
for (const container of selectedTargets) {
|
|
416
|
+
const safe = container.replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
417
|
+
if (!safe) continue;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await deliverToAgent(safe, providerId, secret);
|
|
421
|
+
results.push({ container, status: 'ok' });
|
|
422
|
+
} catch (e: unknown) {
|
|
423
|
+
results.push({ container, status: 'error', error: (e as Error).message });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Update TOOLS.md on selected targets only
|
|
428
|
+
for (const container of selectedTargets) {
|
|
429
|
+
const wasDelivered = results.some(
|
|
430
|
+
(r) => r.container === container && r.status === 'ok',
|
|
431
|
+
);
|
|
432
|
+
toggleProviderRow(container, providerId, wasDelivered ? 'add' : 'remove');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return results;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Clean a single provider from a single agent container and update its TOOLS.md.
|
|
440
|
+
* Used by the De-sync button in the UI and by the sync endpoint for initial cleanup.
|
|
441
|
+
*/
|
|
442
|
+
export function cleanAgentProviders(container: string, providerId: string): void {
|
|
443
|
+
const safe = container.replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
444
|
+
if (!safe) return;
|
|
445
|
+
|
|
446
|
+
cleanProvider(safe, providerId);
|
|
447
|
+
toggleProviderRow(safe, providerId, 'remove');
|
|
448
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { hashToken } from './hash';
|
|
3
|
+
|
|
4
|
+
export interface AgentContainer {
|
|
5
|
+
name: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List running agent containers (those carrying an AGENT_ID label).
|
|
11
|
+
* Shared by the detect API route and the credential delivery layer so both
|
|
12
|
+
* reason over the exact same set of containers.
|
|
13
|
+
*/
|
|
14
|
+
export function listAgentContainers(): AgentContainer[] {
|
|
15
|
+
try {
|
|
16
|
+
const out = execSync(
|
|
17
|
+
"docker ps --filter label=AGENT_ID --format '{{.Names}}\t{{.Label \"AGENT_ID\"}}'",
|
|
18
|
+
{ timeout: 5000, encoding: 'utf-8', maxBuffer: 64 * 1024 },
|
|
19
|
+
).trim();
|
|
20
|
+
if (!out) return [];
|
|
21
|
+
return out.split('\n').filter(Boolean).map((line) => {
|
|
22
|
+
const [name, agentId] = line.split('\t');
|
|
23
|
+
return { name: name ?? '', agentId: agentId ?? name ?? '' };
|
|
24
|
+
});
|
|
25
|
+
} catch (e: unknown) {
|
|
26
|
+
throw new Error('listAgentContainers failed: ' + (e as Error).message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Run a shell snippet inside a container, parsing stdout with a fallback on error. */
|
|
31
|
+
export function execInContainer<T>(
|
|
32
|
+
container: string,
|
|
33
|
+
script: string,
|
|
34
|
+
parse: (raw: string) => T,
|
|
35
|
+
fallback: T,
|
|
36
|
+
): T {
|
|
37
|
+
try {
|
|
38
|
+
const raw = execSync(
|
|
39
|
+
`docker exec ${container.replace(/[^a-zA-Z0-9_.-]/g, '')} sh -c ${JSON.stringify(script)}`,
|
|
40
|
+
{ timeout: 10000, encoding: 'utf-8', shell: '/bin/bash' },
|
|
41
|
+
).trim();
|
|
42
|
+
return parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Trello: presence of the config file marks the CLI as authenticated. */
|
|
49
|
+
export function detectTrello(container: string): { loggedIn: boolean } {
|
|
50
|
+
const hasConfig = execInContainer(
|
|
51
|
+
container,
|
|
52
|
+
'test -f /root/.trello-cli/default/config.json && echo yes || echo no',
|
|
53
|
+
(r) => r.trim(),
|
|
54
|
+
'no',
|
|
55
|
+
);
|
|
56
|
+
return { loggedIn: hasConfig === 'yes' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Notion: no CLI — just check if the config file exists (same pattern as Trello). */
|
|
60
|
+
export function detectNotion(container: string): { loggedIn: boolean } {
|
|
61
|
+
const hasConfig = execInContainer(
|
|
62
|
+
container,
|
|
63
|
+
'test -f /root/.config/notion/config.json && echo yes || echo no',
|
|
64
|
+
(r) => r.trim(),
|
|
65
|
+
'no',
|
|
66
|
+
);
|
|
67
|
+
return { loggedIn: hasConfig === 'yes' };
|
|
68
|
+
}
|
|
69
|
+
export function cliInstalled(container: string, name: string): boolean {
|
|
70
|
+
const raw = execInContainer(
|
|
71
|
+
container,
|
|
72
|
+
`command -v ${name} >/dev/null 2>&1 && echo yes || echo no`,
|
|
73
|
+
(r) => r.trim(),
|
|
74
|
+
'no',
|
|
75
|
+
);
|
|
76
|
+
return raw === 'yes';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Secret field that holds the primary token used to match a stored profile
|
|
81
|
+
* against what is installed in a container, per provider.
|
|
82
|
+
*/
|
|
83
|
+
export const TOKEN_FIELD: Record<string, string> = {
|
|
84
|
+
'github-pat': 'token',
|
|
85
|
+
vercel: 'token',
|
|
86
|
+
supabase: 'accessToken',
|
|
87
|
+
trello: 'token',
|
|
88
|
+
notion: 'token',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Hash of a stored profile secret's token (empty when unavailable). */
|
|
92
|
+
export function profileTokenHash(secret: Record<string, string>, providerId: string): string {
|
|
93
|
+
const field = TOKEN_FIELD[providerId];
|
|
94
|
+
const val = field ? secret[field] : '';
|
|
95
|
+
return val ? hashToken(val) : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the token currently installed for a provider inside a container and hash it.
|
|
100
|
+
* Returns '' when nothing is installed or the token cannot be read — callers must
|
|
101
|
+
* treat '' as "unknown / no match" and never as a wildcard.
|
|
102
|
+
*/
|
|
103
|
+
export function providerTokenHash(container: string, providerId: string): string {
|
|
104
|
+
let raw = '';
|
|
105
|
+
switch (providerId) {
|
|
106
|
+
case 'github-pat':
|
|
107
|
+
raw = execInContainer(
|
|
108
|
+
container,
|
|
109
|
+
"cat /root/.config/gh/hosts.yml 2>/dev/null | grep 'oauth_token:' | head -1 | sed 's/.*: *//'",
|
|
110
|
+
(r) => r.trim(),
|
|
111
|
+
'',
|
|
112
|
+
);
|
|
113
|
+
break;
|
|
114
|
+
case 'vercel':
|
|
115
|
+
raw = execInContainer(
|
|
116
|
+
container,
|
|
117
|
+
"cat /root/.local/share/com.vercel.cli/auth.json 2>/dev/null | grep -oE '\"token\"[^,}]*' | head -1 | grep -oE '\"[^\"]*\"$' | tr -d '\"'",
|
|
118
|
+
(r) => r.trim(),
|
|
119
|
+
'',
|
|
120
|
+
);
|
|
121
|
+
break;
|
|
122
|
+
case 'supabase':
|
|
123
|
+
raw = execInContainer(
|
|
124
|
+
container,
|
|
125
|
+
'cat /root/.supabase/access-token 2>/dev/null | head -1',
|
|
126
|
+
(r) => r.trim(),
|
|
127
|
+
'',
|
|
128
|
+
);
|
|
129
|
+
break;
|
|
130
|
+
case 'trello':
|
|
131
|
+
raw = execInContainer(
|
|
132
|
+
container,
|
|
133
|
+
"cat /root/.trello-cli/default/config.json 2>/dev/null | grep -oE '\"token\"[^,}]*' | head -1 | grep -oE '\"[^\"]*\"$' | tr -d '\"'",
|
|
134
|
+
(r) => r.trim(),
|
|
135
|
+
'',
|
|
136
|
+
);
|
|
137
|
+
break;
|
|
138
|
+
case 'notion':
|
|
139
|
+
raw = execInContainer(
|
|
140
|
+
container,
|
|
141
|
+
"cat /root/.config/notion/config.json 2>/dev/null | grep -oE '\"token\"[^,}]*' | head -1 | grep -oE '\"[^\"]*\"$' | tr -d '\"'",
|
|
142
|
+
(r) => r.trim(),
|
|
143
|
+
'',
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (!raw || raw.length < 10) return '';
|
|
148
|
+
return hashToken(raw);
|
|
149
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash a token for comparison without exposing the raw value in logs/responses.
|
|
5
|
+
* Uses SHA256 — collision-safe, deterministic.
|
|
6
|
+
*/
|
|
7
|
+
export function hashToken(token: string): string {
|
|
8
|
+
return createHash('sha256').update(token.trim(), 'utf-8').digest('hex').slice(0, 12);
|
|
9
|
+
}
|