@xiashe/sdk 0.1.36 → 0.1.38
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/http.mjs +37 -0
- package/index.mjs +109 -73
- package/outputSafety.mjs +21 -0
- package/package.json +1 -1
- package/privateState.mjs +61 -0
- package/runtime.mjs +1 -1
package/http.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
2
|
+
export function requestUrl(value) {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
5
|
+
if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) || url.username || url.password || url.hash) throw Error('HTTP_TARGET_INVALID');
|
|
6
|
+
return url;
|
|
7
|
+
}
|
|
8
|
+
export async function boundedJsonRequest(url, body, { timeoutMs = 20000, headers = {}, maxResponseBytes = MAX_RESPONSE_BYTES, method = 'POST' } = {}) {
|
|
9
|
+
requestUrl(url);
|
|
10
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isSafeInteger(maxResponseBytes) || maxResponseBytes <= 0) throw Error('HTTP_LIMIT_INVALID');
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
let timer;
|
|
13
|
+
const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(Error('REQUEST_TIMEOUT')); }, timeoutMs); });
|
|
14
|
+
const request = async () => {
|
|
15
|
+
const response = await fetch(url, { method, redirect: 'error', signal: controller.signal,
|
|
16
|
+
headers: { 'content-type': 'application/json', ...headers }, ...(method === 'GET' ? {} : { body: JSON.stringify(body ?? {}) }) });
|
|
17
|
+
if (response.status >= 300 && response.status < 400) throw Error('HTTP_REDIRECT_REFUSED');
|
|
18
|
+
if (Number(response.headers.get('content-length')) > maxResponseBytes) { controller.abort(); throw Error('HTTP_RESPONSE_TOO_LARGE'); }
|
|
19
|
+
const reader = response.body?.getReader();
|
|
20
|
+
const chunks = []; let total = 0;
|
|
21
|
+
try {
|
|
22
|
+
if (reader) while (true) {
|
|
23
|
+
const { done, value } = await reader.read(); if (done) break;
|
|
24
|
+
total += value.byteLength;
|
|
25
|
+
if (total > maxResponseBytes) { controller.abort(); throw Error('HTTP_RESPONSE_TOO_LARGE'); }
|
|
26
|
+
chunks.push(Buffer.from(value));
|
|
27
|
+
}
|
|
28
|
+
} finally { reader?.releaseLock(); }
|
|
29
|
+
let payload;
|
|
30
|
+
try { const text = Buffer.concat(chunks).toString('utf8'); payload = text ? JSON.parse(text) : {}; }
|
|
31
|
+
catch { throw Error('HTTP_BAD_JSON'); }
|
|
32
|
+
return { response, payload };
|
|
33
|
+
};
|
|
34
|
+
try { return await Promise.race([request(), deadline]); }
|
|
35
|
+
catch (error) { if (controller.signal.aborted && error.message !== 'HTTP_RESPONSE_TOO_LARGE') throw Error('REQUEST_TIMEOUT'); throw error; }
|
|
36
|
+
finally { clearTimeout(timer); }
|
|
37
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { privateDirectory, checkPrivateFile, withPrivateLock, atomicPrivateJson } from './privateState.mjs';
|
|
2
|
+
import { boundedJsonRequest } from './http.mjs';
|
|
1
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
4
|
import { existsSync } from 'node:fs';
|
|
3
5
|
import { mkdir, readFile, writeFile, stat } from 'node:fs/promises';
|
|
@@ -5,7 +7,7 @@ import os from 'node:os';
|
|
|
5
7
|
import path from 'node:path';
|
|
6
8
|
import { ConvexHttpClient } from 'convex/browser';
|
|
7
9
|
import { anyApi as api } from 'convex/server';
|
|
8
|
-
export { installRuntimePackage, runtimePrincipal, writeRuntimeRecord } from './runtime.mjs';
|
|
10
|
+
export { verifyDirectory as verifyRuntimePackageDirectory, inspectRuntimeArchive, installRuntimePackage, runtimePrincipal, writeRuntimeRecord } from './runtime.mjs';
|
|
9
11
|
export { reportRuntimeResult, retryRuntimeResults } from './runtimeOutbox.mjs';
|
|
10
12
|
export { runRuntimeScript } from './runtimeScript.mjs';
|
|
11
13
|
export { serveRuntimeScriptHost, retryRuntimeHostResults } from './runtimeHost.mjs';
|
|
@@ -13,7 +15,9 @@ export { createRuntimeAgentHost } from './runtimeAgentHost.mjs';
|
|
|
13
15
|
export { createRuntimeAgentSession } from './runtimeAgent.mjs';
|
|
14
16
|
export { serveRuntimeMcp } from './runtimeMcp.mjs';
|
|
15
17
|
|
|
16
|
-
export
|
|
18
|
+
export { safeOutput as redactCredentials } from './outputSafety.mjs';
|
|
19
|
+
|
|
20
|
+
export const VERSION = '0.1.38';
|
|
17
21
|
export { probeRuntimeGateway, runtimeSetupTemplate } from './runtimeSetup.mjs';
|
|
18
22
|
|
|
19
23
|
export const DEFAULTS = {
|
|
@@ -121,6 +125,8 @@ export async function readConfig() {
|
|
|
121
125
|
if (!existsSync(CONFIG_PATH)) {
|
|
122
126
|
return freshConfig();
|
|
123
127
|
}
|
|
128
|
+
await privateDirectory(path.dirname(CONFIG_PATH));
|
|
129
|
+
await checkPrivateFile(CONFIG_PATH);
|
|
124
130
|
const raw = await readFile(CONFIG_PATH, 'utf8');
|
|
125
131
|
try {
|
|
126
132
|
const parsed = JSON.parse(raw);
|
|
@@ -139,14 +145,21 @@ export async function readConfig() {
|
|
|
139
145
|
profiles
|
|
140
146
|
};
|
|
141
147
|
} catch (error) {
|
|
142
|
-
throw new XiasheError(`Invalid ${DEFAULT_PRODUCT_NAME} config JSON at ${CONFIG_PATH}`, { code: 'CONFIG_INVALID_JSON'
|
|
148
|
+
throw new XiasheError(`Invalid ${DEFAULT_PRODUCT_NAME} config JSON at ${CONFIG_PATH}`, { code: 'CONFIG_INVALID_JSON' });
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
async function mutateConfig(operation) {
|
|
153
|
+
await privateDirectory(path.dirname(CONFIG_PATH));
|
|
154
|
+
return withPrivateLock(CONFIG_PATH, async () => {
|
|
155
|
+
const config = await readConfig();
|
|
156
|
+
await operation(config);
|
|
157
|
+
await atomicPrivateJson(CONFIG_PATH, config);
|
|
158
|
+
return config;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
146
161
|
export async function writeConfig(config) {
|
|
147
|
-
|
|
148
|
-
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
149
|
-
return config;
|
|
162
|
+
return mutateConfig(current => { for (const key of Object.keys(current)) delete current[key]; Object.assign(current, config); });
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
export async function getProfile(name) {
|
|
@@ -162,28 +175,40 @@ export async function getProfile(name) {
|
|
|
162
175
|
};
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
name: profileName
|
|
178
|
+
const endpointKeys = ['convexUrl', 'actionsUrl', 'mcpUrl', 'agentClaimUrl', 'relayUrl'];
|
|
179
|
+
function environmentBinding(profile) {
|
|
180
|
+
const actions = normalizeBaseUrl(profile.actionsUrl);
|
|
181
|
+
const endpoints = {
|
|
182
|
+
...profile,
|
|
183
|
+
mcpUrl: profile.mcpUrl || (actions ? `${actions}/mcp/xiashe` : ''),
|
|
184
|
+
agentClaimUrl: profile.agentClaimUrl || (actions ? `${actions}/agent-gateway/claim` : ''),
|
|
173
185
|
};
|
|
174
|
-
return
|
|
186
|
+
return Object.fromEntries(endpointKeys.map(key => [key, normalizeBaseUrl(endpoints[key])]));
|
|
175
187
|
}
|
|
176
188
|
|
|
189
|
+
function sameEnvironment(left, right) {
|
|
190
|
+
const a = environmentBinding(left || {}), b = environmentBinding(right || {});
|
|
191
|
+
return endpointKeys.every(key => a[key] === b[key]);
|
|
192
|
+
}
|
|
193
|
+
export async function updateProfile(name, patch) {
|
|
194
|
+
return mutateConfig(config => {
|
|
195
|
+
const profileName = name || config.profile || DEFAULT_PROFILE;
|
|
196
|
+
const previous = { ...freshConfig().profiles[DEFAULT_PROFILE], ...(config.profiles[profileName] || {}) };
|
|
197
|
+
const next = { ...previous, ...patch, name: profileName };
|
|
198
|
+
if (!sameEnvironment(previous, next)) { delete next.session; delete next.agent; }
|
|
199
|
+
config.profiles[profileName] = next;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
177
202
|
export async function useProfile(name) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return writeConfig(config);
|
|
203
|
+
return mutateConfig(config => {
|
|
204
|
+
config.profile = name || DEFAULT_PROFILE;
|
|
205
|
+
config.profiles[config.profile] ||= { name: config.profile };
|
|
206
|
+
});
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
export async function useEnvironment(envName, profileName) {
|
|
186
210
|
const normalized = String(envName || '').toLowerCase();
|
|
211
|
+
if (!['cn', 'global', 'intl'].includes(normalized)) throw Error('ENVIRONMENT_INVALID');
|
|
187
212
|
const defaults = normalized === 'global' || normalized === 'intl' ? DEFAULTS.global : DEFAULTS.cn;
|
|
188
213
|
await updateProfile(profileName, {
|
|
189
214
|
market: defaults.market,
|
|
@@ -200,10 +225,11 @@ export async function useEnvironment(envName, profileName) {
|
|
|
200
225
|
function envProfile(profile) {
|
|
201
226
|
const defaults = DEFAULTS[DEFAULT_ENV];
|
|
202
227
|
const actionsUrl = normalizeBaseUrl(process.env.XIASHE_ACTIONS_URL || process.env.AGENTPIE_ACTIONS_URL || profile.actionsUrl);
|
|
203
|
-
const
|
|
228
|
+
const changedActions = process.env.XIASHE_ACTIONS_URL || process.env.AGENTPIE_ACTIONS_URL;
|
|
229
|
+
const mcpUrl = process.env.XIASHE_MCP_URL || process.env.AGENTPIE_MCP_URL || (!changedActions && profile.mcpUrl) || (actionsUrl ? `${actionsUrl}/mcp/xiashe` : undefined);
|
|
204
230
|
const agentClaimUrl =
|
|
205
|
-
process.env.XIASHE_AGENT_CLAIM_URL || process.env.AGENTPIE_AGENT_CLAIM_URL || (actionsUrl ? `${actionsUrl}/agent-gateway/claim` :
|
|
206
|
-
|
|
231
|
+
process.env.XIASHE_AGENT_CLAIM_URL || process.env.AGENTPIE_AGENT_CLAIM_URL || (!changedActions && profile.agentClaimUrl) || (actionsUrl ? `${actionsUrl}/agent-gateway/claim` : undefined);
|
|
232
|
+
const resolved = {
|
|
207
233
|
...profile,
|
|
208
234
|
convexUrl: process.env.XIASHE_CONVEX_URL || process.env.AGENTPIE_CONVEX_URL || profile.convexUrl,
|
|
209
235
|
actionsUrl,
|
|
@@ -212,24 +238,33 @@ function envProfile(profile) {
|
|
|
212
238
|
relayUrl: process.env.XIASHE_RELAY_URL || process.env.AGENTPIE_RELAY_URL || profile.relayUrl,
|
|
213
239
|
market: process.env.XIASHE_MARKET || process.env.AGENTPIE_MARKET || profile.market || defaults.market,
|
|
214
240
|
locale: process.env.XIASHE_LOCALE || process.env.AGENTPIE_LOCALE || profile.locale || defaults.locale,
|
|
215
|
-
session: {
|
|
216
|
-
|
|
217
|
-
...(process.env.XIASHE_USER_ID || process.env.XIASHE_SESSION_TOKEN
|
|
218
|
-
? {
|
|
219
|
-
userId: process.env.XIASHE_USER_ID || profile.session?.userId,
|
|
220
|
-
sessionToken: process.env.XIASHE_SESSION_TOKEN || profile.session?.sessionToken
|
|
221
|
-
}
|
|
222
|
-
: {})
|
|
223
|
-
},
|
|
224
|
-
agent: {
|
|
225
|
-
...(profile.agent || {}),
|
|
226
|
-
...(process.env.XIASHE_RUNTIME_TOKEN
|
|
227
|
-
? {
|
|
228
|
-
runtimeToken: process.env.XIASHE_RUNTIME_TOKEN
|
|
229
|
-
}
|
|
230
|
-
: {})
|
|
231
|
-
}
|
|
241
|
+
session: { ...(profile.session || {}) },
|
|
242
|
+
agent: { ...(profile.agent || {}) }
|
|
232
243
|
};
|
|
244
|
+
for (const key of ['session', 'agent']) {
|
|
245
|
+
const binding = profile[key]?.environmentBinding || environmentBinding(profile);
|
|
246
|
+
if (!sameEnvironment(binding, resolved)) resolved[key] = {};
|
|
247
|
+
else if (Object.keys(resolved[key]).length) resolved[key].environmentBinding = binding;
|
|
248
|
+
}
|
|
249
|
+
const userId = process.env.XIASHE_USER_ID || process.env.AGENTPIE_USER_ID;
|
|
250
|
+
const sessionToken = process.env.XIASHE_SESSION_TOKEN || process.env.AGENTPIE_SESSION_TOKEN;
|
|
251
|
+
const runtimeToken = process.env.XIASHE_RUNTIME_TOKEN || process.env.AGENTPIE_RUNTIME_TOKEN;
|
|
252
|
+
let injectedBinding;
|
|
253
|
+
if (userId || sessionToken || runtimeToken) {
|
|
254
|
+
const issuer = process.env.XIASHE_AUTH_ENVIRONMENT || process.env.AGENTPIE_AUTH_ENVIRONMENT;
|
|
255
|
+
const rawBinding = process.env.XIASHE_AUTH_BINDING || process.env.AGENTPIE_AUTH_BINDING;
|
|
256
|
+
try { injectedBinding = rawBinding ? JSON.parse(rawBinding) : DEFAULTS[issuer === 'intl' ? 'global' : issuer]; }
|
|
257
|
+
catch { throw new XiasheError('Invalid environment credential binding.', { code: 'AUTH_ENVIRONMENT_MISMATCH' }); }
|
|
258
|
+
if (!injectedBinding || !sameEnvironment(injectedBinding, resolved))
|
|
259
|
+
throw new XiasheError('Environment credentials require a matching issuer binding. Set AUTH_ENVIRONMENT for this brand or authenticate again.', { code: 'AUTH_ENVIRONMENT_MISMATCH' });
|
|
260
|
+
}
|
|
261
|
+
if (userId || sessionToken) {
|
|
262
|
+
if (!userId || !sessionToken) throw new XiasheError('Environment authentication requires both user ID and session token.', { code: 'AUTH_ENV_INCOMPLETE' });
|
|
263
|
+
resolved.session = { userId, sessionToken, environmentBinding: environmentBinding(injectedBinding) };
|
|
264
|
+
}
|
|
265
|
+
if (runtimeToken) resolved.agent = { runtimeToken, environmentBinding: environmentBinding(injectedBinding) };
|
|
266
|
+
|
|
267
|
+
return resolved;
|
|
233
268
|
}
|
|
234
269
|
|
|
235
270
|
function normalizeConvexError(error) {
|
|
@@ -264,6 +299,9 @@ export async function loadProfile(options = {}) {
|
|
|
264
299
|
profileName,
|
|
265
300
|
profile: envProfile({
|
|
266
301
|
...profile,
|
|
302
|
+
// A legacy saved token has no verifiable issuer. Re-authenticate once.
|
|
303
|
+
session: profile.session?.environmentBinding ? profile.session : {},
|
|
304
|
+
agent: profile.agent?.environmentBinding ? profile.agent : {},
|
|
267
305
|
...cleanOptions
|
|
268
306
|
})
|
|
269
307
|
};
|
|
@@ -285,35 +323,13 @@ async function withTimeout(promiseFactory, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
|
285
323
|
}
|
|
286
324
|
|
|
287
325
|
export async function postJson(url, body, options = {}) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
'content-type': 'application/json',
|
|
296
|
-
...headers
|
|
297
|
-
},
|
|
298
|
-
body: JSON.stringify(body ?? {}),
|
|
299
|
-
signal
|
|
300
|
-
}),
|
|
301
|
-
timeoutMs
|
|
302
|
-
);
|
|
303
|
-
const text = await response.text();
|
|
304
|
-
let payload = {};
|
|
305
|
-
try {
|
|
306
|
-
payload = text ? JSON.parse(text) : {};
|
|
307
|
-
} catch {
|
|
308
|
-
throw new XiasheError(`Bad JSON response from ${url}`, { code: 'HTTP_BAD_JSON', status: response.status });
|
|
309
|
-
}
|
|
310
|
-
if (!response.ok) {
|
|
311
|
-
throw new XiasheError(payload.error || payload.message || `HTTP ${response.status}`, {
|
|
312
|
-
code: payload.error || `HTTP_${response.status}`,
|
|
313
|
-
status: response.status,
|
|
314
|
-
details: payload
|
|
315
|
-
});
|
|
316
|
-
}
|
|
326
|
+
let result;
|
|
327
|
+
try { result = await boundedJsonRequest(url, body, options); }
|
|
328
|
+
catch (error) { throw new XiasheError(error.message, { code: error.message }); }
|
|
329
|
+
const { response, payload } = result;
|
|
330
|
+
if (!response.ok) throw new XiasheError(payload?.error || payload?.message || `HTTP ${response.status}`, {
|
|
331
|
+
code: payload?.error || `HTTP_${response.status}`, status: response.status
|
|
332
|
+
});
|
|
317
333
|
return payload;
|
|
318
334
|
}
|
|
319
335
|
|
|
@@ -381,6 +397,8 @@ export class XiasheClient {
|
|
|
381
397
|
}
|
|
382
398
|
|
|
383
399
|
authArgs(extra = {}) {
|
|
400
|
+
if (this.profile.session?.environmentBinding && !sameEnvironment(this.profile.session.environmentBinding, this.profile))
|
|
401
|
+
throw new XiasheError('Environment changed. Sign in again.', { code: 'AUTH_ENVIRONMENT_MISMATCH' });
|
|
384
402
|
const userId = this.profile.session?.userId;
|
|
385
403
|
const sessionToken = this.profile.session?.sessionToken;
|
|
386
404
|
if (!userId || !sessionToken) {
|
|
@@ -455,10 +473,11 @@ export class XiasheClient {
|
|
|
455
473
|
assertNumbersEnvironment() {
|
|
456
474
|
const pairs = new Map([
|
|
457
475
|
['https://actions.xiashe.chat', 'https://api.xiashe.chat'],
|
|
458
|
-
['https://actions.projectphenix.site', 'https://api.projectphenix.site']
|
|
476
|
+
['https://actions.projectphenix.site', 'https://api.projectphenix.site'],
|
|
477
|
+
['https://agentpie.app', DEFAULTS.global.convexUrl]
|
|
459
478
|
]);
|
|
460
479
|
const actions = normalizeBaseUrl(this.profile.actionsUrl);
|
|
461
|
-
if (this.profile.market !== 'cn' || !pairs.has(actions) || pairs.get(actions) !== normalizeBaseUrl(this.profile.convexUrl)) {
|
|
480
|
+
if (this.profile.market !== (actions === 'https://agentpie.app' ? 'intl' : 'cn') || !pairs.has(actions) || pairs.get(actions) !== normalizeBaseUrl(this.profile.convexUrl)) {
|
|
462
481
|
throw new XiasheError('Skill numbers require matching official XiaShe backend and actions URLs.', { code: 'NUMBER_ENVIRONMENT_INVALID' });
|
|
463
482
|
}
|
|
464
483
|
}
|
|
@@ -467,6 +486,10 @@ export class XiasheClient {
|
|
|
467
486
|
this.assertNumbersEnvironment();
|
|
468
487
|
return this.query(api.skillNumbers.resolve, { number: String(number) });
|
|
469
488
|
}
|
|
489
|
+
async skillNumberAnalytics(number) {
|
|
490
|
+
this.assertNumbersEnvironment();
|
|
491
|
+
return this.query(api.skillNumbers.analytics, this.authArgs({ number: String(number) }));
|
|
492
|
+
}
|
|
470
493
|
async acquireSkillNumber(number) {
|
|
471
494
|
this.assertNumbersEnvironment();
|
|
472
495
|
return this.mutation(api.skillNumbers.acquire, this.authArgs({ number: String(number) }));
|
|
@@ -1477,6 +1500,9 @@ export class XiasheClient {
|
|
|
1477
1500
|
|
|
1478
1501
|
async agentRest(pathname, body = {}, options = {}) {
|
|
1479
1502
|
const baseUrl = normalizeBaseUrl(options.restBaseUrl || this.profile.agent?.restBaseUrl || this.profile.actionsUrl);
|
|
1503
|
+
if (!options.runtimeToken && (new URL(baseUrl).origin !== new URL(this.profile.actionsUrl).origin ||
|
|
1504
|
+
(this.profile.agent?.environmentBinding && !sameEnvironment(this.profile.agent.environmentBinding, this.profile))))
|
|
1505
|
+
throw new XiasheError('Environment changed. Reconnect the Agent.', { code: 'AUTH_ENVIRONMENT_MISMATCH' });
|
|
1480
1506
|
const runtimeToken = options.runtimeToken || this.profile.agent?.runtimeToken;
|
|
1481
1507
|
if (!runtimeToken) {
|
|
1482
1508
|
throw new XiasheError('Missing Agent runtime token. Run `xiashe agent connect --token <token>` first.', {
|
|
@@ -1554,6 +1580,10 @@ export class XiasheClient {
|
|
|
1554
1580
|
}
|
|
1555
1581
|
|
|
1556
1582
|
async mcp(method, params = {}, options = {}) {
|
|
1583
|
+
if (!options.runtimeToken && this.profile.agent?.runtimeToken &&
|
|
1584
|
+
(normalizeBaseUrl(options.mcpUrl || this.profile.mcpUrl) !== normalizeBaseUrl(this.profile.mcpUrl) ||
|
|
1585
|
+
!sameEnvironment(this.profile.agent.environmentBinding || this.profile, this.profile)))
|
|
1586
|
+
throw new XiasheError('Environment changed. Reconnect the Agent.', { code: 'AUTH_ENVIRONMENT_MISMATCH' });
|
|
1557
1587
|
return mcpCall(
|
|
1558
1588
|
options.mcpUrl || this.profile.mcpUrl,
|
|
1559
1589
|
options.runtimeToken || this.profile.agent?.runtimeToken || '',
|
|
@@ -1573,7 +1603,7 @@ export class XiasheClient {
|
|
|
1573
1603
|
}
|
|
1574
1604
|
}
|
|
1575
1605
|
|
|
1576
|
-
export async function saveLoginSession(profileName, authResult) {
|
|
1606
|
+
export async function saveLoginSession(profileName, authResult, authenticatedProfile) {
|
|
1577
1607
|
const session = {
|
|
1578
1608
|
userId: authResult.userId,
|
|
1579
1609
|
email: authResult.email,
|
|
@@ -1582,11 +1612,13 @@ export async function saveLoginSession(profileName, authResult) {
|
|
|
1582
1612
|
sessionExpiresAt: authResult.sessionExpiresAt,
|
|
1583
1613
|
defaultChatId: authResult.defaultChatId || null
|
|
1584
1614
|
};
|
|
1615
|
+
const profile = authenticatedProfile || (await loadProfile({ profile: profileName })).profile;
|
|
1616
|
+
session.environmentBinding = environmentBinding(profile);
|
|
1585
1617
|
await updateProfile(profileName, { session });
|
|
1586
1618
|
return session;
|
|
1587
1619
|
}
|
|
1588
1620
|
|
|
1589
|
-
export async function saveAgentSession(profileName, claimResult) {
|
|
1621
|
+
export async function saveAgentSession(profileName, claimResult, authenticatedProfile) {
|
|
1590
1622
|
const agent = {
|
|
1591
1623
|
runtimeToken: claimResult.runtimeToken,
|
|
1592
1624
|
agentConnectionId: claimResult.agentConnectionId,
|
|
@@ -1598,6 +1630,10 @@ export async function saveAgentSession(profileName, claimResult) {
|
|
|
1598
1630
|
restBaseUrl: claimResult.restBaseUrl,
|
|
1599
1631
|
expiresAt: claimResult.expiresAt
|
|
1600
1632
|
};
|
|
1633
|
+
const profile = authenticatedProfile || (await loadProfile({ profile: profileName })).profile;
|
|
1634
|
+
agent.environmentBinding = environmentBinding(profile);
|
|
1635
|
+
if (agent.mcpUrl && new URL(agent.mcpUrl).origin !== new URL(profile.mcpUrl).origin) throw Error('AGENT_ENVIRONMENT_MISMATCH');
|
|
1636
|
+
if (agent.restBaseUrl && new URL(agent.restBaseUrl).origin !== new URL(profile.actionsUrl).origin) throw Error('AGENT_ENVIRONMENT_MISMATCH');
|
|
1601
1637
|
await updateProfile(profileName, { agent });
|
|
1602
1638
|
return agent;
|
|
1603
1639
|
}
|
package/outputSafety.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Output-only filtering. Never transform source files or generated Skill content.
|
|
2
|
+
const secrets = new Set();
|
|
3
|
+
const credentialKey = key => /(?:token|secret|password|authorization|cookie|apikey|uploadurl|downloadurl)$/i.test(key.replace(/[-_]/g, ''));
|
|
4
|
+
export function rememberSecrets(value) {
|
|
5
|
+
if (!value || typeof value !== 'object') return value;
|
|
6
|
+
for (const [key, child] of Object.entries(value)) {
|
|
7
|
+
if (credentialKey(key) && typeof child === 'string' && child) secrets.add(child);
|
|
8
|
+
else if (child && typeof child === 'object') rememberSecrets(child);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function safeOutput(value) {
|
|
13
|
+
rememberSecrets(value);
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
for (const secret of [...secrets].sort((a, b) => b.length - a.length)) value = value.split(secret).join('[REDACTED]');
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) return value.map(safeOutput);
|
|
19
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, credentialKey(key) && child ? '[REDACTED]' : safeOutput(child)]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
package/package.json
CHANGED
package/privateState.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { chmod, lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const missing = error => { if (error.code !== 'ENOENT') throw error; return null; };
|
|
7
|
+
export async function privateDirectory(directory) {
|
|
8
|
+
for (let parent = path.resolve(directory); parent !== path.dirname(parent); parent = path.dirname(parent)) {
|
|
9
|
+
const info = await lstat(parent).catch(missing);
|
|
10
|
+
// macOS exposes its root-owned temporary filesystem through /var and /tmp.
|
|
11
|
+
const systemAlias = process.platform === 'darwin' && ['/var', '/tmp'].includes(parent) && info?.uid === 0 &&
|
|
12
|
+
info.isSymbolicLink() && await realpath(parent) === `/private${parent}`;
|
|
13
|
+
if (info && !systemAlias && (!info.isDirectory() || info.isSymbolicLink())) throw Error('PRIVATE_STATE_UNSAFE_DIRECTORY');
|
|
14
|
+
}
|
|
15
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
16
|
+
const info = await lstat(directory);
|
|
17
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw Error('PRIVATE_STATE_UNSAFE_DIRECTORY');
|
|
18
|
+
await chmod(directory, 0o700);
|
|
19
|
+
}
|
|
20
|
+
export async function checkPrivateFile(file) {
|
|
21
|
+
const info = await lstat(file).catch(missing);
|
|
22
|
+
if (info && (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1)) throw Error('PRIVATE_STATE_UNSAFE_FILE');
|
|
23
|
+
if (info) await chmod(file, 0o600);
|
|
24
|
+
return info;
|
|
25
|
+
}
|
|
26
|
+
// Never steal a lock: a crashed process requires explicit local reconciliation.
|
|
27
|
+
export async function withPrivateLock(file, operation, { timeoutMs = 3000 } = {}) {
|
|
28
|
+
const lock = `${file}.lock`;
|
|
29
|
+
const deadline = Date.now() + timeoutMs;
|
|
30
|
+
let handle;
|
|
31
|
+
while (!handle) {
|
|
32
|
+
try { handle = await open(lock, 'wx', 0o600); }
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== 'EEXIST') throw error;
|
|
35
|
+
if (Date.now() >= deadline) throw Error('PRIVATE_STATE_BUSY: another process or an interrupted operation owns the lock');
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try { return await operation(); }
|
|
40
|
+
finally { await handle.close(); await rm(lock); }
|
|
41
|
+
}
|
|
42
|
+
// Caller holds the lock around the complete read/modify/write transaction.
|
|
43
|
+
export async function atomicPrivateJson(file, value) {
|
|
44
|
+
const before = await checkPrivateFile(file);
|
|
45
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
46
|
+
let handle;
|
|
47
|
+
try {
|
|
48
|
+
handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
49
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
|
|
50
|
+
await handle.sync();
|
|
51
|
+
await handle.close(); handle = null;
|
|
52
|
+
const after = await checkPrivateFile(file);
|
|
53
|
+
if (Boolean(before) !== Boolean(after) || (before && (before.ino !== after.ino || before.dev !== after.dev))) throw Error('PRIVATE_STATE_CHANGED');
|
|
54
|
+
await rename(temporary, file);
|
|
55
|
+
// Atomic replacement is complete. Persist directory metadata where supported.
|
|
56
|
+
if (process.platform !== 'win32') {
|
|
57
|
+
const directory = await open(path.dirname(file), 'r');
|
|
58
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
59
|
+
}
|
|
60
|
+
} finally { if (handle) await handle.close(); await rm(temporary, { force: true }); }
|
|
61
|
+
}
|
package/runtime.mjs
CHANGED
|
@@ -72,7 +72,7 @@ export function runtimePrincipal(client) {
|
|
|
72
72
|
return digest(`${new URL(client.profile.convexUrl).origin}\0${userId}`);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
async function verifyDirectory(directory, files) {
|
|
75
|
+
export async function verifyDirectory(directory, files) {
|
|
76
76
|
const expected = new Map(files.map(file => [file.path, file]));
|
|
77
77
|
let count = 0;
|
|
78
78
|
const walk = async (folder, prefix = '') => {
|