@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.
Files changed (143) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +33 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +1 -1
  10. package/spec/cli.md +41 -1
  11. package/spec/db/schema.sql +19 -0
  12. package/spec/mcp.md +66 -0
  13. package/spec/output-schemas/ask.schema.json +31 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  22. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  23. package/spec/output-schemas/record-import.schema.json +30 -0
  24. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  25. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  26. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  27. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  28. package/spec/output-schemas/search-results.schema.json +30 -0
  29. package/src/app/context-runtime-contract.ts +16 -7
  30. package/src/cli/commands/ask.ts +7 -0
  31. package/src/cli/commands/collection/index.ts +5 -0
  32. package/src/cli/commands/collection/policy.ts +126 -0
  33. package/src/cli/commands/context-build.ts +5 -0
  34. package/src/cli/commands/daemon.ts +45 -3
  35. package/src/cli/commands/egress-audit.ts +68 -0
  36. package/src/cli/commands/embed.ts +2 -0
  37. package/src/cli/commands/query.ts +12 -0
  38. package/src/cli/commands/replay.ts +13 -3
  39. package/src/cli/commands/vsearch.ts +3 -1
  40. package/src/cli/program.ts +123 -0
  41. package/src/config/index.ts +9 -0
  42. package/src/config/types.ts +50 -0
  43. package/src/core/browser-clip.ts +16 -1
  44. package/src/core/collection-egress-policy-projection.ts +28 -0
  45. package/src/core/collection-egress-policy-service.ts +443 -0
  46. package/src/core/collection-egress-policy-validation.ts +242 -0
  47. package/src/core/config-mutation.ts +31 -19
  48. package/src/core/context-capsule-schema.ts +111 -1
  49. package/src/core/context-capsule-validation.ts +4 -4
  50. package/src/core/context-capsule-verification.ts +5 -1
  51. package/src/core/context-capsule.ts +177 -132
  52. package/src/core/context-evidence.ts +86 -14
  53. package/src/core/destination-classifier.ts +402 -0
  54. package/src/core/egress-audit.ts +239 -0
  55. package/src/core/egress-authorization.ts +50 -0
  56. package/src/core/egress-enforcement.ts +264 -0
  57. package/src/core/egress-policy.ts +440 -0
  58. package/src/core/egress-provenance.ts +336 -0
  59. package/src/core/job-manager.ts +37 -0
  60. package/src/core/network-boundary-inventory.ts +261 -0
  61. package/src/core/retrieval-qrels.ts +6 -0
  62. package/src/core/retrieval-replay.ts +18 -4
  63. package/src/core/retrieval-trace-export.ts +23 -4
  64. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  65. package/src/core/retrieval-trace-management-types.ts +3 -0
  66. package/src/core/retrieval-trace-management.ts +9 -1
  67. package/src/core/retrieval-trace-session.ts +94 -0
  68. package/src/core/retrieval-trace.ts +22 -0
  69. package/src/ingestion/record-adapter.ts +12 -1
  70. package/src/ingestion/record-container.ts +38 -22
  71. package/src/ingestion/sync.ts +15 -0
  72. package/src/ingestion/types.ts +2 -0
  73. package/src/llm/errors.ts +10 -0
  74. package/src/llm/http-inference.ts +175 -0
  75. package/src/llm/http-policy.ts +537 -0
  76. package/src/llm/httpEmbedding.ts +47 -28
  77. package/src/llm/httpGeneration.ts +31 -18
  78. package/src/llm/httpRerank.ts +30 -18
  79. package/src/llm/index.ts +1 -0
  80. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  81. package/src/llm/pinned-http-connection.ts +177 -0
  82. package/src/mcp/context.ts +53 -1
  83. package/src/mcp/http-egress.ts +220 -0
  84. package/src/mcp/http-security.ts +32 -21
  85. package/src/mcp/http-session.ts +13 -2
  86. package/src/mcp/http-transport.ts +96 -6
  87. package/src/mcp/sync-egress.ts +24 -0
  88. package/src/mcp/tools/add-collection.ts +4 -0
  89. package/src/mcp/tools/ask.ts +1 -1
  90. package/src/mcp/tools/context.ts +9 -2
  91. package/src/mcp/tools/egress.ts +247 -0
  92. package/src/mcp/tools/embed.ts +1 -0
  93. package/src/mcp/tools/index-cmd.ts +7 -0
  94. package/src/mcp/tools/index.ts +98 -0
  95. package/src/mcp/tools/query.ts +10 -0
  96. package/src/mcp/tools/remove-collection.ts +4 -0
  97. package/src/mcp/tools/sync.ts +6 -0
  98. package/src/mcp/tools/trace.ts +7 -1
  99. package/src/mcp/tools/vsearch.ts +1 -0
  100. package/src/pipeline/egress-lineage.ts +124 -0
  101. package/src/pipeline/hybrid.ts +14 -0
  102. package/src/pipeline/search.ts +12 -0
  103. package/src/pipeline/types.ts +3 -0
  104. package/src/pipeline/vsearch.ts +41 -20
  105. package/src/publish/artifact.ts +31 -3
  106. package/src/publish/export-service.ts +25 -0
  107. package/src/sdk/client.ts +119 -2
  108. package/src/sdk/embed.ts +1 -0
  109. package/src/sdk/types.ts +33 -0
  110. package/src/serve/clipper-capture.ts +5 -1
  111. package/src/serve/closed-json.ts +61 -0
  112. package/src/serve/config-sync.ts +4 -1
  113. package/src/serve/context.ts +1 -0
  114. package/src/serve/doc-events.ts +110 -39
  115. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  116. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  117. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  118. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  119. package/src/serve/public/globals.built.css +1 -1
  120. package/src/serve/public/pages/Collections.tsx +3 -0
  121. package/src/serve/resident-request.ts +80 -3
  122. package/src/serve/resident-runtime.ts +63 -1
  123. package/src/serve/routes/api.ts +174 -0
  124. package/src/serve/routes/clipper.ts +64 -1
  125. package/src/serve/routes/mcp.ts +6 -0
  126. package/src/serve/routes/traces.ts +27 -4
  127. package/src/serve/server.ts +118 -7
  128. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  129. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  130. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  131. package/src/store/migrations/index.ts +6 -0
  132. package/src/store/retrieval-trace-codec.ts +15 -0
  133. package/src/store/sqlite/adapter.ts +170 -7
  134. package/src/store/sqlite/change-journal-store.ts +41 -2
  135. package/src/store/sqlite/egress-audit-store.ts +485 -0
  136. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  137. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  138. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  139. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  140. package/src/store/types.ts +126 -1
  141. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
  142. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
  143. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -0,0 +1,124 @@
