@flame0510/project-aether 1.1.12 → 1.1.14
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 +4 -1
- package/app/agents/PageClient.tsx +140 -34
- package/app/agents/create/PageClient.tsx +11 -4
- package/app/api/agents/[id]/recreate/route.ts +3 -3
- package/app/api/agents/[id]/route.ts +3 -1
- package/app/api/agents/channels-summary/route.ts +48 -17
- package/app/api/agents/create/route.ts +32 -13
- package/app/api/agents/download-image/route.ts +2 -0
- package/app/api/agents/image-status/route.ts +94 -85
- package/app/api/agents/route.ts +10 -53
- package/app/api/agents/token/route.ts +4 -1
- package/app/api/auth/check/route.ts +2 -23
- package/app/api/auth/login/route.ts +3 -35
- package/app/api/auth/logout/route.ts +1 -1
- package/app/api/auth/status/route.ts +2 -21
- package/app/api/config/env/route.ts +7 -3
- package/app/api/config/restart/route.ts +2 -0
- package/app/api/containers/route.ts +3 -34
- package/app/api/crons/route.ts +2 -0
- package/app/api/debug/route.ts +2 -0
- package/app/api/envcheck/route.ts +3 -1
- package/app/api/gateway/agent/route.ts +8 -6
- package/app/api/gateway/provider/route.ts +43 -5
- package/app/api/gateway/route.ts +104 -136
- package/app/api/gateway/sync.ts +12 -15
- package/app/api/models/route.ts +2 -0
- package/app/api/provider/upstream.ts +12 -3
- package/app/api/setup/agent-image/route.ts +8 -3
- package/app/api/setup/password/route.ts +32 -37
- package/app/api/setup/restart/route.ts +8 -2
- package/app/api/skills/delete/route.js +1 -1
- package/app/api/skills/promote/route.js +1 -1
- package/app/api/skills/route.js +89 -97
- package/app/api/skills/save/route.js +18 -5
- package/app/api/update-check/route.ts +4 -23
- package/app/api/wizard/complete/route.ts +7 -3
- package/app/api/wizard/reset/route.ts +8 -3
- package/app/api/wizard/status/route.ts +8 -5
- package/app/components/AuthGuard.tsx +29 -8
- package/app/components/Skeleton.tsx +71 -0
- package/app/components/ui/ModalityIcons.tsx +157 -0
- package/app/components/ui/index.ts +1 -0
- package/app/crons/PageClient.tsx +32 -7
- package/app/gateway/PageClient.tsx +104 -16
- package/bin/rev4a.js +35 -24
- package/docs/ARCHITECTURE.md +31 -14
- package/docs/FRONTEND-ARCHITECTURE.md +1 -0
- package/docs/REV4A.md +36 -6
- package/docs/dev/API-REFERENCE.md +13 -8
- package/docs/dev/GATEWAY.md +42 -1
- package/docs/dev/PROVIDERS.md +4 -3
- package/docs/rag/REV4A-OVERVIEW.md +1 -1
- package/instrumentation.ts +18 -0
- package/lib/agent-setup.ts +8 -9
- package/lib/buildAgentImage.ts +5 -1
- package/lib/channelManager.ts +23 -16
- package/lib/db.ts +0 -10
- package/lib/docker-exec.ts +195 -0
- package/lib/docker-socket.ts +121 -0
- package/lib/docker-utils.ts +22 -48
- package/lib/openclaw-cron.ts +81 -48
- package/lib/openrouter-pricing.ts +184 -0
- package/lib/rev4a-auth.d.ts +4 -1
- package/lib/rev4a-auth.js +68 -20
- package/lib/rev4a-paths.ts +19 -0
- package/model-pricing.json +50 -50
- package/models.config.json +2 -2
- package/package.json +4 -1
- package/proxy.ts +68 -0
- package/scripts/refresh-model-pricing.mjs +171 -0
- package/lib/auth.ts +0 -28
- package/lib/requireAuth.tsx +0 -38
package/lib/rev4a-auth.js
CHANGED
|
@@ -1,36 +1,84 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { jwtVerify } from "jose";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { ENV_FILE } from "@/lib/rev4a-paths";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Single source of truth for secret resolution. Every route and proxy.ts
|
|
8
|
+
// import these instead of keeping their own env+file-fallback copy — the
|
|
9
|
+
// duplication used to drift (proxy.ts had an env-only version because it
|
|
10
|
+
// used to run on Edge runtime, which can't use fs; now that it runs on
|
|
11
|
+
// Node.js runtime the split serves no purpose and only risks desync).
|
|
12
|
+
//
|
|
13
|
+
// In the supported `rev4a serve` flow the file fallback is rarely hit:
|
|
14
|
+
// bin/rev4a.js injects these into the Next.js child's env, and writeEnvVar()
|
|
15
|
+
// updates process.env in-process. It exists for setups that bypass the CLI
|
|
16
|
+
// (a hand-written systemd unit, `npm run dev` before .env exists).
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const PASSWORD_RE = /^REV4A_PASSWORD=(.*)$/m;
|
|
19
|
+
const JWT_SECRET_RE = /^REV4A_JWT_SECRET=(.*)$/m;
|
|
20
|
+
|
|
21
|
+
function readEnvFileValue(re) {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(ENV_FILE)) {
|
|
24
|
+
const match = fs.readFileSync(ENV_FILE, "utf-8").match(re);
|
|
25
|
+
if (match && match[1].trim()) return match[1].trim();
|
|
26
|
+
}
|
|
27
|
+
} catch { /* ignore */ }
|
|
28
|
+
return "";
|
|
8
29
|
}
|
|
9
30
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
if (!auth || auth !== `Bearer ${getExpectedToken()}`) {
|
|
13
|
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
31
|
+
export function getPassword() {
|
|
32
|
+
return process.env.REV4A_PASSWORD || readEnvFileValue(PASSWORD_RE);
|
|
16
33
|
}
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
process.env.REV4A_JWT_SECRET ||
|
|
20
|
-
|
|
35
|
+
export function getJwtSecret() {
|
|
36
|
+
return process.env.REV4A_JWT_SECRET || readEnvFileValue(JWT_SECRET_RE);
|
|
37
|
+
}
|
|
21
38
|
|
|
39
|
+
export function isConfigured() {
|
|
40
|
+
return getPassword().length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unauthorized() {
|
|
44
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cookie (JWT) auth, falling back to `Authorization: Bearer <REV4A_TOKEN>`.
|
|
49
|
+
* Returns null when authorized, or a 401 response to return as-is.
|
|
50
|
+
*/
|
|
22
51
|
export async function requireAuthJWT(request) {
|
|
23
52
|
const cookieToken = request.cookies?.get?.("rev4a_token")?.value;
|
|
24
|
-
|
|
53
|
+
const secret = getJwtSecret();
|
|
54
|
+
|
|
55
|
+
// An empty secret would mean verifying against a zero-length HMAC key.
|
|
56
|
+
// jose rejects that, but bail explicitly rather than relying on it.
|
|
57
|
+
if (cookieToken && secret) {
|
|
25
58
|
try {
|
|
26
|
-
await jwtVerify(cookieToken,
|
|
59
|
+
await jwtVerify(cookieToken, new TextEncoder().encode(secret));
|
|
27
60
|
return null;
|
|
28
|
-
} catch {}
|
|
61
|
+
} catch { /* fall through to bearer */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const expectedToken = process.env.REV4A_TOKEN || process.env.NEXT_PUBLIC_REV4A_TOKEN || "";
|
|
65
|
+
// Without this guard an unset token would make `Bearer ` match an empty
|
|
66
|
+
// expected value. HTTP header normalisation already strips the trailing
|
|
67
|
+
// space so it isn't reachable in practice — the guard makes that safety
|
|
68
|
+
// explicit instead of dependent on it.
|
|
69
|
+
if (expectedToken) {
|
|
70
|
+
const auth = request.headers.get("authorization");
|
|
71
|
+
if (auth === `Bearer ${expectedToken}`) return null;
|
|
29
72
|
}
|
|
30
73
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (auth === `Bearer ${expectedToken}`) return null;
|
|
74
|
+
return unauthorized();
|
|
75
|
+
}
|
|
34
76
|
|
|
35
|
-
|
|
77
|
+
/**
|
|
78
|
+
* For routes that must stay reachable during first-run setup (before any
|
|
79
|
+
* password exists) but must be protected once the instance is configured.
|
|
80
|
+
*/
|
|
81
|
+
export async function requireAuthIfConfigured(request) {
|
|
82
|
+
if (!isConfigured()) return null;
|
|
83
|
+
return requireAuthJWT(request);
|
|
36
84
|
}
|
package/lib/rev4a-paths.ts
CHANGED
|
@@ -39,6 +39,25 @@ export const AGENTS_TOKEN_FILE = path.join(REV4A_DATA, 'data', 'agents-token.jso
|
|
|
39
39
|
/** Provider API keys JSON */
|
|
40
40
|
export const PROVIDER_KEYS_FILE = path.join(REV4A_DATA, 'data', 'provider-keys.json');
|
|
41
41
|
|
|
42
|
+
/* ── Host directories bind-mounted into every agent container ───── */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* These used to live at `/docker/shared-skills` and `/docker/rev4a-rules`.
|
|
46
|
+
* That path can't work on macOS: the root volume is a sealed APFS system
|
|
47
|
+
* volume, so `/docker` cannot be created even as root. It also required
|
|
48
|
+
* `sudo mkdir` on Linux for no real benefit.
|
|
49
|
+
*
|
|
50
|
+
* Under REV4A_DATA they sit inside the user's home, which additionally
|
|
51
|
+
* falls within the directories Docker Desktop shares with its Linux VM by
|
|
52
|
+
* default — so a bind mount from here works with no extra configuration.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Skills shared across all agents, mounted read-only. */
|
|
56
|
+
export const SHARED_SKILLS_DIR = path.join(REV4A_DATA, 'shared', 'shared-skills');
|
|
57
|
+
|
|
58
|
+
/** Rev4a system rules injected into every agent workspace, mounted read-only. */
|
|
59
|
+
export const REV4A_RULES_DIR = path.join(REV4A_DATA, 'shared', 'rev4a-rules');
|
|
60
|
+
|
|
42
61
|
/**
|
|
43
62
|
* Model catalogue — read-only, shipped with the package.
|
|
44
63
|
* Resolved at module load from the package root (alongside bin/rev4a.js).
|
package/model-pricing.json
CHANGED
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"output": 8.0
|
|
41
41
|
},
|
|
42
42
|
"openrouter/auto": {
|
|
43
|
-
"input": -1,
|
|
44
|
-
"output": -1
|
|
43
|
+
"input": -1.0,
|
|
44
|
+
"output": -1.0
|
|
45
45
|
},
|
|
46
46
|
"google/gemini-3.1-pro-preview": {
|
|
47
47
|
"input": 2.0,
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"output": 2.2
|
|
125
125
|
},
|
|
126
126
|
"glm/glm-4.7-flash": {
|
|
127
|
-
"input": 0,
|
|
128
|
-
"output": 0
|
|
127
|
+
"input": 0.0,
|
|
128
|
+
"output": 0.0
|
|
129
129
|
},
|
|
130
130
|
"glm/glm-4.7-flashx": {
|
|
131
131
|
"input": 0.07,
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"output": 4.0
|
|
137
137
|
},
|
|
138
138
|
"glm/glm-4.6v-flash": {
|
|
139
|
-
"input": 0,
|
|
140
|
-
"output": 0
|
|
139
|
+
"input": 0.0,
|
|
140
|
+
"output": 0.0
|
|
141
141
|
},
|
|
142
142
|
"qwen/qwen3.7-max": {
|
|
143
143
|
"input": 0.83,
|
|
@@ -160,36 +160,36 @@
|
|
|
160
160
|
"output": 0.28
|
|
161
161
|
},
|
|
162
162
|
"groq/llama-3.1-8b-instant": {
|
|
163
|
-
"input": 0,
|
|
164
|
-
"output": 0
|
|
163
|
+
"input": 0.0,
|
|
164
|
+
"output": 0.0
|
|
165
165
|
},
|
|
166
166
|
"groq/llama-3.3-70b-versatile": {
|
|
167
|
-
"input": 0,
|
|
168
|
-
"output": 0
|
|
167
|
+
"input": 0.0,
|
|
168
|
+
"output": 0.0
|
|
169
169
|
},
|
|
170
170
|
"openrouter/deepseek/deepseek-v4-flash": {
|
|
171
|
-
"input": 0.
|
|
172
|
-
"output": 0.
|
|
171
|
+
"input": 0.04886,
|
|
172
|
+
"output": 0.09772
|
|
173
173
|
},
|
|
174
174
|
"openrouter/google/gemini-2.5-flash": {
|
|
175
|
-
"input": 0.
|
|
176
|
-
"output":
|
|
175
|
+
"input": 0.3,
|
|
176
|
+
"output": 2.5
|
|
177
177
|
},
|
|
178
178
|
"openrouter/google/gemini-2.5-pro": {
|
|
179
179
|
"input": 1.25,
|
|
180
|
-
"output":
|
|
180
|
+
"output": 10.0
|
|
181
181
|
},
|
|
182
182
|
"openrouter/anthropic/claude-sonnet-4.6": {
|
|
183
183
|
"input": 3.0,
|
|
184
184
|
"output": 15.0
|
|
185
185
|
},
|
|
186
186
|
"openrouter/deepseek/deepseek-v4-pro": {
|
|
187
|
-
"input": 0.
|
|
188
|
-
"output": 0.
|
|
187
|
+
"input": 0.396894,
|
|
188
|
+
"output": 0.793788
|
|
189
189
|
},
|
|
190
190
|
"openrouter/qwen/qwen3.7-max": {
|
|
191
|
-
"input": 1.
|
|
192
|
-
"output": 4.
|
|
191
|
+
"input": 1.475,
|
|
192
|
+
"output": 4.425
|
|
193
193
|
},
|
|
194
194
|
"openrouter/qwen/qwen3.7-plus": {
|
|
195
195
|
"input": 0.32,
|
|
@@ -200,16 +200,16 @@
|
|
|
200
200
|
"output": 0.13
|
|
201
201
|
},
|
|
202
202
|
"openrouter/qwen/qwen3.6-plus": {
|
|
203
|
-
"input": 0.
|
|
203
|
+
"input": 0.325,
|
|
204
204
|
"output": 1.95
|
|
205
205
|
},
|
|
206
206
|
"openrouter/qwen/qwen3.6-flash": {
|
|
207
|
-
"input": 0.
|
|
208
|
-
"output": 1.
|
|
207
|
+
"input": 0.1875,
|
|
208
|
+
"output": 1.125
|
|
209
209
|
},
|
|
210
210
|
"openrouter/qwen/qwen3.6-27b": {
|
|
211
|
-
"input": 0.
|
|
212
|
-
"output":
|
|
211
|
+
"input": 0.6,
|
|
212
|
+
"output": 3.6
|
|
213
213
|
},
|
|
214
214
|
"openrouter/qwen/qwen3.6-35b-a3b": {
|
|
215
215
|
"input": 0.14,
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
"output": 3.9
|
|
229
229
|
},
|
|
230
230
|
"openrouter/qwen/qwen3-next-80b-a3b-instruct": {
|
|
231
|
-
"input": 0.
|
|
231
|
+
"input": 0.1,
|
|
232
232
|
"output": 1.1
|
|
233
233
|
},
|
|
234
234
|
"openrouter/qwen/qwen3-next-80b-a3b-thinking": {
|
|
@@ -244,20 +244,20 @@
|
|
|
244
244
|
"output": 6.0
|
|
245
245
|
},
|
|
246
246
|
"openrouter/qwen/qwen3.6-max-preview": {
|
|
247
|
-
"input": 1.
|
|
248
|
-
"output": 6.
|
|
247
|
+
"input": 1.027,
|
|
248
|
+
"output": 6.162
|
|
249
249
|
},
|
|
250
250
|
"openrouter/minimax/minimax-m3": {
|
|
251
251
|
"input": 0.3,
|
|
252
252
|
"output": 1.2
|
|
253
253
|
},
|
|
254
254
|
"openrouter/moonshotai/kimi-k2.7-code": {
|
|
255
|
-
"input": 0.
|
|
256
|
-
"output": 3.
|
|
255
|
+
"input": 0.67,
|
|
256
|
+
"output": 3.4
|
|
257
257
|
},
|
|
258
258
|
"openrouter/moonshotai/kimi-k2.6": {
|
|
259
|
-
"input": 0.
|
|
260
|
-
"output":
|
|
259
|
+
"input": 0.95,
|
|
260
|
+
"output": 4.0
|
|
261
261
|
},
|
|
262
262
|
"openrouter/moonshotai/kimi-k2": {
|
|
263
263
|
"input": 0.57,
|
|
@@ -272,16 +272,16 @@
|
|
|
272
272
|
"output": 15.0
|
|
273
273
|
},
|
|
274
274
|
"openrouter/z-ai/glm-5.2": {
|
|
275
|
-
"input": 0.
|
|
276
|
-
"output":
|
|
275
|
+
"input": 0.966,
|
|
276
|
+
"output": 3.036
|
|
277
277
|
},
|
|
278
278
|
"openrouter/z-ai/glm-5.1": {
|
|
279
|
-
"input": 0.
|
|
280
|
-
"output": 3.
|
|
279
|
+
"input": 0.966,
|
|
280
|
+
"output": 3.036
|
|
281
281
|
},
|
|
282
282
|
"openrouter/z-ai/glm-5": {
|
|
283
|
-
"input": 0.
|
|
284
|
-
"output":
|
|
283
|
+
"input": 0.6,
|
|
284
|
+
"output": 1.92
|
|
285
285
|
},
|
|
286
286
|
"openrouter/z-ai/glm-5v-turbo": {
|
|
287
287
|
"input": 1.2,
|
|
@@ -308,15 +308,15 @@
|
|
|
308
308
|
"output": 0.3
|
|
309
309
|
},
|
|
310
310
|
"openrouter/tencent/hy3": {
|
|
311
|
-
"input": 0.
|
|
312
|
-
"output": 0.
|
|
311
|
+
"input": 0.132,
|
|
312
|
+
"output": 0.528
|
|
313
313
|
},
|
|
314
314
|
"openrouter/tencent/hy3-preview": {
|
|
315
|
-
"input": 0.
|
|
316
|
-
"output": 0.
|
|
315
|
+
"input": 0.18,
|
|
316
|
+
"output": 0.6
|
|
317
317
|
},
|
|
318
318
|
"openrouter/xiaomi/mimo-v2.5-pro": {
|
|
319
|
-
"input": 0.
|
|
319
|
+
"input": 0.435,
|
|
320
320
|
"output": 0.87
|
|
321
321
|
},
|
|
322
322
|
"openrouter/xiaomi/mimo-v2.5": {
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
"output": 3.2
|
|
385
385
|
},
|
|
386
386
|
"openrouter/nvidia/nemotron-3-super-120b-a12b": {
|
|
387
|
-
"input": 0.
|
|
387
|
+
"input": 0.085,
|
|
388
388
|
"output": 0.4
|
|
389
389
|
},
|
|
390
390
|
"openrouter/nvidia/nemotron-3-ultra-550b-a55b": {
|
|
@@ -396,7 +396,7 @@
|
|
|
396
396
|
"output": 3.0
|
|
397
397
|
},
|
|
398
398
|
"openrouter/thinkingmachines/inkling": {
|
|
399
|
-
"input":
|
|
399
|
+
"input": 0.95,
|
|
400
400
|
"output": 4.05
|
|
401
401
|
},
|
|
402
402
|
"openrouter/relace/relace-apply-3": {
|
|
@@ -416,8 +416,8 @@
|
|
|
416
416
|
"output": 12.0
|
|
417
417
|
},
|
|
418
418
|
"openrouter/google/gemini-3.6-flash": {
|
|
419
|
-
"input":
|
|
420
|
-
"output":
|
|
419
|
+
"input": 0.75,
|
|
420
|
+
"output": 3.75
|
|
421
421
|
},
|
|
422
422
|
"openrouter/anthropic/claude-opus-5": {
|
|
423
423
|
"input": 5.0,
|
|
@@ -436,12 +436,12 @@
|
|
|
436
436
|
"output": 25.0
|
|
437
437
|
},
|
|
438
438
|
"openrouter/openai/gpt-5.6-sol": {
|
|
439
|
-
"input":
|
|
440
|
-
"output":
|
|
439
|
+
"input": 2.0,
|
|
440
|
+
"output": 10.0
|
|
441
441
|
},
|
|
442
442
|
"openrouter/openai/gpt-5.6-luna": {
|
|
443
|
-
"input": 0.
|
|
444
|
-
"output":
|
|
443
|
+
"input": 0.2,
|
|
444
|
+
"output": 1.2
|
|
445
445
|
},
|
|
446
446
|
"openrouter/openai/gpt-5.4-mini": {
|
|
447
447
|
"input": 0.75,
|
package/models.config.json
CHANGED
|
@@ -796,7 +796,7 @@
|
|
|
796
796
|
"name": "Kimi K3 (OR)",
|
|
797
797
|
"provider": "openrouter",
|
|
798
798
|
"enabled": false,
|
|
799
|
-
"modality": "text+image->text"
|
|
799
|
+
"modality": "text+image+video->text"
|
|
800
800
|
},
|
|
801
801
|
{
|
|
802
802
|
"id": "openrouter/amazon/nova-premier-v1",
|
|
@@ -841,4 +841,4 @@
|
|
|
841
841
|
"modality": "text+image->text"
|
|
842
842
|
}
|
|
843
843
|
]
|
|
844
|
-
}
|
|
844
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flame0510/project-aether",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"lineage.js",
|
|
39
39
|
"terminal-ws-server.js",
|
|
40
40
|
"package.json",
|
|
41
|
+
"proxy.ts",
|
|
42
|
+
"instrumentation.ts",
|
|
41
43
|
"next.config.mjs",
|
|
42
44
|
"tsconfig.json",
|
|
43
45
|
"next-env.d.ts",
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"build": "next build",
|
|
52
54
|
"start": "rev4a serve",
|
|
53
55
|
"start:all": "rev4a serve",
|
|
56
|
+
"refresh:pricing": "node scripts/refresh-model-pricing.mjs",
|
|
54
57
|
"smoke:db-bootstrap": "node scripts/smoke-db-bootstrap.mjs",
|
|
55
58
|
"postinstall": "node bin/postinstall.js"
|
|
56
59
|
},
|
package/proxy.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import { jwtVerify } from 'jose';
|
|
4
|
+
import { getJwtSecret } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/* Public routes — no auth required */
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
const PUBLIC_PREFIXES = ['/login', '/setup'];
|
|
10
|
+
|
|
11
|
+
/* ------------------------------------------------------------------ */
|
|
12
|
+
/* Bypass — static assets and APIs have their own auth */
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
function isStaticOrApi(pathname: string): boolean {
|
|
15
|
+
return (
|
|
16
|
+
pathname.startsWith('/_next') ||
|
|
17
|
+
pathname.startsWith('/api') ||
|
|
18
|
+
pathname.startsWith('/favicon') ||
|
|
19
|
+
pathname.startsWith('/icon') ||
|
|
20
|
+
pathname.startsWith('/apple-touch') ||
|
|
21
|
+
pathname.startsWith('/rev4a-logo') ||
|
|
22
|
+
pathname.startsWith('/manifest') ||
|
|
23
|
+
pathname.includes('.') // file extensions (png, svg, ico, etc.)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ------------------------------------------------------------------ */
|
|
28
|
+
/* Proxy (formerly middleware — Next.js 16 renamed the convention; */
|
|
29
|
+
/* runs on Node.js runtime by default now, so it shares the same */
|
|
30
|
+
/* env+file secret resolution as every API route instead of its own */
|
|
31
|
+
/* env-only copy.) */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
export async function proxy(request: NextRequest) {
|
|
34
|
+
const { pathname } = request.nextUrl;
|
|
35
|
+
|
|
36
|
+
// Public routes
|
|
37
|
+
if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) {
|
|
38
|
+
return NextResponse.next();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Static assets & API routes
|
|
42
|
+
if (isStaticOrApi(pathname)) {
|
|
43
|
+
return NextResponse.next();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Protected route — verify JWT
|
|
47
|
+
const token = request.cookies.get('rev4a_token')?.value;
|
|
48
|
+
if (!token) {
|
|
49
|
+
const loginUrl = new URL('/login', request.url);
|
|
50
|
+
return NextResponse.redirect(loginUrl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const secret = new TextEncoder().encode(getJwtSecret());
|
|
55
|
+
await jwtVerify(token, secret);
|
|
56
|
+
return NextResponse.next();
|
|
57
|
+
} catch {
|
|
58
|
+
const loginUrl = new URL('/login', request.url);
|
|
59
|
+
return NextResponse.redirect(loginUrl);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
/* Matcher — only run on page routes, skip Next.js internals */
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
export const config = {
|
|
67
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
68
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Refresh model-pricing.json and models.config.json from the live OpenRouter
|
|
4
|
+
* catalogue.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: both files are maintained by hand and drift. When this was
|
|
7
|
+
* written, 16 of 79 OpenRouter entries had stale prices — google/gemini-2.5-flash
|
|
8
|
+
* understated the real cost by ~8x. The dashboard reads these files, and so does
|
|
9
|
+
* daemon.js when it writes cost_usd rows, so a wrong number here is a wrong
|
|
10
|
+
* number in every spend figure the product reports.
|
|
11
|
+
*
|
|
12
|
+
* SCOPE — deliberately limited to `openrouter/*` models.
|
|
13
|
+
* OpenRouter resells models we also offer direct (openai, anthropic, google,
|
|
14
|
+
* deepseek, qwen). Their rates are NOT the same: at time of writing 8 of the
|
|
15
|
+
* 25 overlapping models differ — openai/gpt-5.6-sol is $5/$30 direct in our
|
|
16
|
+
* file but $2/$10 on OpenRouter. We can't tell from OpenRouter's catalogue
|
|
17
|
+
* what the direct vendor charges, so those entries are left alone. Keep them
|
|
18
|
+
* updated by hand from each provider's own pricing page.
|
|
19
|
+
*
|
|
20
|
+
* The endpoint is public — no API key, CDN-cached, never billed.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/refresh-model-pricing.mjs # apply
|
|
24
|
+
* node scripts/refresh-model-pricing.mjs --dry-run # preview only
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const PRICING_FILE = path.join(ROOT, 'model-pricing.json');
|
|
33
|
+
const MODELS_FILE = path.join(ROOT, 'models.config.json');
|
|
34
|
+
const CATALOGUE_URL = 'https://openrouter.ai/api/v1/models';
|
|
35
|
+
|
|
36
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
37
|
+
|
|
38
|
+
function perMillion(raw) {
|
|
39
|
+
if (raw === undefined || raw === null) return null;
|
|
40
|
+
const n = parseFloat(raw);
|
|
41
|
+
// -1 marks router models (OpenRouter Auto): dynamic, not a real price.
|
|
42
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
43
|
+
return n * 1_000_000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Round to the precision the file already uses, so we don't churn on float noise. */
|
|
47
|
+
function round(n) {
|
|
48
|
+
return Math.round(n * 1e6) / 1e6;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serialise model-pricing.json preserving its `5.0` style for whole numbers.
|
|
53
|
+
* JSON.stringify writes `5`, which is the same value but rewrites ~114 lines
|
|
54
|
+
* that didn't change — burying the handful of real price moves in noise and
|
|
55
|
+
* making the diff unreviewable on every run.
|
|
56
|
+
*/
|
|
57
|
+
function serialisePricing(pricing) {
|
|
58
|
+
const num = (n) => (Number.isInteger(n) ? n.toFixed(1) : String(n));
|
|
59
|
+
const body = Object.entries(pricing)
|
|
60
|
+
.map(([id, p]) => ` ${JSON.stringify(id)}: {\n "input": ${num(p.input)},\n "output": ${num(p.output)}\n }`)
|
|
61
|
+
.join(',\n');
|
|
62
|
+
return `{\n${body}\n}\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Our catalogue namespaces OpenRouter entries as `openrouter/<upstream id>`,
|
|
67
|
+
* but OpenRouter also publishes house models under a literal `openrouter/`
|
|
68
|
+
* vendor prefix (openrouter/auto). Try the stripped form, then the id as-is.
|
|
69
|
+
*/
|
|
70
|
+
function upstreamCandidates(id) {
|
|
71
|
+
const stripped = id.slice('openrouter/'.length);
|
|
72
|
+
return stripped === id ? [id] : [stripped, id];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
console.log(`Fetching ${CATALOGUE_URL} …`);
|
|
77
|
+
const res = await fetch(CATALOGUE_URL, { headers: { accept: 'application/json' } });
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
console.error(`✗ Catalogue returned HTTP ${res.status}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const body = await res.json();
|
|
83
|
+
if (!Array.isArray(body?.data) || body.data.length === 0) {
|
|
84
|
+
console.error('✗ Unexpected catalogue shape');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const live = new Map(body.data.map((m) => [m.id, m]));
|
|
88
|
+
console.log(` ${live.size} models upstream\n`);
|
|
89
|
+
|
|
90
|
+
const modelsDoc = JSON.parse(fs.readFileSync(MODELS_FILE, 'utf-8'));
|
|
91
|
+
const models = Array.isArray(modelsDoc) ? modelsDoc : modelsDoc.models;
|
|
92
|
+
const pricing = JSON.parse(fs.readFileSync(PRICING_FILE, 'utf-8'));
|
|
93
|
+
|
|
94
|
+
const priceChanges = [];
|
|
95
|
+
const modalityChanges = [];
|
|
96
|
+
let missing = 0;
|
|
97
|
+
let dynamic = 0;
|
|
98
|
+
|
|
99
|
+
for (const m of models) {
|
|
100
|
+
if (m.provider !== 'openrouter') continue;
|
|
101
|
+
|
|
102
|
+
let up = null;
|
|
103
|
+
for (const candidate of upstreamCandidates(m.id)) {
|
|
104
|
+
if (live.has(candidate)) { up = live.get(candidate); break; }
|
|
105
|
+
}
|
|
106
|
+
if (!up) {
|
|
107
|
+
missing++;
|
|
108
|
+
console.log(` ? ${m.id} — not in the upstream catalogue, left as-is`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Pricing
|
|
113
|
+
const input = perMillion(up.pricing?.prompt);
|
|
114
|
+
const output = perMillion(up.pricing?.completion);
|
|
115
|
+
if (input === null || output === null) {
|
|
116
|
+
dynamic++; // router model — keep whatever the file says (-1/-1)
|
|
117
|
+
} else {
|
|
118
|
+
const current = pricing[m.id];
|
|
119
|
+
const next = { input: round(input), output: round(output) };
|
|
120
|
+
if (!current || current.input !== next.input || current.output !== next.output) {
|
|
121
|
+
priceChanges.push({
|
|
122
|
+
id: m.id,
|
|
123
|
+
from: current ? `$${current.input}/$${current.output}` : '(none)',
|
|
124
|
+
to: `$${next.input}/$${next.output}`,
|
|
125
|
+
});
|
|
126
|
+
pricing[m.id] = next;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Modality — updating the curated file (rather than overriding at runtime)
|
|
131
|
+
// keeps the UI and app/api/gateway/sync.ts, which provisions containers
|
|
132
|
+
// from this same value, in agreement.
|
|
133
|
+
const upModality = up.architecture?.modality;
|
|
134
|
+
if (upModality && upModality !== m.modality) {
|
|
135
|
+
modalityChanges.push({ id: m.id, from: m.modality ?? '(none)', to: upModality });
|
|
136
|
+
m.modality = upModality;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`\nPricing: ${priceChanges.length} changed`);
|
|
141
|
+
for (const c of priceChanges) console.log(` ${c.id}\n ${c.from} → ${c.to}`);
|
|
142
|
+
|
|
143
|
+
console.log(`\nModality: ${modalityChanges.length} changed`);
|
|
144
|
+
for (const c of modalityChanges) console.log(` ${c.id}\n ${c.from} → ${c.to}`);
|
|
145
|
+
|
|
146
|
+
const direct = models.filter((m) => m.provider !== 'openrouter').length;
|
|
147
|
+
console.log(
|
|
148
|
+
`\nSkipped: ${dynamic} dynamic (router), ${missing} not upstream, ` +
|
|
149
|
+
`${direct} direct-provider (different vendor, different rates — update by hand)`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (priceChanges.length === 0 && modalityChanges.length === 0) {
|
|
153
|
+
console.log('\n✓ Already up to date.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
console.log('\n(dry run — nothing written)');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(PRICING_FILE, serialisePricing(pricing), 'utf-8');
|
|
163
|
+
fs.writeFileSync(MODELS_FILE, JSON.stringify(modelsDoc, null, 2) + '\n', 'utf-8');
|
|
164
|
+
console.log('\n✓ Wrote model-pricing.json and models.config.json');
|
|
165
|
+
console.log(' Review the diff before committing.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((e) => {
|
|
169
|
+
console.error(`✗ ${e.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
package/lib/auth.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
-
import { jwtVerify } from 'jose';
|
|
3
|
-
import { TOKEN } from '@/lib/db';
|
|
4
|
-
|
|
5
|
-
const JWT_SECRET = new TextEncoder().encode(
|
|
6
|
-
process.env.REV4A_JWT_SECRET ?? 'rev4a-jwt-secret-change-in-prod',
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
export async function requireBrowserAuth(request: NextRequest): Promise<NextResponse | null> {
|
|
10
|
-
const token = new URL(request.url).searchParams.get('token');
|
|
11
|
-
const auth = request.headers.get('authorization');
|
|
12
|
-
|
|
13
|
-
if (token === TOKEN || auth === `Bearer ${TOKEN}`) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const cookieToken = request.cookies.get('rev4a_token')?.value;
|
|
18
|
-
if (cookieToken) {
|
|
19
|
-
try {
|
|
20
|
-
await jwtVerify(cookieToken, JWT_SECRET);
|
|
21
|
-
return null;
|
|
22
|
-
} catch {
|
|
23
|
-
// Fall through to unauthorized.
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
28
|
-
}
|