@xmemo/skill 1.1.25

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.
@@ -0,0 +1,236 @@
1
+ import {
2
+ SCRIPT_COMMAND,
3
+ EXIT_CODE,
4
+ exitCodeForHttpStatus,
5
+ exitCodeForErrorCode,
6
+ exitCodeForError,
7
+ } from '../lib/core.mjs';
8
+
9
+ import {
10
+ makeHttpRequest,
11
+ parseJsonResponse,
12
+ extractRequestId,
13
+ extractId,
14
+ extractList,
15
+ apiErrorMessage,
16
+ safeJson,
17
+ sanitizeTerminalText,
18
+ formatMemoryContent,
19
+ } from '../lib/api.mjs';
20
+
21
+ function discoveryString(value) {
22
+ if (typeof value !== 'string') return null;
23
+ const sanitized = sanitizeTerminalText(value).trim();
24
+ return sanitized ? sanitized.slice(0, 200) : null;
25
+ }
26
+
27
+ function discoveryStringList(value, maxItems = 24) {
28
+ if (!Array.isArray(value)) return [];
29
+ return value
30
+ .filter((item) => typeof item === 'string')
31
+ .map(discoveryString)
32
+ .filter(Boolean)
33
+ .slice(0, maxItems);
34
+ }
35
+
36
+ function summarizeDoctorDiscovery(discovery, discoveryUrl) {
37
+ const standalone = discovery?.standalone_skill ?? discovery?.integrations?.standalone_skill ?? {};
38
+ return {
39
+ status: 'available',
40
+ url: discoveryUrl,
41
+ schemaVersion: discoveryString(discovery?.schema_version),
42
+ protocol: discoveryString(discovery?.protocol),
43
+ service: discoveryString(discovery?.service),
44
+ serviceVersion: discoveryString(discovery?.service_version),
45
+ mcpUrl: discoveryString(discovery?.mcp_url),
46
+ supportedClients: discoveryStringList(discovery?.supported_clients),
47
+ standaloneSkill: {
48
+ status: discoveryString(standalone.status),
49
+ runtimeModel: discoveryString(standalone.runtime_model),
50
+ packageVersion: discoveryString(standalone.package?.version),
51
+ operations: discoveryStringList(standalone.operations),
52
+ defaultScopes: discoveryStringList(standalone.auth?.default_scopes),
53
+ },
54
+ };
55
+ }
56
+
57
+ function discoveryFailureCode(error) {
58
+ const message = String(error?.message ?? '').toLowerCase();
59
+ if (message.includes('timed out')) return 'timeout';
60
+ if (message.includes('non-json')) return 'invalid_response';
61
+ return 'request_failed';
62
+ }
63
+
64
+ async function fetchDoctorDiscovery(baseUrl, timeoutMs) {
65
+ const discoveryUrl = new URL('/.well-known/agent-discovery.json', baseUrl).toString();
66
+ try {
67
+ const res = await makeHttpRequest(baseUrl, '/.well-known/agent-discovery.json', 'GET', null, {}, timeoutMs);
68
+ if (res.statusCode < 200 || res.statusCode >= 300) {
69
+ return {
70
+ status: 'unavailable',
71
+ url: discoveryUrl,
72
+ errorCode: 'http_error',
73
+ httpStatus: res.statusCode ?? null,
74
+ };
75
+ }
76
+ return summarizeDoctorDiscovery(parseJsonResponse(res, 'Doctor discovery'), discoveryUrl);
77
+ } catch (error) {
78
+ return {
79
+ status: 'unavailable',
80
+ url: discoveryUrl,
81
+ errorCode: discoveryFailureCode(error),
82
+ };
83
+ }
84
+ }
85
+
86
+ function doctorNextAction({ credential, anonymous }) {
87
+ if (!anonymous && !credential) {
88
+ return {
89
+ command: `${SCRIPT_COMMAND} login --allow-plaintext`,
90
+ reason: 'Sign in before using account-scoped memory operations.',
91
+ };
92
+ }
93
+ return {
94
+ command: `${SCRIPT_COMMAND} auth status --verify`,
95
+ reason: 'Verify the credential separately when an authenticated follow-up is needed.',
96
+ };
97
+ }
98
+
99
+ function withDoctorDiagnostics(data, discovery, nextAction) {
100
+ const report = data && typeof data === 'object' && !Array.isArray(data)
101
+ ? { ...data }
102
+ : { ok: true, result: data };
103
+ return {
104
+ ...report,
105
+ clientDiagnostics: {
106
+ discovery,
107
+ nextAction,
108
+ },
109
+ };
110
+ }
111
+
112
+ export async function handleOps(ctx) {
113
+ const { command, options, flags, credential, token } = ctx;
114
+
115
+ // Doctor can be anonymous
116
+ if (command === 'doctor' && !token) {
117
+ try {
118
+ const discovery = options.json
119
+ ? await fetchDoctorDiscovery(options.baseUrl, options.timeoutMs)
120
+ : null;
121
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
122
+ operation: 'doctor',
123
+ arguments: {}
124
+ }, {}, options.timeoutMs);
125
+ const data = parseJsonResponse(res, 'Doctor health check');
126
+ if (res.statusCode < 200 || res.statusCode >= 300 || data.ok === false) {
127
+ const reqId = extractRequestId(data);
128
+ const reqSuffix = reqId ? ` (request_id: ${reqId})` : '';
129
+ console.error(`Doctor health check failed: ${apiErrorMessage(data, safeJson(data))}${reqSuffix}`);
130
+ process.exit(exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode));
131
+ }
132
+ if (options.json) {
133
+ console.log(safeJson(withDoctorDiagnostics(data, discovery, doctorNextAction({
134
+ credential,
135
+ anonymous: options.anonymous,
136
+ }))));
137
+ } else {
138
+ const authentication = options.anonymous
139
+ ? 'Not checked (anonymous mode)'
140
+ : 'Missing/Unauthenticated';
141
+ const nextStep = options.anonymous
142
+ ? ''
143
+ : `\nNext: ${SCRIPT_COMMAND} login --allow-plaintext`;
144
+ console.log(`XMemo Service Status: OK\nAuthentication: ${authentication}${nextStep}`);
145
+ }
146
+ process.exit(EXIT_CODE.SUCCESS);
147
+ } catch (e) {
148
+ console.error('Doctor health check failed:', e.message);
149
+ process.exit(exitCodeForError(e));
150
+ }
151
+ }
152
+
153
+ // Normalize commands for operations mapping
154
+ let opName = command;
155
+ if (command === 'save-state' || command === 'state-save') opName = 'state-save';
156
+ if (command === 'restore-state' || command === 'state-restore') opName = 'state-restore';
157
+
158
+ try {
159
+ const discovery = command === 'doctor' && options.json
160
+ ? await fetchDoctorDiscovery(options.baseUrl, options.timeoutMs)
161
+ : null;
162
+ // Dispatches operation query to XMemo API over HTTPS with Bearer authorization.
163
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
164
+ operation: opName,
165
+ arguments: flags,
166
+ }, {
167
+ 'Authorization': `Bearer ${token}`
168
+ }, options.timeoutMs);
169
+
170
+ const data = parseJsonResponse(res, `${opName} request`);
171
+ const isDoctorAuthInvalid = opName === 'doctor' && data.result?.auth_valid === false;
172
+ const succeeded = res.statusCode >= 200 && res.statusCode < 300 && data.ok !== false && !isDoctorAuthInvalid;
173
+ if (options.json) {
174
+ const output = opName === 'doctor'
175
+ ? withDoctorDiagnostics(data, discovery, doctorNextAction({ credential, anonymous: false }))
176
+ : data;
177
+ console.log(safeJson(output));
178
+ const failureExitCode = isDoctorAuthInvalid
179
+ ? EXIT_CODE.AUTH_ERROR
180
+ : (exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode));
181
+ process.exit(succeeded ? EXIT_CODE.SUCCESS : failureExitCode);
182
+ }
183
+
184
+ if (!succeeded) {
185
+ const reqId = extractRequestId(data);
186
+ const reqSuffix = reqId ? ` (request_id: ${reqId})` : '';
187
+ console.error(`Error: ${apiErrorMessage(data)} (Code: ${data.error?.code || `HTTP ${res.statusCode}`})${reqSuffix}`);
188
+ const failureExitCode = exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode);
189
+ process.exit(failureExitCode);
190
+ }
191
+
192
+ if (opName === 'doctor') {
193
+ const isValid = !!data.result?.auth_valid;
194
+ if (isValid) {
195
+ console.log(`XMemo Service Status: OK\nAuthentication: Valid\nScopes: ${extractList(data.result?.scopes).join(', ')}`);
196
+ } else {
197
+ console.log(`XMemo Service Status: OK\nAuthentication: Invalid`);
198
+ process.exit(EXIT_CODE.AUTH_ERROR);
199
+ }
200
+ } else if (opName === 'todo-list') {
201
+ const todos = extractList(data.result);
202
+ if (todos.length === 0) {
203
+ console.log('No TODOs found.');
204
+ } else {
205
+ todos.forEach((todo) => {
206
+ console.log(`- [${todo?.status === 'done' ? 'x' : ' '}] ${sanitizeTerminalText(todo?.content || '')} (ID: ${sanitizeTerminalText(todo?.id || todo?.memory_id || '(unknown)')})`);
207
+ });
208
+ }
209
+ } else if (opName === 'state-restore') {
210
+ const state = data.result;
211
+ if (!state || typeof state !== 'object') {
212
+ console.log('No saved working state found for the requested key.');
213
+ } else {
214
+ const stateKey = state.state_key || flags.key || flags.state_key || '(unknown)';
215
+ const content = state.content === undefined || state.content === null || state.content === ''
216
+ ? '(empty)'
217
+ : state.content;
218
+ console.log(`Working State restored:\nKey: ${sanitizeTerminalText(stateKey)}\nContent: ${formatMemoryContent(content, false)}`);
219
+ }
220
+ } else if (opName === 'expense-add') {
221
+ console.log(`✅ Expense recorded.\nID: ${sanitizeTerminalText(extractId(data.result))}`);
222
+ } else if (opName === 'todo-add') {
223
+ const id = extractId(data.result);
224
+ console.log(`✅ TODO added.${id ? `\nID: ${sanitizeTerminalText(id)}` : ''}`);
225
+ } else if (opName === 'todo-done') {
226
+ const id = flags.id || flags.todo_id || extractId(data.result);
227
+ console.log(`✅ TODO completed.${id ? `\nID: ${sanitizeTerminalText(id)}` : ''}`);
228
+ } else {
229
+ console.log(`✅ Operation succeeded.`);
230
+ }
231
+ process.exit(EXIT_CODE.SUCCESS);
232
+ } catch (e) {
233
+ console.error('Request failed:', e.message);
234
+ process.exit(exitCodeForError(e));
235
+ }
236
+ }
@@ -0,0 +1,311 @@
1
+ import https from 'node:https';
2
+ import http from 'node:http';
3
+ import {
4
+ DEFAULT_BASE_URL,
5
+ DEFAULT_TIMEOUT_MS,
6
+ MAX_RESPONSE_BYTES,
7
+ DEFAULT_TEMPORARY_LIMITS,
8
+ EXIT_CODE,
9
+ exitCodeForHttpStatus,
10
+ exitCodeForErrorCode,
11
+ exitCodeForError,
12
+ } from './core.mjs';
13
+ import { configureEgressRequest, sanitizeSensitiveValue } from './openclaw-egress.mjs';
14
+
15
+ export const warnedCredentialOrigins = new Set();
16
+
17
+ export function redactSensitiveResponse(value) {
18
+ if (value === null || typeof value !== 'object') {
19
+ return sanitizeSensitiveValue(value);
20
+ }
21
+ if (Array.isArray(value)) return value.map(redactSensitiveResponse);
22
+ const sensitiveKeys = new Set([
23
+ 'access_token', 'refresh_token', 'id_token', 'temporary_token', 'formal_token',
24
+ 'confirmation_token', 'pending_confirmation_token', 'device_code', 'token',
25
+ 'authorization', 'api_key', 'apikey', 'cookie', 'set-cookie',
26
+ ]);
27
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
28
+ key,
29
+ sensitiveKeys.has(key.toLowerCase()) ? '[REDACTED]' : redactSensitiveResponse(item),
30
+ ]));
31
+ }
32
+
33
+ export function safeJson(value) {
34
+ return JSON.stringify(redactSensitiveResponse(value));
35
+ }
36
+
37
+ export function sanitizeTerminalText(value) {
38
+ return String(value ?? '')
39
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
40
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
41
+ }
42
+
43
+ export function formatMemoryContent(content, compact) {
44
+ const value = sanitizeTerminalText(content);
45
+ const rendered = compact ? value.replace(/\s+/g, ' ').trim() : value;
46
+ const limit = compact ? 280 : 2_000;
47
+ return rendered.length > limit ? `${rendered.slice(0, limit)}… (truncated)` : rendered;
48
+ }
49
+
50
+ export function formatDuration(seconds) {
51
+ if (seconds % 86_400 === 0) return `${seconds / 86_400} days`;
52
+ if (seconds % 3_600 === 0) return `${seconds / 3_600} hours`;
53
+ return `${seconds} seconds`;
54
+ }
55
+
56
+ export function parseJsonResponse(res, context) {
57
+ const body = typeof res.body === 'string' ? res.body.trim() : '';
58
+ if (!body) {
59
+ const error = new Error(`${context}: server returned an empty response (HTTP ${res.statusCode}).`);
60
+ error.statusCode = res.statusCode;
61
+ throw error;
62
+ }
63
+ try {
64
+ return JSON.parse(body);
65
+ } catch {
66
+ const safeBody = sanitizeTerminalText(body);
67
+ const preview = safeBody.length > 2_000 ? `${safeBody.slice(0, 2_000)}…` : safeBody;
68
+ const error = new Error(`${context}: server returned a non-JSON response (HTTP ${res.statusCode}): ${preview}`);
69
+ error.statusCode = res.statusCode;
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ export function extractList(result) {
75
+ if (Array.isArray(result)) return result;
76
+ if (Array.isArray(result?.results)) return result.results;
77
+ if (Array.isArray(result?.todos)) return result.todos;
78
+ if (Array.isArray(result?.reminders)) return result.reminders;
79
+ return [];
80
+ }
81
+
82
+ export function extractId(result) {
83
+ if (typeof result === 'string') return result;
84
+ if (result?.id) return result.id;
85
+ if (result?.memory_id) return result.memory_id;
86
+ return JSON.stringify(result) ?? String(result ?? '');
87
+ }
88
+
89
+ export function apiErrorMessage(data, fallback = 'Operation failed') {
90
+ const candidate = data?.error?.message || data?.error_description || data?.detail || data?.error;
91
+ if (typeof candidate === 'string') return sanitizeTerminalText(candidate);
92
+ if (candidate !== undefined && candidate !== null) return safeJson(candidate);
93
+ return fallback;
94
+ }
95
+
96
+ export function extractRequestId(data) {
97
+ if (!data || typeof data !== 'object') return null;
98
+ const candidate = data.error?.request_id || data.request_id;
99
+ if (typeof candidate === 'string' && candidate.trim()) {
100
+ return sanitizeTerminalText(candidate.trim());
101
+ }
102
+ return null;
103
+ }
104
+
105
+ export function extractExpiresInSeconds(data) {
106
+ if (data?.expires_in !== undefined && data?.expires_in !== null) {
107
+ const num = Number(data.expires_in);
108
+ if (Number.isFinite(num) && num > 0) return num;
109
+ }
110
+ const raw = data?.expires;
111
+ const exp = (typeof raw === 'number' || typeof raw === 'string') ? Number(raw) : NaN;
112
+ if (Number.isFinite(exp) && exp > 0) {
113
+ if (exp > 1e11) return Math.max(1, Math.round((exp - Date.now()) / 1000));
114
+ if (exp > 1e8) return Math.max(1, Math.round(exp - Date.now() / 1000));
115
+ return exp;
116
+ }
117
+ if (typeof raw === 'string') {
118
+ const parsedDate = Date.parse(raw);
119
+ if (Number.isFinite(parsedDate) && parsedDate > Date.now()) {
120
+ return Math.max(1, Math.round((parsedDate - Date.now()) / 1000));
121
+ }
122
+ }
123
+ return 600;
124
+ }
125
+
126
+ export function formatRemainingValidity(seconds) {
127
+ const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
128
+ const hours = Math.floor(totalSeconds / 3600);
129
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
130
+ const secs = totalSeconds % 60;
131
+ if (hours > 0) {
132
+ return `${hours}h${minutes}m${secs}s`;
133
+ }
134
+ if (minutes > 0) {
135
+ return `${minutes}m${secs}s`;
136
+ }
137
+ return `${secs}s`;
138
+ }
139
+
140
+ export function outputRestError(code, message, options, dataOrRequestId, explicitExitCode = null) {
141
+ const reqId = typeof dataOrRequestId === 'string'
142
+ ? sanitizeTerminalText(dataOrRequestId.trim())
143
+ : extractRequestId(dataOrRequestId);
144
+ if (options && options.json) {
145
+ const errorObj = { code, message };
146
+ if (reqId) {
147
+ errorObj.request_id = reqId;
148
+ }
149
+ console.log(safeJson({ ok: false, error: errorObj }));
150
+ } else {
151
+ const reqSuffix = reqId ? ` (request_id: ${reqId})` : '';
152
+ console.error(`Error: ${message} (Code: ${code})${reqSuffix}`);
153
+ }
154
+ const resolvedExitCode = explicitExitCode !== null
155
+ ? explicitExitCode
156
+ : (exitCodeForErrorCode(code) ?? EXIT_CODE.USER_ERROR);
157
+ process.exit(resolvedExitCode);
158
+ }
159
+
160
+ export function outputContentTooLarge(message, options) {
161
+ if (options && options.json) {
162
+ console.log(safeJson({ ok: false, error: { code: 'content_too_large', message } }));
163
+ } else {
164
+ console.error(`Error: ${message}`);
165
+ }
166
+ process.exit(EXIT_CODE.USER_ERROR);
167
+ }
168
+
169
+ export function outputJsonFailure(data, statusCode) {
170
+ const reqId = extractRequestId(data);
171
+ let payload;
172
+ if (data && typeof data === 'object' && data.ok === false && data.error && typeof data.error === 'object') {
173
+ const errorObj = { ...data.error };
174
+ if (reqId && !errorObj.request_id) errorObj.request_id = reqId;
175
+ payload = { ...data, ok: false, error: errorObj };
176
+ } else {
177
+ const code = data?.error?.code || (Number(statusCode) === 400 ? 'invalid_request' : `HTTP ${statusCode}`);
178
+ const errorObj = { code, message: apiErrorMessage(data) };
179
+ if (reqId) errorObj.request_id = reqId;
180
+ payload = { ok: false, error: errorObj };
181
+ }
182
+ console.log(safeJson(payload));
183
+ process.exit(exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(statusCode));
184
+ }
185
+
186
+ export function handleRestError(res, { notFoundMessage, context = 'REST request', options }) {
187
+ if (res.statusCode === 401 || res.statusCode === 403) {
188
+ let errData = null;
189
+ try { errData = parseJsonResponse(res, context); } catch {}
190
+ const code = errData?.error?.code || (res.statusCode === 401 ? 'unauthorized' : 'forbidden');
191
+ const defaultMsg = res.statusCode === 401
192
+ ? 'Authentication required or token invalid.'
193
+ : 'Access denied. Re-authorization is required to explicitly grant the required scope.';
194
+ let msg = apiErrorMessage(errData, defaultMsg);
195
+ if (res.statusCode === 403 && !/re-?authorization/i.test(msg)) {
196
+ msg = `${msg.replace(/\.*$/, '')}. Re-authorization is required to explicitly grant the required scope.`;
197
+ }
198
+ outputRestError(code, msg, options, errData, EXIT_CODE.AUTH_ERROR);
199
+ }
200
+
201
+ if (res.statusCode === 404) {
202
+ let errData = null;
203
+ try { errData = parseJsonResponse(res, context); } catch {}
204
+ const code = 'not_found';
205
+ const msg = apiErrorMessage(errData, notFoundMessage || 'Resource not found.');
206
+ outputRestError(code, msg, options, errData, EXIT_CODE.USER_ERROR);
207
+ }
208
+
209
+ const data = parseJsonResponse(res, context);
210
+ if (res.statusCode < 200 || res.statusCode >= 300 || data.ok === false) {
211
+ const code = data?.error?.code || (res.statusCode === 400 ? 'invalid_request' : `HTTP ${res.statusCode}`);
212
+ const msg = apiErrorMessage(data);
213
+ const exitCode = exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode);
214
+ outputRestError(code, msg, options, data, exitCode);
215
+ }
216
+ return data;
217
+ }
218
+
219
+ // HTTP request helper
220
+ export function makeHttpRequest(baseUrl, apiPath, method, body = null, headers = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
221
+ return new Promise((resolve, reject) => {
222
+ try {
223
+ const url = new URL(apiPath, baseUrl);
224
+ const client = url.protocol === 'https:' ? https : http;
225
+ const bodyStr = body ? JSON.stringify(body) : null;
226
+ const reqHeaders = {
227
+ 'Content-Type': 'application/json',
228
+ ...headers,
229
+ };
230
+ if (bodyStr) {
231
+ reqHeaders['Content-Length'] = Buffer.byteLength(bodyStr);
232
+ }
233
+ const options = {
234
+ method: method.toUpperCase(),
235
+ headers: reqHeaders,
236
+ };
237
+ const authorizationHeader = Object.entries(reqHeaders)
238
+ .find(([key]) => key.toLowerCase() === 'authorization')?.[1];
239
+ configureEgressRequest(options, authorizationHeader, url);
240
+ if (authorizationHeader && url.origin !== new URL(DEFAULT_BASE_URL).origin && !warnedCredentialOrigins.has(url.origin)) {
241
+ warnedCredentialOrigins.add(url.origin);
242
+ console.error(`⚠️ Sending an XMemo credential to custom origin ${url.origin}. Continue only if this host is trusted.`);
243
+ }
244
+
245
+ let settled = false;
246
+ const settleResolve = (value) => {
247
+ if (settled) return;
248
+ settled = true;
249
+ resolve(value);
250
+ };
251
+ const settleReject = (error) => {
252
+ if (settled) return;
253
+ settled = true;
254
+ reject(error);
255
+ };
256
+ const req = client.request(url, options, (res) => {
257
+ let data = '';
258
+ let responseBytes = 0;
259
+ res.on('data', (chunk) => {
260
+ responseBytes += Buffer.byteLength(chunk);
261
+ if (responseBytes > MAX_RESPONSE_BYTES) {
262
+ const error = new Error(`Server response exceeded the ${MAX_RESPONSE_BYTES}-byte safety limit.`);
263
+ settleReject(error);
264
+ res.destroy();
265
+ return;
266
+ }
267
+ data += chunk;
268
+ });
269
+ res.on('end', () => {
270
+ settleResolve({
271
+ statusCode: res.statusCode,
272
+ headers: res.headers,
273
+ body: data,
274
+ });
275
+ });
276
+ res.on('error', settleReject);
277
+ res.on('aborted', () => settleReject(new Error('Server response was interrupted.')));
278
+ });
279
+ req.setTimeout(timeoutMs, () => {
280
+ req.destroy(new Error(`Request timed out after ${timeoutMs} ms.`));
281
+ });
282
+ req.on('error', settleReject);
283
+ if (bodyStr) {
284
+ req.write(bodyStr);
285
+ }
286
+ req.end();
287
+ } catch (e) {
288
+ reject(e);
289
+ }
290
+ });
291
+ }
292
+
293
+ export async function fetchTemporaryLimits(baseUrl, timeoutMs) {
294
+ try {
295
+ const res = await makeHttpRequest(baseUrl, '/.well-known/xmemo-agent.json', 'GET', null, {}, timeoutMs);
296
+ if (res.statusCode < 200 || res.statusCode >= 300) return { ...DEFAULT_TEMPORARY_LIMITS };
297
+ const data = parseJsonResponse(res, 'Temporary-memory policy discovery');
298
+ const limits = data?.temporary_token?.limits;
299
+ const max_items = Number(limits?.max_items);
300
+ const ttl_seconds = Number(limits?.ttl_seconds);
301
+ const max_lifetime_seconds = Number(limits?.max_lifetime_seconds);
302
+ if (![max_items, ttl_seconds, max_lifetime_seconds].every(Number.isSafeInteger)
303
+ || max_items <= 0 || ttl_seconds <= 0 || max_lifetime_seconds <= 0) {
304
+ return { ...DEFAULT_TEMPORARY_LIMITS };
305
+ }
306
+ return { max_items, ttl_seconds, max_lifetime_seconds };
307
+ } catch {
308
+ // Discovery must not make an otherwise available registration endpoint unusable.
309
+ return { ...DEFAULT_TEMPORARY_LIMITS };
310
+ }
311
+ }