@gmickel/gno 1.27.1 → 1.29.0
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 +59 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.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 +2 -2
- package/spec/cli.md +82 -5
- package/spec/db/schema.sql +31 -0
- package/spec/mcp.md +71 -0
- package/spec/output-schemas/ask.schema.json +156 -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 +183 -3
- 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/get.schema.json +94 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +223 -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-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +155 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- 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/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +127 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +66 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -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 +198 -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 -112
- package/src/core/context-evidence.ts +93 -15
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/document-capabilities.ts +12 -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/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -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-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +448 -0
- package/src/ingestion/record-container.ts +704 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +243 -36
- package/src/ingestion/types.ts +71 -1
- package/src/ingestion/walker.ts +31 -11
- 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/get.ts +10 -2
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/multi-get.ts +9 -2
- 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/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +21 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +23 -4
- package/src/pipeline/types.ts +6 -0
- package/src/pipeline/vsearch.ts +67 -28
- 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/documents.ts +13 -5
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/browse-tree.ts +4 -2
- 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 +237 -65
- 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/022-record-export-lineage.ts +42 -0
- 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 +8 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +284 -14
- 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 +166 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-vn5f663b.js +0 -50
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shared current-policy authorization boundary for derived artifacts. */
|
|
2
|
+
|
|
3
|
+
import type { Collection } from "../config/types";
|
|
4
|
+
import type { StorePort, StoreResult } from "../store/types";
|
|
5
|
+
import type {
|
|
6
|
+
EgressAction,
|
|
7
|
+
EgressCallerContext,
|
|
8
|
+
EgressContentClass,
|
|
9
|
+
EgressDestinationZone,
|
|
10
|
+
} from "./egress-policy";
|
|
11
|
+
import type { EgressLineage } from "./egress-provenance";
|
|
12
|
+
|
|
13
|
+
import { err, ok } from "../store/types";
|
|
14
|
+
import { currentEgressSources } from "./collection-egress-policy-service";
|
|
15
|
+
import { EgressAuditService } from "./egress-audit";
|
|
16
|
+
import { evaluateEgressPolicy } from "./egress-policy";
|
|
17
|
+
import { resolveEgressLineage } from "./egress-provenance";
|
|
18
|
+
|
|
19
|
+
export const authorizeCurrentEgress = async (input: {
|
|
20
|
+
store: StorePort;
|
|
21
|
+
config: { collections: readonly Collection[] };
|
|
22
|
+
lineage: EgressLineage;
|
|
23
|
+
action: EgressAction;
|
|
24
|
+
destinationZone: EgressDestinationZone;
|
|
25
|
+
caller: EgressCallerContext;
|
|
26
|
+
contentClass: EgressContentClass;
|
|
27
|
+
}): Promise<StoreResult<EgressLineage>> => {
|
|
28
|
+
const currentLineage = resolveEgressLineage(
|
|
29
|
+
currentEgressSources(
|
|
30
|
+
input.config,
|
|
31
|
+
input.lineage.sources.map(({ collection }) => collection)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
const decision = evaluateEgressPolicy({
|
|
35
|
+
collections: currentLineage.sources,
|
|
36
|
+
action: input.action,
|
|
37
|
+
destination: { zone: input.destinationZone },
|
|
38
|
+
caller: input.caller,
|
|
39
|
+
contentClass: input.contentClass,
|
|
40
|
+
});
|
|
41
|
+
const recorded = await new EgressAuditService(input.store).record({
|
|
42
|
+
decision,
|
|
43
|
+
lineage: currentLineage,
|
|
44
|
+
contentClass: input.contentClass,
|
|
45
|
+
});
|
|
46
|
+
if (!recorded.ok) return recorded;
|
|
47
|
+
return decision.allowed
|
|
48
|
+
? ok(currentLineage)
|
|
49
|
+
: err("EGRESS_DENIED", "Operation blocked by collection egress policy");
|
|
50
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/** Shared fail-closed enforcement helpers for content-transfer boundaries. */
|
|
2
|
+
|
|
3
|
+
import type { Collection } from "../config/types";
|
|
4
|
+
import type { StorePort } from "../store/types";
|
|
5
|
+
import type {
|
|
6
|
+
EgressAction,
|
|
7
|
+
EgressCallerContext,
|
|
8
|
+
EgressContentClass,
|
|
9
|
+
EgressDecision,
|
|
10
|
+
EgressDestinationZone,
|
|
11
|
+
} from "./egress-policy";
|
|
12
|
+
import type { EgressLineage, EgressLineageInput } from "./egress-provenance";
|
|
13
|
+
|
|
14
|
+
import { resolveConfiguredEgressPolicy } from "../config/types";
|
|
15
|
+
import { EgressAuditService } from "./egress-audit";
|
|
16
|
+
import { evaluateEgressPolicy } from "./egress-policy";
|
|
17
|
+
import {
|
|
18
|
+
EgressProvenanceError,
|
|
19
|
+
resolveEgressLineage,
|
|
20
|
+
} from "./egress-provenance";
|
|
21
|
+
|
|
22
|
+
export const EGRESS_DENIED_MESSAGE =
|
|
23
|
+
"Operation blocked by collection egress policy";
|
|
24
|
+
|
|
25
|
+
export class EgressDeniedError extends Error {
|
|
26
|
+
readonly code = "EGRESS_DENIED";
|
|
27
|
+
readonly decision: EgressDecision;
|
|
28
|
+
|
|
29
|
+
constructor(decision: EgressDecision) {
|
|
30
|
+
super(`${decision.code}: ${EGRESS_DENIED_MESSAGE}`);
|
|
31
|
+
this.name = "EgressDeniedError";
|
|
32
|
+
this.decision = decision;
|
|
33
|
+
this.stack = `${this.name}: ${this.message}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toJSON(): {
|
|
37
|
+
code: "EGRESS_DENIED";
|
|
38
|
+
message: string;
|
|
39
|
+
reason: EgressDecision["reason"];
|
|
40
|
+
audit: EgressDecision["audit"];
|
|
41
|
+
} {
|
|
42
|
+
return {
|
|
43
|
+
code: this.code,
|
|
44
|
+
message: EGRESS_DENIED_MESSAGE,
|
|
45
|
+
reason: this.decision.reason,
|
|
46
|
+
audit: this.decision.audit,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const collectionLineageInputs = (
|
|
52
|
+
collections: readonly Collection[]
|
|
53
|
+
): readonly EgressLineageInput[] => {
|
|
54
|
+
try {
|
|
55
|
+
if (!Array.isArray(collections)) {
|
|
56
|
+
throw new EgressProvenanceError(
|
|
57
|
+
"INVALID_EGRESS_LINEAGE",
|
|
58
|
+
"Collection policy scope must be a readable dense bounded array"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const { length } = collections;
|
|
62
|
+
if (!Number.isSafeInteger(length) || length > 128) {
|
|
63
|
+
throw new EgressProvenanceError(
|
|
64
|
+
"INVALID_EGRESS_LINEAGE",
|
|
65
|
+
"Collection policy scope must be a readable dense bounded array"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const inputs: EgressLineageInput[] = [];
|
|
69
|
+
for (let index = 0; index < length; index += 1) {
|
|
70
|
+
if (!(index in collections)) {
|
|
71
|
+
throw new EgressProvenanceError(
|
|
72
|
+
"INVALID_EGRESS_LINEAGE",
|
|
73
|
+
"Collection policy scope must be a readable dense bounded array"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const collection: unknown = collections[index];
|
|
77
|
+
if (collection === null || typeof collection !== "object") {
|
|
78
|
+
throw new EgressProvenanceError(
|
|
79
|
+
"INVALID_EGRESS_LINEAGE",
|
|
80
|
+
"Collection policy scope contains an invalid collection"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const candidate = collection as Partial<Collection>;
|
|
84
|
+
const name = candidate.name;
|
|
85
|
+
const egressPolicy = candidate.egressPolicy;
|
|
86
|
+
const effective = resolveConfiguredEgressPolicy({ egressPolicy });
|
|
87
|
+
inputs.push({
|
|
88
|
+
collection: name as string,
|
|
89
|
+
policy: effective.policy,
|
|
90
|
+
source: effective.source,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return inputs;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof EgressProvenanceError) throw error;
|
|
96
|
+
throw new EgressProvenanceError(
|
|
97
|
+
"INVALID_EGRESS_LINEAGE",
|
|
98
|
+
"Collection policy scope could not be validated"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolveCollectionEgressLineage = (
|
|
104
|
+
collections: readonly Collection[],
|
|
105
|
+
names?: readonly string[]
|
|
106
|
+
): EgressLineage =>
|
|
107
|
+
resolveEgressLineage(collectionLineageInputs(collections), names);
|
|
108
|
+
|
|
109
|
+
export const collectionEgressStates = (
|
|
110
|
+
collections: readonly Collection[],
|
|
111
|
+
names?: readonly string[]
|
|
112
|
+
) => {
|
|
113
|
+
return resolveCollectionEgressLineage(collections, names).sources;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export interface EnforceCollectionEgressInput {
|
|
117
|
+
collections: readonly Collection[];
|
|
118
|
+
collectionNames?: readonly string[];
|
|
119
|
+
action: EgressAction;
|
|
120
|
+
destinationZone: EgressDestinationZone;
|
|
121
|
+
caller: EgressCallerContext;
|
|
122
|
+
contentClass: EgressContentClass;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface MixedCollectionEgressPlan {
|
|
126
|
+
mode: "complete" | "partial";
|
|
127
|
+
sourceLineage: EgressLineage;
|
|
128
|
+
allowedCollections: string[];
|
|
129
|
+
omittedCollections: Array<{
|
|
130
|
+
collection: string;
|
|
131
|
+
reason: EgressDecision["reason"];
|
|
132
|
+
}>;
|
|
133
|
+
disclosure: null | {
|
|
134
|
+
code: "EGRESS_PARTIAL_RESULT";
|
|
135
|
+
omittedCount: number;
|
|
136
|
+
omittedCollections: string[];
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Plan one mixed-source boundary. Denial is the default; partial emission
|
|
142
|
+
* requires an explicit caller opt-in and always discloses sorted omissions.
|
|
143
|
+
*/
|
|
144
|
+
export const planCollectionEgress = (
|
|
145
|
+
input: EnforceCollectionEgressInput & {
|
|
146
|
+
partialResults?: "deny" | "explicit";
|
|
147
|
+
}
|
|
148
|
+
): MixedCollectionEgressPlan => {
|
|
149
|
+
const sourceLineage = resolveCollectionEgressLineage(
|
|
150
|
+
input.collections,
|
|
151
|
+
input.collectionNames
|
|
152
|
+
);
|
|
153
|
+
const aggregate = evaluateEgressPolicy({
|
|
154
|
+
collections: sourceLineage.sources,
|
|
155
|
+
action: input.action,
|
|
156
|
+
destination: { zone: input.destinationZone },
|
|
157
|
+
caller: input.caller,
|
|
158
|
+
contentClass: input.contentClass,
|
|
159
|
+
});
|
|
160
|
+
if (aggregate.allowed) {
|
|
161
|
+
return {
|
|
162
|
+
mode: "complete",
|
|
163
|
+
sourceLineage,
|
|
164
|
+
allowedCollections: sourceLineage.sources.map(
|
|
165
|
+
({ collection }) => collection
|
|
166
|
+
),
|
|
167
|
+
omittedCollections: [],
|
|
168
|
+
disclosure: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (input.partialResults !== "explicit") {
|
|
172
|
+
throw new EgressDeniedError(aggregate);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const allowedCollections: string[] = [];
|
|
176
|
+
const omittedCollections: MixedCollectionEgressPlan["omittedCollections"] =
|
|
177
|
+
[];
|
|
178
|
+
for (const source of sourceLineage.sources) {
|
|
179
|
+
const perCollection = evaluateEgressPolicy({
|
|
180
|
+
collections: [source],
|
|
181
|
+
action: input.action,
|
|
182
|
+
destination: { zone: input.destinationZone },
|
|
183
|
+
caller: input.caller,
|
|
184
|
+
contentClass: input.contentClass,
|
|
185
|
+
});
|
|
186
|
+
if (perCollection.allowed) allowedCollections.push(source.collection);
|
|
187
|
+
else {
|
|
188
|
+
omittedCollections.push({
|
|
189
|
+
collection: source.collection,
|
|
190
|
+
reason: perCollection.reason,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (allowedCollections.length === 0) {
|
|
195
|
+
throw new EgressDeniedError(aggregate);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
mode: "partial",
|
|
199
|
+
sourceLineage,
|
|
200
|
+
allowedCollections,
|
|
201
|
+
omittedCollections,
|
|
202
|
+
disclosure: {
|
|
203
|
+
code: "EGRESS_PARTIAL_RESULT",
|
|
204
|
+
omittedCount: omittedCollections.length,
|
|
205
|
+
omittedCollections: omittedCollections.map(
|
|
206
|
+
({ collection }) => collection
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const enforceCollectionEgress = (
|
|
213
|
+
input: EnforceCollectionEgressInput
|
|
214
|
+
): EgressDecision => {
|
|
215
|
+
const plan = planCollectionEgress(input);
|
|
216
|
+
const decision = evaluateEgressPolicy({
|
|
217
|
+
collections: plan.sourceLineage.sources,
|
|
218
|
+
action: input.action,
|
|
219
|
+
destination: { zone: input.destinationZone },
|
|
220
|
+
caller: input.caller,
|
|
221
|
+
contentClass: input.contentClass,
|
|
222
|
+
});
|
|
223
|
+
if (!decision.allowed) throw new EgressDeniedError(decision);
|
|
224
|
+
return decision;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Enforce and durably record the content-free decision before transfer. */
|
|
228
|
+
export const enforceCollectionEgressWithAudit = async (
|
|
229
|
+
input: EnforceCollectionEgressInput & { store: StorePort }
|
|
230
|
+
): Promise<{ decision: EgressDecision; lineage: EgressLineage }> => {
|
|
231
|
+
const lineage = resolveCollectionEgressLineage(
|
|
232
|
+
input.collections,
|
|
233
|
+
input.collectionNames
|
|
234
|
+
);
|
|
235
|
+
let decision: EgressDecision;
|
|
236
|
+
let denied: EgressDeniedError | null = null;
|
|
237
|
+
try {
|
|
238
|
+
decision = enforceCollectionEgress(input);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!(error instanceof EgressDeniedError)) throw error;
|
|
241
|
+
decision = error.decision;
|
|
242
|
+
denied = error;
|
|
243
|
+
}
|
|
244
|
+
const recorded = await new EgressAuditService(input.store).record({
|
|
245
|
+
decision,
|
|
246
|
+
lineage,
|
|
247
|
+
contentClass: input.contentClass,
|
|
248
|
+
});
|
|
249
|
+
if (!recorded.ok) {
|
|
250
|
+
throw new Error(`Failed to record egress audit: ${recorded.error.code}`);
|
|
251
|
+
}
|
|
252
|
+
if (denied) throw denied;
|
|
253
|
+
return { decision, lineage };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const maximumDestinationZoneForCollections = (
|
|
257
|
+
collections: readonly Collection[],
|
|
258
|
+
names?: readonly string[]
|
|
259
|
+
): Exclude<EgressDestinationZone, "local_process"> => {
|
|
260
|
+
const states = collectionEgressStates(collections, names);
|
|
261
|
+
if (states.some(({ policy }) => policy === "local_only")) return "loopback";
|
|
262
|
+
if (states.some(({ policy }) => policy === "lan")) return "lan";
|
|
263
|
+
return "remote";
|
|
264
|
+
};
|