@gmickel/gno 1.28.0 → 1.29.1
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/README.md +46 -20
- package/assets/skill/SKILL.md +33 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +6 -6
- package/package.json +1 -1
- package/spec/cli.md +41 -1
- package/spec/db/schema.sql +19 -0
- package/spec/mcp.md +66 -0
- package/spec/output-schemas/ask.schema.json +31 -1
- package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
- package/spec/output-schemas/collection-egress-check.schema.json +91 -0
- package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
- package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
- package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
- package/spec/output-schemas/egress-audit-management.schema.json +88 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- package/spec/output-schemas/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +30 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
- package/spec/output-schemas/search-results.schema.json +30 -0
- package/src/app/context-runtime-contract.ts +16 -7
- package/src/cli/commands/ask.ts +7 -0
- package/src/cli/commands/collection/index.ts +5 -0
- package/src/cli/commands/collection/policy.ts +126 -0
- package/src/cli/commands/context-build.ts +5 -0
- package/src/cli/commands/daemon.ts +45 -3
- package/src/cli/commands/egress-audit.ts +68 -0
- package/src/cli/commands/embed.ts +2 -0
- package/src/cli/commands/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +123 -0
- package/src/config/index.ts +9 -0
- package/src/config/types.ts +50 -0
- package/src/core/browser-clip.ts +16 -1
- package/src/core/collection-egress-policy-projection.ts +28 -0
- package/src/core/collection-egress-policy-service.ts +443 -0
- package/src/core/collection-egress-policy-validation.ts +242 -0
- package/src/core/config-mutation.ts +31 -19
- package/src/core/context-capsule-schema.ts +111 -1
- package/src/core/context-capsule-validation.ts +4 -4
- package/src/core/context-capsule-verification.ts +5 -1
- package/src/core/context-capsule.ts +177 -132
- package/src/core/context-evidence.ts +86 -14
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/egress-audit.ts +239 -0
- package/src/core/egress-authorization.ts +50 -0
- package/src/core/egress-enforcement.ts +264 -0
- package/src/core/egress-policy.ts +440 -0
- package/src/core/egress-provenance.ts +336 -0
- package/src/core/job-manager.ts +37 -0
- package/src/core/network-boundary-inventory.ts +261 -0
- package/src/core/retrieval-qrels.ts +6 -0
- package/src/core/retrieval-replay.ts +18 -4
- package/src/core/retrieval-trace-export.ts +23 -4
- package/src/core/retrieval-trace-management-helpers.ts +1 -0
- package/src/core/retrieval-trace-management-types.ts +3 -0
- package/src/core/retrieval-trace-management.ts +9 -1
- package/src/core/retrieval-trace-session.ts +94 -0
- package/src/core/retrieval-trace.ts +22 -0
- package/src/ingestion/record-adapter.ts +12 -1
- package/src/ingestion/record-container.ts +38 -22
- package/src/ingestion/sync.ts +15 -0
- package/src/ingestion/types.ts +2 -0
- package/src/llm/errors.ts +10 -0
- package/src/llm/http-inference.ts +175 -0
- package/src/llm/http-policy.ts +537 -0
- package/src/llm/httpEmbedding.ts +47 -28
- package/src/llm/httpGeneration.ts +31 -18
- package/src/llm/httpRerank.ts +30 -18
- package/src/llm/index.ts +1 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
- package/src/llm/pinned-http-connection.ts +177 -0
- package/src/mcp/context.ts +53 -1
- package/src/mcp/http-egress.ts +220 -0
- package/src/mcp/http-security.ts +32 -21
- package/src/mcp/http-session.ts +13 -2
- package/src/mcp/http-transport.ts +96 -6
- package/src/mcp/sync-egress.ts +24 -0
- package/src/mcp/tools/add-collection.ts +4 -0
- package/src/mcp/tools/ask.ts +1 -1
- package/src/mcp/tools/context.ts +9 -2
- package/src/mcp/tools/egress.ts +247 -0
- package/src/mcp/tools/embed.ts +1 -0
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/query.ts +10 -0
- package/src/mcp/tools/remove-collection.ts +4 -0
- package/src/mcp/tools/sync.ts +6 -0
- package/src/mcp/tools/trace.ts +7 -1
- package/src/mcp/tools/vsearch.ts +1 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/hybrid.ts +14 -0
- package/src/pipeline/search.ts +12 -0
- package/src/pipeline/types.ts +3 -0
- package/src/pipeline/vsearch.ts +41 -20
- package/src/publish/artifact.ts +31 -3
- package/src/publish/export-service.ts +25 -0
- package/src/sdk/client.ts +119 -2
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/clipper-capture.ts +5 -1
- package/src/serve/closed-json.ts +61 -0
- package/src/serve/config-sync.ts +4 -1
- package/src/serve/context.ts +1 -0
- package/src/serve/doc-events.ts +110 -39
- package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
- package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
- package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
- package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +3 -0
- package/src/serve/resident-request.ts +80 -3
- package/src/serve/resident-runtime.ts +63 -1
- package/src/serve/routes/api.ts +174 -0
- package/src/serve/routes/clipper.ts +64 -1
- package/src/serve/routes/mcp.ts +6 -0
- package/src/serve/routes/traces.ts +27 -4
- package/src/serve/server.ts +118 -7
- package/src/store/migrations/023-collection-egress-policy.ts +61 -0
- package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
- package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
- package/src/store/migrations/index.ts +6 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +170 -7
- package/src/store/sqlite/change-journal-store.ts +41 -2
- package/src/store/sqlite/egress-audit-store.ts +485 -0
- package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
- package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
- package/src/store/sqlite/retrieval-trace-store.ts +104 -5
- package/src/store/types.ts +126 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-vn5f663b.js +0 -50
package/src/sdk/client.ts
CHANGED
|
@@ -79,6 +79,11 @@ import {
|
|
|
79
79
|
planCapture,
|
|
80
80
|
} from "../core/capture";
|
|
81
81
|
import { writeCapturePlanFile } from "../core/capture-write";
|
|
82
|
+
import { projectCollectionEgressPolicy } from "../core/collection-egress-policy-projection";
|
|
83
|
+
import { CollectionEgressPolicyService } from "../core/collection-egress-policy-service";
|
|
84
|
+
import { applyConfigChange } from "../core/config-mutation";
|
|
85
|
+
import { EgressAuditService } from "../core/egress-audit";
|
|
86
|
+
import { authorizeCurrentEgress } from "../core/egress-authorization";
|
|
82
87
|
import {
|
|
83
88
|
atomicWrite,
|
|
84
89
|
copyFilePath,
|
|
@@ -270,7 +275,7 @@ async function resolveClientState(
|
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
class GnoClientImpl implements GnoClient {
|
|
273
|
-
|
|
278
|
+
config: Config;
|
|
274
279
|
readonly dbPath: string;
|
|
275
280
|
readonly configPath: string | null;
|
|
276
281
|
readonly configSource: "file" | "inline";
|
|
@@ -331,6 +336,9 @@ class GnoClientImpl implements GnoClient {
|
|
|
331
336
|
rerankModel?: string;
|
|
332
337
|
}): Promise<RuntimePorts> {
|
|
333
338
|
this.assertOpen();
|
|
339
|
+
const egressCollections = options.collection
|
|
340
|
+
? [options.collection]
|
|
341
|
+
: ("all" as const);
|
|
334
342
|
|
|
335
343
|
let embedPort: EmbeddingPort | null = null;
|
|
336
344
|
let expandPort: GenerationPort | null = null;
|
|
@@ -347,6 +355,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
347
355
|
options.collection
|
|
348
356
|
),
|
|
349
357
|
{
|
|
358
|
+
egressCollections,
|
|
350
359
|
policy: this.downloadPolicy,
|
|
351
360
|
}
|
|
352
361
|
);
|
|
@@ -391,6 +400,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
391
400
|
options.collection
|
|
392
401
|
),
|
|
393
402
|
{
|
|
403
|
+
egressCollections,
|
|
394
404
|
policy: this.downloadPolicy,
|
|
395
405
|
}
|
|
396
406
|
);
|
|
@@ -415,6 +425,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
415
425
|
options.collection
|
|
416
426
|
),
|
|
417
427
|
{
|
|
428
|
+
egressCollections,
|
|
418
429
|
policy: this.downloadPolicy,
|
|
419
430
|
}
|
|
420
431
|
);
|
|
@@ -442,6 +453,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
442
453
|
options.collection
|
|
443
454
|
),
|
|
444
455
|
{
|
|
456
|
+
egressCollections,
|
|
445
457
|
policy: this.downloadPolicy,
|
|
446
458
|
}
|
|
447
459
|
);
|
|
@@ -1230,7 +1242,19 @@ class GnoClientImpl implements GnoClient {
|
|
|
1230
1242
|
) {
|
|
1231
1243
|
this.assertOpen();
|
|
1232
1244
|
return unwrapTraceStore(
|
|
1233
|
-
await new RetrievalTraceManagementService(this.store
|
|
1245
|
+
await new RetrievalTraceManagementService(this.store, {
|
|
1246
|
+
authorizeExport: async (lineage) => {
|
|
1247
|
+
return authorizeCurrentEgress({
|
|
1248
|
+
store: this.store,
|
|
1249
|
+
config: this.config,
|
|
1250
|
+
lineage,
|
|
1251
|
+
action: "export",
|
|
1252
|
+
destinationZone: "local_process",
|
|
1253
|
+
caller: { authenticated: true, operationAuthorized: true },
|
|
1254
|
+
contentClass: "retrieval_trace",
|
|
1255
|
+
});
|
|
1256
|
+
},
|
|
1257
|
+
}).export(input)
|
|
1234
1258
|
);
|
|
1235
1259
|
}
|
|
1236
1260
|
|
|
@@ -1248,6 +1272,99 @@ class GnoClientImpl implements GnoClient {
|
|
|
1248
1272
|
);
|
|
1249
1273
|
}
|
|
1250
1274
|
|
|
1275
|
+
async getCollectionEgressPolicy(collection: string) {
|
|
1276
|
+
this.assertOpen();
|
|
1277
|
+
const state = new CollectionEgressPolicyService({
|
|
1278
|
+
getConfig: () => this.config,
|
|
1279
|
+
}).get(collection);
|
|
1280
|
+
if (!state.ok) {
|
|
1281
|
+
throw sdkError(
|
|
1282
|
+
state.code === "NOT_FOUND" ? "NOT_FOUND" : "VALIDATION",
|
|
1283
|
+
state.error
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
return state.value;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async setCollectionEgressPolicy(
|
|
1290
|
+
collection: string,
|
|
1291
|
+
policy: import("../config/types").EgressPolicy,
|
|
1292
|
+
confirmation?: import("../core/collection-egress-policy-service").EgressRelaxationConfirmation
|
|
1293
|
+
) {
|
|
1294
|
+
this.assertOpen();
|
|
1295
|
+
if (!this.configPath) {
|
|
1296
|
+
throw sdkError(
|
|
1297
|
+
"VALIDATION",
|
|
1298
|
+
"Inline-config clients cannot persist collection policy changes"
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
const service = new CollectionEgressPolicyService({
|
|
1302
|
+
getConfig: () => this.config,
|
|
1303
|
+
mutateConfig: (mutate) =>
|
|
1304
|
+
applyConfigChange(
|
|
1305
|
+
{
|
|
1306
|
+
store: this.store,
|
|
1307
|
+
configPath: this.configPath ?? undefined,
|
|
1308
|
+
onConfigUpdated: (config) => {
|
|
1309
|
+
this.config = config;
|
|
1310
|
+
},
|
|
1311
|
+
projectStore: (store, config) =>
|
|
1312
|
+
projectCollectionEgressPolicy(store, config, collection),
|
|
1313
|
+
},
|
|
1314
|
+
mutate
|
|
1315
|
+
),
|
|
1316
|
+
});
|
|
1317
|
+
const result = await service.set({
|
|
1318
|
+
collection,
|
|
1319
|
+
policy,
|
|
1320
|
+
confirmation,
|
|
1321
|
+
});
|
|
1322
|
+
if (!result.ok) throw sdkError("VALIDATION", result.error);
|
|
1323
|
+
return result.value;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async checkEgress(
|
|
1327
|
+
input: import("../core/collection-egress-policy-service").CollectionEgressCheckInput
|
|
1328
|
+
) {
|
|
1329
|
+
this.assertOpen();
|
|
1330
|
+
const result = new CollectionEgressPolicyService({
|
|
1331
|
+
getConfig: () => this.config,
|
|
1332
|
+
}).check(input);
|
|
1333
|
+
if (!result.ok) throw sdkError("VALIDATION", result.error);
|
|
1334
|
+
return result.value;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async listEgressAudits(options: { limit?: number; cursor?: string } = {}) {
|
|
1338
|
+
this.assertOpen();
|
|
1339
|
+
return unwrapTraceStore(
|
|
1340
|
+
await new EgressAuditService(this.store).list(options)
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async getEgressAudit(auditId: string) {
|
|
1345
|
+
this.assertOpen();
|
|
1346
|
+
return unwrapTraceStore(
|
|
1347
|
+
await new EgressAuditService(this.store).show(auditId)
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
async getEgressAuditStatus() {
|
|
1352
|
+
this.assertOpen();
|
|
1353
|
+
return unwrapTraceStore(await new EgressAuditService(this.store).status());
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
async deleteEgressAudit(auditId: string) {
|
|
1357
|
+
this.assertOpen();
|
|
1358
|
+
return unwrapTraceStore(
|
|
1359
|
+
await new EgressAuditService(this.store).delete(auditId)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
async purgeEgressAudits() {
|
|
1364
|
+
this.assertOpen();
|
|
1365
|
+
return unwrapTraceStore(await new EgressAuditService(this.store).purge());
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1251
1368
|
async update(options: GnoUpdateOptions = {}): Promise<SyncResult> {
|
|
1252
1369
|
this.assertOpen();
|
|
1253
1370
|
const collections = this.getCollections(options.collection);
|
package/src/sdk/embed.ts
CHANGED
|
@@ -283,6 +283,7 @@ export async function runEmbed(
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
const embedResult = await runtime.llm.createEmbeddingPort(modelUri, {
|
|
286
|
+
egressCollections: options.collection ? [options.collection] : "all",
|
|
286
287
|
policy: runtime.downloadPolicy,
|
|
287
288
|
});
|
|
288
289
|
if (!embedResult.ok) {
|
package/src/sdk/types.ts
CHANGED
|
@@ -11,6 +11,13 @@ import type {
|
|
|
11
11
|
import type { ContentTypeBoostStatus } from "../config/content-types";
|
|
12
12
|
import type { Config } from "../config/types";
|
|
13
13
|
import type { CaptureInput, CaptureReceipt } from "../core/capture";
|
|
14
|
+
import type {
|
|
15
|
+
CollectionEgressCheckInput,
|
|
16
|
+
CollectionEgressCheckResult,
|
|
17
|
+
CollectionEgressPolicySetResult,
|
|
18
|
+
CollectionEgressPolicyState,
|
|
19
|
+
EgressRelaxationConfirmation,
|
|
20
|
+
} from "../core/collection-egress-policy-service";
|
|
14
21
|
import type {
|
|
15
22
|
ContextCapsuleErrorCode,
|
|
16
23
|
ContextCapsuleV1,
|
|
@@ -18,6 +25,13 @@ import type {
|
|
|
18
25
|
} from "../core/context-capsule";
|
|
19
26
|
import type { ContextEvidenceErrorCode } from "../core/context-evidence";
|
|
20
27
|
import type { ContextVerifierErrorCode } from "../core/context-verifier";
|
|
28
|
+
import type {
|
|
29
|
+
EgressAuditDeleteResult,
|
|
30
|
+
EgressAuditListResult,
|
|
31
|
+
EgressAuditPurgeManagementResult,
|
|
32
|
+
EgressAuditShowResult,
|
|
33
|
+
EgressAuditStatusResult,
|
|
34
|
+
} from "../core/egress-audit";
|
|
21
35
|
import type {
|
|
22
36
|
KnowledgeChangesResult,
|
|
23
37
|
KnowledgeDiffResult,
|
|
@@ -300,6 +314,25 @@ export interface GnoClient {
|
|
|
300
314
|
): Promise<RetrievalTraceExportResult>;
|
|
301
315
|
deleteRetrievalTrace(traceId: string): Promise<RetrievalTraceDeleteResult>;
|
|
302
316
|
purgeRetrievalTraces(): Promise<RetrievalTraceManagementPurgeResult>;
|
|
317
|
+
getCollectionEgressPolicy(
|
|
318
|
+
collection: string
|
|
319
|
+
): Promise<CollectionEgressPolicyState>;
|
|
320
|
+
setCollectionEgressPolicy(
|
|
321
|
+
collection: string,
|
|
322
|
+
policy: import("../config/types").EgressPolicy,
|
|
323
|
+
confirmation?: EgressRelaxationConfirmation
|
|
324
|
+
): Promise<CollectionEgressPolicySetResult>;
|
|
325
|
+
checkEgress(
|
|
326
|
+
input: CollectionEgressCheckInput
|
|
327
|
+
): Promise<CollectionEgressCheckResult>;
|
|
328
|
+
listEgressAudits(options?: {
|
|
329
|
+
limit?: number;
|
|
330
|
+
cursor?: string;
|
|
331
|
+
}): Promise<EgressAuditListResult>;
|
|
332
|
+
getEgressAudit(auditId: string): Promise<EgressAuditShowResult>;
|
|
333
|
+
getEgressAuditStatus(): Promise<EgressAuditStatusResult>;
|
|
334
|
+
deleteEgressAudit(auditId: string): Promise<EgressAuditDeleteResult>;
|
|
335
|
+
purgeEgressAudits(): Promise<EgressAuditPurgeManagementResult>;
|
|
303
336
|
update(options?: GnoUpdateOptions): Promise<SyncResult>;
|
|
304
337
|
embed(options?: GnoEmbedOptions): Promise<GnoEmbedResult>;
|
|
305
338
|
index(options?: GnoIndexOptions): Promise<GnoIndexResult>;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Database } from "bun:sqlite";
|
|
4
4
|
|
|
5
5
|
import type { PreparedBrowserClip } from "../core/browser-clip";
|
|
6
|
+
import type { EgressLineage } from "../core/egress-provenance";
|
|
6
7
|
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
7
8
|
import type { ClipperIdempotencyReplay } from "../store/sqlite/clipper-store-types";
|
|
8
9
|
import type { ResidentCaptureContext } from "./capture-service";
|
|
@@ -35,6 +36,7 @@ interface ExecuteClipperCaptureInput {
|
|
|
35
36
|
context: ResidentCaptureContext;
|
|
36
37
|
store: SqliteAdapter;
|
|
37
38
|
pairing: ClipperPairingService;
|
|
39
|
+
egressLineage?: EgressLineage;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const inFlightRequests = new Set<string>();
|
|
@@ -153,7 +155,9 @@ export const executeClipperCapture = async (
|
|
|
153
155
|
);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
const prepared = prepareBrowserClip(parsed.data.payload
|
|
158
|
+
const prepared = prepareBrowserClip(parsed.data.payload, {
|
|
159
|
+
egressLineage: input.egressLineage,
|
|
160
|
+
});
|
|
157
161
|
if (prepared.preview.digest !== parsed.data.previewDigest) {
|
|
158
162
|
return clipperErrorResponse(
|
|
159
163
|
"CLIPPER_PREVIEW_MISMATCH",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Bounded, content-free JSON request parsing for closed REST contracts. */
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_BYTES = 16 * 1024;
|
|
4
|
+
|
|
5
|
+
export type ClosedJsonResult =
|
|
6
|
+
| { ok: true; value: unknown }
|
|
7
|
+
| { ok: false; code: "VALIDATION"; error: string };
|
|
8
|
+
|
|
9
|
+
export const parseClosedJson = async (
|
|
10
|
+
request: Request,
|
|
11
|
+
maxBytes = DEFAULT_MAX_BYTES
|
|
12
|
+
): Promise<ClosedJsonResult> => {
|
|
13
|
+
try {
|
|
14
|
+
const contentLength = Number(request.headers.get("content-length"));
|
|
15
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
code: "VALIDATION",
|
|
19
|
+
error: "JSON body exceeds the allowed size",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const reader = request.body?.getReader();
|
|
23
|
+
if (!reader) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
code: "VALIDATION",
|
|
27
|
+
error: "Invalid JSON body",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const chunks: Uint8Array[] = [];
|
|
31
|
+
let totalBytes = 0;
|
|
32
|
+
while (true) {
|
|
33
|
+
const chunk = await reader.read();
|
|
34
|
+
if (chunk.done) break;
|
|
35
|
+
totalBytes += chunk.value.byteLength;
|
|
36
|
+
if (totalBytes > maxBytes) {
|
|
37
|
+
await reader.cancel("JSON body exceeds the allowed size");
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
code: "VALIDATION",
|
|
41
|
+
error: "JSON body exceeds the allowed size",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
chunks.push(chunk.value);
|
|
45
|
+
}
|
|
46
|
+
const bytes = new Uint8Array(totalBytes);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
bytes.set(chunk, offset);
|
|
50
|
+
offset += chunk.byteLength;
|
|
51
|
+
}
|
|
52
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
53
|
+
return { ok: true, value: JSON.parse(text) as unknown };
|
|
54
|
+
} catch {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
code: "VALIDATION",
|
|
58
|
+
error: "Invalid JSON body",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
package/src/serve/config-sync.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ContextHolder } from "./routes/api";
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
applyConfigChange as applyConfigChangeCore,
|
|
14
|
+
type ConfigMutationContext,
|
|
14
15
|
type ApplyConfigResult,
|
|
15
16
|
type MutationResult,
|
|
16
17
|
} from "../core/config-mutation";
|
|
@@ -67,11 +68,13 @@ export async function applyConfigChangeTyped<T>(
|
|
|
67
68
|
ctxHolder: ContextHolder,
|
|
68
69
|
store: SqliteAdapter,
|
|
69
70
|
mutate: (config: Config) => Promise<MutationResult<T>> | MutationResult<T>,
|
|
70
|
-
configPath?: string
|
|
71
|
+
configPath?: string,
|
|
72
|
+
options: Pick<ConfigMutationContext, "projectStore"> = {}
|
|
71
73
|
): Promise<ApplyConfigResult<T>> {
|
|
72
74
|
const result = await applyConfigChangeCore(
|
|
73
75
|
{
|
|
74
76
|
store,
|
|
77
|
+
projectStore: options.projectStore,
|
|
75
78
|
configPath: configPath ?? ctxHolder.actualConfigPath,
|
|
76
79
|
onConfigUpdated: (config) => {
|
|
77
80
|
ctxHolder.config = config;
|
package/src/serve/context.ts
CHANGED
|
@@ -115,6 +115,7 @@ export async function createServerContext(
|
|
|
115
115
|
|
|
116
116
|
// Progress callback updates downloadState for WebUI polling
|
|
117
117
|
const createPortOptions = (type: ModelType): CreatePortOptions => ({
|
|
118
|
+
egressCollections: "all",
|
|
118
119
|
policy,
|
|
119
120
|
onProgress: (progress) => {
|
|
120
121
|
downloadState.active = true;
|
package/src/serve/doc-events.ts
CHANGED
|
@@ -25,46 +25,87 @@ export interface DocumentEventBusState {
|
|
|
25
25
|
retryMs: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface DocumentEventStreamAuthorization {
|
|
29
|
+
authorizationEpoch: string;
|
|
30
|
+
isAuthorizationEpochCurrent: () => boolean;
|
|
31
|
+
onClose?: () => void;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DocumentEventSubscriber {
|
|
36
|
+
authorization: DocumentEventStreamAuthorization;
|
|
37
|
+
closed: boolean;
|
|
38
|
+
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
39
|
+
keepaliveTimer: ReturnType<typeof setInterval> | null;
|
|
40
|
+
removeAbortListener: (() => void) | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
const encoder = new TextEncoder();
|
|
29
44
|
const EVENT_RETRY_MS = 2_000;
|
|
30
45
|
const KEEPALIVE_MS = 15_000;
|
|
46
|
+
const POLICY_CHANGED_FRAME = encoder.encode(
|
|
47
|
+
`retry: ${EVENT_RETRY_MS}\nevent: egress-policy-changed\ndata: {"error":{"code":"EGRESS_POLICY_CHANGED","message":"Collection policy changed; retry"}}\n\n`
|
|
48
|
+
);
|
|
31
49
|
|
|
32
50
|
export class DocumentEventBus {
|
|
33
|
-
readonly #
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
readonly #subscribers = new Set<DocumentEventSubscriber>();
|
|
52
|
+
readonly #keepaliveMs: number;
|
|
53
|
+
|
|
54
|
+
constructor(options: { keepaliveMs?: number } = {}) {
|
|
55
|
+
this.#keepaliveMs = options.keepaliveMs ?? KEEPALIVE_MS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createResponse(
|
|
59
|
+
authorization: DocumentEventStreamAuthorization = {
|
|
60
|
+
authorizationEpoch: "unrestricted",
|
|
61
|
+
isAuthorizationEpochCurrent: () => true,
|
|
62
|
+
}
|
|
63
|
+
): Response {
|
|
64
|
+
let subscriber: DocumentEventSubscriber | null = null;
|
|
42
65
|
const stream = new ReadableStream<Uint8Array>({
|
|
43
|
-
start(controller) {
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
start: (controller) => {
|
|
67
|
+
subscriber = {
|
|
68
|
+
authorization,
|
|
69
|
+
closed: false,
|
|
70
|
+
controller,
|
|
71
|
+
keepaliveTimer: null,
|
|
72
|
+
removeAbortListener: null,
|
|
73
|
+
};
|
|
74
|
+
this.#subscribers.add(subscriber);
|
|
75
|
+
if (!authorization.isAuthorizationEpochCurrent()) {
|
|
76
|
+
this.#invalidateSubscriber(subscriber);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
46
79
|
controller.enqueue(
|
|
47
80
|
encoder.encode(`retry: ${EVENT_RETRY_MS}\n: connected\n\n`)
|
|
48
81
|
);
|
|
49
|
-
keepaliveTimer = setInterval(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
clearInterval(keepaliveTimer);
|
|
82
|
+
subscriber.keepaliveTimer = setInterval(
|
|
83
|
+
() => {
|
|
84
|
+
if (!authorization.isAuthorizationEpochCurrent()) {
|
|
85
|
+
this.#invalidateSubscriber(subscriber);
|
|
86
|
+
return;
|
|
55
87
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
88
|
+
try {
|
|
89
|
+
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
90
|
+
} catch {
|
|
91
|
+
this.#closeSubscriber(subscriber);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
this.#keepaliveMs
|
|
95
|
+
);
|
|
96
|
+
if (authorization.signal) {
|
|
97
|
+
const onAbort = (): void => this.#closeSubscriber(subscriber);
|
|
98
|
+
authorization.signal.addEventListener("abort", onAbort, {
|
|
99
|
+
once: true,
|
|
100
|
+
});
|
|
101
|
+
subscriber.removeAbortListener = () =>
|
|
102
|
+
authorization.signal?.removeEventListener("abort", onAbort);
|
|
103
|
+
if (authorization.signal.aborted) onAbort();
|
|
66
104
|
}
|
|
67
105
|
},
|
|
106
|
+
cancel: () => {
|
|
107
|
+
this.#closeSubscriber(subscriber, false);
|
|
108
|
+
},
|
|
68
109
|
});
|
|
69
110
|
|
|
70
111
|
return new Response(stream, {
|
|
@@ -81,30 +122,60 @@ export class DocumentEventBus {
|
|
|
81
122
|
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`
|
|
82
123
|
);
|
|
83
124
|
|
|
84
|
-
for (const
|
|
125
|
+
for (const subscriber of this.#subscribers) {
|
|
126
|
+
if (!subscriber.authorization.isAuthorizationEpochCurrent()) {
|
|
127
|
+
this.#invalidateSubscriber(subscriber);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
85
130
|
try {
|
|
86
|
-
controller.enqueue(payload);
|
|
131
|
+
subscriber.controller.enqueue(payload);
|
|
87
132
|
} catch {
|
|
88
|
-
this.#
|
|
133
|
+
this.#closeSubscriber(subscriber);
|
|
89
134
|
}
|
|
90
135
|
}
|
|
91
136
|
}
|
|
92
137
|
|
|
93
138
|
close(): void {
|
|
94
|
-
for (const
|
|
95
|
-
|
|
96
|
-
controller.close();
|
|
97
|
-
} catch {
|
|
98
|
-
// Best-effort shutdown.
|
|
99
|
-
}
|
|
139
|
+
for (const subscriber of this.#subscribers) {
|
|
140
|
+
this.#closeSubscriber(subscriber);
|
|
100
141
|
}
|
|
101
|
-
this.#controllers.clear();
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
getState(): DocumentEventBusState {
|
|
105
145
|
return {
|
|
106
|
-
connectedClients: this.#
|
|
146
|
+
connectedClients: this.#subscribers.size,
|
|
107
147
|
retryMs: EVENT_RETRY_MS,
|
|
108
148
|
};
|
|
109
149
|
}
|
|
150
|
+
|
|
151
|
+
#invalidateSubscriber(subscriber: DocumentEventSubscriber | null): void {
|
|
152
|
+
if (!subscriber || subscriber.closed) return;
|
|
153
|
+
try {
|
|
154
|
+
subscriber.controller.enqueue(POLICY_CHANGED_FRAME);
|
|
155
|
+
} catch {
|
|
156
|
+
// The stream may already be cancelled.
|
|
157
|
+
}
|
|
158
|
+
this.#closeSubscriber(subscriber);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#closeSubscriber(
|
|
162
|
+
subscriber: DocumentEventSubscriber | null,
|
|
163
|
+
closeController = true
|
|
164
|
+
): void {
|
|
165
|
+
if (!subscriber || subscriber.closed) return;
|
|
166
|
+
subscriber.closed = true;
|
|
167
|
+
if (subscriber.keepaliveTimer) {
|
|
168
|
+
clearInterval(subscriber.keepaliveTimer);
|
|
169
|
+
}
|
|
170
|
+
subscriber.removeAbortListener?.();
|
|
171
|
+
this.#subscribers.delete(subscriber);
|
|
172
|
+
if (closeController) {
|
|
173
|
+
try {
|
|
174
|
+
subscriber.controller.close();
|
|
175
|
+
} catch {
|
|
176
|
+
// Best-effort stream shutdown.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
subscriber.authorization.onClose?.();
|
|
180
|
+
}
|
|
110
181
|
}
|