@venturewild/workspace 0.1.11 → 0.1.13
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/LICENSE +21 -21
- package/README.md +112 -112
- package/package.json +76 -76
- package/server/bin/wild-workspace.mjs +763 -763
- package/server/src/agent.mjs +386 -386
- package/server/src/config.mjs +365 -325
- package/server/src/daemon-supervisor.mjs +216 -216
- package/server/src/inbox.mjs +86 -86
- package/server/src/index.mjs +1726 -1566
- package/server/src/logpaths.mjs +98 -98
- package/server/src/pairing.mjs +146 -0
- package/server/src/service.mjs +419 -419
- package/server/src/share.mjs +148 -115
- package/server/src/sync.mjs +248 -248
- package/web/dist/assets/{index-n0-hsCzL.js → index-CAzFAt7W.js} +19 -19
- package/web/dist/index.html +1 -1
package/server/src/config.mjs
CHANGED
|
@@ -1,325 +1,365 @@
|
|
|
1
|
-
// Central config + role definitions.
|
|
2
|
-
// One UI permission-flagged per AR-19.
|
|
3
|
-
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import crypto from 'node:crypto';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import { loadAccount } from './account.mjs';
|
|
11
|
-
import { loadOperatorToken } from './operator.mjs';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
// so
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
env.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1
|
+
// Central config + role definitions.
|
|
2
|
+
// One UI permission-flagged per AR-19.
|
|
3
|
+
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { loadAccount } from './account.mjs';
|
|
11
|
+
import { loadOperatorToken } from './operator.mjs';
|
|
12
|
+
import { globalDir } from './logpaths.mjs';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// Secrets that shipped as scaffold defaults. Forgeable — never allowed on a
|
|
17
|
+
// non-localhost bind. Kept only so the startup guard can recognise them.
|
|
18
|
+
export const WEAK_SECRETS = Object.freeze(
|
|
19
|
+
new Set(['dev-partner-token', 'dev-share-secret-change-me']),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const LOCAL_HOSTS = Object.freeze(new Set(['127.0.0.1', 'localhost', '::1']));
|
|
23
|
+
|
|
24
|
+
export function isLocalhost(host) {
|
|
25
|
+
return LOCAL_HOSTS.has(host);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The bmo-sync daemon's HTTP origin, derived from its WebSocket event-feed
|
|
29
|
+
// URL so the two stay consistent when WILD_WORKSPACE_DAEMON_URL is overridden.
|
|
30
|
+
function daemonHttpBase(wsUrl) {
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(wsUrl);
|
|
33
|
+
return `${u.protocol === 'wss:' ? 'https:' : 'http:'}//${u.host}`;
|
|
34
|
+
} catch {
|
|
35
|
+
return 'http://127.0.0.1:8320';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Per-install secrets (partnerToken + shareSecret). They sign the owner's login
|
|
40
|
+
// cookie, so they MUST be stable across restarts/upgrades — if they rotate, every
|
|
41
|
+
// browser holding a cookie is silently logged out (the public URL then 401s until
|
|
42
|
+
// the owner re-authenticates with a token). They live in the per-install GLOBAL
|
|
43
|
+
// dir (~/.wild-workspace) — an ABSOLUTE path that never moves — NOT in the
|
|
44
|
+
// workspace's `.wild-workspace`, which (a) is keyed off `process.cwd()` so it
|
|
45
|
+
// shifts when the always-on supervisor relaunches from a different dir (the
|
|
46
|
+
// real-world "logged out after upgrade" bug), and (b) is inside the synced
|
|
47
|
+
// folder (CLAUDE.md rule #1: no system state in the synced workspace).
|
|
48
|
+
//
|
|
49
|
+
// A legacy in-workspace secrets.json is migrated forward (its tokens preserved,
|
|
50
|
+
// so cookies issued before this version keep validating) and then removed from
|
|
51
|
+
// the synced folder. Replaces the weak scaffold defaults so share tokens can't
|
|
52
|
+
// be forged. (Concerns C1/C2.)
|
|
53
|
+
function readSecretsFile(file) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
56
|
+
if (parsed && parsed.partnerToken && parsed.shareSecret) {
|
|
57
|
+
return { partnerToken: parsed.partnerToken, shareSecret: parsed.shareSecret };
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// missing / unreadable / malformed
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadOrCreateSecrets(dataDir, env = process.env) {
|
|
66
|
+
const stablePath = path.join(globalDir(env), 'secrets.json');
|
|
67
|
+
const legacyPath = path.join(dataDir, 'secrets.json');
|
|
68
|
+
|
|
69
|
+
// 1. The stable per-install location wins.
|
|
70
|
+
const stable = readSecretsFile(stablePath);
|
|
71
|
+
if (stable) return stable;
|
|
72
|
+
|
|
73
|
+
// 2. Migrate a legacy in-workspace secrets file (preserve its tokens so
|
|
74
|
+
// pre-existing login cookies keep validating); otherwise generate fresh.
|
|
75
|
+
const legacy = readSecretsFile(legacyPath);
|
|
76
|
+
const secrets = legacy || {
|
|
77
|
+
partnerToken: crypto.randomBytes(24).toString('base64url'),
|
|
78
|
+
shareSecret: crypto.randomBytes(32).toString('base64url'),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 3. Persist to the stable location.
|
|
82
|
+
try {
|
|
83
|
+
fs.mkdirSync(path.dirname(stablePath), { recursive: true });
|
|
84
|
+
fs.writeFileSync(stablePath, JSON.stringify(secrets, null, 2), { mode: 0o600 });
|
|
85
|
+
} catch {
|
|
86
|
+
// can't persist (read-only fs?) — still use the secrets for this run
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 4. Best-effort: drop the legacy copy so the signing secret stops living in
|
|
90
|
+
// the synced workspace folder. Only after a successful migration.
|
|
91
|
+
if (legacy) {
|
|
92
|
+
try {
|
|
93
|
+
fs.rmSync(legacyPath, { force: true });
|
|
94
|
+
} catch {
|
|
95
|
+
// leave it; it's now inert (the stable copy takes precedence)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return secrets;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Refuse to start in public mode with a forgeable secret. (Concerns C1/C2.)
|
|
103
|
+
export function assertSecureBinding(config) {
|
|
104
|
+
if (!config.publicMode) return;
|
|
105
|
+
if (WEAK_SECRETS.has(config.partnerToken) || WEAK_SECRETS.has(config.shareSecret)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Refusing to run in public mode with a default secret. ` +
|
|
108
|
+
`Set WILD_WORKSPACE_PARTNER_TOKEN and WILD_WORKSPACE_SHARE_SECRET, ` +
|
|
109
|
+
`or remove them so per-install secrets are generated.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const ROLES = Object.freeze({
|
|
115
|
+
PARTNER: 'partner',
|
|
116
|
+
VIEWER: 'viewer',
|
|
117
|
+
CLIENT: 'client',
|
|
118
|
+
// The consented support/operator channel (off by default — see operator.mjs).
|
|
119
|
+
OPERATOR: 'operator',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const ROLE_CAPABILITIES = Object.freeze({
|
|
123
|
+
partner: {
|
|
124
|
+
chat: true,
|
|
125
|
+
chatWrite: true,
|
|
126
|
+
preview: true,
|
|
127
|
+
fileTree: true,
|
|
128
|
+
terminal: true,
|
|
129
|
+
inbox: true,
|
|
130
|
+
share: true,
|
|
131
|
+
sync: true,
|
|
132
|
+
deploy: true,
|
|
133
|
+
requestChanges: false,
|
|
134
|
+
operate: true, // the owner can also drive the operator allowlist locally
|
|
135
|
+
},
|
|
136
|
+
viewer: {
|
|
137
|
+
chat: true,
|
|
138
|
+
chatWrite: false,
|
|
139
|
+
preview: true,
|
|
140
|
+
fileTree: false,
|
|
141
|
+
terminal: false,
|
|
142
|
+
inbox: false,
|
|
143
|
+
share: false,
|
|
144
|
+
sync: false,
|
|
145
|
+
deploy: false,
|
|
146
|
+
requestChanges: false,
|
|
147
|
+
operate: false,
|
|
148
|
+
},
|
|
149
|
+
client: {
|
|
150
|
+
chat: true,
|
|
151
|
+
chatWrite: true,
|
|
152
|
+
preview: true,
|
|
153
|
+
fileTree: false,
|
|
154
|
+
terminal: false,
|
|
155
|
+
inbox: false,
|
|
156
|
+
share: false,
|
|
157
|
+
sync: false,
|
|
158
|
+
deploy: false,
|
|
159
|
+
requestChanges: true,
|
|
160
|
+
operate: false,
|
|
161
|
+
},
|
|
162
|
+
// Operator: remote diagnose + a curated remediation allowlist. Read-only on
|
|
163
|
+
// chat (can SEE the conversation to help, cannot drive the agent — chatWrite
|
|
164
|
+
// stays false), plus the `operate` capability the /api/operator/* routes gate
|
|
165
|
+
// on. Reachable only with the dedicated operator token (operator.mjs).
|
|
166
|
+
operator: {
|
|
167
|
+
chat: true,
|
|
168
|
+
chatWrite: false,
|
|
169
|
+
preview: true,
|
|
170
|
+
fileTree: true,
|
|
171
|
+
terminal: false,
|
|
172
|
+
inbox: false,
|
|
173
|
+
share: false,
|
|
174
|
+
sync: false,
|
|
175
|
+
deploy: false,
|
|
176
|
+
requestChanges: false,
|
|
177
|
+
operate: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export const DEFAULT_AGENTS = Object.freeze([
|
|
182
|
+
{
|
|
183
|
+
id: 'claude',
|
|
184
|
+
binary: 'claude',
|
|
185
|
+
label: 'Claude Code',
|
|
186
|
+
description: 'Anthropic Claude Code',
|
|
187
|
+
args: ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'],
|
|
188
|
+
streamFormat: 'claude-stream-json',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'gemini',
|
|
192
|
+
binary: 'gemini',
|
|
193
|
+
label: 'Gemini CLI',
|
|
194
|
+
description: 'Google Gemini',
|
|
195
|
+
args: ['-p'],
|
|
196
|
+
streamFormat: 'text',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'glm',
|
|
200
|
+
binary: 'glm',
|
|
201
|
+
label: 'GLM (Z.AI)',
|
|
202
|
+
description: 'GLM-4.6 via Z.AI',
|
|
203
|
+
args: ['-p', '--permission-mode', 'bypassPermissions'],
|
|
204
|
+
streamFormat: 'text',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'codex',
|
|
208
|
+
binary: 'codex',
|
|
209
|
+
label: 'Codex (OpenAI)',
|
|
210
|
+
description: 'GPT-5 / o3 via Codex CLI',
|
|
211
|
+
args: ['exec', '--skip-git-repo-check'],
|
|
212
|
+
streamFormat: 'text',
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
export function buildConfig(overrides = {}) {
|
|
217
|
+
const env = overrides.env || process.env;
|
|
218
|
+
const portOverride = overrides.port;
|
|
219
|
+
const resolvedPort =
|
|
220
|
+
typeof portOverride === 'number'
|
|
221
|
+
? portOverride
|
|
222
|
+
: Number(env.WILD_WORKSPACE_PORT || 5173);
|
|
223
|
+
const workspaceDir = path.resolve(
|
|
224
|
+
overrides.workspaceDir || env.WILD_WORKSPACE_DIR || process.cwd(),
|
|
225
|
+
);
|
|
226
|
+
const dataDir = path.resolve(
|
|
227
|
+
overrides.dataDir ||
|
|
228
|
+
env.WILD_WORKSPACE_DATA_DIR ||
|
|
229
|
+
path.join(workspaceDir, '.wild-workspace'),
|
|
230
|
+
);
|
|
231
|
+
// Lazy: only load/generate persisted secrets if neither an override nor an
|
|
232
|
+
// env var supplies one — keeps tests that pass both from touching the fs.
|
|
233
|
+
let _secrets = null;
|
|
234
|
+
const secrets = () => (_secrets ??= loadOrCreateSecrets(dataDir, env));
|
|
235
|
+
const host = overrides.host || env.WILD_WORKSPACE_HOST || '127.0.0.1';
|
|
236
|
+
// Per-install bmo-sync account — null until the user runs `wild-workspace
|
|
237
|
+
// login` with the payload from `workspace.venturewild.llc`. Its presence
|
|
238
|
+
// upgrades the install to a real slug (shareBaseUrl flips to the user's
|
|
239
|
+
// subdomain) and lights up the /api/session.account field for the UI.
|
|
240
|
+
// Loaded BEFORE publicMode because a logged-in install is publicly reachable.
|
|
241
|
+
const account =
|
|
242
|
+
overrides.account === undefined ? loadAccount(dataDir) : overrides.account;
|
|
243
|
+
// publicMode = "treat every request as untrusted". True for a non-localhost
|
|
244
|
+
// bind, OR when WILD_WORKSPACE_PUBLIC=1 — needed when a tunnel (Cloudflare
|
|
245
|
+
// etc.) forwards public traffic to a localhost-bound server, since the bind
|
|
246
|
+
// address alone would otherwise look local. Drives the C1 auth posture.
|
|
247
|
+
const publicMode =
|
|
248
|
+
overrides.publicMode ??
|
|
249
|
+
(env.WILD_WORKSPACE_PUBLIC === '1' ||
|
|
250
|
+
!isLocalhost(host) ||
|
|
251
|
+
// CRITICAL (C1): a slug-linked install is reachable from the public
|
|
252
|
+
// internet via <slug>.venturewild.llc — the daemon forwards that traffic
|
|
253
|
+
// to this server FROM 127.0.0.1, so a non-public server would auto-grant
|
|
254
|
+
// `partner` (full RCE) to every anonymous visitor. Having an account token
|
|
255
|
+
// ⟺ the tunnel is exposing this machine, so force public mode even when the
|
|
256
|
+
// always-on supervisor (or a bare `wild-workspace`) launches without
|
|
257
|
+
// WILD_WORKSPACE_PUBLIC=1. The local owner authenticates via the partner
|
|
258
|
+
// token the launcher appends to the localhost URL (?t=, then S1 cookie).
|
|
259
|
+
Boolean(account?.accountToken));
|
|
260
|
+
// bmo-sync: the local daemon's event feed (a WebSocket URL).
|
|
261
|
+
const daemonUrl =
|
|
262
|
+
overrides.daemonUrl ||
|
|
263
|
+
env.WILD_WORKSPACE_DAEMON_URL ||
|
|
264
|
+
'ws://127.0.0.1:8320/api/events';
|
|
265
|
+
const accountShareBase = account?.slug
|
|
266
|
+
? `https://${account.slug}.venturewild.llc`
|
|
267
|
+
: null;
|
|
268
|
+
return {
|
|
269
|
+
port: resolvedPort,
|
|
270
|
+
host,
|
|
271
|
+
publicMode,
|
|
272
|
+
workspaceDir,
|
|
273
|
+
dataDir,
|
|
274
|
+
webDir:
|
|
275
|
+
overrides.webDir ||
|
|
276
|
+
env.WILD_WORKSPACE_WEB_DIR ||
|
|
277
|
+
path.resolve(__dirname, '..', '..', 'web', 'dist'),
|
|
278
|
+
openBrowser: overrides.openBrowser ?? env.WILD_WORKSPACE_NO_OPEN !== '1',
|
|
279
|
+
shareBaseUrl:
|
|
280
|
+
overrides.shareBaseUrl ||
|
|
281
|
+
env.WILD_WORKSPACE_SHARE_BASE_URL ||
|
|
282
|
+
accountShareBase ||
|
|
283
|
+
`http://${host}:${resolvedPort}`,
|
|
284
|
+
// The signed-in account (if any). Kept on the server-side config so
|
|
285
|
+
// /api/session can expose the public bits (slug, email, accountId, displayName)
|
|
286
|
+
// to the UI while accountToken stays here only.
|
|
287
|
+
account: account
|
|
288
|
+
? {
|
|
289
|
+
slug: account.slug,
|
|
290
|
+
email: account.email,
|
|
291
|
+
accountId: account.accountId,
|
|
292
|
+
displayName: account.displayName,
|
|
293
|
+
loggedInAt: account.loggedInAt,
|
|
294
|
+
// accountToken is intentionally kept out of the broadcasted config
|
|
295
|
+
// shape — it's read separately by code that needs to authenticate
|
|
296
|
+
// against bmo-sync.
|
|
297
|
+
}
|
|
298
|
+
: null,
|
|
299
|
+
accountToken: account?.accountToken || null,
|
|
300
|
+
partnerToken:
|
|
301
|
+
overrides.partnerToken ||
|
|
302
|
+
env.WILD_WORKSPACE_PARTNER_TOKEN ||
|
|
303
|
+
secrets().partnerToken,
|
|
304
|
+
shareSecret:
|
|
305
|
+
overrides.shareSecret ||
|
|
306
|
+
env.WILD_WORKSPACE_SHARE_SECRET ||
|
|
307
|
+
secrets().shareSecret,
|
|
308
|
+
// The operator-channel token — null unless the user explicitly enabled the
|
|
309
|
+
// channel (`wild-workspace operator enable`). Off by default. Server-side
|
|
310
|
+
// only; never broadcast to the browser.
|
|
311
|
+
operatorToken:
|
|
312
|
+
overrides.operatorToken ??
|
|
313
|
+
env.WILD_WORKSPACE_OPERATOR_TOKEN ??
|
|
314
|
+
loadOperatorToken(dataDir),
|
|
315
|
+
workspaceId:
|
|
316
|
+
overrides.workspaceId ||
|
|
317
|
+
env.WILD_WORKSPACE_ID ||
|
|
318
|
+
path.basename(workspaceDir) ||
|
|
319
|
+
'workspace',
|
|
320
|
+
role: overrides.role || env.WILD_WORKSPACE_ROLE || ROLES.PARTNER,
|
|
321
|
+
// bmo-sync daemon — a separate local process; the bridge retries quietly
|
|
322
|
+
// when it is absent. daemonUrl is the WebSocket event feed; daemonHttpUrl
|
|
323
|
+
// is the same origin's HTTP API (pair / detach / list).
|
|
324
|
+
daemonUrl,
|
|
325
|
+
daemonHttpUrl: daemonHttpBase(daemonUrl),
|
|
326
|
+
// Auto-start the bmo-sync daemon when the server boots. On by default;
|
|
327
|
+
// WILD_WORKSPACE_DAEMON_AUTOSTART=0 disables it. Forced off under the test
|
|
328
|
+
// runner (VITEST / NODE_ENV=test) so the suite never spawns a real daemon.
|
|
329
|
+
daemonAutostart:
|
|
330
|
+
overrides.daemonAutostart ??
|
|
331
|
+
(env.WILD_WORKSPACE_DAEMON_AUTOSTART !== '0' &&
|
|
332
|
+
!env.VITEST &&
|
|
333
|
+
env.NODE_ENV !== 'test'),
|
|
334
|
+
// Central bmo-sync server (Fly.io). Used to redeem invites (via the
|
|
335
|
+
// daemon) and — only when an admin key is set — to mint them.
|
|
336
|
+
bmoSyncServerUrl:
|
|
337
|
+
overrides.bmoSyncServerUrl ||
|
|
338
|
+
env.WILD_WORKSPACE_BMO_SYNC_URL ||
|
|
339
|
+
env.BMO_SYNC_URL ||
|
|
340
|
+
'https://sync.venturewild.llc',
|
|
341
|
+
// Optional. Present only on an install that mints invites (the folder
|
|
342
|
+
// owner). Absent installs can still redeem. Never sent to the browser.
|
|
343
|
+
bmoSyncAdminKey:
|
|
344
|
+
overrides.bmoSyncAdminKey ||
|
|
345
|
+
env.BMO_SYNC_ADMIN_KEY ||
|
|
346
|
+
env.WILD_WORKSPACE_BMO_ADMIN_KEY ||
|
|
347
|
+
null,
|
|
348
|
+
home: os.homedir(),
|
|
349
|
+
nodeEnv: env.NODE_ENV || 'production',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Read from package.json (the single source of truth) so the reported version
|
|
354
|
+
// can never drift from the published release — `npm version` bumps it for free.
|
|
355
|
+
// This previously hardcoded '0.1.0', so every release (0.1.1/0.1.2/0.1.3) shipped
|
|
356
|
+
// a stale version to `--version`, /api/health, doctor, and all telemetry.
|
|
357
|
+
function readAppVersion() {
|
|
358
|
+
try {
|
|
359
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'));
|
|
360
|
+
return pkg.version || '0.0.0';
|
|
361
|
+
} catch {
|
|
362
|
+
return '0.0.0';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
export const APP_VERSION = readAppVersion();
|