svamp-cli 0.2.341 → 0.2.346
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/bin/skills/artifact/SKILL.md +1 -1
- package/bin/skills/crew/SKILL.md +21 -3
- package/dist/DefaultTransport-vfp78fug.mjs +47 -0
- package/dist/GeminiTransport-3OkhtFRO.mjs +130 -0
- package/dist/KimiTransport-D8n009Tr.mjs +141 -0
- package/dist/acpAgentConfig-CObOPlA8.mjs +48 -0
- package/dist/acpBackend-BZ1MJhQV.mjs +922 -0
- package/dist/{adminCommands-Btl3is3z.mjs → adminCommands-BvsYnRDA.mjs} +2 -18
- package/dist/{agentCommands-B7JaByb0.mjs → agentCommands-B2Nl7Fof.mjs} +6 -23
- package/dist/api-BHjyC0Ph.mjs +157 -0
- package/dist/{auth-pTUm-zt4.mjs → auth-CRz8TsEl.mjs} +1 -22
- package/dist/bootStatus-DqtNgJch.mjs +143 -0
- package/dist/claudeAuth-iAerdaMr.mjs +247 -0
- package/dist/cli.mjs +2939 -5
- package/dist/codexAppServerBackend-VGsYtbqg.mjs +786 -0
- package/dist/codexProvider-BtxYc5Rj.mjs +68 -0
- package/dist/{commands-BaxjRuxM.mjs → commands-BB1Kcyt-.mjs} +13 -19
- package/dist/{commands-BUkV04q6.mjs → commands-BOpoVBOM.mjs} +55 -25
- package/dist/{commands-C1L_R1H6.mjs → commands-C2aODeVc.mjs} +58 -33
- package/dist/{commands-znQSQbTX.mjs → commands-CiRCwjkJ.mjs} +10 -32
- package/dist/{commands-C4whLEwv.mjs → commands-D1pZq3d4.mjs} +103 -41
- package/dist/{commands-CMCSUI4A.mjs → commands-D69mnkYu.mjs} +20 -30
- package/dist/{commands-CfP6gPZF.mjs → commands-DW5RnHZw.mjs} +2 -19
- package/dist/{commands-EcIOSJHF.mjs → commands-H4PsTGn8.mjs} +58 -26
- package/dist/cron-hHGdb5kU.mjs +132 -0
- package/dist/detectIsolation-CzoDCEnh.mjs +319 -0
- package/dist/envFlag-COOuJ3AH.mjs +11 -0
- package/dist/fileLock-BMPwCrB6.mjs +80 -0
- package/dist/{fleet-U-WXVbW_.mjs → fleet-DM_Q7w69.mjs} +85 -24
- package/dist/flushExit-C-i6SGjj.mjs +37 -0
- package/dist/friendlyName-1UZcyDhu.mjs +586 -0
- package/dist/{headlessCli-CptF7_lZ.mjs → headlessCli-C8HzQvis.mjs} +3 -21
- package/dist/hookSettings-DYMTUgVA.mjs +10399 -0
- package/dist/{httpServer-Be1Hcddr.mjs → httpServer-BzEDbOGn.mjs} +14 -5
- package/dist/hyphaClient-ybivOjZo.mjs +49 -0
- package/dist/index.mjs +40 -8
- package/dist/instance-CeHz5C8v.mjs +203 -0
- package/dist/kimiProvider-DbxODebR.mjs +69 -0
- package/dist/kimiSetup-NGsoYB5h.mjs +48 -0
- package/dist/loopVerify-B47sSjxe.mjs +444 -0
- package/dist/machineTools-DygM1jXW.mjs +580 -0
- package/dist/model-CUHojegn.mjs +66 -0
- package/dist/{notifyCommands-DitnhzYz.mjs → notifyCommands-D-IKIxiq.mjs} +3 -18
- package/dist/package-DtuINqf5.mjs +64 -0
- package/dist/pinnedCodex-XW4qvOAe.mjs +11 -0
- package/dist/pinnedNono-5ZHb6328.mjs +15 -0
- package/dist/{rpc-zndUgvJ7.mjs → rpc-BB6RF4-F.mjs} +38 -6
- package/dist/{rpc-BPT9DJXL.mjs → rpc-Cf0grcru.mjs} +28 -20
- package/dist/{run-CrUTBmi4.mjs → run-ChdsgaSD.mjs} +17 -9
- package/dist/run-DkjwF-zA.mjs +9695 -0
- package/dist/sandboxDetect-Cdzsj5TC.mjs +19 -0
- package/dist/{scheduler-B0h2sfGn.mjs → scheduler-BY30ZrB9.mjs} +38 -6
- package/dist/securityContext-Cm5UTCuD.mjs +218 -0
- package/dist/serveAuth-DbVLRCRR.mjs +418 -0
- package/dist/{serveCommands-DpF0xKvz.mjs → serveCommands-CUHUiYnZ.mjs} +13 -34
- package/dist/sharingNotificationSync-qGIQ-3yf.mjs +359 -0
- package/dist/{sideband-B5n9fNxM.mjs → sideband-68HhGT0i.mjs} +1 -21
- package/dist/store-Bn0ByYev.mjs +347 -0
- package/dist/store-fi0f-ff1.mjs +184 -0
- package/dist/types-57J3Re0n.mjs +17 -0
- package/dist/wantsHelp-6bMOeNUz.mjs +5 -0
- package/dist/wrapCommand-CdNawSaa.mjs +117 -0
- package/package.json +3 -3
- package/dist/cli-pFCLXDrP.mjs +0 -2749
- package/dist/package-CigJ8nhu.mjs +0 -64
- package/dist/run-LHJh7jLy.mjs +0 -26384
- package/dist/sandboxDetect-DNTcbgWD.mjs +0 -12
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
const AUTH0_NAMESPACES = ["https://api.imjoy.io/", "https://amun.ai/"];
|
|
2
|
+
const COOKIE_NAME = "svamp_serve_token";
|
|
3
|
+
const SVAMP_SERVE_TOKEN_PARAM = "__svamp_serve_token";
|
|
4
|
+
function stripServeAuthCookie(cookieHeader) {
|
|
5
|
+
if (!cookieHeader) return cookieHeader;
|
|
6
|
+
const kept = cookieHeader.split(";").map((s) => s.trim()).filter((s) => s && !s.toLowerCase().startsWith(COOKIE_NAME + "="));
|
|
7
|
+
return kept.length ? kept.join("; ") : void 0;
|
|
8
|
+
}
|
|
9
|
+
function stripServeTokenParam(pathWithQuery) {
|
|
10
|
+
const qIdx = pathWithQuery.indexOf("?");
|
|
11
|
+
if (qIdx < 0) return pathWithQuery;
|
|
12
|
+
const base = pathWithQuery.slice(0, qIdx);
|
|
13
|
+
const params = new URLSearchParams(pathWithQuery.slice(qIdx + 1));
|
|
14
|
+
params.delete(SVAMP_SERVE_TOKEN_PARAM);
|
|
15
|
+
const qs = params.toString();
|
|
16
|
+
return qs ? `${base}?${qs}` : base;
|
|
17
|
+
}
|
|
18
|
+
function sanitizeLoginReturn(returnUrl) {
|
|
19
|
+
const PLACEHOLDER = "https://svamp.invalid";
|
|
20
|
+
if (typeof returnUrl !== "string" || returnUrl.length === 0) return "/";
|
|
21
|
+
if (returnUrl.includes("\\")) return "/";
|
|
22
|
+
let u;
|
|
23
|
+
try {
|
|
24
|
+
u = new URL(returnUrl, PLACEHOLDER);
|
|
25
|
+
} catch {
|
|
26
|
+
return "/";
|
|
27
|
+
}
|
|
28
|
+
if (u.origin !== PLACEHOLDER) return "/";
|
|
29
|
+
if (!u.pathname.startsWith("/") || u.pathname.startsWith("//")) return "/";
|
|
30
|
+
if (u.pathname === "/__login__" || u.pathname.startsWith("/__login__/")) return "/";
|
|
31
|
+
return u.pathname + u.search + u.hash;
|
|
32
|
+
}
|
|
33
|
+
function sanitizeForwardHeaders(headers) {
|
|
34
|
+
const out = { ...headers };
|
|
35
|
+
const cleaned = stripServeAuthCookie(typeof out.cookie === "string" ? out.cookie : void 0);
|
|
36
|
+
if (cleaned === void 0) delete out.cookie;
|
|
37
|
+
else out.cookie = cleaned;
|
|
38
|
+
delete out.authorization;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
42
|
+
const DEFAULT_CACHE_MAX_SIZE = 1e3;
|
|
43
|
+
function jsStringLiteral(value) {
|
|
44
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
45
|
+
}
|
|
46
|
+
class TokenCache {
|
|
47
|
+
cache = /* @__PURE__ */ new Map();
|
|
48
|
+
ttlMs;
|
|
49
|
+
maxSize;
|
|
50
|
+
constructor(ttlMs = DEFAULT_CACHE_TTL_MS, maxSize = DEFAULT_CACHE_MAX_SIZE) {
|
|
51
|
+
this.ttlMs = ttlMs;
|
|
52
|
+
this.maxSize = maxSize;
|
|
53
|
+
}
|
|
54
|
+
get(token) {
|
|
55
|
+
const info = this.cache.get(token);
|
|
56
|
+
if (!info) return null;
|
|
57
|
+
if (Date.now() > info.expiresAt) {
|
|
58
|
+
this.cache.delete(token);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
this.cache.delete(token);
|
|
62
|
+
this.cache.set(token, info);
|
|
63
|
+
return info;
|
|
64
|
+
}
|
|
65
|
+
set(token, email) {
|
|
66
|
+
if (this.cache.size >= this.maxSize) {
|
|
67
|
+
const oldest = this.cache.keys().next().value;
|
|
68
|
+
if (oldest) this.cache.delete(oldest);
|
|
69
|
+
}
|
|
70
|
+
const ttlExpiry = Date.now() + this.ttlMs;
|
|
71
|
+
const jwtExpMs = parseJwtExpMs(token);
|
|
72
|
+
this.cache.set(token, {
|
|
73
|
+
email,
|
|
74
|
+
expiresAt: jwtExpMs !== null ? Math.min(ttlExpiry, jwtExpMs) : ttlExpiry
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
clear() {
|
|
78
|
+
this.cache.clear();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function hasCookieToken(req) {
|
|
82
|
+
return !!parseCookies(req.headers.cookie || "")[COOKIE_NAME];
|
|
83
|
+
}
|
|
84
|
+
function extractToken(req) {
|
|
85
|
+
const cookies = parseCookies(req.headers.cookie || "");
|
|
86
|
+
if (cookies[COOKIE_NAME]) return cookies[COOKIE_NAME];
|
|
87
|
+
const auth = req.headers.authorization;
|
|
88
|
+
if (auth?.startsWith("Bearer ") || auth?.startsWith("bearer ")) {
|
|
89
|
+
return auth.slice(7).trim();
|
|
90
|
+
}
|
|
91
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
92
|
+
return url.searchParams.get(SVAMP_SERVE_TOKEN_PARAM);
|
|
93
|
+
}
|
|
94
|
+
function parseCookies(header) {
|
|
95
|
+
const cookies = {};
|
|
96
|
+
for (const pair of header.split(";")) {
|
|
97
|
+
const eq = pair.indexOf("=");
|
|
98
|
+
if (eq > 0) {
|
|
99
|
+
const key = pair.slice(0, eq).trim();
|
|
100
|
+
const value = pair.slice(eq + 1).trim();
|
|
101
|
+
cookies[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return cookies;
|
|
105
|
+
}
|
|
106
|
+
function parseJwtExpMs(token) {
|
|
107
|
+
try {
|
|
108
|
+
const parts = token.split(".");
|
|
109
|
+
if (parts.length !== 3) return null;
|
|
110
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
111
|
+
return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseJwtEmail(token) {
|
|
117
|
+
try {
|
|
118
|
+
const parts = token.split(".");
|
|
119
|
+
if (parts.length !== 3) return null;
|
|
120
|
+
const payload = JSON.parse(
|
|
121
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
122
|
+
);
|
|
123
|
+
if (payload.exp && payload.exp * 1e3 < Date.now()) return null;
|
|
124
|
+
for (const ns of AUTH0_NAMESPACES) {
|
|
125
|
+
const email = payload[ns + "email"];
|
|
126
|
+
if (typeof email === "string" && email) return email.toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
if (typeof payload.email === "string" && payload.email) return payload.email.toLowerCase();
|
|
129
|
+
return null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isStructurallyLiveJwt(token) {
|
|
135
|
+
try {
|
|
136
|
+
const parts = token.split(".");
|
|
137
|
+
if (parts.length !== 3) return false;
|
|
138
|
+
const payload = JSON.parse(
|
|
139
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
140
|
+
);
|
|
141
|
+
if (payload.exp && payload.exp * 1e3 < Date.now()) return false;
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function verifyTokenViaHypha(token, hyphaServerUrl) {
|
|
148
|
+
try {
|
|
149
|
+
const baseUrl = hyphaServerUrl.replace(/\/$/, "");
|
|
150
|
+
const url = `${baseUrl}/public/services/ws/parse_token`;
|
|
151
|
+
const resp = await fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${token}`,
|
|
155
|
+
"Content-Type": "application/json"
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({ token }),
|
|
158
|
+
signal: AbortSignal.timeout(1e4)
|
|
159
|
+
});
|
|
160
|
+
if (!resp.ok) return null;
|
|
161
|
+
const data = await resp.json();
|
|
162
|
+
if (data?.is_anonymous === true) return null;
|
|
163
|
+
const email = data?.email;
|
|
164
|
+
if (typeof email === "string" && email) return email.toLowerCase();
|
|
165
|
+
return null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
class ServeAuth {
|
|
171
|
+
cache;
|
|
172
|
+
hyphaServerUrl;
|
|
173
|
+
constructor(options) {
|
|
174
|
+
this.hyphaServerUrl = options.hyphaServerUrl.replace(/\/$/, "");
|
|
175
|
+
this.cache = new TokenCache(
|
|
176
|
+
options.cacheTtlMs || DEFAULT_CACHE_TTL_MS,
|
|
177
|
+
options.cacheMaxSize || DEFAULT_CACHE_MAX_SIZE
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Authenticate a request and return the user's email, or null if not authenticated.
|
|
182
|
+
*/
|
|
183
|
+
async authenticate(req) {
|
|
184
|
+
const token = extractToken(req);
|
|
185
|
+
if (!token) return null;
|
|
186
|
+
const cached = this.cache.get(token);
|
|
187
|
+
if (cached) return cached.email;
|
|
188
|
+
if (!isStructurallyLiveJwt(token)) return null;
|
|
189
|
+
const serverEmail = await verifyTokenViaHypha(token, this.hyphaServerUrl);
|
|
190
|
+
if (serverEmail) {
|
|
191
|
+
this.cache.set(token, serverEmail);
|
|
192
|
+
return serverEmail;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if a user email is authorized for a mount's access level.
|
|
198
|
+
* Note: 'link' mounts are gated by the auth proxy at the path-segment
|
|
199
|
+
* level (capability token), not by this method — they should never reach
|
|
200
|
+
* here. We treat them as deny-by-default just in case.
|
|
201
|
+
*/
|
|
202
|
+
isAuthorized(email, access, ownerEmail) {
|
|
203
|
+
if (access === "public") return true;
|
|
204
|
+
if (access === "link") return false;
|
|
205
|
+
if (!email) return false;
|
|
206
|
+
if (access === "owner") {
|
|
207
|
+
return !!ownerEmail && email.toLowerCase() === ownerEmail.toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
return access.some((e) => e.toLowerCase() === email.toLowerCase());
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Generate the login page HTML.
|
|
213
|
+
*
|
|
214
|
+
* Uses the Hypha-Login HTTP endpoints directly (no SDK / no WebSocket):
|
|
215
|
+
* 1. GET /public/services/hypha-login/start → { login_url, key }
|
|
216
|
+
* 2. open popup to login_url so the user signs in
|
|
217
|
+
* 3. GET /public/services/hypha-login/check?key=… → token (long-polled)
|
|
218
|
+
*
|
|
219
|
+
* This matches the proven pattern in svamp-app/.../share/[id].tsx and
|
|
220
|
+
* removes a class of hangs we previously saw when the JS SDK never
|
|
221
|
+
* invoked its `login_callback` (button-stuck-after-click symptom).
|
|
222
|
+
*/
|
|
223
|
+
getLoginPageHtml(redirectUrl) {
|
|
224
|
+
return `<!DOCTYPE html>
|
|
225
|
+
<html><head>
|
|
226
|
+
<meta charset="utf-8">
|
|
227
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
228
|
+
<title>Sign in \u2014 Svamp File Server</title>
|
|
229
|
+
<style>
|
|
230
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
231
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f6f8fa;color:#24292f;padding:16px}
|
|
232
|
+
.card{background:#fff;border:1px solid #d0d7de;border-radius:12px;padding:32px;max-width:420px;width:100%;text-align:center;box-shadow:0 4px 12px rgba(31,35,40,0.06)}
|
|
233
|
+
h1{font-size:1.25rem;margin-bottom:8px}
|
|
234
|
+
.subtitle{color:#656d76;font-size:0.9rem;margin-bottom:24px}
|
|
235
|
+
button{background:#0969da;color:#fff;border:none;border-radius:8px;padding:12px 24px;font-size:1rem;cursor:pointer;font-weight:500;width:100%}
|
|
236
|
+
button:hover{background:#0860c4}
|
|
237
|
+
button:disabled{background:#94d3a2;cursor:wait}
|
|
238
|
+
.status{margin-top:16px;color:#656d76;font-size:0.85rem;min-height:20px;word-break:break-word}
|
|
239
|
+
.error{color:#cf222e}
|
|
240
|
+
.ok{color:#1a7f37}
|
|
241
|
+
a{color:#0969da;text-decoration:none}
|
|
242
|
+
a:hover{text-decoration:underline}
|
|
243
|
+
</style>
|
|
244
|
+
</head><body>
|
|
245
|
+
<div class="card">
|
|
246
|
+
<h1>Sign in required</h1>
|
|
247
|
+
<p class="subtitle">This resource is private. Sign in with your Hypha account to continue.</p>
|
|
248
|
+
<button id="login-btn">Sign in with Hypha</button>
|
|
249
|
+
<div class="status" id="status"></div>
|
|
250
|
+
</div>
|
|
251
|
+
<script>
|
|
252
|
+
const hyphaServer = ${jsStringLiteral(this.hyphaServerUrl)};
|
|
253
|
+
const redirectUrl = ${jsStringLiteral(redirectUrl)};
|
|
254
|
+
const cookieName = ${jsStringLiteral(COOKIE_NAME)};
|
|
255
|
+
const tokenParam = ${jsStringLiteral(SVAMP_SERVE_TOKEN_PARAM)};
|
|
256
|
+
|
|
257
|
+
const btn = document.getElementById('login-btn');
|
|
258
|
+
const statusEl = document.getElementById('status');
|
|
259
|
+
|
|
260
|
+
function setError(msg) {
|
|
261
|
+
statusEl.innerHTML = '<span class="error">' + msg + '</span>';
|
|
262
|
+
btn.disabled = false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
btn.addEventListener('click', async () => {
|
|
266
|
+
// Open the popup SYNCHRONOUSLY inside the click handler so the browser's
|
|
267
|
+
// popup blocker treats it as user-initiated. The HTTP request below is
|
|
268
|
+
// awaited, by which time the user-gesture flag has cleared and any
|
|
269
|
+
// window.open() call would be silently blocked.
|
|
270
|
+
const popup = window.open('about:blank', '_blank');
|
|
271
|
+
if (!popup) {
|
|
272
|
+
setError('Popup was blocked by the browser. Please allow popups for this site and retry.');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try { popup.document.write('<html><body style="font-family:system-ui;padding:24px;color:#656d76">Opening Hypha sign-in\u2026</body></html>'); } catch (e) {}
|
|
276
|
+
|
|
277
|
+
btn.disabled = true;
|
|
278
|
+
statusEl.textContent = 'Contacting Hypha\u2026';
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// 1. Ask the Hypha-Login service for a login URL + polling key.
|
|
282
|
+
const startResp = await fetch(hyphaServer + '/public/services/hypha-login/start');
|
|
283
|
+
if (!startResp.ok) throw new Error('Login start failed: HTTP ' + startResp.status);
|
|
284
|
+
const ctx = await startResp.json();
|
|
285
|
+
if (!ctx || !ctx.login_url || !ctx.key) throw new Error('Login start returned no URL/key');
|
|
286
|
+
|
|
287
|
+
// 2. Redirect the popup to the actual login UI.
|
|
288
|
+
statusEl.textContent = 'Waiting for you to sign in\u2026';
|
|
289
|
+
try { popup.location.href = ctx.login_url; }
|
|
290
|
+
catch (e) { window.open(ctx.login_url, '_blank'); }
|
|
291
|
+
|
|
292
|
+
// 3. Long-poll the check endpoint until the user finishes signing in.
|
|
293
|
+
// Each request waits up to 3 s server-side; we retry for ~3 minutes total.
|
|
294
|
+
const checkBase = hyphaServer + '/public/services/hypha-login/check';
|
|
295
|
+
let token = null;
|
|
296
|
+
for (let i = 0; i < 60; i++) {
|
|
297
|
+
if (popup.closed) {
|
|
298
|
+
// User dismissed the popup before completing \u2014 keep polling briefly
|
|
299
|
+
// (the report endpoint may still fire even if window is gone).
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const r = await fetch(checkBase + '?key=' + encodeURIComponent(ctx.key) + '&timeout=3');
|
|
303
|
+
if (r.ok) {
|
|
304
|
+
const body = await r.json();
|
|
305
|
+
const t = typeof body === 'string' ? body : (body && (body.token || body));
|
|
306
|
+
if (typeof t === 'string' && t.length > 20) { token = t; break; }
|
|
307
|
+
}
|
|
308
|
+
} catch (e) { /* transient \u2014 retry */ }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!token) throw new Error('Timed out waiting for sign-in. Please try again.');
|
|
312
|
+
|
|
313
|
+
// 4. Set the cookie so subsequent requests to this origin are authenticated.
|
|
314
|
+
// Over HTTPS use SameSite=None; Secure so the cookie is still sent when
|
|
315
|
+
// this page is framed cross-site (e.g. embedded as an <artifact> iframe
|
|
316
|
+
// in the Svamp app) \u2014 SameSite=Lax would be dropped in that third-party
|
|
317
|
+
// context. Fall back to Lax on plain HTTP (SameSite=None requires Secure).
|
|
318
|
+
const isHttps = location.protocol === 'https:';
|
|
319
|
+
const sameSite = isHttps ? '; SameSite=None; Secure' : '; SameSite=Lax';
|
|
320
|
+
document.cookie = cookieName + '=' + token + '; path=/' + sameSite;
|
|
321
|
+
|
|
322
|
+
// Also carry the token in the redirect URL as a fallback: browsers that
|
|
323
|
+
// block third-party cookies entirely (Safari ITP, Chrome 3p-cookie
|
|
324
|
+
// phaseout) won't send the cookie above when we're framed cross-site, but
|
|
325
|
+
// the proxy also accepts the param on the request. Harmless when the
|
|
326
|
+
// cookie works \u2014 extractToken() prefers the cookie.
|
|
327
|
+
//
|
|
328
|
+
// Use the dedicated __svamp_serve_token name (not plain "token") so it
|
|
329
|
+
// can never collide with any token the originally-requested URL had
|
|
330
|
+
// for its own purposes. URLSearchParams handles encoding and preserves
|
|
331
|
+
// everything else (hash, other query params).
|
|
332
|
+
//
|
|
333
|
+
// #1492: this used to say "redirectUrl is always a validated same-origin path, so new URL()
|
|
334
|
+
// is safe" and then append the user's Hypha JWT to whatever came out. That assumption was
|
|
335
|
+
// false \u2014 the server validated the RAW ?return= string while this line normalizes it, and
|
|
336
|
+
// normalization can turn a string the server accepted into a protocol-relative cross-origin
|
|
337
|
+
// URL ('/..//evil.com' -> pathname '//evil.com'). The token then went to the attacker.
|
|
338
|
+
// The server-side guard is fixed (sanitizeLoginReturn), and we ALSO re-check here at the
|
|
339
|
+
// point of use: a security boundary should not depend on another layer's validation of a
|
|
340
|
+
// value it is about to act on, especially when that value carries a credential.
|
|
341
|
+
var u = new URL(redirectUrl, location.origin);
|
|
342
|
+
u.searchParams.set(tokenParam, token);
|
|
343
|
+
var dest = u.pathname + u.search + u.hash;
|
|
344
|
+
var finalUrl;
|
|
345
|
+
try { finalUrl = new URL(dest, location.origin); } catch (e) { finalUrl = null; }
|
|
346
|
+
if (!finalUrl || finalUrl.origin !== location.origin) {
|
|
347
|
+
var safe = new URL('/', location.origin);
|
|
348
|
+
safe.searchParams.set(tokenParam, token);
|
|
349
|
+
dest = safe.pathname + safe.search;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try { popup.close(); } catch (e) {}
|
|
353
|
+
statusEl.innerHTML = '<span class="ok">Signed in. Redirecting\u2026</span>';
|
|
354
|
+
setTimeout(() => { window.location.replace(dest); }, 300);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
try { popup.close(); } catch (e) {}
|
|
357
|
+
setError('Login failed: ' + (err && err.message ? err.message : err));
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
<\/script>
|
|
361
|
+
</body></html>`;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Render an "access denied" page for a request that IS authenticated (valid
|
|
365
|
+
* Hypha token) but whose email is not authorized for the mount — or for a
|
|
366
|
+
* misconfigured owner mount with no resolvable owner. This is served as a
|
|
367
|
+
* 403; it deliberately does NOT redirect to the login page, which would
|
|
368
|
+
* loop forever and tell the user nothing about why they were rejected.
|
|
369
|
+
*/
|
|
370
|
+
getAccessDeniedHtml(opts) {
|
|
371
|
+
const { email, access, ownerEmail, misconfigured } = opts;
|
|
372
|
+
const loginHref = `/__login__?return=${encodeURIComponent(opts.returnUrl || "/")}`;
|
|
373
|
+
let requirement;
|
|
374
|
+
if (misconfigured) {
|
|
375
|
+
requirement = `This resource is set to <b>owner-only</b> access, but the server never resolved an owner email (the daemon authenticated with a token that has no email claim). No account can satisfy this \u2014 it is a server-side misconfiguration, not a problem with your account. Fix it by re-creating the mount with an explicit email allowlist, e.g. <code>access: ["${escapeHtml(email || "you@example.com")}"]</code>.`;
|
|
376
|
+
} else if (access === "owner") {
|
|
377
|
+
requirement = `This resource is restricted to its owner${ownerEmail ? ` (<code>${escapeHtml(ownerEmail)}</code>)` : ""}.`;
|
|
378
|
+
} else if (Array.isArray(access)) {
|
|
379
|
+
const list = access.map((e) => `<code>${escapeHtml(e)}</code>`).join(", ");
|
|
380
|
+
requirement = `This resource is restricted to: ${list}.`;
|
|
381
|
+
} else {
|
|
382
|
+
requirement = "This resource is restricted.";
|
|
383
|
+
}
|
|
384
|
+
const signedInLine = email ? `You are signed in as <code>${escapeHtml(email)}</code>, which is not on the allow list.` : `You are not signed in.`;
|
|
385
|
+
return `<!DOCTYPE html>
|
|
386
|
+
<html><head>
|
|
387
|
+
<meta charset="utf-8">
|
|
388
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
389
|
+
<title>Access denied \u2014 Svamp File Server</title>
|
|
390
|
+
<style>
|
|
391
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
392
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f6f8fa;color:#24292f;padding:16px}
|
|
393
|
+
.card{background:#fff;border:1px solid #d0d7de;border-radius:12px;padding:32px;max-width:480px;width:100%;box-shadow:0 4px 12px rgba(31,35,40,0.06)}
|
|
394
|
+
h1{font-size:1.25rem;margin-bottom:8px}
|
|
395
|
+
.lead{color:#cf222e;font-weight:600;margin-bottom:16px}
|
|
396
|
+
p{color:#3d444d;font-size:0.92rem;line-height:1.5;margin-bottom:14px}
|
|
397
|
+
code{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:1px 6px;font-size:0.85em}
|
|
398
|
+
a.btn{display:inline-block;background:#0969da;color:#fff;border-radius:8px;padding:10px 20px;font-weight:500;text-decoration:none;margin-top:4px}
|
|
399
|
+
a.btn:hover{background:#0860c4}
|
|
400
|
+
</style>
|
|
401
|
+
</head><body>
|
|
402
|
+
<div class="card">
|
|
403
|
+
<h1>${misconfigured ? "\u26D4 Access not possible" : "\u26D4 Access denied"}</h1>
|
|
404
|
+
<p class="lead">${signedInLine}</p>
|
|
405
|
+
<p>${requirement}</p>
|
|
406
|
+
${misconfigured ? "" : `<a class="btn" href="${loginHref}">Sign in with a different account</a>`}
|
|
407
|
+
</div>
|
|
408
|
+
</body></html>`;
|
|
409
|
+
}
|
|
410
|
+
destroy() {
|
|
411
|
+
this.cache.clear();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function escapeHtml(s) {
|
|
415
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export { ServeAuth as S, stripServeTokenParam as a, sanitizeForwardHeaders as b, stripServeAuthCookie as c, hasCookieToken as h, parseJwtEmail as p, sanitizeLoginReturn as s };
|
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { w as wantsHelp } from './
|
|
3
|
-
import './run-LHJh7jLy.mjs';
|
|
4
|
-
import 'os';
|
|
5
|
-
import 'fs/promises';
|
|
6
|
-
import 'fs';
|
|
7
|
-
import 'url';
|
|
8
|
-
import 'child_process';
|
|
9
|
-
import 'crypto';
|
|
10
|
-
import 'node:fs';
|
|
11
|
-
import 'node:path';
|
|
12
|
-
import 'node:crypto';
|
|
13
|
-
import 'node:child_process';
|
|
14
|
-
import 'util';
|
|
15
|
-
import 'http';
|
|
16
|
-
import 'net';
|
|
17
|
-
import 'node:os';
|
|
18
|
-
import 'node:events';
|
|
19
|
-
import '@agentclientprotocol/sdk';
|
|
20
|
-
import 'node:util';
|
|
21
|
-
import 'node:readline';
|
|
22
|
-
import 'node:fs/promises';
|
|
23
|
-
import 'yaml';
|
|
24
|
-
import './serviceManager-hlOVxkhW.mjs';
|
|
2
|
+
import { w as wantsHelp } from './wantsHelp-6bMOeNUz.mjs';
|
|
25
3
|
|
|
26
4
|
function getFlag(args, flag) {
|
|
27
5
|
const idx = args.indexOf(flag);
|
|
@@ -30,11 +8,12 @@ function getFlag(args, flag) {
|
|
|
30
8
|
function hasFlag(args, ...flags) {
|
|
31
9
|
return flags.some((f) => args.includes(f));
|
|
32
10
|
}
|
|
11
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set(["--access", "--machine", "-m", "--session"]);
|
|
33
12
|
function positionalArgs(args) {
|
|
34
13
|
const result = [];
|
|
35
14
|
for (let i = 0; i < args.length; i++) {
|
|
36
15
|
if (args[i].startsWith("-")) {
|
|
37
|
-
if (
|
|
16
|
+
if (VALUE_FLAGS.has(args[i]) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
38
17
|
i++;
|
|
39
18
|
}
|
|
40
19
|
continue;
|
|
@@ -77,7 +56,7 @@ async function handleServeCommand() {
|
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
async function serveAdd(args, machineId) {
|
|
80
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
59
|
+
const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
|
|
81
60
|
const pos = positionalArgs(args);
|
|
82
61
|
const name = pos[0];
|
|
83
62
|
if (!name) {
|
|
@@ -114,7 +93,7 @@ async function serveAdd(args, machineId) {
|
|
|
114
93
|
}
|
|
115
94
|
if (sessionId && result?.url) {
|
|
116
95
|
try {
|
|
117
|
-
const agentCommands = await import('./agentCommands-
|
|
96
|
+
const agentCommands = await import('./agentCommands-B2Nl7Fof.mjs');
|
|
118
97
|
if (regenerateUrl) agentCommands.removeSessionLinkByService("serve", name, sessionId);
|
|
119
98
|
agentCommands.autoAddSessionLink(String(result.url), name, void 0, { kind: "serve", name });
|
|
120
99
|
} catch {
|
|
@@ -129,7 +108,7 @@ async function serveAdd(args, machineId) {
|
|
|
129
108
|
}
|
|
130
109
|
}
|
|
131
110
|
async function serveApply(args, machineId) {
|
|
132
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
111
|
+
const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
|
|
133
112
|
const fs = await import('fs');
|
|
134
113
|
const yaml = await import('yaml');
|
|
135
114
|
const file = positionalArgs(args)[0];
|
|
@@ -214,7 +193,7 @@ async function serveApply(args, machineId) {
|
|
|
214
193
|
console.log(`URL: ${result.url}`);
|
|
215
194
|
if (params.sessionId && result?.url) {
|
|
216
195
|
try {
|
|
217
|
-
const { autoAddSessionLink } = await import('./agentCommands-
|
|
196
|
+
const { autoAddSessionLink } = await import('./agentCommands-B2Nl7Fof.mjs');
|
|
218
197
|
const prevSession = process.env.SVAMP_SESSION_ID;
|
|
219
198
|
process.env.SVAMP_SESSION_ID = String(params.sessionId);
|
|
220
199
|
try {
|
|
@@ -235,7 +214,7 @@ async function serveApply(args, machineId) {
|
|
|
235
214
|
}
|
|
236
215
|
}
|
|
237
216
|
async function serveRemove(args, machineId) {
|
|
238
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
217
|
+
const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
|
|
239
218
|
const pos = positionalArgs(args);
|
|
240
219
|
const name = pos[0];
|
|
241
220
|
if (!name) {
|
|
@@ -245,7 +224,7 @@ async function serveRemove(args, machineId) {
|
|
|
245
224
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
246
225
|
try {
|
|
247
226
|
await machine.serveRemove({ name });
|
|
248
|
-
const { removeSessionLinkByService } = await import('./agentCommands-
|
|
227
|
+
const { removeSessionLinkByService } = await import('./agentCommands-B2Nl7Fof.mjs');
|
|
249
228
|
removeSessionLinkByService("serve", name);
|
|
250
229
|
console.log(`Mount '${name}' removed.`);
|
|
251
230
|
} catch (err) {
|
|
@@ -257,10 +236,10 @@ async function serveRemove(args, machineId) {
|
|
|
257
236
|
}
|
|
258
237
|
}
|
|
259
238
|
async function serveList(args, machineId) {
|
|
260
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
239
|
+
const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
|
|
261
240
|
const all = hasFlag(args, "--all", "-a");
|
|
262
241
|
const json = hasFlag(args, "--json");
|
|
263
|
-
const sessionId = getFlag(args, "--session");
|
|
242
|
+
const sessionId = getFlag(args, "--session") ?? process.env.SVAMP_SESSION_ID;
|
|
264
243
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
265
244
|
try {
|
|
266
245
|
const mounts = await machine.serveList({ sessionId, all });
|
|
@@ -291,7 +270,7 @@ async function serveList(args, machineId) {
|
|
|
291
270
|
}
|
|
292
271
|
}
|
|
293
272
|
async function serveInfo(machineId) {
|
|
294
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
273
|
+
const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
|
|
295
274
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
296
275
|
try {
|
|
297
276
|
const info = await machine.serveInfo();
|
|
@@ -384,4 +363,4 @@ Declarative apply (svamp serve apply <yaml>):
|
|
|
384
363
|
`);
|
|
385
364
|
}
|
|
386
365
|
|
|
387
|
-
export { handleServeCommand };
|
|
366
|
+
export { handleServeCommand, positionalArgs };
|