1
+ /** Attach complete collection policy ownership to observable search results. */
2
+
3
+ import type { EgressLineage } from "../core/egress-provenance";
4
+ import type {
5
+ CollectionRow,
6
+ DocumentRow,
7
+ StorePort,
8
+ StoreResult,
9
+ } from "../store/types";
10
+ import type { SearchResult } from "./types";
11
+
12
+ import { parseUri } from "../app/constants";
13
+ import { createEgressLineage } from "../core/egress-provenance";
14
+ import { err, ok } from "../store/types";
15
+
16
+ type EgressLineageStore = Pick<
17
+ StorePort,
18
+ "getCollections" | "getDocumentsByMirrorHashes"
19
+ >;
20
+
21
+ export const attachSearchResultEgressLineage = async (
22
+ store: EgressLineageStore,
23
+ results: SearchResult[],
24
+ options: {
25
+ ownershipHashes?: readonly string[];
26
+ ownershipDocuments?: readonly Pick<
27
+ DocumentRow,
28
+ "collection" | "mirrorHash"
29
+ >[];
30
+ collections?: readonly CollectionRow[];
31
+ } = {}
32
+ ): Promise<StoreResult<void>> => {
33
+ if (results.length === 0) return ok(undefined);
34
+ const mirrorHashes = [
35
+ ...new Set(
36
+ options.ownershipHashes ??
37
+ results
38
+ .map((result) => result.conversion?.mirrorHash)
39
+ .filter((hash): hash is string => Boolean(hash))
40
+ ),
41
+ ];
42
+ let ownershipDocuments = options.ownershipDocuments;
43
+ if (ownershipDocuments === undefined) {
44
+ if (typeof store.getDocumentsByMirrorHashes !== "function") {
45
+ ownershipDocuments = [];
46
+ } else {
47
+ const documentsResult = await store.getDocumentsByMirrorHashes(
48
+ mirrorHashes,
49
+ {
50
+ activeOnly: true,
51
+ }
52
+ );
53
+ if (!documentsResult.ok) return documentsResult;
54
+ ownershipDocuments = documentsResult.value;
55
+ }
56
+ }
57
+ let collectionRows = options.collections;
58
+ if (collectionRows === undefined) {
59
+ const collectionsResult = await store.getCollections();
60
+ if (!collectionsResult.ok) return collectionsResult;
61
+ collectionRows = collectionsResult.value;
62
+ }
63
+ const policyByCollection = new Map(
64
+ collectionRows.map((collection) => [
65
+ collection.name,
66
+ {
67
+ collection: collection.name,
68
+ policy: collection.egressPolicy,
69
+ source: collection.egressPolicySource,
70
+ },
71
+ ])
72
+ );
73
+ const lineageByMirrorHash = new Map<string, EgressLineage>();
74
+ for (const mirrorHash of mirrorHashes) {
75
+ const ownerCollections = [
76
+ ...new Set([
77
+ ...ownershipDocuments
78
+ .filter((document) => document.mirrorHash === mirrorHash)
79
+ .map((owner) => owner.collection),
80
+ ...results
81
+ .filter((result) => result.conversion?.mirrorHash === mirrorHash)
82
+ .map((result) => parseUri(result.uri)?.collection)
83
+ .filter((collection): collection is string => Boolean(collection)),
84
+ ]),
85
+ ];
86
+ const policies = ownerCollections.map((collection) =>
87
+ policyByCollection.get(collection)
88
+ );
89
+ if (policies.some((policy) => policy === undefined)) {
90
+ return err(
91
+ "INVALID_INPUT",
92
+ "Search result policy lineage references an unknown collection"
93
+ );
94
+ }
95
+ if (policies.length === 0) continue;
96
+ try {
97
+ lineageByMirrorHash.set(
98
+ mirrorHash,
99
+ createEgressLineage(
100
+ policies as NonNullable<(typeof policies)[number]>[]
101
+ )
102
+ );
103
+ } catch (error) {
104
+ return err(
105
+ "INVALID_INPUT",
106
+ "Search result policy lineage is incomplete",
107
+ error
108
+ );
109
+ }
110
+ }
111
+ for (const result of results) {
112
+ const lineage = result.conversion?.mirrorHash
113
+ ? lineageByMirrorHash.get(result.conversion.mirrorHash)
114
+ : undefined;
115
+ if (!lineage) {
116
+ return err(
117
+ "INVALID_INPUT",
118
+ "Search result policy lineage could not be resolved"
119
+ );
120
+ }
121
+ result.egressLineage = lineage;
122
+ }
123
+ return ok(undefined);
124
+ };
@@ -32,6 +32,7 @@ import {
32
32
  sortByFinalScoreStable,
33
33
  } from "./content-type-boost";
