@git-stunts/git-warp 10.8.0 → 11.3.3
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 +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- package/src/hooks/post-merge.sh +0 -60
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { EXIT_CODES, usageError, parseCommandArgs } from '../infrastructure.js';
|
|
2
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
6
|
+
|
|
7
|
+
const TREE_OPTIONS = {
|
|
8
|
+
edge: { type: 'string' },
|
|
9
|
+
prop: { type: 'string', multiple: true },
|
|
10
|
+
'max-depth': { type: 'string' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const treeSchema = z.object({
|
|
14
|
+
edge: z.string().optional(),
|
|
15
|
+
prop: z.union([z.string(), z.array(z.string())]).optional(),
|
|
16
|
+
'max-depth': z.coerce.number().int().nonnegative().optional(),
|
|
17
|
+
}).strict().transform((val) => ({
|
|
18
|
+
edgeLabel: val.edge ?? null,
|
|
19
|
+
props: Array.isArray(val.prop) ? val.prop : val.prop ? [val.prop] : [],
|
|
20
|
+
maxDepth: val['max-depth'],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds a parent-to-children adjacency map from edges.
|
|
25
|
+
* @param {Array<{from: string, to: string, label?: string}>} edges
|
|
26
|
+
* @param {string|null} labelFilter
|
|
27
|
+
* @returns {Map<string, Array<{id: string, label: string}>>}
|
|
28
|
+
*/
|
|
29
|
+
function buildChildMap(edges, labelFilter) {
|
|
30
|
+
/** @type {Map<string, Array<{id: string, label: string}>>} */
|
|
31
|
+
const children = new Map();
|
|
32
|
+
/** @type {Set<string>} */
|
|
33
|
+
const hasParent = new Set();
|
|
34
|
+
|
|
35
|
+
for (const edge of edges) {
|
|
36
|
+
if (labelFilter && edge.label !== labelFilter) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!children.has(edge.from)) {
|
|
40
|
+
children.set(edge.from, []);
|
|
41
|
+
}
|
|
42
|
+
const fromChildren = children.get(edge.from);
|
|
43
|
+
if (fromChildren) {
|
|
44
|
+
fromChildren.push({ id: edge.to, label: edge.label || '' });
|
|
45
|
+
}
|
|
46
|
+
hasParent.add(edge.to);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return children;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Finds root nodes (nodes with outgoing edges but no incoming edges in the filtered set).
|
|
54
|
+
* @param {string[]} nodeIds
|
|
55
|
+
* @param {Array<{from: string, to: string, label?: string}>} edges
|
|
56
|
+
* @param {string|null} labelFilter
|
|
57
|
+
* @returns {string[]}
|
|
58
|
+
*/
|
|
59
|
+
function findRoots(nodeIds, edges, labelFilter) {
|
|
60
|
+
const hasParent = new Set();
|
|
61
|
+
const hasChild = new Set();
|
|
62
|
+
|
|
63
|
+
for (const edge of edges) {
|
|
64
|
+
if (labelFilter && edge.label !== labelFilter) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
hasParent.add(edge.to);
|
|
68
|
+
hasChild.add(edge.from);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Roots: nodes that have children but no parents in the filtered edge set
|
|
72
|
+
const roots = nodeIds.filter((id) => !hasParent.has(id) && hasChild.has(id));
|
|
73
|
+
if (roots.length > 0) {
|
|
74
|
+
return roots.sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fallback: nodes with no incoming edges at all
|
|
78
|
+
return nodeIds.filter((id) => !hasParent.has(id)).sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Formats annotation string for a node based on requested props.
|
|
83
|
+
* @param {Record<string, unknown>} nodeProps
|
|
84
|
+
* @param {string[]} propKeys
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function formatAnnotation(nodeProps, propKeys) {
|
|
88
|
+
if (propKeys.length === 0 || !nodeProps) {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
const parts = [];
|
|
92
|
+
for (const key of propKeys) {
|
|
93
|
+
if (Object.prototype.hasOwnProperty.call(nodeProps, key)) {
|
|
94
|
+
parts.push(`${key}: ${nodeProps[key]}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Renders a tree structure as lines with box-drawing characters.
|
|
102
|
+
* @param {object} params
|
|
103
|
+
* @param {string} params.nodeId
|
|
104
|
+
* @param {Map<string, Array<{id: string, label: string}>>} params.childMap
|
|
105
|
+
* @param {Map<string, Record<string, unknown>>} params.propsMap
|
|
106
|
+
* @param {string[]} params.propKeys
|
|
107
|
+
* @param {string} params.prefix
|
|
108
|
+
* @param {boolean} params.isLast
|
|
109
|
+
* @param {Set<string>} params.visited
|
|
110
|
+
* @param {number} params.depth
|
|
111
|
+
* @param {number|undefined} params.maxDepth
|
|
112
|
+
* @param {string[]} params.lines
|
|
113
|
+
*/
|
|
114
|
+
function renderTreeNode({ nodeId, childMap, propsMap, propKeys, prefix, isLast, visited, depth, maxDepth, lines }) {
|
|
115
|
+
const connector = depth === 0 ? '' : (isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ');
|
|
116
|
+
const annotation = formatAnnotation(propsMap.get(nodeId) || {}, propKeys);
|
|
117
|
+
lines.push(`${prefix}${connector}${nodeId}${annotation}`);
|
|
118
|
+
|
|
119
|
+
if (visited.has(nodeId)) {
|
|
120
|
+
lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} (cycle)`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
visited.add(nodeId);
|
|
124
|
+
|
|
125
|
+
if (maxDepth !== undefined && depth >= maxDepth) {
|
|
126
|
+
const kids = childMap.get(nodeId);
|
|
127
|
+
if (kids && kids.length > 0) {
|
|
128
|
+
lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} ... (${kids.length} children)`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const kids = childMap.get(nodeId) || [];
|
|
134
|
+
const childPrefix = depth === 0 ? '' : `${prefix}${isLast ? ' ' : '\u2502 '}`;
|
|
135
|
+
for (let i = 0; i < kids.length; i++) {
|
|
136
|
+
renderTreeNode({
|
|
137
|
+
nodeId: kids[i].id,
|
|
138
|
+
childMap,
|
|
139
|
+
propsMap,
|
|
140
|
+
propKeys,
|
|
141
|
+
prefix: childPrefix,
|
|
142
|
+
isLast: i === kids.length - 1,
|
|
143
|
+
visited,
|
|
144
|
+
depth: depth + 1,
|
|
145
|
+
maxDepth,
|
|
146
|
+
lines,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Handles the `tree` command: ASCII tree output from graph edges.
|
|
153
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
154
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
155
|
+
*/
|
|
156
|
+
export default async function handleTree({ options, args }) {
|
|
157
|
+
const { values, positionals } = parseCommandArgs(
|
|
158
|
+
args, TREE_OPTIONS, treeSchema, { allowPositionals: true },
|
|
159
|
+
);
|
|
160
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
161
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
162
|
+
emitCursorWarning(cursorInfo, null);
|
|
163
|
+
|
|
164
|
+
const queryResult = await graph.query().run();
|
|
165
|
+
const edges = await graph.getEdges();
|
|
166
|
+
const rootArg = positionals[0] || null;
|
|
167
|
+
|
|
168
|
+
const nodeIds = queryResult.nodes.map((/** @type {{id: string}} */ n) => n.id);
|
|
169
|
+
const propsMap = new Map(queryResult.nodes.map((/** @type {{id: string, props?: Record<string, unknown>}} */ n) => [n.id, n.props || {}]));
|
|
170
|
+
const childMap = buildChildMap(edges, values.edgeLabel);
|
|
171
|
+
|
|
172
|
+
const roots = rootArg ? [rootArg] : findRoots(nodeIds, edges, values.edgeLabel);
|
|
173
|
+
|
|
174
|
+
if (rootArg && !nodeIds.includes(rootArg)) {
|
|
175
|
+
throw usageError(`Node not found: ${rootArg}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @type {string[]} */
|
|
179
|
+
const lines = [];
|
|
180
|
+
for (const root of roots) {
|
|
181
|
+
renderTreeNode({
|
|
182
|
+
nodeId: root,
|
|
183
|
+
childMap,
|
|
184
|
+
propsMap,
|
|
185
|
+
propKeys: values.props,
|
|
186
|
+
prefix: '',
|
|
187
|
+
isLast: true,
|
|
188
|
+
visited: new Set(),
|
|
189
|
+
depth: 0,
|
|
190
|
+
maxDepth: values.maxDepth,
|
|
191
|
+
lines,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Collect orphans (nodes not reachable from any root)
|
|
196
|
+
const reachable = new Set();
|
|
197
|
+
collectReachable(roots, childMap, reachable);
|
|
198
|
+
const orphans = nodeIds.filter((/** @type {string} */ id) => !reachable.has(id));
|
|
199
|
+
|
|
200
|
+
const payload = {
|
|
201
|
+
graph: graphName,
|
|
202
|
+
roots,
|
|
203
|
+
tree: lines.join('\n'),
|
|
204
|
+
orphanCount: orphans.length,
|
|
205
|
+
orphans: orphans.length > 0 ? orphans : undefined,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return { payload, exitCode: EXIT_CODES.OK };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Collects all reachable node IDs via DFS.
|
|
213
|
+
* @param {string[]} roots
|
|
214
|
+
* @param {Map<string, Array<{id: string, label: string}>>} childMap
|
|
215
|
+
* @param {Set<string>} reachable
|
|
216
|
+
*/
|
|
217
|
+
function collectReachable(roots, childMap, reachable) {
|
|
218
|
+
const stack = [...roots];
|
|
219
|
+
while (stack.length > 0) {
|
|
220
|
+
const id = /** @type {string} */ (stack.pop());
|
|
221
|
+
if (reachable.has(id)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
reachable.add(id);
|
|
225
|
+
const kids = childMap.get(id) || [];
|
|
226
|
+
for (const kid of kids) {
|
|
227
|
+
stack.push(kid.id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for `git warp trust`.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates writer trust status against signed evidence in the trust
|
|
5
|
+
* record chain. Returns a TrustAssessment payload.
|
|
6
|
+
*
|
|
7
|
+
* @module cli/commands/trust
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EXIT_CODES, parseCommandArgs, getEnvVar } from '../infrastructure.js';
|
|
11
|
+
import { trustSchema } from '../schemas.js';
|
|
12
|
+
import { createPersistence, resolveGraphName } from '../shared.js';
|
|
13
|
+
import defaultCodec from '../../../src/domain/utils/defaultCodec.js';
|
|
14
|
+
import { TrustRecordService } from '../../../src/domain/trust/TrustRecordService.js';
|
|
15
|
+
import { buildState } from '../../../src/domain/trust/TrustStateBuilder.js';
|
|
16
|
+
import { evaluateWriters } from '../../../src/domain/trust/TrustEvaluator.js';
|
|
17
|
+
|
|
18
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
19
|
+
|
|
20
|
+
const TRUST_OPTIONS = {
|
|
21
|
+
mode: { type: 'string' },
|
|
22
|
+
'trust-pin': { type: 'string' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string[]} args
|
|
27
|
+
* @returns {{ mode: string|null, trustPin: string|null }}
|
|
28
|
+
*/
|
|
29
|
+
export function parseTrustArgs(args) {
|
|
30
|
+
const { values } = parseCommandArgs(args, TRUST_OPTIONS, trustSchema);
|
|
31
|
+
return values;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the trust pin from CLI flag → env → live ref.
|
|
36
|
+
* @param {string|null} cliPin
|
|
37
|
+
* @returns {{pin: string|null, source: string, sourceDetail: string|null, status: string}}
|
|
38
|
+
*/
|
|
39
|
+
function resolveTrustPin(cliPin) {
|
|
40
|
+
if (cliPin) {
|
|
41
|
+
return { pin: cliPin, source: 'cli_pin', sourceDetail: cliPin, status: 'pinned' };
|
|
42
|
+
}
|
|
43
|
+
const envPin = getEnvVar('WARP_TRUST_PIN');
|
|
44
|
+
if (envPin) {
|
|
45
|
+
return { pin: envPin, source: 'env_pin', sourceDetail: envPin, status: 'pinned' };
|
|
46
|
+
}
|
|
47
|
+
return { pin: null, source: 'ref', sourceDetail: null, status: 'configured' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Discovers all writer IDs from the writers prefix refs.
|
|
52
|
+
* @param {import('../types.js').Persistence} persistence
|
|
53
|
+
* @param {string} graphName
|
|
54
|
+
* @returns {Promise<string[]>}
|
|
55
|
+
*/
|
|
56
|
+
async function discoverWriterIds(persistence, graphName) {
|
|
57
|
+
const prefix = `refs/warp/${graphName}/writers/`;
|
|
58
|
+
const refs = await persistence.listRefs(prefix);
|
|
59
|
+
return refs
|
|
60
|
+
.map((/** @type {string} */ ref) => ref.slice(prefix.length))
|
|
61
|
+
.filter((/** @type {string} */ id) => id.length > 0)
|
|
62
|
+
.sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds a not_configured assessment when no trust records exist.
|
|
67
|
+
* @param {string} graphName
|
|
68
|
+
* @returns {{payload: *, exitCode: number}}
|
|
69
|
+
*/
|
|
70
|
+
function buildNotConfiguredResult(graphName) {
|
|
71
|
+
return {
|
|
72
|
+
payload: {
|
|
73
|
+
graph: graphName,
|
|
74
|
+
trustSchemaVersion: 1,
|
|
75
|
+
mode: 'signed_evidence_v1',
|
|
76
|
+
trustVerdict: 'not_configured',
|
|
77
|
+
trust: {
|
|
78
|
+
status: 'not_configured',
|
|
79
|
+
source: 'none',
|
|
80
|
+
sourceDetail: null,
|
|
81
|
+
evaluatedWriters: [],
|
|
82
|
+
untrustedWriters: [],
|
|
83
|
+
explanations: [],
|
|
84
|
+
evidenceSummary: {
|
|
85
|
+
recordsScanned: 0,
|
|
86
|
+
activeKeys: 0,
|
|
87
|
+
revokedKeys: 0,
|
|
88
|
+
activeBindings: 0,
|
|
89
|
+
revokedBindings: 0,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
exitCode: EXIT_CODES.OK,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
99
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
100
|
+
*/
|
|
101
|
+
export default async function handleTrust({ options, args }) {
|
|
102
|
+
const { mode, trustPin } = parseTrustArgs(args);
|
|
103
|
+
const { persistence } = await createPersistence(options.repo);
|
|
104
|
+
const graphName = await resolveGraphName(persistence, options.graph);
|
|
105
|
+
|
|
106
|
+
const recordService = new TrustRecordService({
|
|
107
|
+
persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
|
|
108
|
+
codec: defaultCodec,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Resolve pin (determines source + status)
|
|
112
|
+
const { pin, source, sourceDetail, status } = resolveTrustPin(trustPin);
|
|
113
|
+
|
|
114
|
+
// Read trust records
|
|
115
|
+
const records = await recordService.readRecords(graphName, pin ? { tip: pin } : {});
|
|
116
|
+
|
|
117
|
+
if (records.length === 0) {
|
|
118
|
+
return buildNotConfiguredResult(graphName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build trust state
|
|
122
|
+
const trustState = buildState(records);
|
|
123
|
+
|
|
124
|
+
// Discover writers
|
|
125
|
+
const writerIds = await discoverWriterIds(persistence, graphName);
|
|
126
|
+
|
|
127
|
+
// Build policy
|
|
128
|
+
const policy = {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
mode: mode ?? 'warn',
|
|
131
|
+
writerPolicy: 'all_writers_must_be_trusted',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Evaluate
|
|
135
|
+
const assessment = evaluateWriters(writerIds, trustState, policy);
|
|
136
|
+
|
|
137
|
+
// Override source/status from pin resolution (evaluator sets defaults)
|
|
138
|
+
const payload = {
|
|
139
|
+
graph: graphName,
|
|
140
|
+
...assessment,
|
|
141
|
+
trust: {
|
|
142
|
+
...assessment.trust,
|
|
143
|
+
status,
|
|
144
|
+
source,
|
|
145
|
+
sourceDetail,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const exitCode = assessment.trustVerdict === 'fail' && (mode === 'enforce')
|
|
150
|
+
? EXIT_CODES.TRUST_FAIL
|
|
151
|
+
: EXIT_CODES.OK;
|
|
152
|
+
|
|
153
|
+
return { payload, exitCode };
|
|
154
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AuditVerifierService } from '../../../src/domain/services/AuditVerifierService.js';
|
|
2
|
+
import defaultCodec from '../../../src/domain/utils/defaultCodec.js';
|
|
3
|
+
import { EXIT_CODES, parseCommandArgs, getEnvVar } from '../infrastructure.js';
|
|
4
|
+
import { verifyAuditSchema } from '../schemas.js';
|
|
5
|
+
import { createPersistence, resolveGraphName } from '../shared.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detects trust configuration from environment and returns a structured warning.
|
|
11
|
+
* Domain services never read process.env — detection happens at the CLI boundary.
|
|
12
|
+
* @returns {{ code: string, message: string, sources: string[] } | null}
|
|
13
|
+
*/
|
|
14
|
+
function detectTrustWarning() {
|
|
15
|
+
const sources = [];
|
|
16
|
+
if (getEnvVar('WARP_TRUSTED_ROOT')) {
|
|
17
|
+
sources.push('env');
|
|
18
|
+
}
|
|
19
|
+
if (sources.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
code: 'TRUST_CONFIG_PRESENT_UNENFORCED',
|
|
24
|
+
message: 'Trust root configured but signature verification is not implemented in v1',
|
|
25
|
+
sources,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VERIFY_AUDIT_OPTIONS = {
|
|
30
|
+
since: { type: 'string' },
|
|
31
|
+
writer: { type: 'string' },
|
|
32
|
+
'trust-mode': { type: 'string' },
|
|
33
|
+
'trust-pin': { type: 'string' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** @param {string[]} args */
|
|
37
|
+
export function parseVerifyAuditArgs(args) {
|
|
38
|
+
const { values } = parseCommandArgs(args, VERIFY_AUDIT_OPTIONS, verifyAuditSchema);
|
|
39
|
+
return {
|
|
40
|
+
since: values.since,
|
|
41
|
+
writerFilter: values.writer,
|
|
42
|
+
trustMode: values['trust-mode'],
|
|
43
|
+
trustPin: values['trust-pin'],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
49
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
50
|
+
*/
|
|
51
|
+
export default async function handleVerifyAudit({ options, args }) {
|
|
52
|
+
const { since, writerFilter, trustMode, trustPin } = parseVerifyAuditArgs(args);
|
|
53
|
+
const { persistence } = await createPersistence(options.repo);
|
|
54
|
+
const graphName = await resolveGraphName(persistence, options.graph);
|
|
55
|
+
const verifier = new AuditVerifierService({
|
|
56
|
+
persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
|
|
57
|
+
codec: defaultCodec,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const trustWarning = detectTrustWarning();
|
|
61
|
+
|
|
62
|
+
/** @type {Record<string, unknown>} */
|
|
63
|
+
let payload;
|
|
64
|
+
if (writerFilter !== undefined) {
|
|
65
|
+
const chain = await verifier.verifyChain(graphName, writerFilter, { since });
|
|
66
|
+
const invalid = chain.status !== 'VALID' && chain.status !== 'PARTIAL' ? 1 : 0;
|
|
67
|
+
payload = {
|
|
68
|
+
graph: graphName,
|
|
69
|
+
verifiedAt: new Date().toISOString(),
|
|
70
|
+
summary: {
|
|
71
|
+
total: 1,
|
|
72
|
+
valid: chain.status === 'VALID' ? 1 : 0,
|
|
73
|
+
partial: chain.status === 'PARTIAL' ? 1 : 0,
|
|
74
|
+
invalid,
|
|
75
|
+
},
|
|
76
|
+
chains: [chain],
|
|
77
|
+
trustWarning,
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
payload = await verifier.verifyAll(graphName, { since, trustWarning });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Attach trust assessment only when explicitly requested via --trust-mode
|
|
84
|
+
if (trustMode) {
|
|
85
|
+
try {
|
|
86
|
+
const trustAssessment = await verifier.evaluateTrust(graphName, {
|
|
87
|
+
pin: trustPin,
|
|
88
|
+
mode: trustMode,
|
|
89
|
+
});
|
|
90
|
+
payload.trustAssessment = trustAssessment;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (trustMode === 'enforce') {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
payload.trustAssessment = {
|
|
96
|
+
trustSchemaVersion: 1,
|
|
97
|
+
mode: 'signed_evidence_v1',
|
|
98
|
+
trustVerdict: 'error',
|
|
99
|
+
error: err instanceof Error ? err.message : 'Trust evaluation failed',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { summary, trustAssessment } = /** @type {{summary: {invalid: number}, trustAssessment?: {trustVerdict?: string}}} */ (payload);
|
|
105
|
+
const hasInvalid = summary.invalid > 0;
|
|
106
|
+
const trustFailed = trustMode === 'enforce' &&
|
|
107
|
+
trustAssessment?.trustVerdict === 'fail';
|
|
108
|
+
return {
|
|
109
|
+
payload,
|
|
110
|
+
exitCode: trustFailed ? EXIT_CODES.TRUST_FAIL
|
|
111
|
+
: hasInvalid ? EXIT_CODES.INTERNAL
|
|
112
|
+
: EXIT_CODES.OK,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { parseCommandArgs, usageError } from '../infrastructure.js';
|
|
3
|
+
import { viewSchema } from '../schemas.js';
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
6
|
+
|
|
7
|
+
const VIEW_OPTIONS = {
|
|
8
|
+
list: { type: 'boolean', default: false },
|
|
9
|
+
log: { type: 'boolean', default: false },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
14
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
15
|
+
*/
|
|
16
|
+
export default async function handleView({ options, args }) {
|
|
17
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
18
|
+
throw usageError('view command requires an interactive terminal (TTY)');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { values, positionals } = parseCommandArgs(args, VIEW_OPTIONS, viewSchema, { allowPositionals: true });
|
|
22
|
+
const viewMode = values.log || positionals[0] === 'log' ? 'log' : 'list';
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// @ts-expect-error — optional peer dependency, may not be installed
|
|
26
|
+
const { startTui } = await import('@git-stunts/git-warp-tui');
|
|
27
|
+
await startTui({
|
|
28
|
+
repo: options.repo || '.',
|
|
29
|
+
graph: options.graph || 'default',
|
|
30
|
+
mode: viewMode,
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const errObj = /** @type {{code?: string, message?: string, specifier?: string}} */ (typeof err === 'object' && err !== null ? err : {});
|
|
34
|
+
const isMissing = errObj.code === 'ERR_MODULE_NOT_FOUND' || (errObj.message && errObj.message.includes('Cannot find module'));
|
|
35
|
+
const isTui = errObj.specifier?.includes('git-warp-tui') ||
|
|
36
|
+
/cannot find (?:package|module) ['"]@git-stunts\/git-warp-tui/i.test(errObj.message || '');
|
|
37
|
+
if (isMissing && isTui) {
|
|
38
|
+
throw usageError(
|
|
39
|
+
'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
|
|
40
|
+
' Install with: npm install -g @git-stunts/git-warp-tui',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
return { payload: undefined, exitCode: 0 };
|
|
46
|
+
}
|