@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,46 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
const CREATE_TABLE = `CREATE TABLE IF NOT EXISTS cost_override
|
|
8
|
+
(month TEXT PRIMARY KEY, amount REAL, note TEXT, updated_at INTEGER)`;
|
|
9
|
+
|
|
10
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
11
|
+
const denied = await requireAuthJWT(request);
|
|
12
|
+
if (denied) return denied;
|
|
13
|
+
try {
|
|
14
|
+
const db = openDb(false);
|
|
15
|
+
db.prepare(CREATE_TABLE).run();
|
|
16
|
+
const month = new Date().toISOString().slice(0, 7);
|
|
17
|
+
const row = db.prepare('SELECT * FROM cost_override WHERE month = ?').get(month);
|
|
18
|
+
db.close();
|
|
19
|
+
return NextResponse.json(row ?? { month, amount: null, note: null });
|
|
20
|
+
} catch (e: unknown) {
|
|
21
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
26
|
+
const denied = await requireAuthJWT(request);
|
|
27
|
+
if (denied) return denied;
|
|
28
|
+
try {
|
|
29
|
+
const body = (await request.json()) as { amount?: unknown; note?: string; month?: string };
|
|
30
|
+
const { amount, note, month } = body ?? {};
|
|
31
|
+
const m = month ?? new Date().toISOString().slice(0, 7);
|
|
32
|
+
const parsed = Number.parseFloat(String(amount));
|
|
33
|
+
if (amount === undefined || !Number.isFinite(parsed)) {
|
|
34
|
+
return NextResponse.json({ error: 'amount required' }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
const db = openDb(false);
|
|
37
|
+
db.prepare(CREATE_TABLE).run();
|
|
38
|
+
db.prepare(
|
|
39
|
+
'INSERT OR REPLACE INTO cost_override (month, amount, note, updated_at) VALUES (?,?,?,?)',
|
|
40
|
+
).run(m, parsed, note ?? null, Date.now());
|
|
41
|
+
db.close();
|
|
42
|
+
return NextResponse.json({ ok: true, month: m, amount: parsed });
|
|
43
|
+
} catch (e: unknown) {
|
|
44
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { isBillableDollarModel } from '@/lib/billing';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
// In-memory cache: 30 second TTL for cost aggregation
|
|
9
|
+
let costsCache: { data: unknown; ts: number } | null = null;
|
|
10
|
+
const CACHE_TTL_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
interface ModelCostRow {
|
|
13
|
+
model: string | null;
|
|
14
|
+
cost_usd: number;
|
|
15
|
+
tokens_in: number;
|
|
16
|
+
tokens_out: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sumBillableCosts(rows: ModelCostRow[]): number {
|
|
20
|
+
return rows.reduce(
|
|
21
|
+
(sum, row) => (isBillableDollarModel(row.model) ? sum + Number(row.cost_usd ?? 0) : sum),
|
|
22
|
+
0,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getStartOfDaySeconds(): number {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
now.setHours(0, 0, 0, 0);
|
|
29
|
+
return Math.floor(now.getTime() / 1000);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
33
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
34
|
+
// Return cached result if fresh
|
|
35
|
+
if (costsCache && Date.now() - costsCache.ts < CACHE_TTL_MS) {
|
|
36
|
+
return NextResponse.json(costsCache.data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const db = openDb();
|
|
41
|
+
const startOfDay = getStartOfDaySeconds();
|
|
42
|
+
|
|
43
|
+
const todayRows = db
|
|
44
|
+
.prepare(
|
|
45
|
+
`SELECT model,
|
|
46
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
47
|
+
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
48
|
+
COALESCE(SUM(tokens_out), 0) AS tokens_out
|
|
49
|
+
FROM sessions WHERE started_at >= ?
|
|
50
|
+
GROUP BY model ORDER BY cost_usd DESC`,
|
|
51
|
+
)
|
|
52
|
+
.all(startOfDay) as ModelCostRow[];
|
|
53
|
+
|
|
54
|
+
const allTimeRows = db
|
|
55
|
+
.prepare(
|
|
56
|
+
`SELECT model,
|
|
57
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
58
|
+
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
59
|
+
COALESCE(SUM(tokens_out), 0) AS tokens_out
|
|
60
|
+
FROM sessions
|
|
61
|
+
GROUP BY model ORDER BY cost_usd DESC`,
|
|
62
|
+
)
|
|
63
|
+
.all() as ModelCostRow[];
|
|
64
|
+
|
|
65
|
+
const today = sumBillableCosts(todayRows);
|
|
66
|
+
const sessionsAllTime = sumBillableCosts(allTimeRows);
|
|
67
|
+
|
|
68
|
+
const byModel = todayRows.filter((row) => isBillableDollarModel(row.model));
|
|
69
|
+
|
|
70
|
+
db.close();
|
|
71
|
+
|
|
72
|
+
const result = {
|
|
73
|
+
today,
|
|
74
|
+
allTime: sessionsAllTime,
|
|
75
|
+
allTimeSource: 'sessions_billable',
|
|
76
|
+
byModel,
|
|
77
|
+
};
|
|
78
|
+
costsCache = { data: result, ts: Date.now() };
|
|
79
|
+
|
|
80
|
+
return NextResponse.json(result);
|
|
81
|
+
} catch (e: unknown) {
|
|
82
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getProfile, revealSecret } from '@/lib/credentials/vault';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
9
|
+
): Promise<NextResponse> {
|
|
10
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
11
|
+
try {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
const profile = getProfile(id);
|
|
14
|
+
if (!profile) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
15
|
+
|
|
16
|
+
const secret = revealSecret(id);
|
|
17
|
+
return NextResponse.json({ secret });
|
|
18
|
+
} catch (e: unknown) {
|
|
19
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getProfile, updateProfile, deleteProfile } from '@/lib/credentials/vault';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: Request,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
9
|
+
): Promise<NextResponse> {
|
|
10
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
11
|
+
try {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
const profile = getProfile(id);
|
|
14
|
+
if (!profile) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
15
|
+
return NextResponse.json(profile);
|
|
16
|
+
} catch (e: unknown) {
|
|
17
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function PATCH(request: Request,
|
|
22
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
23
|
+
): Promise<NextResponse> {
|
|
24
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
25
|
+
try {
|
|
26
|
+
const { id } = await params;
|
|
27
|
+
const body = await request.json() as { label?: string; secret?: Record<string, string> };
|
|
28
|
+
const { label, secret } = body;
|
|
29
|
+
|
|
30
|
+
if (!label) {
|
|
31
|
+
return NextResponse.json({ error: 'label is required' }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
updateProfile(id, label, secret ?? {});
|
|
35
|
+
return NextResponse.json({ ok: true });
|
|
36
|
+
} catch (e: unknown) {
|
|
37
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function DELETE(request: Request,
|
|
42
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
43
|
+
): Promise<NextResponse> {
|
|
44
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
45
|
+
try {
|
|
46
|
+
const { id } = await params;
|
|
47
|
+
deleteProfile(id);
|
|
48
|
+
return NextResponse.json({ ok: true });
|
|
49
|
+
} catch (e: unknown) {
|
|
50
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getProfile, markSynced } from '@/lib/credentials/vault';
|
|
3
|
+
import { syncProfileToAgents, cleanAgentProviders } from '@/lib/credentials/delivery';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function POST(request: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
10
|
+
): Promise<NextResponse> {
|
|
11
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
12
|
+
try {
|
|
13
|
+
const { id } = await params;
|
|
14
|
+
const profile = getProfile(id);
|
|
15
|
+
if (!profile) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
16
|
+
|
|
17
|
+
const body = await request.json().catch(() => ({})) as { containers?: string[] };
|
|
18
|
+
const selectedContainers = body.containers;
|
|
19
|
+
|
|
20
|
+
const results = await syncProfileToAgents(id, profile.providerId, selectedContainers);
|
|
21
|
+
|
|
22
|
+
const anyError = results.some((r) => r.status === 'error');
|
|
23
|
+
markSynced(id, anyError ? 'error' : 'active');
|
|
24
|
+
|
|
25
|
+
return NextResponse.json({ results });
|
|
26
|
+
} catch (e: unknown) {
|
|
27
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* DELETE /api/credentials/[id]/sync
|
|
33
|
+
* De-sync a credential from a single container (removes it without touching others).
|
|
34
|
+
* Body: { container: "prometheus" }
|
|
35
|
+
*/
|
|
36
|
+
export async function DELETE(request: Request,
|
|
37
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
38
|
+
): Promise<NextResponse> {
|
|
39
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
40
|
+
try {
|
|
41
|
+
const { id } = await params;
|
|
42
|
+
const profile = getProfile(id);
|
|
43
|
+
if (!profile) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
44
|
+
|
|
45
|
+
const body = await request.json().catch(() => ({})) as { container?: string };
|
|
46
|
+
const container = body.container;
|
|
47
|
+
if (!container) {
|
|
48
|
+
return NextResponse.json({ error: 'container is required' }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cleanAgentProviders(container, profile.providerId);
|
|
52
|
+
markSynced(id, 'active');
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({ ok: true });
|
|
55
|
+
} catch (e: unknown) {
|
|
56
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { listProfiles, getSecret } from '@/lib/credentials/vault';
|
|
3
|
+
import {
|
|
4
|
+
listAgentContainers,
|
|
5
|
+
execInContainer,
|
|
6
|
+
cliInstalled,
|
|
7
|
+
providerTokenHash,
|
|
8
|
+
profileTokenHash,
|
|
9
|
+
detectTrello,
|
|
10
|
+
detectNotion,
|
|
11
|
+
} from '@/lib/credentials/detect';
|
|
12
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
13
|
+
|
|
14
|
+
export const dynamic = 'force-dynamic';
|
|
15
|
+
|
|
16
|
+
interface DetectedStatus {
|
|
17
|
+
container: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
displayName?: string;
|
|
20
|
+
githubLoggedIn: boolean;
|
|
21
|
+
githubUser: string;
|
|
22
|
+
githubCliInstalled: boolean;
|
|
23
|
+
vercelLoggedIn: boolean;
|
|
24
|
+
vercelUser: string;
|
|
25
|
+
vercelTeam: string;
|
|
26
|
+
vercelCliInstalled: boolean;
|
|
27
|
+
supabaseLoggedIn: boolean;
|
|
28
|
+
supabaseLinked: boolean;
|
|
29
|
+
supabaseProjectRef: string;
|
|
30
|
+
supabaseCliInstalled: boolean;
|
|
31
|
+
trelloLoggedIn: boolean;
|
|
32
|
+
trelloCliInstalled: boolean;
|
|
33
|
+
notionLoggedIn: boolean;
|
|
34
|
+
toolsMdHasMarkers: boolean;
|
|
35
|
+
matchedProfiles: { profileId: string; providerId: string; label: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DetectionError {
|
|
39
|
+
container: string;
|
|
40
|
+
agentId: string;
|
|
41
|
+
error: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectSupabase(container: string): { loggedIn: boolean; linked: boolean; projectRef: string } {
|
|
45
|
+
if (!cliInstalled(container, 'supabase')) {
|
|
46
|
+
return { loggedIn: false, linked: false, projectRef: '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Verify login by calling the Supabase API (analogous to gh auth status / vercel whoami)
|
|
50
|
+
const raw = execInContainer(
|
|
51
|
+
container,
|
|
52
|
+
'supabase projects list --output json 2>&1',
|
|
53
|
+
(r) => r.trim(),
|
|
54
|
+
'',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Unauthorized → not logged in
|
|
58
|
+
if (!raw || raw.includes('Unauthorized')) {
|
|
59
|
+
return { loggedIn: false, linked: false, projectRef: '' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if a linked project exists in the workspace
|
|
63
|
+
let linked = false;
|
|
64
|
+
let projectRef = '';
|
|
65
|
+
const linkedRef = execInContainer(
|
|
66
|
+
container,
|
|
67
|
+
'cat /data/supabase/config.toml 2>/dev/null | grep project_id | head -1 | sed "s/.*= *\"//;s/\".*//"',
|
|
68
|
+
(r) => r.trim(),
|
|
69
|
+
'',
|
|
70
|
+
);
|
|
71
|
+
if (linkedRef && linkedRef.length > 0) {
|
|
72
|
+
linked = true;
|
|
73
|
+
projectRef = linkedRef;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { loggedIn: true, linked, projectRef };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectGithub(container: string): { loggedIn: boolean; user: string } {
|
|
80
|
+
if (!cliInstalled(container, 'gh')) {
|
|
81
|
+
return { loggedIn: false, user: '' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const raw = execInContainer(
|
|
85
|
+
container,
|
|
86
|
+
'gh auth status 2>&1',
|
|
87
|
+
(r) => r,
|
|
88
|
+
'',
|
|
89
|
+
);
|
|
90
|
+
if (!raw || raw.includes('not logged into any GitHub hosts')) {
|
|
91
|
+
return { loggedIn: false, user: '' };
|
|
92
|
+
}
|
|
93
|
+
const match = raw.match(/Logged in to github\.com as (\S+)/);
|
|
94
|
+
return { loggedIn: !!match, user: match?.[1] ?? '' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function detectToolsMd(container: string): boolean {
|
|
98
|
+
const raw = execInContainer(
|
|
99
|
+
container,
|
|
100
|
+
`grep -c 'REV4A:INTEGRATIONS:START' /root/.openclaw/workspace/TOOLS.md 2>/dev/null || echo 0`,
|
|
101
|
+
(r) => r.trim(),
|
|
102
|
+
'0',
|
|
103
|
+
);
|
|
104
|
+
return raw === '1' || parseInt(raw, 10) > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function detectVercel(container: string): { loggedIn: boolean; user: string; team: string } {
|
|
108
|
+
if (!cliInstalled(container, 'vercel')) {
|
|
109
|
+
return { loggedIn: false, user: '', team: '' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const whoami = execInContainer(
|
|
113
|
+
container,
|
|
114
|
+
'vercel whoami 2>&1 || true',
|
|
115
|
+
(r) => r.trim(),
|
|
116
|
+
'',
|
|
117
|
+
);
|
|
118
|
+
if (whoami && !whoami.includes('Error:') && !whoami.includes('Not authorized')) {
|
|
119
|
+
const teamRaw = execInContainer(
|
|
120
|
+
container,
|
|
121
|
+
"cat /root/.local/share/com.vercel.cli/config.json 2>/dev/null | grep 'currentTeam' | head -1 | sed 's/.*: *\"//;s/\".*//'",
|
|
122
|
+
(r) => r.trim(),
|
|
123
|
+
'',
|
|
124
|
+
);
|
|
125
|
+
return { loggedIn: true, user: extractUsername(whoami), team: teamRaw };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { loggedIn: false, user: '', team: '' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract just the username from 'vercel whoami' output (strip header lines).
|
|
133
|
+
*/
|
|
134
|
+
function extractUsername(raw: string): string {
|
|
135
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
136
|
+
// Return the last non-empty line (the actual username)
|
|
137
|
+
return lines[lines.length - 1]?.trim() || raw;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Match detected state to stored credential profiles by comparing token hashes.
|
|
142
|
+
* `hashes` maps a providerId to the hash of the token currently installed in the
|
|
143
|
+
* container. A profile matches when its own token hashes to the same value.
|
|
144
|
+
* Providers whose token cannot be read yield '' and therefore never match.
|
|
145
|
+
*/
|
|
146
|
+
function matchProfiles(
|
|
147
|
+
hashes: Record<string, string>,
|
|
148
|
+
): { profileId: string; providerId: string; label: string }[] {
|
|
149
|
+
const profiles = listProfiles();
|
|
150
|
+
const matched: { profileId: string; providerId: string; label: string }[] = [];
|
|
151
|
+
|
|
152
|
+
for (const profile of profiles) {
|
|
153
|
+
const installedHash = hashes[profile.providerId];
|
|
154
|
+
if (!installedHash) continue;
|
|
155
|
+
|
|
156
|
+
const storedHash = profileTokenHash(getSecret(profile.id), profile.providerId);
|
|
157
|
+
if (storedHash && storedHash === installedHash) {
|
|
158
|
+
matched.push({ profileId: profile.id, providerId: profile.providerId, label: profile.label });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return matched;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
166
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
167
|
+
try {
|
|
168
|
+
const url = new URL(request.url);
|
|
169
|
+
const filterAgent = url.searchParams.get('agent');
|
|
170
|
+
let containers = listAgentContainers();
|
|
171
|
+
|
|
172
|
+
if (filterAgent) {
|
|
173
|
+
containers = containers.filter((c) => c.name === filterAgent);
|
|
174
|
+
if (containers.length === 0) {
|
|
175
|
+
return NextResponse.json({ error: `Agent "${filterAgent}" not found` }, { status: 404 });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const results: (DetectedStatus | DetectionError)[] = [];
|
|
180
|
+
|
|
181
|
+
for (const { name, agentId } of containers) {
|
|
182
|
+
try {
|
|
183
|
+
const { loggedIn: githubLoggedIn, user: githubUser } = detectGithub(name);
|
|
184
|
+
const { loggedIn: trelloLoggedIn } = detectTrello(name);
|
|
185
|
+
const { loggedIn: notionLoggedIn } = detectNotion(name);
|
|
186
|
+
const { loggedIn: vercelLoggedIn, user: vercelUser, team: vercelTeam } = detectVercel(name);
|
|
187
|
+
const { loggedIn: supabaseLoggedIn, linked: supabaseLinked, projectRef: supabaseProjectRef } = detectSupabase(name);
|
|
188
|
+
|
|
189
|
+
const githubCliInstalled = cliInstalled(name, 'gh');
|
|
190
|
+
const vercelCliInstalled = cliInstalled(name, 'vercel');
|
|
191
|
+
const supabaseCliInstalled = cliInstalled(name, 'supabase');
|
|
192
|
+
const trelloCliInstalled = cliInstalled(name, 'trello');
|
|
193
|
+
const toolsMdHasMarkers = detectToolsMd(name);
|
|
194
|
+
|
|
195
|
+
// Read AGENT_NAME env for display name (may differ from container name)
|
|
196
|
+
const displayName = execInContainer(
|
|
197
|
+
name,
|
|
198
|
+
'printenv AGENT_NAME',
|
|
199
|
+
(r) => r.trim(),
|
|
200
|
+
'',
|
|
201
|
+
) || undefined;
|
|
202
|
+
|
|
203
|
+
// Precise profile matching by token hash — one source of truth for all providers.
|
|
204
|
+
const matchedProfiles = matchProfiles({
|
|
205
|
+
'github-pat': providerTokenHash(name, 'github-pat'),
|
|
206
|
+
vercel: providerTokenHash(name, 'vercel'),
|
|
207
|
+
supabase: providerTokenHash(name, 'supabase'),
|
|
208
|
+
trello: providerTokenHash(name, 'trello'),
|
|
209
|
+
notion: providerTokenHash(name, 'notion'),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
results.push({
|
|
213
|
+
container: name,
|
|
214
|
+
agentId,
|
|
215
|
+
displayName,
|
|
216
|
+
githubLoggedIn,
|
|
217
|
+
githubUser,
|
|
218
|
+
githubCliInstalled,
|
|
219
|
+
vercelLoggedIn,
|
|
220
|
+
vercelUser,
|
|
221
|
+
vercelTeam,
|
|
222
|
+
vercelCliInstalled,
|
|
223
|
+
supabaseLoggedIn,
|
|
224
|
+
supabaseLinked,
|
|
225
|
+
supabaseProjectRef,
|
|
226
|
+
supabaseCliInstalled,
|
|
227
|
+
trelloLoggedIn,
|
|
228
|
+
trelloCliInstalled,
|
|
229
|
+
notionLoggedIn,
|
|
230
|
+
toolsMdHasMarkers,
|
|
231
|
+
matchedProfiles,
|
|
232
|
+
});
|
|
233
|
+
} catch (e: unknown) {
|
|
234
|
+
results.push({
|
|
235
|
+
container: name,
|
|
236
|
+
agentId,
|
|
237
|
+
error: (e as Error).message,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return NextResponse.json({ agents: results });
|
|
243
|
+
} catch (e: unknown) {
|
|
244
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { listProfiles, createProfile } from '@/lib/credentials/vault';
|
|
4
|
+
import { CREDENTIAL_PROVIDERS } from '@/lib/credentials/providers';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
10
|
+
try {
|
|
11
|
+
const profiles = listProfiles();
|
|
12
|
+
return NextResponse.json({ profiles, providers: CREDENTIAL_PROVIDERS });
|
|
13
|
+
} catch (e: unknown) {
|
|
14
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
19
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
20
|
+
try {
|
|
21
|
+
const body = await request.json() as { providerId?: string; label?: string; secret?: Record<string, string> };
|
|
22
|
+
const { providerId, label, secret } = body;
|
|
23
|
+
|
|
24
|
+
if (!providerId || !label || !secret) {
|
|
25
|
+
return NextResponse.json({ error: 'providerId, label, and secret are required' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const id = createProfile(providerId, label, secret);
|
|
29
|
+
return NextResponse.json({ id }, { status: 201 });
|
|
30
|
+
} catch (e: unknown) {
|
|
31
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
import { patchCronJob, removeCronJob } from '@/lib/openclaw-cron';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
const CONTAINER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
9
|
+
|
|
10
|
+
export async function PATCH(
|
|
11
|
+
request: NextRequest,
|
|
12
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
13
|
+
): Promise<NextResponse> {
|
|
14
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
15
|
+
|
|
16
|
+
const { id } = await params;
|
|
17
|
+
let body: Record<string, unknown>;
|
|
18
|
+
try {
|
|
19
|
+
body = (await request.json()) as Record<string, unknown>;
|
|
20
|
+
} catch {
|
|
21
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const allowed: Record<string, unknown> = {};
|
|
25
|
+
if (typeof body.enabled === 'boolean') allowed.enabled = body.enabled;
|
|
26
|
+
if (typeof body.containerName === 'string') {
|
|
27
|
+
if (!CONTAINER_NAME_RE.test(body.containerName)) {
|
|
28
|
+
return NextResponse.json({ error: 'Invalid containerName format' }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
allowed.containerName = body.containerName;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Object.keys(allowed).length === 0) {
|
|
34
|
+
return NextResponse.json({ error: 'No patchable fields provided' }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = patchCronJob(id, allowed);
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
const status = result.error?.includes('not found') ? 404
|
|
40
|
+
: result.error?.includes('gateway') || result.error?.includes('Failed') ? 502
|
|
41
|
+
: 500;
|
|
42
|
+
return NextResponse.json({ error: result.error }, { status });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return NextResponse.json(result.job);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function DELETE(
|
|
49
|
+
request: NextRequest,
|
|
50
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
51
|
+
): Promise<NextResponse> {
|
|
52
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
53
|
+
|
|
54
|
+
const { id } = await params;
|
|
55
|
+
const { searchParams } = new URL(request.url);
|
|
56
|
+
const containerName = searchParams.get('containerName') ?? undefined;
|
|
57
|
+
|
|
58
|
+
if (containerName !== undefined && !CONTAINER_NAME_RE.test(containerName)) {
|
|
59
|
+
return NextResponse.json({ error: 'Invalid containerName format' }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = removeCronJob(id, containerName);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
const status = result.error?.includes('not found') ? 404 : 502;
|
|
65
|
+
return NextResponse.json({ error: result.error }, { status });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({ ok: true });
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import { listOpenClawCronJobs } from '@/lib/openclaw-cron';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
10
|
+
const result = await listOpenClawCronJobs();
|
|
11
|
+
return NextResponse.json(result.jobs);
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
import { getCronRuns } from '@/lib/openclaw-cron';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
export const maxDuration = 60; // docker exec + gateway API call can take up to 15s
|
|
8
|
+
|
|
9
|
+
const CONTAINER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
10
|
+
|
|
11
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
12
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
13
|
+
const { searchParams } = new URL(request.url);
|
|
14
|
+
const containerName = searchParams.get('containerName');
|
|
15
|
+
const jobId = searchParams.get('jobId');
|
|
16
|
+
const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10) || 20, 100);
|
|
17
|
+
|
|
18
|
+
if (!containerName || !CONTAINER_NAME_RE.test(containerName)) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing or invalid containerName' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
if (!jobId || typeof jobId !== 'string' || jobId.length < 8) {
|
|
22
|
+
return NextResponse.json({ error: 'Missing or invalid jobId' }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = getCronRuns(containerName, jobId, limit);
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
return NextResponse.json({ error: result.error, entries: [] }, { status: 502 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ entries: result.entries });
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
hasPassword: !!process.env.REV4A_PASSWORD,
|
|
8
|
+
hasJwtSecret: !!process.env.REV4A_JWT_SECRET,
|
|
9
|
+
hasToken: !!process.env.REV4A_TOKEN,
|
|
10
|
+
});
|
|
11
|
+
}
|