@lazyingart/agent-web 0.1.40
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 +22 -0
- package/README.md +438 -0
- package/docs/architecture.md +503 -0
- package/package.json +43 -0
- package/src/aginti-adapter.js +602 -0
- package/src/chat-context.js +1020 -0
- package/src/chat-migrations.js +947 -0
- package/src/chat-store.js +3308 -0
- package/src/cli.js +134 -0
- package/src/cloud-server.js +2043 -0
- package/src/contracts.js +103 -0
- package/src/deterministic-context-summarizer.js +254 -0
- package/src/direct-chat-capability-limits.js +66 -0
- package/src/direct-chat-contract.js +3 -0
- package/src/errors.js +50 -0
- package/src/http-contract.js +592 -0
- package/src/index.js +88 -0
- package/src/localllm-connector.js +667 -0
- package/src/migrations.js +231 -0
- package/src/operator-health.js +184 -0
- package/src/password-verifier.js +131 -0
- package/src/service-config.js +547 -0
- package/src/service.js +408 -0
- package/src/sqlite-health.js +83 -0
- package/src/storage-path.js +130 -0
- package/src/store.js +914 -0
- package/src/validation.js +181 -0
- package/src/vision-attachment.js +404 -0
- package/src/web/aginti-client.js +552 -0
- package/src/web/aginti-protocol.js +1146 -0
- package/src/web/asset-map.js +462 -0
- package/src/web/browser-app.js +6491 -0
- package/src/web/cloud-session-client.js +427 -0
- package/src/web/direct-chat-client.js +1482 -0
- package/src/web/index.js +10 -0
- package/src/web/presentation-state.js +107 -0
- package/src/web/pwa-assets.js +854 -0
- package/src/web/pwa-update-handoff-store.js +179 -0
- package/src/web/safe-rendering.js +836 -0
- package/src/web/vision-image-client.js +546 -0
- package/src/web/vision-image-sanitizer.js +168 -0
- package/src/web/web-release.js +28 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertBoundedString,
|
|
3
|
+
assertExactKeys,
|
|
4
|
+
assertIdentifier,
|
|
5
|
+
assertInteger
|
|
6
|
+
} from './validation.js';
|
|
7
|
+
import {
|
|
8
|
+
AGINTI_RPC_PATHS,
|
|
9
|
+
rpcPathIsMutation,
|
|
10
|
+
validateAgentRequest,
|
|
11
|
+
validateIdempotencyKey
|
|
12
|
+
} from './web/aginti-protocol.js';
|
|
13
|
+
import { verifyStandaloneAssetMap } from './web/asset-map.js';
|
|
14
|
+
import {
|
|
15
|
+
validateVisionAttachmentRequest,
|
|
16
|
+
validateVisionAttachmentsRequest
|
|
17
|
+
} from './vision-attachment.js';
|
|
18
|
+
import { WEB_RELEASE_HEADER_NAME } from './web/web-release.js';
|
|
19
|
+
|
|
20
|
+
const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,95}$/u;
|
|
21
|
+
const CONTENT_TYPE_PATTERN = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:; charset=utf-8)?$/u;
|
|
22
|
+
const HEADER_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
23
|
+
const IDEMPOTENCY_PATTERN = /^[A-Za-z0-9._~-]{16,160}$/u;
|
|
24
|
+
const EVENT_HASH_PATTERN = /^[a-f0-9]{64}$/u;
|
|
25
|
+
const CONTENT_DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
26
|
+
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
27
|
+
const UNSAFE_MESSAGE_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
28
|
+
const ENCODED_PATH_PATTERN = /%(?:2e|2f|5c)/iu;
|
|
29
|
+
const ARTIFACT_CONTENT_TARGET_PATTERN = /^\/api\/agent\/artifacts\/(art_[A-Za-z0-9_-]{32,86})\/content(?:\?v=([A-Za-z0-9][A-Za-z0-9._~-]{0,95})(?:&download=(1))?)?$/u;
|
|
30
|
+
|
|
31
|
+
export const CLOUD_HTTP_LIMITS = Object.freeze({
|
|
32
|
+
requestTargetBytes: 2_048,
|
|
33
|
+
cookieBytes: 4_096,
|
|
34
|
+
loginBodyBytes: 2_048,
|
|
35
|
+
sessionBodyBytes: 64,
|
|
36
|
+
chatBodyBytes: 72 * 1024,
|
|
37
|
+
visionChatBodyBytes: 24 * 1024 * 1024,
|
|
38
|
+
agentBodyBytes: 64 * 1024,
|
|
39
|
+
responseJsonBytes: 512 * 1024,
|
|
40
|
+
connectorDeltaBytes: 16 * 1024,
|
|
41
|
+
connectorOutputBytes: 64 * 1024,
|
|
42
|
+
bodyTimeoutMs: 5_000,
|
|
43
|
+
visionBodyTimeoutMs: 240_000,
|
|
44
|
+
dependencyTimeoutMs: 30_000,
|
|
45
|
+
jobTimeoutMs: 120_000,
|
|
46
|
+
visionJobTimeoutMs: 600_000,
|
|
47
|
+
sseLifetimeMs: 30_000,
|
|
48
|
+
ssePollMs: 100,
|
|
49
|
+
concurrentBodies: 64,
|
|
50
|
+
concurrentBodiesPerSource: 4,
|
|
51
|
+
concurrentLogins: 8,
|
|
52
|
+
concurrentLoginsPerSource: 2,
|
|
53
|
+
concurrentStreams: 8,
|
|
54
|
+
concurrentStreamsPerSession: 2,
|
|
55
|
+
loginAttemptsPerMinute: 6,
|
|
56
|
+
directChatJobs: 1
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const SESSION_COOKIE_NAME = '__Host-lazying_session';
|
|
60
|
+
export const CSRF_COOKIE_NAME = '__Host-lazying_csrf';
|
|
61
|
+
export const CSRF_HEADER_NAME = 'x-csrf-token';
|
|
62
|
+
export const IDEMPOTENCY_HEADER_NAME = 'idempotency-key';
|
|
63
|
+
export const CLIENT_RELEASE_HEADER_NAME = WEB_RELEASE_HEADER_NAME;
|
|
64
|
+
// The standalone listener is loopback-only. Caddy must delete caller values and
|
|
65
|
+
// overwrite both assertions on every upstream request; they are never accepted
|
|
66
|
+
// from a non-loopback socket peer.
|
|
67
|
+
export const TRUSTED_CLIENT_ADDRESS_HEADER = 'x-lazying-client-address';
|
|
68
|
+
export const TRUSTED_PUBLIC_AUTHORITY_HEADER = 'x-lazying-public-authority';
|
|
69
|
+
|
|
70
|
+
export const CLOUD_ROUTES = Object.freeze({
|
|
71
|
+
login: '/api/login',
|
|
72
|
+
session: '/api/session',
|
|
73
|
+
logout: '/api/logout',
|
|
74
|
+
chatCapabilities: '/api/chat/capabilities',
|
|
75
|
+
chatThreadsList: '/api/chat/threads/list',
|
|
76
|
+
chatThreadsCreate: '/api/chat/threads/create',
|
|
77
|
+
chatThreadsGet: '/api/chat/threads/get',
|
|
78
|
+
chatThreadsDelete: '/api/chat/threads/delete',
|
|
79
|
+
chatMessagesList: '/api/chat/messages/list',
|
|
80
|
+
chatAttachmentsGet: '/api/chat/attachments/get',
|
|
81
|
+
chatRunsStart: '/api/chat/runs/start',
|
|
82
|
+
chatRunsStatus: '/api/chat/runs/status',
|
|
83
|
+
chatRunsEvents: '/api/chat/runs/events',
|
|
84
|
+
chatRunsCancel: '/api/chat/runs/cancel'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const CHAT_POST_ROUTES = Object.freeze([
|
|
88
|
+
CLOUD_ROUTES.chatCapabilities,
|
|
89
|
+
CLOUD_ROUTES.chatThreadsList,
|
|
90
|
+
CLOUD_ROUTES.chatThreadsCreate,
|
|
91
|
+
CLOUD_ROUTES.chatThreadsGet,
|
|
92
|
+
CLOUD_ROUTES.chatThreadsDelete,
|
|
93
|
+
CLOUD_ROUTES.chatMessagesList,
|
|
94
|
+
CLOUD_ROUTES.chatAttachmentsGet,
|
|
95
|
+
CLOUD_ROUTES.chatRunsStart,
|
|
96
|
+
CLOUD_ROUTES.chatRunsStatus,
|
|
97
|
+
CLOUD_ROUTES.chatRunsEvents,
|
|
98
|
+
CLOUD_ROUTES.chatRunsCancel
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
export const CHAT_MUTATION_ROUTES = Object.freeze([
|
|
102
|
+
CLOUD_ROUTES.chatThreadsCreate,
|
|
103
|
+
CLOUD_ROUTES.chatThreadsDelete,
|
|
104
|
+
CLOUD_ROUTES.chatRunsStart,
|
|
105
|
+
CLOUD_ROUTES.chatRunsCancel
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
export const AGENT_TRANSPORT_PREFIX = '/api/transport';
|
|
109
|
+
export const AGENT_ARTIFACT_CONTENT_PREFIX = '/api/agent/artifacts/';
|
|
110
|
+
export const AGENT_ROUTE_MAP = Object.freeze(Object.fromEntries(
|
|
111
|
+
Object.values(AGINTI_RPC_PATHS).map((nativePath) => [`${AGENT_TRANSPORT_PREFIX}${nativePath}`, nativePath])
|
|
112
|
+
));
|
|
113
|
+
|
|
114
|
+
const DYNAMIC_POST_ROUTES = new Set([
|
|
115
|
+
CLOUD_ROUTES.login,
|
|
116
|
+
CLOUD_ROUTES.session,
|
|
117
|
+
CLOUD_ROUTES.logout,
|
|
118
|
+
...CHAT_POST_ROUTES,
|
|
119
|
+
...Object.keys(AGENT_ROUTE_MAP)
|
|
120
|
+
]);
|
|
121
|
+
const CHAT_MUTATIONS = new Set(CHAT_MUTATION_ROUTES);
|
|
122
|
+
|
|
123
|
+
export class CloudHttpError extends Error {
|
|
124
|
+
constructor(status, code, message, { retryAfter, cause } = {}) {
|
|
125
|
+
super(message, { cause });
|
|
126
|
+
this.name = 'CloudHttpError';
|
|
127
|
+
this.status = status;
|
|
128
|
+
this.code = code;
|
|
129
|
+
this.retryAfter = retryAfter;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalid(message = 'The request is invalid.') {
|
|
134
|
+
throw new CloudHttpError(400, 'invalid_request', message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function utf8Bytes(value) {
|
|
138
|
+
return Buffer.byteLength(value, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function exactObject(value, required, optional, name) {
|
|
142
|
+
try {
|
|
143
|
+
return assertExactKeys(value, { required, optional }, name);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new CloudHttpError(400, 'invalid_request', `${name} is invalid.`, { cause: error });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function identifier(value, name) {
|
|
150
|
+
try {
|
|
151
|
+
return assertIdentifier(value, name);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new CloudHttpError(400, 'invalid_request', `${name} is invalid.`, { cause: error });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function integer(value, name, limits) {
|
|
158
|
+
try {
|
|
159
|
+
return assertInteger(value, name, limits);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new CloudHttpError(400, 'invalid_request', `${name} is invalid.`, { cause: error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function text(value, name, {
|
|
166
|
+
minimum = 1,
|
|
167
|
+
maximum,
|
|
168
|
+
trim = false,
|
|
169
|
+
allowControl = false,
|
|
170
|
+
allowMessageFormatting = false
|
|
171
|
+
} = {}) {
|
|
172
|
+
if (typeof value !== 'string' || utf8Bytes(value) < minimum || utf8Bytes(value) > maximum) {
|
|
173
|
+
invalid(`${name} is invalid.`);
|
|
174
|
+
}
|
|
175
|
+
if (!allowControl && (allowMessageFormatting
|
|
176
|
+
? UNSAFE_MESSAGE_CONTROL_PATTERN.test(value)
|
|
177
|
+
: CONTROL_PATTERN.test(value))) invalid(`${name} is invalid.`);
|
|
178
|
+
const result = trim ? value.trim() : value;
|
|
179
|
+
if (trim && utf8Bytes(result) < minimum) invalid(`${name} is invalid.`);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function cursor(revision, hash) {
|
|
184
|
+
const checkedRevision = integer(revision, 'expectedRevision', { min: 0, max: 2_000 });
|
|
185
|
+
if ((checkedRevision === 0 && hash !== null) || (checkedRevision > 0 && !EVENT_HASH_PATTERN.test(hash))) {
|
|
186
|
+
invalid('The ledger cursor is invalid.');
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({ revision: checkedRevision, hash });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function validateReleaseId(value) {
|
|
192
|
+
if (typeof value !== 'string' || !RELEASE_ID_PATTERN.test(value)) {
|
|
193
|
+
throw new TypeError('releaseId must be a portable immutable release identifier');
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function validatePublicOrigin(value) {
|
|
199
|
+
if (typeof value !== 'string') throw new TypeError('publicOrigin is required');
|
|
200
|
+
const url = new URL(value);
|
|
201
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
202
|
+
throw new TypeError('publicOrigin must be an HTTPS origin without credentials, path, query, or fragment');
|
|
203
|
+
}
|
|
204
|
+
return url.origin;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function validateAccountConfig(value) {
|
|
208
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
209
|
+
throw new TypeError('account must be an object');
|
|
210
|
+
}
|
|
211
|
+
const keys = Object.keys(value);
|
|
212
|
+
if (keys.length !== 2 || !Object.hasOwn(value, 'username') || !Object.hasOwn(value, 'principalId')) {
|
|
213
|
+
throw new TypeError('account must contain only username and principalId');
|
|
214
|
+
}
|
|
215
|
+
let username;
|
|
216
|
+
let principalId;
|
|
217
|
+
try {
|
|
218
|
+
username = assertBoundedString(value.username, 'username', { min: 1, max: 128 });
|
|
219
|
+
principalId = assertIdentifier(value.principalId, 'principalId');
|
|
220
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/u.test(principalId)) {
|
|
221
|
+
throw new TypeError('principalId must match the frozen LazyEdge opaque principal contract');
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new TypeError('account is invalid', { cause: error });
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze({ username, principalId });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateDescriptor(route, descriptor) {
|
|
230
|
+
if (descriptor === null || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
|
|
231
|
+
throw new TypeError(`asset ${route} has no descriptor`);
|
|
232
|
+
}
|
|
233
|
+
if (typeof descriptor.contentType !== 'string' || !CONTENT_TYPE_PATTERN.test(descriptor.contentType.toLowerCase())) {
|
|
234
|
+
throw new TypeError(`asset ${route} has an invalid content type`);
|
|
235
|
+
}
|
|
236
|
+
if (typeof descriptor.cacheControl !== 'string' || descriptor.cacheControl.length > 160 || /[\r\n\u0000]/u.test(descriptor.cacheControl)) {
|
|
237
|
+
throw new TypeError(`asset ${route} has an invalid cache policy`);
|
|
238
|
+
}
|
|
239
|
+
const body = typeof descriptor.body === 'string'
|
|
240
|
+
? Buffer.from(descriptor.body, 'utf8')
|
|
241
|
+
: (descriptor.body instanceof Uint8Array ? Buffer.from(descriptor.body) : null);
|
|
242
|
+
if (!body || body.byteLength < 1 || body.byteLength > 4 * 1024 * 1024) {
|
|
243
|
+
throw new TypeError(`asset ${route} has an invalid body`);
|
|
244
|
+
}
|
|
245
|
+
const sourceHeaders = descriptor.headers ?? {};
|
|
246
|
+
if (sourceHeaders === null || typeof sourceHeaders !== 'object' || Array.isArray(sourceHeaders)) {
|
|
247
|
+
throw new TypeError(`asset ${route} has invalid headers`);
|
|
248
|
+
}
|
|
249
|
+
const headers = {};
|
|
250
|
+
for (const [rawName, headerValue] of Object.entries(sourceHeaders)) {
|
|
251
|
+
const name = rawName.toLowerCase();
|
|
252
|
+
if (!HEADER_NAME_PATTERN.test(name) || typeof headerValue !== 'string' || /[\r\n\u0000]/u.test(headerValue)) {
|
|
253
|
+
throw new TypeError(`asset ${route} has an invalid header`);
|
|
254
|
+
}
|
|
255
|
+
if (['set-cookie', 'content-length', 'content-type', 'cache-control', 'connection', 'transfer-encoding'].includes(name)) {
|
|
256
|
+
throw new TypeError(`asset ${route} tries to control a reserved response header`);
|
|
257
|
+
}
|
|
258
|
+
headers[name] = headerValue;
|
|
259
|
+
}
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
contentType: descriptor.contentType,
|
|
262
|
+
cacheControl: descriptor.cacheControl,
|
|
263
|
+
headers: Object.freeze(headers),
|
|
264
|
+
body
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function validateStaticTarget(target) {
|
|
269
|
+
if (typeof target !== 'string' || target.length < 1 || utf8Bytes(target) > CLOUD_HTTP_LIMITS.requestTargetBytes) {
|
|
270
|
+
throw new TypeError('asset route is invalid');
|
|
271
|
+
}
|
|
272
|
+
if (!target.startsWith('/') || target.includes('#') || target.includes('\\') || CONTROL_PATTERN.test(target) || ENCODED_PATH_PATTERN.test(target)) {
|
|
273
|
+
throw new TypeError(`asset route ${target} is not normalized`);
|
|
274
|
+
}
|
|
275
|
+
const [pathname, ...queryParts] = target.split('?');
|
|
276
|
+
if (queryParts.length > 1 || pathname.includes('//') || (pathname !== '/' && pathname.endsWith('/'))
|
|
277
|
+
|| pathname.split('/').some((part) => part === '.' || part === '..')) {
|
|
278
|
+
throw new TypeError(`asset route ${target} is not normalized`);
|
|
279
|
+
}
|
|
280
|
+
if (queryParts.length === 1 && (!queryParts[0] || /[^A-Za-z0-9._~=&-]/u.test(queryParts[0]))) {
|
|
281
|
+
throw new TypeError(`asset route ${target} has an unsafe query`);
|
|
282
|
+
}
|
|
283
|
+
return { pathname, hasQuery: queryParts.length === 1 };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sourceProvesRelease(source, releaseId, kind) {
|
|
287
|
+
const escaped = releaseId.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
288
|
+
if (kind === 'html') {
|
|
289
|
+
return new RegExp(`<meta\\s+name=["']lazying-agent-release["']\\s+content=["']${escaped}["']`, 'u').test(source)
|
|
290
|
+
|| new RegExp(`<meta\\s+content=["']${escaped}["']\\s+name=["']lazying-agent-release["']`, 'u').test(source);
|
|
291
|
+
}
|
|
292
|
+
return new RegExp(`(?:const\\s+VERSION\\s*=|release(?:Id|Version)\\s*:)\\s*["']${escaped}["']`, 'u').test(source);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function snapshotAndValidateAssetMap(assetMap, releaseIdInput) {
|
|
296
|
+
const releaseId = validateReleaseId(releaseIdInput);
|
|
297
|
+
verifyStandaloneAssetMap(assetMap);
|
|
298
|
+
if (!assetMap || !Array.isArray(assetMap.routes) || typeof assetMap.get !== 'function') {
|
|
299
|
+
throw new TypeError('assetMap must provide exact routes and get()');
|
|
300
|
+
}
|
|
301
|
+
const declaredRelease = assetMap.releaseId ?? assetMap.releaseVersion;
|
|
302
|
+
if (declaredRelease !== releaseId) throw new TypeError('assetMap release does not match releaseId');
|
|
303
|
+
if (typeof assetMap.contentDigest !== 'string' || !CONTENT_DIGEST_PATTERN.test(assetMap.contentDigest)
|
|
304
|
+
|| !declaredRelease.includes(assetMap.contentDigest)) {
|
|
305
|
+
throw new TypeError('assetMap release must contain its full content-derived SHA-256 digest');
|
|
306
|
+
}
|
|
307
|
+
if (typeof assetMap.releaseNamespace !== 'string'
|
|
308
|
+
|| !assetMap.releaseNamespace.endsWith(`/assets/r/${releaseId}`)
|
|
309
|
+
|| assetMap.releaseNamespace.includes('?')) {
|
|
310
|
+
throw new TypeError('assetMap release namespace does not prove releaseId');
|
|
311
|
+
}
|
|
312
|
+
if (assetMap.serviceWorkerRoute !== '/sw.js') {
|
|
313
|
+
throw new TypeError('assetMap must expose the current worker at stable /sw.js');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const routeSet = new Set();
|
|
317
|
+
const descriptors = new Map();
|
|
318
|
+
const versionedRootTarget = `/?v=${releaseId}`;
|
|
319
|
+
const manifestTarget = `/manifest.webmanifest?v=${releaseId}`;
|
|
320
|
+
for (const route of assetMap.routes) {
|
|
321
|
+
const normalized = validateStaticTarget(route);
|
|
322
|
+
if (routeSet.has(route)) throw new TypeError(`asset route ${route} is duplicated`);
|
|
323
|
+
if (normalized.hasQuery && route !== manifestTarget && route !== versionedRootTarget) {
|
|
324
|
+
throw new TypeError('only the exact release-bound root and manifest queries may be static asset targets');
|
|
325
|
+
}
|
|
326
|
+
routeSet.add(route);
|
|
327
|
+
descriptors.set(route, validateDescriptor(route, assetMap.get(route)));
|
|
328
|
+
}
|
|
329
|
+
for (const required of ['/', versionedRootTarget, manifestTarget, '/sw.js']) {
|
|
330
|
+
if (!descriptors.has(required)) throw new TypeError(`assetMap is missing ${required}`);
|
|
331
|
+
}
|
|
332
|
+
const root = descriptors.get('/');
|
|
333
|
+
const versionedRoot = descriptors.get(versionedRootTarget);
|
|
334
|
+
const worker = descriptors.get('/sw.js');
|
|
335
|
+
if (!root.contentType.toLowerCase().startsWith('text/html')
|
|
336
|
+
|| !sourceProvesRelease(root.body.toString('utf8'), releaseId, 'html')) {
|
|
337
|
+
throw new TypeError('root HTML does not prove releaseId');
|
|
338
|
+
}
|
|
339
|
+
if (versionedRoot.contentType !== root.contentType || versionedRoot.cacheControl !== root.cacheControl
|
|
340
|
+
|| JSON.stringify(versionedRoot.headers) !== JSON.stringify(root.headers)
|
|
341
|
+
|| !versionedRoot.body.equals(root.body)) {
|
|
342
|
+
throw new TypeError('versioned root must exactly mirror the stable no-store shell');
|
|
343
|
+
}
|
|
344
|
+
if (!worker.contentType.toLowerCase().startsWith('text/javascript')
|
|
345
|
+
|| !sourceProvesRelease(worker.body.toString('utf8'), releaseId, 'worker')) {
|
|
346
|
+
throw new TypeError('service worker does not prove releaseId');
|
|
347
|
+
}
|
|
348
|
+
const workerCache = worker.cacheControl.toLowerCase();
|
|
349
|
+
if (!workerCache.includes('no-store') || !workerCache.includes('no-cache')) {
|
|
350
|
+
throw new TypeError('/sw.js must be no-store and no-cache');
|
|
351
|
+
}
|
|
352
|
+
if (String(worker.headers.pragma ?? '').toLowerCase() !== 'no-cache' || worker.headers.expires !== '0') {
|
|
353
|
+
throw new TypeError('/sw.js must disable intermediary caching');
|
|
354
|
+
}
|
|
355
|
+
for (const route of routeSet) {
|
|
356
|
+
if (route.startsWith(`${assetMap.releaseNamespace}/`) && route.includes('?')) {
|
|
357
|
+
throw new TypeError('immutable release assets must not contain a query');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const immutableRoutes = new Set([...routeSet].filter((route) => route.startsWith(`${assetMap.releaseNamespace}/`)));
|
|
362
|
+
return Object.freeze({
|
|
363
|
+
releaseId,
|
|
364
|
+
contentDigest: assetMap.contentDigest,
|
|
365
|
+
releaseNamespace: assetMap.releaseNamespace,
|
|
366
|
+
routes: Object.freeze([...routeSet]),
|
|
367
|
+
has: (target) => descriptors.has(target),
|
|
368
|
+
isImmutable: (target) => immutableRoutes.has(target),
|
|
369
|
+
get(target) {
|
|
370
|
+
const value = descriptors.get(target);
|
|
371
|
+
if (!value) return undefined;
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
contentType: value.contentType,
|
|
374
|
+
cacheControl: value.cacheControl,
|
|
375
|
+
headers: value.headers,
|
|
376
|
+
body: Buffer.from(value.body)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function classifyRequestTarget(rawTarget, assets) {
|
|
383
|
+
if (typeof rawTarget !== 'string' || rawTarget.length < 1 || utf8Bytes(rawTarget) > CLOUD_HTTP_LIMITS.requestTargetBytes
|
|
384
|
+
|| !rawTarget.startsWith('/') || rawTarget.includes('#') || rawTarget.includes('\\')
|
|
385
|
+
|| CONTROL_PATTERN.test(rawTarget) || ENCODED_PATH_PATTERN.test(rawTarget)) {
|
|
386
|
+
return Object.freeze({ kind: 'invalid' });
|
|
387
|
+
}
|
|
388
|
+
if (assets.has(rawTarget)) return Object.freeze({ kind: 'asset', target: rawTarget });
|
|
389
|
+
const artifactContent = ARTIFACT_CONTENT_TARGET_PATTERN.exec(rawTarget);
|
|
390
|
+
if (artifactContent) {
|
|
391
|
+
return Object.freeze({
|
|
392
|
+
kind: 'agent_artifact',
|
|
393
|
+
pathname: rawTarget,
|
|
394
|
+
artifactId: artifactContent[1],
|
|
395
|
+
releaseId: artifactContent[2] ?? null,
|
|
396
|
+
download: artifactContent[3] === '1'
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (rawTarget.startsWith(AGENT_ARTIFACT_CONTENT_PREFIX)) return Object.freeze({ kind: 'invalid' });
|
|
400
|
+
if (rawTarget.includes('?')) return Object.freeze({ kind: 'invalid' });
|
|
401
|
+
if (rawTarget.includes('//') || (rawTarget !== '/' && rawTarget.endsWith('/'))
|
|
402
|
+
|| rawTarget.split('/').some((part) => part === '.' || part === '..') || rawTarget.includes('%')) {
|
|
403
|
+
return Object.freeze({ kind: 'invalid' });
|
|
404
|
+
}
|
|
405
|
+
if (DYNAMIC_POST_ROUTES.has(rawTarget)) {
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
kind: Object.hasOwn(AGENT_ROUTE_MAP, rawTarget) ? 'agent' : (rawTarget.startsWith('/api/chat/') ? 'chat' : 'session'),
|
|
408
|
+
pathname: rawTarget,
|
|
409
|
+
nativeAgentPath: AGENT_ROUTE_MAP[rawTarget]
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return Object.freeze({ kind: 'not_found' });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function bodyLimitForRoute(pathname) {
|
|
416
|
+
if (pathname === CLOUD_ROUTES.login) return CLOUD_HTTP_LIMITS.loginBodyBytes;
|
|
417
|
+
if (pathname === CLOUD_ROUTES.session || pathname === CLOUD_ROUTES.logout) return CLOUD_HTTP_LIMITS.sessionBodyBytes;
|
|
418
|
+
if (pathname === CLOUD_ROUTES.chatRunsStart) return CLOUD_HTTP_LIMITS.visionChatBodyBytes;
|
|
419
|
+
if (pathname.startsWith('/api/chat/')) return CLOUD_HTTP_LIMITS.chatBodyBytes;
|
|
420
|
+
if (pathname.startsWith(`${AGENT_TRANSPORT_PREFIX}/agent/v1/`)) return CLOUD_HTTP_LIMITS.agentBodyBytes;
|
|
421
|
+
throw new TypeError('route has no body limit');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function routeRequiresIdempotency(pathname, nativeAgentPath) {
|
|
425
|
+
return CHAT_MUTATIONS.has(pathname) || (nativeAgentPath !== undefined && rpcPathIsMutation(nativeAgentPath));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function validateRequestIdempotencyKey(value) {
|
|
429
|
+
if (typeof value !== 'string' || !IDEMPOTENCY_PATTERN.test(value)) {
|
|
430
|
+
throw new CloudHttpError(400, 'invalid_idempotency_key', 'A valid Idempotency-Key header is required.');
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function validateLoginBody(value) {
|
|
436
|
+
const body = exactObject(value, ['username', 'password', 'remember'], ['sessionMode'], 'login request');
|
|
437
|
+
const username = text(body.username, 'username', { maximum: 128 });
|
|
438
|
+
const password = text(body.password, 'password', { maximum: 1_024, allowControl: true });
|
|
439
|
+
if (typeof body.remember !== 'boolean') invalid('remember is invalid.');
|
|
440
|
+
if (body.sessionMode !== undefined && body.sessionMode !== 'ephemeral-memory') {
|
|
441
|
+
invalid('sessionMode is invalid.');
|
|
442
|
+
}
|
|
443
|
+
return Object.freeze({
|
|
444
|
+
username,
|
|
445
|
+
password,
|
|
446
|
+
remember: body.remember,
|
|
447
|
+
...(body.sessionMode === undefined ? {} : { sessionMode: body.sessionMode })
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function validateEmptyBody(value, name = 'request') {
|
|
452
|
+
exactObject(value, [], [], name);
|
|
453
|
+
return Object.freeze({});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function validateChatRequest(pathname, value) {
|
|
457
|
+
switch (pathname) {
|
|
458
|
+
case CLOUD_ROUTES.chatCapabilities:
|
|
459
|
+
return validateEmptyBody(value, 'chat capabilities request');
|
|
460
|
+
case CLOUD_ROUTES.chatThreadsList: {
|
|
461
|
+
const body = exactObject(value, [], ['limit'], 'chat thread list');
|
|
462
|
+
return Object.freeze({ limit: integer(body.limit ?? 50, 'limit', { min: 1, max: 200 }) });
|
|
463
|
+
}
|
|
464
|
+
case CLOUD_ROUTES.chatThreadsCreate: {
|
|
465
|
+
const body = exactObject(value, ['threadId'], ['title'], 'chat thread creation');
|
|
466
|
+
return Object.freeze({
|
|
467
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
468
|
+
title: text(body.title ?? '', 'title', { minimum: 0, maximum: 512 })
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
case CLOUD_ROUTES.chatThreadsGet: {
|
|
472
|
+
const body = exactObject(value, ['threadId'], [], 'chat thread lookup');
|
|
473
|
+
return Object.freeze({ threadId: identifier(body.threadId, 'threadId') });
|
|
474
|
+
}
|
|
475
|
+
case CLOUD_ROUTES.chatThreadsDelete: {
|
|
476
|
+
const body = exactObject(
|
|
477
|
+
value,
|
|
478
|
+
['threadId', 'expectedRevision', 'expectedHash'],
|
|
479
|
+
[],
|
|
480
|
+
'chat thread deletion'
|
|
481
|
+
);
|
|
482
|
+
const expected = cursor(body.expectedRevision, body.expectedHash);
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
485
|
+
expectedRevision: expected.revision,
|
|
486
|
+
expectedHash: expected.hash
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
case CLOUD_ROUTES.chatMessagesList: {
|
|
490
|
+
const body = exactObject(value, ['threadId'], ['afterRevision', 'limit', 'attachmentSchema'], 'chat message list');
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
493
|
+
afterRevision: integer(body.afterRevision ?? 0, 'afterRevision', { min: 0, max: 2_000 }),
|
|
494
|
+
limit: integer(body.limit ?? 100, 'limit', { min: 1, max: 200 }),
|
|
495
|
+
attachmentSchema: integer(body.attachmentSchema ?? 1, 'attachmentSchema', { min: 1, max: 2 })
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
case CLOUD_ROUTES.chatAttachmentsGet: {
|
|
499
|
+
const body = exactObject(value, ['threadId', 'attachmentId'], [], 'chat attachment lookup');
|
|
500
|
+
return Object.freeze({
|
|
501
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
502
|
+
attachmentId: identifier(body.attachmentId, 'attachmentId')
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
case CLOUD_ROUTES.chatRunsStart: {
|
|
506
|
+
const body = exactObject(value, [
|
|
507
|
+
'threadId', 'messageId', 'generationId', 'assistantMessageId',
|
|
508
|
+
'content', 'expectedRevision', 'expectedHash'
|
|
509
|
+
], ['attachment', 'attachments'], 'chat run start');
|
|
510
|
+
const expected = cursor(body.expectedRevision, body.expectedHash);
|
|
511
|
+
const normalized = {
|
|
512
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
513
|
+
messageId: identifier(body.messageId, 'messageId'),
|
|
514
|
+
generationId: identifier(body.generationId, 'generationId'),
|
|
515
|
+
assistantMessageId: identifier(body.assistantMessageId, 'assistantMessageId'),
|
|
516
|
+
content: text(body.content, 'content', {
|
|
517
|
+
maximum: 64 * 1024,
|
|
518
|
+
trim: true,
|
|
519
|
+
allowMessageFormatting: true
|
|
520
|
+
}),
|
|
521
|
+
expectedRevision: expected.revision,
|
|
522
|
+
expectedHash: expected.hash
|
|
523
|
+
};
|
|
524
|
+
if (body.attachment !== undefined && body.attachments !== undefined) {
|
|
525
|
+
throw new CloudHttpError(400, 'invalid_attachment', 'Use either attachment or attachments, not both.');
|
|
526
|
+
}
|
|
527
|
+
let attachments;
|
|
528
|
+
if (body.attachments !== undefined) {
|
|
529
|
+
try {
|
|
530
|
+
attachments = validateVisionAttachmentsRequest(body.attachments);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
throw new CloudHttpError(400, 'invalid_attachment', 'The image attachments are invalid.', { cause: error });
|
|
533
|
+
}
|
|
534
|
+
} else if (body.attachment !== undefined) {
|
|
535
|
+
try {
|
|
536
|
+
attachments = Object.freeze([validateVisionAttachmentRequest(body.attachment)]);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
throw new CloudHttpError(400, 'invalid_attachment', 'The image attachment is invalid.', { cause: error });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
...normalized,
|
|
543
|
+
...(attachments === undefined ? {} : { attachments })
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
case CLOUD_ROUTES.chatRunsStatus: {
|
|
547
|
+
const body = exactObject(value, ['threadId', 'generationId'], [], 'chat run status');
|
|
548
|
+
return Object.freeze({
|
|
549
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
550
|
+
generationId: identifier(body.generationId, 'generationId')
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
case CLOUD_ROUTES.chatRunsEvents: {
|
|
554
|
+
const body = exactObject(value, ['threadId', 'generationId'], ['afterSequence'], 'chat run events');
|
|
555
|
+
return Object.freeze({
|
|
556
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
557
|
+
generationId: identifier(body.generationId, 'generationId'),
|
|
558
|
+
afterSequence: integer(body.afterSequence ?? 0, 'afterSequence', { min: 0, max: 8_192 })
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
case CLOUD_ROUTES.chatRunsCancel: {
|
|
562
|
+
const body = exactObject(value, ['threadId', 'generationId'], [], 'chat run cancellation');
|
|
563
|
+
return Object.freeze({
|
|
564
|
+
threadId: identifier(body.threadId, 'threadId'),
|
|
565
|
+
generationId: identifier(body.generationId, 'generationId')
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
default:
|
|
569
|
+
throw new CloudHttpError(404, 'not_found', 'The requested route does not exist.');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function validateTransportAgentRequest(nativePath, value) {
|
|
574
|
+
try {
|
|
575
|
+
return validateAgentRequest(nativePath, value);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
throw new CloudHttpError(
|
|
578
|
+
error?.code === 'NOT_FOUND' ? 404 : 400,
|
|
579
|
+
error?.code === 'NOT_FOUND' ? 'not_found' : 'invalid_agent_request',
|
|
580
|
+
error?.code === 'NOT_FOUND' ? 'The requested route does not exist.' : 'The Agent request is invalid.',
|
|
581
|
+
{ cause: error }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function validateAgentIdempotencyKey(value) {
|
|
587
|
+
try {
|
|
588
|
+
return validateIdempotencyKey(value);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
throw new CloudHttpError(400, 'invalid_idempotency_key', 'A valid Idempotency-Key header is required.', { cause: error });
|
|
591
|
+
}
|
|
592
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export {
|
|
2
|
+
COMPONENT_ID,
|
|
3
|
+
COMPONENT_ROLE,
|
|
4
|
+
CONTRACT_VERSION,
|
|
5
|
+
createCapabilityContract,
|
|
6
|
+
createHealthContract
|
|
7
|
+
} from './contracts.js';
|
|
8
|
+
export {
|
|
9
|
+
OPERATOR_HEALTH_SCHEMA,
|
|
10
|
+
OPERATOR_HEALTH_TIMEOUT_MS,
|
|
11
|
+
createOperatorHealthReport
|
|
12
|
+
} from './operator-health.js';
|
|
13
|
+
export {
|
|
14
|
+
ConflictError,
|
|
15
|
+
ControlPlaneError,
|
|
16
|
+
IdempotencyConflictError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
StorageCorruptionError,
|
|
19
|
+
StorageSecurityError,
|
|
20
|
+
UnsupportedSchemaError,
|
|
21
|
+
ValidationError
|
|
22
|
+
} from './errors.js';
|
|
23
|
+
export {
|
|
24
|
+
LATEST_SCHEMA_VERSION,
|
|
25
|
+
MIGRATIONS,
|
|
26
|
+
SQLITE_APPLICATION_ID
|
|
27
|
+
} from './migrations.js';
|
|
28
|
+
export {
|
|
29
|
+
CHAT_MIGRATIONS,
|
|
30
|
+
CHAT_SQLITE_APPLICATION_ID,
|
|
31
|
+
LATEST_CHAT_SCHEMA_VERSION
|
|
32
|
+
} from './chat-migrations.js';
|
|
33
|
+
export {
|
|
34
|
+
CloudIndexStore,
|
|
35
|
+
IDEMPOTENCY_RECEIPT_TTL_MS,
|
|
36
|
+
MAX_BROWSER_SESSIONS_PER_ACCOUNT,
|
|
37
|
+
MAX_IDEMPOTENCY_RECEIPTS_PER_ACCOUNT
|
|
38
|
+
} from './store.js';
|
|
39
|
+
export {
|
|
40
|
+
DIRECT_CHAT_DISPATCH_LEASE_LIMITS,
|
|
41
|
+
DIRECT_CHAT_IDEMPOTENCY_TTL_MS,
|
|
42
|
+
DIRECT_CHAT_LIMITS,
|
|
43
|
+
DIRECT_CHAT_TERMINAL_DELTA_RETENTION_MS,
|
|
44
|
+
DirectChatStore
|
|
45
|
+
} from './chat-store.js';
|
|
46
|
+
export {
|
|
47
|
+
DIRECT_CHAT_CONTEXT_DEFAULTS,
|
|
48
|
+
DIRECT_CHAT_SUMMARY_LABEL,
|
|
49
|
+
DirectChatContextCoordinator
|
|
50
|
+
} from './chat-context.js';
|
|
51
|
+
export { DIRECT_CHAT_CONTEXT_ENTRY_LIMIT } from './direct-chat-contract.js';
|
|
52
|
+
export { createDeterministicContextSummarizer } from './deterministic-context-summarizer.js';
|
|
53
|
+
export {
|
|
54
|
+
AGINTI_ARTIFACT_CONTENT_PATH,
|
|
55
|
+
AGINTI_INTERNAL_HEADERS,
|
|
56
|
+
AgintiAdapterError,
|
|
57
|
+
createAgintiAgentAdapter,
|
|
58
|
+
validateArtifactContentRequest,
|
|
59
|
+
validateAgintiTransportCredential
|
|
60
|
+
} from './aginti-adapter.js';
|
|
61
|
+
export {
|
|
62
|
+
LocalLlmConnectorError,
|
|
63
|
+
createLocalLlmConnector
|
|
64
|
+
} from './localllm-connector.js';
|
|
65
|
+
export {
|
|
66
|
+
CLOUD_AGENT_PUBLIC_ROUTES,
|
|
67
|
+
createCloudRequestHandler,
|
|
68
|
+
createCloudServer,
|
|
69
|
+
resolveTrustedClientAddress
|
|
70
|
+
} from './cloud-server.js';
|
|
71
|
+
export {
|
|
72
|
+
AGENT_ARTIFACT_CONTENT_PREFIX,
|
|
73
|
+
AGENT_ROUTE_MAP,
|
|
74
|
+
AGENT_TRANSPORT_PREFIX,
|
|
75
|
+
CHAT_MUTATION_ROUTES,
|
|
76
|
+
CHAT_POST_ROUTES,
|
|
77
|
+
CLOUD_HTTP_LIMITS,
|
|
78
|
+
CLOUD_ROUTES,
|
|
79
|
+
CLIENT_RELEASE_HEADER_NAME,
|
|
80
|
+
CSRF_COOKIE_NAME,
|
|
81
|
+
CSRF_HEADER_NAME,
|
|
82
|
+
IDEMPOTENCY_HEADER_NAME,
|
|
83
|
+
SESSION_COOKIE_NAME,
|
|
84
|
+
TRUSTED_CLIENT_ADDRESS_HEADER,
|
|
85
|
+
TRUSTED_PUBLIC_AUTHORITY_HEADER,
|
|
86
|
+
CloudHttpError
|
|
87
|
+
} from './http-contract.js';
|
|
88
|
+
export * from './web/index.js';
|