34
34
  import { formatQueryForEmbedding } from "./contextual";
35
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
35
36
  import { expandQuery } from "./expansion";
36
37
  import {
37
38
  buildExplainResults,
@@ -1169,6 +1170,19 @@ export async function searchHybrid(
1169
1170
  }
1170
1171
  : undefined;
1171
1172
  await attachSearchResultContexts(store, finalResults);
1173
+ const lineageResult = await attachSearchResultEgressLineage(
1174
+ store,
1175
+ finalResults,
1176
+ {
1177
+ ownershipHashes: [...neededHashes],
1178
+ ownershipDocuments:
1179
+ options.collection === undefined ? documents : undefined,
1180
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
1181
+ }
1182
+ );
1183
+ if (!lineageResult.ok) {
1184
+ return err("QUERY_FAILED", lineageResult.error.message);
1185
+ }
1172
1186
 
1173
1187
  const output: SearchResults = {
1174
1188
  results: finalResults,
@@ -24,6 +24,7 @@ import {
24
24
  hasAuxiliaryRanking,
25
25
  sortByFinalScoreStable,
26
26
  } from "./content-type-boost";
27
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
27
28
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
28
29
  import { selectBestChunkForSteering } from "./intent";
29
30
  import { hasProjectAffinity } from "./project-affinity";
@@ -433,6 +434,17 @@ export async function searchBm25(
433
434
  if (metadata) metadata.retrievalRank = index + 1;
434
435
  }
435
436
  await attachSearchResultContexts(store, finalResults);
437
+ const lineageResult = await attachSearchResultEgressLineage(
438
+ store,
439
+ finalResults,
440
+ {
441
+ ownershipHashes: uniqueHashes,
442
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
443
+ }
444
+ );
445
+ if (!lineageResult.ok) {
446
+ return err("QUERY_FAILED", lineageResult.error.message);
447
+ }
436
448
 
437
449
  const output: SearchResults = {
438
450
  results: finalResults,
@@ -10,6 +10,7 @@ import type {
10
10
  ContextCapsuleV1,
11
11
  ContextCapsuleVerification,
12
12
  } from "../core/context-capsule";
13
+ import type { EgressLineage } from "../core/egress-provenance";
13
14
  import type { RecordEvidenceMetadata } from "../core/record-metadata";
14
15
  import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
15
16
  import type { StoreResult } from "../store/types";
@@ -89,6 +90,8 @@ export interface SearchResult {
89
90
  conversion?: SearchResultConversion;
90
91
  /** Bounded logical-record provenance for file/export adapters. */
91
92
  record?: RecordEvidenceMetadata;
93
+ /** Complete, most-restrictive collection ownership for this derived result. */
94
+ egressLineage?: EgressLineage;
92
95
  [SEARCH_RESULT_PLANNER_METADATA]?: SearchResultPlannerMetadata;
93
96
  }
94
97
 
@@ -22,6 +22,7 @@ import {
22
22
  sortByFinalScoreStable,
23
23
  } from "./content-type-boost";
24
24
  import { formatQueryForEmbedding } from "./contextual";
25
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
25
26
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
26
27
  import { selectBestChunkForSteering } from "./intent";
27
28
  import { hasProjectAffinity } from "./project-affinity";
@@ -143,17 +144,18 @@ export async function searchVectorWithEmbedding(
143
144
  }
144
145
 
145
146
  // Cache docs to avoid N+1 queries (filtered by collection and tags)
146
- const docsByMirrorHash = await buildDocumentMap(store, {
147
- collection: options.collection,
148
- relPathPrefix: options.retrievalScope?.relPathPrefix,
149
- tagsAll: options.tagsAll,
150
- tagsAny: options.tagsAny,
151
- since: temporalRange.since,
152
- until: temporalRange.until,
153
- categories: options.categories,
154
- author: options.author,
155
- mirrorHashes: uniqueHashes,
156
- });
147
+ const { documents: docsByMirrorHash, ownershipDocuments } =
148
+ await buildDocumentMap(store, {
149
+ collection: options.collection,
150
+ relPathPrefix: options.retrievalScope?.relPathPrefix,
151
+ tagsAll: options.tagsAll,
152
+ tagsAny: options.tagsAny,
153
+ since: temporalRange.since,
154
+ until: temporalRange.until,
155
+ categories: options.categories,
156
+ author: options.author,
157
+ mirrorHashes: uniqueHashes,
158
+ });
157
159
 
158
160
  // Pre-fetch all chunks in one batch query (eliminates N+1)
159
161
  const chunksMapResult = await store.getChunksBatch(uniqueHashes);
@@ -423,6 +425,18 @@ export async function searchVectorWithEmbedding(
423
425
  if (metadata) metadata.retrievalRank = index + 1;
424
426
  }
425
427
  await attachSearchResultContexts(store, finalResults);
428
+ const lineageResult = await attachSearchResultEgressLineage(
429
+ store,
430
+ finalResults,
431
+ {
432
+ ownershipHashes: uniqueHashes,
433
+ ownershipDocuments,
434
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
435
+ }
436
+ );
437
+ if (!lineageResult.ok) {
438
+ return err("QUERY_FAILED", lineageResult.error.message);
439
+ }
426
440
 
427
441
  const output: SearchResults = {
428
442
  results: finalResults,
@@ -560,28 +574,35 @@ function matchesCategoryFilter(
560
574
  async function buildDocumentMap(
561
575
  store: StorePort,
562
576
  options: DocumentMapOptions = {}
563
- ): Promise<Map<string, DocumentInfo[]>> {
564
- const result = new Map<string, DocumentInfo[]>();
577
+ ): Promise<{
578
+ documents: Map<string, DocumentInfo[]>;
579
+ ownershipDocuments: DocumentRow[];
580
+ }> {
581
+ const documents = new Map<string, DocumentInfo[]>();
565
582
 
566
583
  if (options.mirrorHashes && options.mirrorHashes.length === 0) {
567
- return result;
584
+ return { documents, ownershipDocuments: [] };
568
585
  }
569
586
 
570
587
  const docs = options.mirrorHashes
571
588
  ? await store.getDocumentsByMirrorHashes(options.mirrorHashes, {
572
- collection: options.collection,
573
589
  activeOnly: true,
574
590
  })
575
591
  : await store.listDocuments(options.collection);
576
592
  if (!docs.ok) {
577
- return result;
593
+ return { documents, ownershipDocuments: [] };
578
594
  }
579
595
 
580
596
  // Filter docs with mirrorHash.
581
597
  // listDocuments path still needs explicit active filter.
582
- const activeDocs = options.mirrorHashes
598
+ const ownershipDocuments = options.mirrorHashes
583
599
  ? docs.value.filter((d) => d.mirrorHash)
584
600
  : docs.value.filter((d) => d.mirrorHash && d.active);
601
+ const activeDocs = options.collection
602
+ ? ownershipDocuments.filter(
603
+ (document) => document.collection === options.collection
604
+ )
605
+ : ownershipDocuments;
585
606
  const temporalRange: TemporalRange = {
586
607
  since: options.since,
587
608
  until: options.until,
@@ -674,10 +695,10 @@ async function buildDocumentMap(
674
695
  recordAnchors: doc.recordAnchors,
675
696
  recordAdapterFingerprint: doc.recordAdapterFingerprint,
676
697
  };
677
- const matchingDocuments = result.get(doc.mirrorHash!) ?? [];
698
+ const matchingDocuments = documents.get(doc.mirrorHash!) ?? [];
678
699
  matchingDocuments.push(documentInfo);
679
- result.set(doc.mirrorHash!, matchingDocuments);
700
+ documents.set(doc.mirrorHash!, matchingDocuments);
680
701
  }
681
702
 
682
- return result;
703
+ return { documents, ownershipDocuments };
683
704
  }
@@ -4,6 +4,7 @@
4
4
  * @module src/publish/artifact
5
5
  */
6
6
 
7
+ import type { EgressLineage } from "../core/egress-provenance";
7
8
  import type { DocumentRow } from "../store/types";
8
9
 
9
10
  import { deriveDocid } from "../app/constants";
@@ -11,6 +12,10 @@ import {
11
12
  contextCapsuleEvidenceIdentity,
12
13
  sha256Text,
13
14
  } from "../core/context-capsule-validation";
15
+ import {
16
+ egressLineageSchema,
17
+ legacyLocalOnlyEgressLineage,
18
+ } from "../core/egress-provenance";
14
19
  import { stripFrontmatter } from "../ingestion/frontmatter";
15
20
  import {
16
21
  MAX_PUBLISH_SLUG_LENGTH,
@@ -71,6 +76,7 @@ export interface PublicPublishDocument {
71
76
  export interface PublicPublishManifest {
72
77
  capabilities: typeof PUBLIC_PUBLISH_CAPABILITIES;
73
78
  documents: PublicPublishDocument[];
79
+ egressLineage: EgressLineage;
74
80
  generatedAt: string;
75
81
  projectionRevision: string;
76
82
  schemaVersion: typeof PUBLIC_PUBLISH_MANIFEST_SCHEMA_VERSION;
@@ -116,6 +122,7 @@ export interface EncryptedPublishArtifactSpace {
116
122
  }
117
123
 
118
124
  export interface PublishArtifactV1 {
125
+ egressLineage: EgressLineage;
119
126
  exportedAt: string;
120
127
  source: string;
121
128
  spaces: PublishArtifactSpace[];
@@ -123,6 +130,7 @@ export interface PublishArtifactV1 {
123
130
  }
124
131
 
125
132
  export interface PublishArtifactV2 {
133
+ egressLineage: EgressLineage;
126
134
  exportedAt: string;
127
135
  source: string;
128
136
  spaces: EncryptedPublishArtifactSpace[];
@@ -279,6 +287,7 @@ const buildPublicPublishDocument = (
279
287
  };
280
288
 
281
289
  export const buildPublicPublishManifest = (input: {
290
+ egressLineage?: EgressLineage;
282
291
  exportedAt: string;
283
292
  homeNoteSlug?: string;
284
293
  notes: PublishArtifactNote[];
@@ -288,7 +297,11 @@ export const buildPublicPublishManifest = (input: {
288
297
  title: string;
289
298
  visibility: "public";
290
299
  }): PublicPublishManifest => {
291
- const validated = validateAndProjectPublishSpaceInput(input);
300
+ const { egressLineage: providedLineage, ...spaceInput } = input;
301
+ const validated = validateAndProjectPublishSpaceInput(spaceInput);
302
+ const egressLineage = egressLineageSchema.parse(
303
+ providedLineage ?? legacyLocalOnlyEgressLineage("legacy")
304
+ );
292
305
  if (validated.visibility !== "public") {
293
306
  throw new Error("Public publish manifests require public visibility");
294
307
  }
@@ -304,6 +317,7 @@ export const buildPublicPublishManifest = (input: {
304
317
  const revisionProjection = {
305
318
  capabilities: PUBLIC_PUBLISH_CAPABILITIES,
306
319
  documents,
320
+ egressLineage,
307
321
  homeNoteSlug: validated.homeNoteSlug ?? null,
308
322
  notes: validated.notes
309
323
  .map((note) => ({
@@ -324,6 +338,7 @@ export const buildPublicPublishManifest = (input: {
324
338
  return {
325
339
  capabilities: { ...PUBLIC_PUBLISH_CAPABILITIES },
326
340
  documents,
341
+ egressLineage,
327
342
  generatedAt: exportedAt,
328
343
  projectionRevision: sha256Text(canonicalJson(revisionProjection)),
329
344
  schemaVersion: PUBLIC_PUBLISH_MANIFEST_SCHEMA_VERSION,
@@ -332,6 +347,7 @@ export const buildPublicPublishManifest = (input: {
332
347
  };
333
348
 
334
349
  export const buildPublishArtifact = (input: {
350
+ egressLineage?: EgressLineage;
335
351
  homeNoteSlug?: string;
336
352
  notes: PublishArtifactNote[];
337
353
  routeSlug: string;
@@ -340,7 +356,11 @@ export const buildPublishArtifact = (input: {
340
356
  title: string;
341
357
  visibility: Exclude<PublishVisibility, "encrypted">;
342
358
  }): PublishArtifactV1 => {
343
- const validated = validateAndProjectPublishSpaceInput(input);
359
+ const { egressLineage: providedLineage, ...spaceInput } = input;
360
+ const validated = validateAndProjectPublishSpaceInput(spaceInput);
361
+ const egressLineage = egressLineageSchema.parse(
362
+ providedLineage ?? legacyLocalOnlyEgressLineage("legacy")
363
+ );
344
364
  const exportedAt = new Date().toISOString();
345
365
  const base = {
346
366
  ...(validated.homeNoteSlug === undefined
@@ -357,6 +377,7 @@ export const buildPublishArtifact = (input: {
357
377
  ? {
358
378
  ...base,
359
379
  manifest: buildPublicPublishManifest({
380
+ egressLineage,
360
381
  exportedAt,
361
382
  homeNoteSlug: validated.homeNoteSlug,
362
383
  notes: validated.notes,
@@ -374,6 +395,7 @@ export const buildPublishArtifact = (input: {
374
395
  };
375
396
 
376
397
  return {
398
+ egressLineage,
377
399
  exportedAt,
378
400
  source: validated.routeSlug,
379
401
  spaces: [space],
@@ -382,14 +404,20 @@ export const buildPublishArtifact = (input: {
382
404
  };
383
405
 
384
406
  export const buildEncryptedPublishArtifact = (input: {
407
+ egressLineage?: EgressLineage;
385
408
  encryptedPayload: EncryptedArtifactPayload;
386
409
  routeSlug: string;
387
410
  secretToken: string;
388
411
  sourceType: "note" | "collection";
389
412
  }): PublishArtifactV2 => {
390
- const validated = validateAndProjectEncryptedPublishInput(input);
413
+ const { egressLineage: providedLineage, ...encryptedInput } = input;
414
+ const validated = validateAndProjectEncryptedPublishInput(encryptedInput);
415
+ const egressLineage = egressLineageSchema.parse(
416
+ providedLineage ?? legacyLocalOnlyEgressLineage("legacy")
417
+ );
391
418
  const exportedAt = requirePublishDateTime(new Date().toISOString());
392
419
  return {
420
+ egressLineage,
393
421
  exportedAt,
394
422
  source: validated.routeSlug,
395
423
  spaces: [
@@ -7,6 +7,7 @@
7
7
  import type { Collection } from "../config/types";
8
8
  import type { DocumentRow, StorePort, TagRow } from "../store/types";
9
9
 
10
+ import { enforceCollectionEgressWithAudit } from "../core/egress-enforcement";
10
11
  import { parseRef } from "../core/ref-parser";
11
12
  import { parseFrontmatter, stripFrontmatter } from "../ingestion/frontmatter";
12
13
  import { getContentBatch } from "../store/content-batch";
@@ -217,6 +218,15 @@ async function exportCollectionArtifact(
217
218
  target,
218
219
  ]);
219
220
  const visibility = resolveVisibility(options.visibility);
221
+ const { lineage } = await enforceCollectionEgressWithAudit({
222
+ collections,
223
+ collectionNames: [collection.name],
224
+ action: "export",
225
+ destinationZone: "local_process",
226
+ caller: { authenticated: true, operationAuthorized: true },
227
+ contentClass: "source",
228
+ store,
229
+ });
220
230
 
221
231
  if (visibility === "encrypted") {
222
232
  if (!options.encryptionPassphrase) {
@@ -237,6 +247,7 @@ async function exportCollectionArtifact(
237
247
  });
238
248
 
239
249
  return buildEncryptedPublishArtifact({
250
+ egressLineage: lineage,
240
251
  encryptedPayload: encrypted.encryptedPayload,
241
252
  routeSlug,
242
253
  secretToken: encrypted.secretToken,
@@ -245,6 +256,7 @@ async function exportCollectionArtifact(
245
256
  }
246
257
 
247
258
  return buildPublishArtifact({
259
+ egressLineage: lineage,
248
260
  homeNoteSlug: chooseHomeNoteSlug(notes),
249
261
  notes,
250
262
  routeSlug,
@@ -257,6 +269,7 @@ async function exportCollectionArtifact(
257
269
 
258
270
  async function exportDocumentArtifact(
259
271
  store: StorePort,
272
+ collections: Collection[],
260
273
  target: string,
261
274
  options: PublishExportCoreOptions,
262
275
  warnings: SanitizeWarning[]
@@ -283,6 +296,15 @@ async function exportDocumentArtifact(
283
296
  const slug = deriveExportedSlug(doc);
284
297
  const visibility = resolveVisibility(options.visibility);
285
298
  const routeSlug = derivePublishSlug([options.routeSlug ?? "", slug, target]);
299
+ const { lineage } = await enforceCollectionEgressWithAudit({
300
+ collections,
301
+ collectionNames: [doc.collection],
302
+ action: "export",
303
+ destinationZone: "local_process",
304
+ caller: { authenticated: true, operationAuthorized: true },
305
+ contentClass: "source",
306
+ store,
307
+ });
286
308
 
287
309
  if (visibility === "encrypted") {
288
310
  if (!options.encryptionPassphrase) {
@@ -310,6 +332,7 @@ async function exportDocumentArtifact(
310
332
  });
311
333
 
312
334
  return buildEncryptedPublishArtifact({
335
+ egressLineage: lineage,
313
336
  encryptedPayload: encrypted.encryptedPayload,
314
337
  routeSlug,
315
338
  secretToken: encrypted.secretToken,
@@ -318,6 +341,7 @@ async function exportDocumentArtifact(
318
341
  }
319
342
 
320
343
  return buildPublishArtifact({
344
+ egressLineage: lineage,
321
345
  notes: [
322
346
  {
323
347
  markdown,
@@ -357,6 +381,7 @@ export async function exportPublishArtifact(input: {
357
381
  )) ??
358
382
  (await exportDocumentArtifact(
359
383
  input.store,
384
+ input.collections,
360
385
  input.target,
361
386
  input.options,
362
387
  warnings