@c956180462/awbs 0.0.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/AWBS_CORE_DESIGN.md +983 -0
- package/AWBS_CURRENT_FEATURES.md +463 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/TASK_001_VIEW_AUTHORITY.md +446 -0
- package/TASK_003_AUTHORITY_LEDGER_AND_DB_AUDIT.md +268 -0
- package/TASK_004_TRUSTED_AUTHORITY_LAYER.md +547 -0
- package/TASK_005_AUTHORITY_SESSION.md +218 -0
- package/TASK_006_TRUST_BOUNDARY_HARDENING.md +381 -0
- package/TASK_007_TRUSTED_OPERATION_ENTRY.md +129 -0
- package/bin/awbs.js +2 -0
- package/docs/DEVELOPMENT_LEARNING.md +319 -0
- package/docs/FULL_CHAIN.md +295 -0
- package/docs/PRODUCT.md +188 -0
- package/docs/USAGE.md +294 -0
- package/package.json +45 -0
- package/src/adapters/file-summary-store.ts +88 -0
- package/src/adapters/git-cli.ts +107 -0
- package/src/adapters/local-authority-session.ts +606 -0
- package/src/adapters/local-file-database.ts +199 -0
- package/src/adapters/sealed-authority.ts +725 -0
- package/src/adapters/session-authority-client.ts +176 -0
- package/src/adapters/sqlite-index-store.ts +176 -0
- package/src/cli.ts +491 -0
- package/src/domain/authority-types.ts +194 -0
- package/src/domain/constants.ts +11 -0
- package/src/domain/errors.ts +6 -0
- package/src/domain/hash.ts +27 -0
- package/src/domain/path-policy.ts +36 -0
- package/src/domain/paths.ts +65 -0
- package/src/domain/session-proof.ts +140 -0
- package/src/domain/session-types.ts +101 -0
- package/src/domain/types.ts +94 -0
- package/src/ports/authority-session.ts +8 -0
- package/src/ports/authority.ts +26 -0
- package/src/ports/file-database.ts +18 -0
- package/src/ports/git.ts +23 -0
- package/src/ports/index-store.ts +7 -0
- package/src/ports/summary-store.ts +16 -0
- package/src/runtime.ts +56 -0
- package/src/session-entry.ts +1 -0
- package/src/usecases/authority.ts +53 -0
- package/src/usecases/changeset.ts +437 -0
- package/src/usecases/db.ts +192 -0
- package/src/usecases/index.ts +136 -0
- package/src/usecases/init.ts +48 -0
- package/src/usecases/ledger.ts +146 -0
- package/src/usecases/session.ts +48 -0
- package/src/usecases/trusted-chain.ts +56 -0
- package/src/usecases/view.ts +166 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { INDEX_PATH, SUMMARY_PATH } from "../domain/constants.ts";
|
|
3
|
+
import { assertUserDataPath } from "../domain/path-policy.ts";
|
|
4
|
+
import { fromPosixPath, normalizeUserPath, toPosixPath } from "../domain/paths.ts";
|
|
5
|
+
import type { IndexEntry, IndexKind, IndexStatus, SummaryEntry } from "../domain/types.ts";
|
|
6
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
7
|
+
import type { GitPort } from "../ports/git.ts";
|
|
8
|
+
import type { IndexStorePort } from "../ports/index-store.ts";
|
|
9
|
+
import type { SummaryStorePort } from "../ports/summary-store.ts";
|
|
10
|
+
import { requireTrustedCommit, withTrustedWorktree } from "./trusted-chain.ts";
|
|
11
|
+
|
|
12
|
+
export type IndexUseCases = {
|
|
13
|
+
rebuildIndex(cwd: string): { active: number; removed: number; path: string };
|
|
14
|
+
queryIndex(cwd: string, term: string | null, options: { json?: boolean; status?: IndexStatus | "all" }): IndexEntry[];
|
|
15
|
+
setSummary(cwd: string, args: { path: string; summary: string }): SummaryEntry;
|
|
16
|
+
getSummary(cwd: string, path: string): SummaryEntry | null;
|
|
17
|
+
listSummaries(cwd: string): SummaryEntry[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createIndexUseCases(deps: {
|
|
21
|
+
files: FileDatabasePort;
|
|
22
|
+
git: GitPort;
|
|
23
|
+
index: IndexStorePort;
|
|
24
|
+
summaries: SummaryStorePort;
|
|
25
|
+
}): IndexUseCases {
|
|
26
|
+
return {
|
|
27
|
+
rebuildIndex(cwd: string): { active: number; removed: number; path: string } {
|
|
28
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
29
|
+
const indexFile = join(root, INDEX_PATH);
|
|
30
|
+
const oldEntries = deps.index.readIndex(indexFile);
|
|
31
|
+
const commit = requireTrustedCommit(deps.git, root);
|
|
32
|
+
const activePaths = new Set<string>();
|
|
33
|
+
const nextEntries: IndexEntry[] = [];
|
|
34
|
+
|
|
35
|
+
withTrustedWorktree(deps, root, commit, "awbs-index-", (trustedRoot) => {
|
|
36
|
+
for (const fileEntry of deps.files.walkIndexableEntries(trustedRoot)) {
|
|
37
|
+
const absPath = join(trustedRoot, fromPosixPath(fileEntry.path));
|
|
38
|
+
const entry: IndexEntry = {
|
|
39
|
+
path: fileEntry.path,
|
|
40
|
+
kind: fileEntry.kind,
|
|
41
|
+
sha256: fileEntry.sha256,
|
|
42
|
+
size: fileEntry.size,
|
|
43
|
+
mtime: fileEntry.mtime,
|
|
44
|
+
commit,
|
|
45
|
+
status: "active",
|
|
46
|
+
...resolveSummary(deps.summaries, join(root, SUMMARY_PATH), absPath, fileEntry.path, fileEntry.kind, fileEntry.sha256)
|
|
47
|
+
};
|
|
48
|
+
activePaths.add(fileEntry.path);
|
|
49
|
+
nextEntries.push(entry);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const oldEntry of oldEntries) {
|
|
54
|
+
if (!activePaths.has(oldEntry.path)) {
|
|
55
|
+
nextEntries.push({
|
|
56
|
+
...oldEntry,
|
|
57
|
+
status: "removed",
|
|
58
|
+
summary: oldEntry.summary || `Removed ${oldEntry.kind}: ${oldEntry.path}`,
|
|
59
|
+
summarySource: oldEntry.summarySource ?? "fallback"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
nextEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
65
|
+
deps.index.writeIndex(indexFile, nextEntries);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
active: nextEntries.filter((entry) => entry.status === "active").length,
|
|
69
|
+
removed: nextEntries.filter((entry) => entry.status === "removed").length,
|
|
70
|
+
path: toPosixPath(relative(root, indexFile))
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
queryIndex(cwd: string, term: string | null, options: { json?: boolean; status?: IndexStatus | "all" }): IndexEntry[] {
|
|
75
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
76
|
+
const indexFile = join(root, INDEX_PATH);
|
|
77
|
+
return deps.index.queryIndex(indexFile, term, { status: options.status });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
setSummary(cwd: string, args: { path: string; summary: string }): SummaryEntry {
|
|
81
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
82
|
+
const relPath = normalizeUserPath(args.path);
|
|
83
|
+
assertUserDataPath(relPath, "write a summary for");
|
|
84
|
+
const commit = requireTrustedCommit(deps.git, root);
|
|
85
|
+
let kind: IndexKind | "unknown" = "unknown";
|
|
86
|
+
let sha256: string | null = null;
|
|
87
|
+
withTrustedWorktree(deps, root, commit, "awbs-summary-", (trustedRoot) => {
|
|
88
|
+
const absPath = join(trustedRoot, fromPosixPath(relPath));
|
|
89
|
+
const exists = deps.files.pathExists(absPath);
|
|
90
|
+
kind = exists ? (deps.files.isDirectory(absPath) ? "directory" : "file") : "unknown";
|
|
91
|
+
sha256 = exists && kind === "file" ? deps.files.sha256File(absPath) : null;
|
|
92
|
+
});
|
|
93
|
+
return deps.summaries.writeSummary(join(root, SUMMARY_PATH), {
|
|
94
|
+
path: relPath,
|
|
95
|
+
kind,
|
|
96
|
+
sha256,
|
|
97
|
+
commit,
|
|
98
|
+
summary: args.summary
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
getSummary(cwd: string, path: string): SummaryEntry | null {
|
|
103
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
104
|
+
const relPath = normalizeUserPath(path);
|
|
105
|
+
assertUserDataPath(relPath, "read a summary for");
|
|
106
|
+
const commit = requireTrustedCommit(deps.git, root);
|
|
107
|
+
let sha256: string | null = null;
|
|
108
|
+
withTrustedWorktree(deps, root, commit, "awbs-summary-", (trustedRoot) => {
|
|
109
|
+
const absPath = join(trustedRoot, fromPosixPath(relPath));
|
|
110
|
+
sha256 = deps.files.pathExists(absPath) && !deps.files.isDirectory(absPath) ? deps.files.sha256File(absPath) : null;
|
|
111
|
+
});
|
|
112
|
+
return deps.summaries.findSummary(join(root, SUMMARY_PATH), relPath, sha256);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
listSummaries(cwd: string): SummaryEntry[] {
|
|
116
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
117
|
+
return deps.summaries.readSummaries(join(root, SUMMARY_PATH));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveSummary(
|
|
123
|
+
summaries: SummaryStorePort,
|
|
124
|
+
summaryFile: string,
|
|
125
|
+
absPath: string,
|
|
126
|
+
relPath: string,
|
|
127
|
+
kind: IndexKind,
|
|
128
|
+
sha256: string | null
|
|
129
|
+
): { summary: string; summarySource: "external" | "path-level" | "fallback" } {
|
|
130
|
+
const external = summaries.findSummary(summaryFile, relPath, sha256);
|
|
131
|
+
if (external) {
|
|
132
|
+
const summarySource = sha256 !== null && external.sha256 === null ? "path-level" : "external";
|
|
133
|
+
return { summary: external.summary, summarySource };
|
|
134
|
+
}
|
|
135
|
+
return { summary: summaries.fallbackSummary(absPath, relPath, kind), summarySource: "fallback" };
|
|
136
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { AWBS_DIR } from "../domain/constants.ts";
|
|
3
|
+
import type { AuthorityPort } from "../ports/authority.ts";
|
|
4
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
5
|
+
import type { GitPort } from "../ports/git.ts";
|
|
6
|
+
|
|
7
|
+
export type InitUseCases = {
|
|
8
|
+
initProject(cwd: string): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createInitUseCases(deps: { files: FileDatabasePort; git: GitPort; authority: AuthorityPort }): InitUseCases {
|
|
12
|
+
return {
|
|
13
|
+
initProject(cwd: string): void {
|
|
14
|
+
if (!deps.git.isRepository(cwd)) {
|
|
15
|
+
deps.git.init(cwd);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
deps.files.ensureDir(join(cwd, AWBS_DIR, "index"));
|
|
19
|
+
deps.files.ensureDir(join(cwd, AWBS_DIR, "summaries"));
|
|
20
|
+
deps.files.ensureDir(join(cwd, AWBS_DIR, "views"));
|
|
21
|
+
deps.files.ensureDir(join(cwd, AWBS_DIR, "changesets"));
|
|
22
|
+
deps.authority.ensureInitialized(cwd);
|
|
23
|
+
|
|
24
|
+
const configPath = join(cwd, AWBS_DIR, "config.json");
|
|
25
|
+
if (!deps.files.pathExists(configPath)) {
|
|
26
|
+
deps.files.writeJson(configPath, {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
name: "awbs-project",
|
|
29
|
+
createdAt: new Date().toISOString()
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const awbsGitignore = join(cwd, AWBS_DIR, ".gitignore");
|
|
34
|
+
const ignored = ensureIgnoredLines(deps.files.pathExists(awbsGitignore) ? deps.files.readText(awbsGitignore) : "", ["/index/", "/views/", "/changesets/", "/private/"]);
|
|
35
|
+
deps.files.writeText(awbsGitignore, ignored);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureIgnoredLines(existing: string, required: string[]): string {
|
|
41
|
+
const lines = existing.split(/\r?\n/).filter(Boolean);
|
|
42
|
+
for (const line of required) {
|
|
43
|
+
if (!lines.includes(line)) {
|
|
44
|
+
lines.push(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `${lines.join("\n")}\n`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { TRUSTED_REF } from "../domain/constants.ts";
|
|
2
|
+
import { AwbsError } from "../domain/errors.ts";
|
|
3
|
+
import { contentHash } from "../domain/hash.ts";
|
|
4
|
+
import type { AuthorityLedger, AuthorityLedgerInspectReport } from "../domain/authority-types.ts";
|
|
5
|
+
import { filterIgnoredStatus } from "../domain/paths.ts";
|
|
6
|
+
import type { AuthorityPort } from "../ports/authority.ts";
|
|
7
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
8
|
+
import type { GitPort } from "../ports/git.ts";
|
|
9
|
+
import { withTrustedWorktree } from "./trusted-chain.ts";
|
|
10
|
+
|
|
11
|
+
export type LedgerBootstrapResult = {
|
|
12
|
+
parentTrustedCommit: string;
|
|
13
|
+
currentTrustedCommit: string;
|
|
14
|
+
headEntryId: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type LedgerUseCases = {
|
|
18
|
+
bootstrapLedger(cwd: string): LedgerBootstrapResult;
|
|
19
|
+
inspectLedger(cwd: string): AuthorityLedgerInspectReport;
|
|
20
|
+
verifyLedger(cwd: string): AuthorityLedgerInspectReport;
|
|
21
|
+
formatLedgerReport(report: AuthorityLedgerInspectReport): string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createLedgerUseCases(deps: { files: FileDatabasePort; git: GitPort; authority: AuthorityPort }): LedgerUseCases {
|
|
25
|
+
return {
|
|
26
|
+
bootstrapLedger(cwd: string): LedgerBootstrapResult {
|
|
27
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
28
|
+
deps.authority.ensureInitialized(root);
|
|
29
|
+
if (deps.git.refCommit(root, TRUSTED_REF) || deps.authority.hasLedger(root)) {
|
|
30
|
+
throw new AwbsError("AWBS trusted chain is already bootstrapped.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parentTrustedCommit = deps.git.requireHeadCommit(root);
|
|
34
|
+
const dirtyBefore = filterIgnoredStatus(deps.git.statusPorcelain(root), root, [".awbs/authority/catalog.mirror.json"]);
|
|
35
|
+
if (dirtyBefore.trim().length > 0) {
|
|
36
|
+
throw new AwbsError(`Working tree must be clean before bootstrapping the trusted chain:\n${dirtyBefore}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ledger = deps.authority.bootstrapLedger(root, parentTrustedCommit);
|
|
40
|
+
deps.git.addAll(root, [".awbs/authority"]);
|
|
41
|
+
deps.git.commit(root, `awbs: bootstrap trusted chain\n\nAWBS-Ledger-Entry: ${ledger.headEntryId ?? ""}`);
|
|
42
|
+
const currentTrustedCommit = deps.git.requireHeadCommit(root);
|
|
43
|
+
deps.git.updateRef(root, TRUSTED_REF, currentTrustedCommit);
|
|
44
|
+
return {
|
|
45
|
+
parentTrustedCommit,
|
|
46
|
+
currentTrustedCommit,
|
|
47
|
+
headEntryId: ledger.headEntryId
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
inspectLedger(cwd: string): AuthorityLedgerInspectReport {
|
|
52
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
53
|
+
return inspectTrustedLedger(deps, root);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
verifyLedger(cwd: string): AuthorityLedgerInspectReport {
|
|
57
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
58
|
+
return inspectTrustedLedger(deps, root);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
formatLedgerReport(report: AuthorityLedgerInspectReport): string {
|
|
62
|
+
const lines = [
|
|
63
|
+
`Trusted chain: ${report.ok ? "ok" : "failed"}`,
|
|
64
|
+
`Current trusted commit: ${report.currentTrustedCommit ?? "(none)"}`,
|
|
65
|
+
`Head entry: ${report.headEntryId ?? "(none)"}`,
|
|
66
|
+
`Entries: ${report.entries}`
|
|
67
|
+
];
|
|
68
|
+
if (report.errors.length > 0) {
|
|
69
|
+
lines.push("", "Errors:");
|
|
70
|
+
for (const error of report.errors) {
|
|
71
|
+
lines.push(` ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function inspectTrustedLedger(
|
|
80
|
+
deps: { files: FileDatabasePort; git: GitPort; authority: AuthorityPort },
|
|
81
|
+
root: string
|
|
82
|
+
): AuthorityLedgerInspectReport {
|
|
83
|
+
const errors: string[] = [];
|
|
84
|
+
const currentTrustedCommit = deps.git.refCommit(root, TRUSTED_REF);
|
|
85
|
+
let ledger: AuthorityLedger | null = null;
|
|
86
|
+
|
|
87
|
+
if (!currentTrustedCommit) {
|
|
88
|
+
errors.push("AWBS trusted chain is not bootstrapped.");
|
|
89
|
+
} else {
|
|
90
|
+
try {
|
|
91
|
+
withTrustedWorktree(deps, root, currentTrustedCommit, "awbs-ledger-", (trustedRoot) => {
|
|
92
|
+
ledger = deps.authority.readLedger(trustedRoot);
|
|
93
|
+
});
|
|
94
|
+
const headEntry = ledger?.entries.find((entry) => entry.entryId === ledger?.headEntryId);
|
|
95
|
+
if (!headEntry) {
|
|
96
|
+
errors.push("Trusted ledger head entry is missing.");
|
|
97
|
+
}
|
|
98
|
+
if (ledger) {
|
|
99
|
+
errors.push(...verifyLedgerHashChain(ledger));
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: errors.length === 0,
|
|
108
|
+
currentTrustedCommit,
|
|
109
|
+
headEntryId: ledger?.headEntryId ?? null,
|
|
110
|
+
entries: ledger?.entries.length ?? 0,
|
|
111
|
+
errors,
|
|
112
|
+
ledger
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function verifyLedgerHashChain(ledger: AuthorityLedger): string[] {
|
|
117
|
+
const errors: string[] = [];
|
|
118
|
+
let previousHash: string | null = null;
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const entry of ledger.entries) {
|
|
122
|
+
if (seen.has(entry.entryId)) {
|
|
123
|
+
errors.push(`Duplicate ledger entry id: ${entry.entryId}`);
|
|
124
|
+
}
|
|
125
|
+
seen.add(entry.entryId);
|
|
126
|
+
|
|
127
|
+
if (entry.previousEntryHash !== previousHash) {
|
|
128
|
+
errors.push(`Ledger entry ${entry.entryId} does not link to the previous entry hash.`);
|
|
129
|
+
}
|
|
130
|
+
if (entry.entryHash !== ledgerEntryHash(entry)) {
|
|
131
|
+
errors.push(`Ledger entry ${entry.entryId} hash mismatch.`);
|
|
132
|
+
}
|
|
133
|
+
previousHash = entry.entryHash;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const last = ledger.entries.at(-1);
|
|
137
|
+
if (last && ledger.headEntryId !== last.entryId) {
|
|
138
|
+
errors.push("Trusted ledger head entry is not the latest ledger entry.");
|
|
139
|
+
}
|
|
140
|
+
return errors;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ledgerEntryHash(entry: AuthorityLedger["entries"][number]): string {
|
|
144
|
+
const { entryHash: _entryHash, ...hashable } = entry;
|
|
145
|
+
return contentHash(hashable);
|
|
146
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AuthoritySessionControlInput, AuthoritySessionRecoverResult, AuthoritySessionStartResult, AuthoritySessionStatusReport, AuthoritySessionStopResult } from "../domain/session-types.ts";
|
|
2
|
+
import type { AuthoritySessionPort } from "../ports/authority-session.ts";
|
|
3
|
+
|
|
4
|
+
export type AuthoritySessionUseCases = {
|
|
5
|
+
startSession(cwd: string, input: AuthoritySessionControlInput): Promise<AuthoritySessionStartResult>;
|
|
6
|
+
statusSession(cwd: string): AuthoritySessionStatusReport;
|
|
7
|
+
stopSession(cwd: string, controllerToken: string): AuthoritySessionStopResult;
|
|
8
|
+
recoverSession(cwd: string, recoverySecret: string): AuthoritySessionRecoverResult;
|
|
9
|
+
formatStatus(report: AuthoritySessionStatusReport): string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createAuthoritySessionUseCases(deps: { session: AuthoritySessionPort }): AuthoritySessionUseCases {
|
|
13
|
+
return {
|
|
14
|
+
startSession(cwd: string, input: AuthoritySessionControlInput): Promise<AuthoritySessionStartResult> {
|
|
15
|
+
return deps.session.start(cwd, input);
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
statusSession(cwd: string): AuthoritySessionStatusReport {
|
|
19
|
+
return deps.session.status(cwd);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
stopSession(cwd: string, controllerToken: string): AuthoritySessionStopResult {
|
|
23
|
+
return deps.session.stop(cwd, controllerToken);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
recoverSession(cwd: string, recoverySecret: string): AuthoritySessionRecoverResult {
|
|
27
|
+
return deps.session.recover(cwd, recoverySecret);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
formatStatus(report: AuthoritySessionStatusReport): string {
|
|
31
|
+
const lines = [
|
|
32
|
+
`Authority session: ${report.status}`,
|
|
33
|
+
`Active: ${report.active ? "yes" : "no"}`,
|
|
34
|
+
`Repo: ${report.repoId ?? "(none)"}`,
|
|
35
|
+
`PID: ${report.pid ?? "(none)"}`,
|
|
36
|
+
`Endpoint: ${report.socketPath ?? "(none)"}`,
|
|
37
|
+
`Started: ${report.startedAt ?? "(none)"}`
|
|
38
|
+
];
|
|
39
|
+
if (report.errors.length > 0) {
|
|
40
|
+
lines.push("", "Errors:");
|
|
41
|
+
for (const error of report.errors) {
|
|
42
|
+
lines.push(` ${error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { TRUSTED_REF } from "../domain/constants.ts";
|
|
5
|
+
import { AwbsError } from "../domain/errors.ts";
|
|
6
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
7
|
+
import type { GitPort } from "../ports/git.ts";
|
|
8
|
+
|
|
9
|
+
export function requireTrustedCommit(git: GitPort, root: string): string {
|
|
10
|
+
const trustedCommit = git.refCommit(root, TRUSTED_REF);
|
|
11
|
+
if (!trustedCommit) {
|
|
12
|
+
throw new AwbsError("AWBS trusted chain is not bootstrapped. Run `awbs ledger bootstrap` after creating an initial Git commit.");
|
|
13
|
+
}
|
|
14
|
+
return trustedCommit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function withTrustedWorktree<T>(
|
|
18
|
+
deps: { files: FileDatabasePort; git: GitPort },
|
|
19
|
+
root: string,
|
|
20
|
+
commit: string,
|
|
21
|
+
prefix: string,
|
|
22
|
+
fn: (worktreeRoot: string) => T
|
|
23
|
+
): T {
|
|
24
|
+
const parent = mkdtempSync(join(tmpdir(), prefix));
|
|
25
|
+
const worktreeRoot = join(parent, "worktree");
|
|
26
|
+
try {
|
|
27
|
+
deps.git.createDetachedWorktree(root, worktreeRoot, commit);
|
|
28
|
+
copyPrivateMaterial(deps.files, root, worktreeRoot);
|
|
29
|
+
return fn(worktreeRoot);
|
|
30
|
+
} finally {
|
|
31
|
+
try {
|
|
32
|
+
deps.git.removeWorktree(root, worktreeRoot);
|
|
33
|
+
} catch {
|
|
34
|
+
// Best-effort cleanup; the caller's primary error should remain visible.
|
|
35
|
+
}
|
|
36
|
+
rmSync(parent, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copyPrivateMaterial(files: FileDatabasePort, root: string, worktreeRoot: string): void {
|
|
41
|
+
const source = join(root, ".awbs", "private");
|
|
42
|
+
const destination = join(worktreeRoot, ".awbs", "private");
|
|
43
|
+
if (!existsSync(source)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
files.copyPath(source, destination);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertInsideParent(parent: string, child: string): void {
|
|
50
|
+
const normalizedParent = resolve(parent);
|
|
51
|
+
const normalizedChild = resolve(child);
|
|
52
|
+
const childRelative = relative(normalizedParent, normalizedChild);
|
|
53
|
+
if (childRelative.startsWith("..") || dirname(childRelative) === ".." || normalizedChild === normalizedParent) {
|
|
54
|
+
throw new AwbsError(`Path escapes expected parent: ${child}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { VIEW_MANIFEST } from "../domain/constants.ts";
|
|
4
|
+
import { AwbsError } from "../domain/errors.ts";
|
|
5
|
+
import { assertUserDataPaths } from "../domain/path-policy.ts";
|
|
6
|
+
import { fromPosixPath, normalizeUserPaths } from "../domain/paths.ts";
|
|
7
|
+
import type { AuthorityCatalogView, AuthorityViewContract, AuthorityViewSource } from "../domain/authority-types.ts";
|
|
8
|
+
import type { IndexKind, ViewManifest } from "../domain/types.ts";
|
|
9
|
+
import type { AuthorityPort } from "../ports/authority.ts";
|
|
10
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
11
|
+
import type { GitPort } from "../ports/git.ts";
|
|
12
|
+
import { requireTrustedCommit, withTrustedWorktree } from "./trusted-chain.ts";
|
|
13
|
+
|
|
14
|
+
export type ViewUseCases = {
|
|
15
|
+
createView(cwd: string, args: { out: string; readPaths: string[]; writePaths: string[] }): ViewManifest;
|
|
16
|
+
inspectView(cwd: string, viewId: string): { contract: AuthorityViewContract; catalogView: AuthorityCatalogView };
|
|
17
|
+
revokeView(cwd: string, viewId: string): AuthorityViewContract;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createViewUseCases(deps: { files: FileDatabasePort; git: GitPort; authority: AuthorityPort }): ViewUseCases {
|
|
21
|
+
return {
|
|
22
|
+
createView(cwd: string, args: { out: string; readPaths: string[]; writePaths: string[] }): ViewManifest {
|
|
23
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
24
|
+
deps.authority.ensureInitialized(root);
|
|
25
|
+
const baseCommit = requireTrustedCommit(deps.git, root);
|
|
26
|
+
const workspacePath = resolve(root, args.out);
|
|
27
|
+
assertWorkspaceOutputPath(root, workspacePath);
|
|
28
|
+
deps.files.assertSafeOutputDirectory(workspacePath);
|
|
29
|
+
|
|
30
|
+
const readPaths = normalizeUserPaths(args.readPaths);
|
|
31
|
+
const writePaths = normalizeUserPaths(args.writePaths);
|
|
32
|
+
if (readPaths.length === 0 && writePaths.length === 0) {
|
|
33
|
+
throw new AwbsError("view create requires at least one --read or --write path.");
|
|
34
|
+
}
|
|
35
|
+
assertUserDataPaths(readPaths, "project");
|
|
36
|
+
assertUserDataPaths(writePaths, "project");
|
|
37
|
+
|
|
38
|
+
const selectedPaths = uniquePaths([...readPaths, ...writePaths]);
|
|
39
|
+
|
|
40
|
+
const viewId = randomUUID();
|
|
41
|
+
const baselineRoot = join(root, ".awbs", "views", viewId, "baseline");
|
|
42
|
+
const sources: ViewManifest["sources"] = [];
|
|
43
|
+
const contractSources: AuthorityViewSource[] = [];
|
|
44
|
+
|
|
45
|
+
deps.files.ensureDir(workspacePath);
|
|
46
|
+
deps.files.ensureDir(baselineRoot);
|
|
47
|
+
|
|
48
|
+
withTrustedWorktree(deps, root, baseCommit, "awbs-view-", (trustedRoot) => {
|
|
49
|
+
for (const relPath of selectedPaths) {
|
|
50
|
+
const trustedSourceAbs = join(trustedRoot, fromPosixPath(relPath));
|
|
51
|
+
if (!deps.files.pathExists(trustedSourceAbs)) {
|
|
52
|
+
throw new AwbsError(`Selected path does not exist in trusted database: ${relPath}`);
|
|
53
|
+
}
|
|
54
|
+
const sourceAbs = join(root, fromPosixPath(relPath));
|
|
55
|
+
const workspaceAbs = join(workspacePath, fromPosixPath(relPath));
|
|
56
|
+
const baselineAbs = join(baselineRoot, fromPosixPath(relPath));
|
|
57
|
+
deps.files.copyPath(trustedSourceAbs, workspaceAbs);
|
|
58
|
+
deps.files.copyPath(trustedSourceAbs, baselineAbs);
|
|
59
|
+
const kind: IndexKind = deps.files.isDirectory(trustedSourceAbs) ? "directory" : "file";
|
|
60
|
+
const sha256 = kind === "file" ? deps.files.sha256File(trustedSourceAbs) : null;
|
|
61
|
+
sources.push({
|
|
62
|
+
path: relPath,
|
|
63
|
+
sourcePath: sourceAbs,
|
|
64
|
+
workspacePath: workspaceAbs,
|
|
65
|
+
baselinePath: baselineAbs,
|
|
66
|
+
kind,
|
|
67
|
+
sha256
|
|
68
|
+
});
|
|
69
|
+
contractSources.push({
|
|
70
|
+
path: relPath,
|
|
71
|
+
sourcePath: sourceAbs,
|
|
72
|
+
workspacePath: workspaceAbs,
|
|
73
|
+
baselinePath: baselineAbs,
|
|
74
|
+
kind,
|
|
75
|
+
sha256,
|
|
76
|
+
mode: writePaths.includes(relPath) ? "write" : "read",
|
|
77
|
+
ext: {}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const createdAt = new Date().toISOString();
|
|
83
|
+
const contract: AuthorityViewContract = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
viewId,
|
|
86
|
+
baseCommit,
|
|
87
|
+
createdAt,
|
|
88
|
+
readPaths,
|
|
89
|
+
writePaths,
|
|
90
|
+
sources: contractSources,
|
|
91
|
+
ext: { workspacePath }
|
|
92
|
+
};
|
|
93
|
+
deps.authority.createView(root, contract);
|
|
94
|
+
|
|
95
|
+
const manifest: ViewManifest = {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
viewId,
|
|
98
|
+
projectRoot: root,
|
|
99
|
+
workspacePath,
|
|
100
|
+
baseCommit,
|
|
101
|
+
createdAt,
|
|
102
|
+
readPaths,
|
|
103
|
+
writePaths,
|
|
104
|
+
sources
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
deps.files.writeJson(join(workspacePath, VIEW_MANIFEST), manifest);
|
|
108
|
+
deps.files.writeJson(join(root, ".awbs", "views", viewId, "manifest.json"), manifest);
|
|
109
|
+
return manifest;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
inspectView(cwd: string, viewId: string): { contract: AuthorityViewContract; catalogView: AuthorityCatalogView } {
|
|
113
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
114
|
+
const contract = deps.authority.getViewContract(root, viewId, { allowRevoked: true });
|
|
115
|
+
const catalog = deps.authority.readCatalog(root);
|
|
116
|
+
const catalogView = catalog.views.find((view) => view.viewId === viewId);
|
|
117
|
+
if (!catalogView) {
|
|
118
|
+
throw new AwbsError(`View is not registered in authority catalog: ${viewId}`);
|
|
119
|
+
}
|
|
120
|
+
return { contract, catalogView };
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
revokeView(cwd: string, viewId: string): AuthorityViewContract {
|
|
124
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
125
|
+
return deps.authority.revokeView(root, viewId);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function uniquePaths(paths: string[]): string[] {
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
const result: string[] = [];
|
|
133
|
+
for (const path of paths) {
|
|
134
|
+
if (!seen.has(path)) {
|
|
135
|
+
seen.add(path);
|
|
136
|
+
result.push(path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function assertWorkspaceOutputPath(root: string, workspacePath: string): void {
|
|
143
|
+
const rootPath = resolve(root);
|
|
144
|
+
const outputPath = resolve(workspacePath);
|
|
145
|
+
if (outputPath === rootPath) {
|
|
146
|
+
throw new AwbsError("Workspace output cannot be the database root.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const reserved of [".awbs", ".git"]) {
|
|
150
|
+
const reservedPath = resolve(rootPath, reserved);
|
|
151
|
+
if (isSameOrInside(reservedPath, outputPath)) {
|
|
152
|
+
throw new AwbsError(`Workspace output cannot be inside AWBS reserved directory: ${reserved}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isSameOrInside(parent: string, child: string): boolean {
|
|
158
|
+
const parentPath = pathKey(resolve(parent));
|
|
159
|
+
const childPath = pathKey(resolve(child));
|
|
160
|
+
const rel = relative(parentPath, childPath);
|
|
161
|
+
return childPath === parentPath || (!rel.startsWith("..") && !rel.includes(":"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pathKey(path: string): string {
|
|
165
|
+
return path.replace(/\\/g, "/").toLowerCase();
|
|
166
|
+
}
|