@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,27 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function sha256String(value: string | Buffer): string {
|
|
4
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function canonicalJson(value: unknown): string {
|
|
8
|
+
return JSON.stringify(sortForCanonicalJson(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function contentHash(value: unknown): string {
|
|
12
|
+
return sha256String(canonicalJson(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sortForCanonicalJson(value: unknown): unknown {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map(sortForCanonicalJson);
|
|
18
|
+
}
|
|
19
|
+
if (value && typeof value === "object") {
|
|
20
|
+
const result: Record<string, unknown> = {};
|
|
21
|
+
for (const key of Object.keys(value).sort()) {
|
|
22
|
+
result[key] = sortForCanonicalJson((value as Record<string, unknown>)[key]);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { VIEW_MANIFEST } from "./constants.ts";
|
|
2
|
+
import { AwbsError } from "./errors.ts";
|
|
3
|
+
import { assertSafeRelativePath, isPathUnderAny } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
export const RESERVED_DATA_PATHS = [".git", ".awbs", VIEW_MANIFEST] as const;
|
|
6
|
+
|
|
7
|
+
export function assertUserDataPath(path: string, action: string): void {
|
|
8
|
+
assertSafeRelativePath(path);
|
|
9
|
+
if (!path || path === ".") {
|
|
10
|
+
throw new AwbsError(`Cannot ${action} the database root as a data path.`);
|
|
11
|
+
}
|
|
12
|
+
if (isReservedDataPath(path)) {
|
|
13
|
+
throw new AwbsError(`Cannot ${action} AWBS reserved path: ${path}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function assertUserDataPaths(paths: string[], action: string): void {
|
|
18
|
+
for (const path of paths) {
|
|
19
|
+
assertUserDataPath(path, action);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isReservedDataPath(path: string): boolean {
|
|
24
|
+
const normalized = pathKey(path);
|
|
25
|
+
return RESERVED_DATA_PATHS.some((reserved) => normalized === pathKey(reserved) || normalized.startsWith(`${pathKey(reserved)}/`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isPathAllowedByWritePaths(path: string, writePaths: string[]): boolean {
|
|
29
|
+
assertUserDataPath(path, "write");
|
|
30
|
+
assertUserDataPaths(writePaths, "declare write access to");
|
|
31
|
+
return isPathUnderAny(path, writePaths);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pathKey(path: string): string {
|
|
35
|
+
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
36
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isAbsolute, sep } from "node:path";
|
|
2
|
+
import { AwbsError } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
export function normalizeUserPaths(paths: string[]): string[] {
|
|
5
|
+
return uniquePaths(paths.flatMap((path) => path.split(",")).map((path) => normalizeUserPath(path)).filter(Boolean));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeUserPath(input: string): string {
|
|
9
|
+
const trimmed = input.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
14
|
+
const displayPath = normalized === "." ? "." : normalized.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
15
|
+
assertSafeRelativePath(displayPath);
|
|
16
|
+
return toPosixPath(displayPath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assertSafeRelativePath(input: string): void {
|
|
20
|
+
const normalized = input.replace(/\\/g, "/");
|
|
21
|
+
const segments = normalized.split("/");
|
|
22
|
+
if (isAbsolute(input) || normalized.startsWith("/") || segments.some((segment) => segment === "." || segment === "..")) {
|
|
23
|
+
throw new AwbsError(`Unsafe path: ${input}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function uniquePaths(paths: string[]): string[] {
|
|
28
|
+
const seen = new Set<string>();
|
|
29
|
+
const result: string[] = [];
|
|
30
|
+
for (const path of paths) {
|
|
31
|
+
if (!seen.has(path)) {
|
|
32
|
+
seen.add(path);
|
|
33
|
+
result.push(path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isPathUnderAny(path: string, roots: string[]): boolean {
|
|
40
|
+
return roots.some((root) => path === root || path.startsWith(`${root}/`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function makeId(prefix: string): string {
|
|
44
|
+
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
45
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
46
|
+
return `${prefix}_${stamp}_${random}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toPosixPath(path: string): string {
|
|
50
|
+
return path.split(sep).join("/");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fromPosixPath(path: string): string {
|
|
54
|
+
return path.split("/").join(sep);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function filterIgnoredStatus(status: string, root: string, ignoredRelPaths: string[]): string {
|
|
58
|
+
const normalizedIgnored = ignoredRelPaths.map((path) => path.replace(/\\/g, "/").replace(/\/+$/, "")).filter(Boolean);
|
|
59
|
+
const lines = status.split(/\r?\n/).filter(Boolean);
|
|
60
|
+
const kept = lines.filter((line) => {
|
|
61
|
+
const statusPath = line.slice(3).replace(/\\/g, "/");
|
|
62
|
+
return !normalizedIgnored.some((ignored) => statusPath === ignored || statusPath.startsWith(`${ignored}/`));
|
|
63
|
+
});
|
|
64
|
+
return kept.length > 0 ? `${kept.join("\n")}\n` : "";
|
|
65
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { AuthoritySessionRequest, AuthoritySessionResponse } from "./session-types.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_PROOF_AGE_MS = 5 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
export function attachControllerProof(request: AuthoritySessionRequest, controllerToken: string): AuthoritySessionRequest {
|
|
7
|
+
const base = proofBase(request);
|
|
8
|
+
const requestHash = sha256String(canonicalJson(base));
|
|
9
|
+
const nonce = randomBytes(16).toString("hex");
|
|
10
|
+
const createdAt = new Date().toISOString();
|
|
11
|
+
return {
|
|
12
|
+
...base,
|
|
13
|
+
controllerProof: {
|
|
14
|
+
algorithm: "AWBS-HMAC-SHA256-v1",
|
|
15
|
+
requestHash,
|
|
16
|
+
nonce,
|
|
17
|
+
createdAt,
|
|
18
|
+
proof: hmacProof(tokenHash(controllerToken), proofMessage(requestHash, nonce, createdAt))
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function verifyControllerProof(expectedTokenHash: string, request: AuthoritySessionRequest, usedNonces?: Set<string>): boolean {
|
|
24
|
+
const proof = request.controllerProof;
|
|
25
|
+
if (!proof || proof.algorithm !== "AWBS-HMAC-SHA256-v1") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (!proof.nonce || usedNonces?.has(proof.nonce)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const createdAtTime = Date.parse(proof.createdAt);
|
|
32
|
+
if (!Number.isFinite(createdAtTime) || Math.abs(Date.now() - createdAtTime) > MAX_PROOF_AGE_MS) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const base = proofBase(request);
|
|
36
|
+
const requestHash = sha256String(canonicalJson(base));
|
|
37
|
+
if (proof.requestHash !== requestHash) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const expectedProof = hmacProof(expectedTokenHash, proofMessage(requestHash, proof.nonce, proof.createdAt));
|
|
41
|
+
const actual = Buffer.from(proof.proof, "hex");
|
|
42
|
+
const expected = Buffer.from(expectedProof, "hex");
|
|
43
|
+
const ok = actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
44
|
+
if (ok) {
|
|
45
|
+
usedNonces?.add(proof.nonce);
|
|
46
|
+
}
|
|
47
|
+
return ok;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function attachControllerResponseProof(response: AuthoritySessionResponse, request: AuthoritySessionRequest, expectedTokenHash: string): AuthoritySessionResponse {
|
|
51
|
+
const requestNonce = request.controllerProof?.nonce;
|
|
52
|
+
if (!requestNonce) {
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
const base = responseBase(response);
|
|
56
|
+
const responseHash = sha256String(canonicalJson(base));
|
|
57
|
+
return {
|
|
58
|
+
...base,
|
|
59
|
+
controllerResponseProof: {
|
|
60
|
+
algorithm: "AWBS-HMAC-SHA256-v1",
|
|
61
|
+
requestNonce,
|
|
62
|
+
responseHash,
|
|
63
|
+
proof: hmacProof(expectedTokenHash, responseProofMessage(responseHash, requestNonce))
|
|
64
|
+
}
|
|
65
|
+
} as AuthoritySessionResponse;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function verifyControllerResponseProof(controllerToken: string, request: AuthoritySessionRequest, response: AuthoritySessionResponse): boolean {
|
|
69
|
+
const requestNonce = request.controllerProof?.nonce;
|
|
70
|
+
const proof = response.controllerResponseProof;
|
|
71
|
+
if (!requestNonce || !proof || proof.algorithm !== "AWBS-HMAC-SHA256-v1" || proof.requestNonce !== requestNonce) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const base = responseBase(response);
|
|
75
|
+
const responseHash = sha256String(canonicalJson(base));
|
|
76
|
+
if (proof.responseHash !== responseHash) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const expectedProof = hmacProof(tokenHash(controllerToken), responseProofMessage(responseHash, requestNonce));
|
|
80
|
+
const actual = Buffer.from(proof.proof, "hex");
|
|
81
|
+
const expected = Buffer.from(expectedProof, "hex");
|
|
82
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function proofBase(request: AuthoritySessionRequest): AuthoritySessionRequest {
|
|
86
|
+
const base: AuthoritySessionRequest = {
|
|
87
|
+
schemaVersion: request.schemaVersion,
|
|
88
|
+
method: request.method,
|
|
89
|
+
root: request.root
|
|
90
|
+
};
|
|
91
|
+
if (request.args !== undefined) {
|
|
92
|
+
base.args = request.args;
|
|
93
|
+
}
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function responseBase(response: AuthoritySessionResponse): AuthoritySessionResponse {
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
return { ok: true, result: response.result };
|
|
100
|
+
}
|
|
101
|
+
return { ok: false, error: response.error };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function tokenHash(controllerToken: string): string {
|
|
105
|
+
return sha256String(controllerToken);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function proofMessage(requestHash: string, nonce: string, createdAt: string): string {
|
|
109
|
+
return canonicalJson({ createdAt, nonce, requestHash });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function responseProofMessage(responseHash: string, requestNonce: string): string {
|
|
113
|
+
return canonicalJson({ kind: "response", requestNonce, responseHash });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hmacProof(tokenHashValue: string, message: string): string {
|
|
117
|
+
return createHmac("sha256", Buffer.from(tokenHashValue.slice("sha256:".length), "hex")).update(message).digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sha256String(value: string): string {
|
|
121
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function canonicalJson(value: unknown): string {
|
|
125
|
+
return JSON.stringify(sortForCanonicalJson(value));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sortForCanonicalJson(value: unknown): unknown {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.map(sortForCanonicalJson);
|
|
131
|
+
}
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
const result: Record<string, unknown> = {};
|
|
134
|
+
for (const key of Object.keys(value).sort()) {
|
|
135
|
+
result[key] = sortForCanonicalJson((value as Record<string, unknown>)[key]);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { AuthorityLocal, AuthorityTrustMode } from "./authority-types.ts";
|
|
2
|
+
|
|
3
|
+
export type AuthoritySessionStatus = "active" | "inactive" | "stale" | "unavailable";
|
|
4
|
+
|
|
5
|
+
export type AuthoritySessionControlInput = {
|
|
6
|
+
recoverySecret: string;
|
|
7
|
+
controllerToken: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type AuthoritySessionFile = {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
repoId: string;
|
|
13
|
+
trustMode: AuthorityTrustMode;
|
|
14
|
+
pid: number;
|
|
15
|
+
socketPath: string;
|
|
16
|
+
startedAt: string;
|
|
17
|
+
status: "active";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AuthoritySessionStatusReport = {
|
|
21
|
+
status: AuthoritySessionStatus;
|
|
22
|
+
active: boolean;
|
|
23
|
+
repoId: string | null;
|
|
24
|
+
pid: number | null;
|
|
25
|
+
socketPath: string | null;
|
|
26
|
+
startedAt: string | null;
|
|
27
|
+
errors: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AuthoritySessionStartResult = AuthoritySessionStatusReport & {
|
|
31
|
+
recoverySealPath: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AuthoritySessionStopResult = {
|
|
35
|
+
stopped: boolean;
|
|
36
|
+
localRestored: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AuthoritySessionRecoverResult = {
|
|
40
|
+
recovered: boolean;
|
|
41
|
+
localPath: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type RecoverySealEnvelope = {
|
|
45
|
+
schemaVersion: 1;
|
|
46
|
+
sealType: "awbs.recovery.seal.v1";
|
|
47
|
+
payloadType: "authority.local";
|
|
48
|
+
kdf: "scrypt-recovery-secret-v1";
|
|
49
|
+
aad: {
|
|
50
|
+
repoId: string;
|
|
51
|
+
payloadType: "authority.local";
|
|
52
|
+
};
|
|
53
|
+
salt: string;
|
|
54
|
+
nonce: string;
|
|
55
|
+
ciphertext: string;
|
|
56
|
+
tag: string;
|
|
57
|
+
contentHash: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type AuthoritySessionDaemonStartup = {
|
|
61
|
+
schemaVersion: 1;
|
|
62
|
+
root: string;
|
|
63
|
+
repoId: string;
|
|
64
|
+
local: AuthorityLocal;
|
|
65
|
+
controllerTokenHash: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type AuthoritySessionRequest = {
|
|
69
|
+
schemaVersion: 1;
|
|
70
|
+
method: string;
|
|
71
|
+
root: string;
|
|
72
|
+
controllerProof?: AuthoritySessionControllerProof;
|
|
73
|
+
args?: unknown[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type AuthoritySessionControllerProof = {
|
|
77
|
+
algorithm: "AWBS-HMAC-SHA256-v1";
|
|
78
|
+
requestHash: string;
|
|
79
|
+
nonce: string;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
proof: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type AuthoritySessionControllerResponseProof = {
|
|
85
|
+
algorithm: "AWBS-HMAC-SHA256-v1";
|
|
86
|
+
requestNonce: string;
|
|
87
|
+
responseHash: string;
|
|
88
|
+
proof: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type AuthoritySessionResponse =
|
|
92
|
+
| {
|
|
93
|
+
ok: true;
|
|
94
|
+
result: unknown;
|
|
95
|
+
controllerResponseProof?: AuthoritySessionControllerResponseProof;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
ok: false;
|
|
99
|
+
error: string;
|
|
100
|
+
controllerResponseProof?: AuthoritySessionControllerResponseProof;
|
|
101
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type IndexStatus = "active" | "removed";
|
|
2
|
+
export type IndexKind = "file" | "directory";
|
|
3
|
+
export type ChangeKind = "add" | "modify" | "delete";
|
|
4
|
+
export type ChangesetStatus = "valid" | "invalid";
|
|
5
|
+
export type SummarySource = "external" | "fallback" | "path-level";
|
|
6
|
+
|
|
7
|
+
export type IndexEntry = {
|
|
8
|
+
path: string;
|
|
9
|
+
kind: IndexKind;
|
|
10
|
+
sha256: string | null;
|
|
11
|
+
size: number | null;
|
|
12
|
+
mtime: string;
|
|
13
|
+
commit: string | null;
|
|
14
|
+
status: IndexStatus;
|
|
15
|
+
summary: string;
|
|
16
|
+
summarySource?: SummarySource;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SummaryEntry = {
|
|
20
|
+
schemaVersion: 1;
|
|
21
|
+
path: string;
|
|
22
|
+
kind: IndexKind | "unknown";
|
|
23
|
+
sha256: string | null;
|
|
24
|
+
commit: string | null;
|
|
25
|
+
summary: string;
|
|
26
|
+
source: "external";
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
ext: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ViewManifest = {
|
|
32
|
+
schemaVersion: 1;
|
|
33
|
+
viewId: string;
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
workspacePath: string;
|
|
36
|
+
baseCommit: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
readPaths: string[];
|
|
39
|
+
writePaths: string[];
|
|
40
|
+
sources: Array<{
|
|
41
|
+
path: string;
|
|
42
|
+
sourcePath: string;
|
|
43
|
+
workspacePath: string;
|
|
44
|
+
baselinePath: string;
|
|
45
|
+
kind: IndexKind;
|
|
46
|
+
sha256: string | null;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ChangeRecord = {
|
|
51
|
+
path: string;
|
|
52
|
+
kind: ChangeKind;
|
|
53
|
+
allowed: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
file?: string;
|
|
56
|
+
sha256?: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ChangesetManifest = {
|
|
60
|
+
schemaVersion: 1;
|
|
61
|
+
changesetId: string;
|
|
62
|
+
viewId: string;
|
|
63
|
+
baseCommit: string;
|
|
64
|
+
createdAt: string;
|
|
65
|
+
projectRoot: string;
|
|
66
|
+
workspacePath: string;
|
|
67
|
+
status: ChangesetStatus;
|
|
68
|
+
readPaths: string[];
|
|
69
|
+
writePaths: string[];
|
|
70
|
+
changes: ChangeRecord[];
|
|
71
|
+
violations: ChangeRecord[];
|
|
72
|
+
payloadHash: string;
|
|
73
|
+
operationHash: string;
|
|
74
|
+
summary: {
|
|
75
|
+
added: number;
|
|
76
|
+
modified: number;
|
|
77
|
+
deleted: number;
|
|
78
|
+
violations: number;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type SnapshotEntry = {
|
|
83
|
+
path: string;
|
|
84
|
+
sha256: string;
|
|
85
|
+
size: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type FileEntry = {
|
|
89
|
+
path: string;
|
|
90
|
+
kind: IndexKind;
|
|
91
|
+
size: number | null;
|
|
92
|
+
mtime: string;
|
|
93
|
+
sha256: string | null;
|
|
94
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthoritySessionControlInput, AuthoritySessionRecoverResult, AuthoritySessionStartResult, AuthoritySessionStatusReport, AuthoritySessionStopResult } from "../domain/session-types.ts";
|
|
2
|
+
|
|
3
|
+
export interface AuthoritySessionPort {
|
|
4
|
+
start(cwd: string, input: AuthoritySessionControlInput): Promise<AuthoritySessionStartResult>;
|
|
5
|
+
status(cwd: string): AuthoritySessionStatusReport;
|
|
6
|
+
stop(cwd: string, controllerToken: string): AuthoritySessionStopResult;
|
|
7
|
+
recover(cwd: string, recoverySecret: string): AuthoritySessionRecoverResult;
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthorityCatalog,
|
|
3
|
+
AuthorityChangesetApplyOperation,
|
|
4
|
+
AuthorityChangesetReceipt,
|
|
5
|
+
AuthorityLedger,
|
|
6
|
+
AuthorityLedgerEntry,
|
|
7
|
+
AuthorityRepairReport,
|
|
8
|
+
AuthorityVerifyReport,
|
|
9
|
+
AuthorityViewContract
|
|
10
|
+
} from "../domain/authority-types.ts";
|
|
11
|
+
|
|
12
|
+
export interface AuthorityPort {
|
|
13
|
+
ensureInitialized(root: string): void;
|
|
14
|
+
createView(root: string, contract: AuthorityViewContract): AuthorityViewContract;
|
|
15
|
+
getViewContract(root: string, viewId: string, options?: { allowRevoked?: boolean }): AuthorityViewContract;
|
|
16
|
+
revokeView(root: string, viewId: string): AuthorityViewContract;
|
|
17
|
+
verify(root: string): AuthorityVerifyReport;
|
|
18
|
+
repairMirrors(root: string): AuthorityRepairReport;
|
|
19
|
+
readCatalog(root: string): AuthorityCatalog;
|
|
20
|
+
hasLedger(root: string): boolean;
|
|
21
|
+
bootstrapLedger(root: string, parentTrustedCommit: string): AuthorityLedger;
|
|
22
|
+
readLedger(root: string): AuthorityLedger;
|
|
23
|
+
recordChangesetApply(root: string, operation: AuthorityChangesetApplyOperation): AuthorityLedgerEntry;
|
|
24
|
+
sealChangesetReceipt(root: string, changesetRoot: string, receipt: AuthorityChangesetReceipt): AuthorityChangesetReceipt;
|
|
25
|
+
openChangesetReceipt(root: string, changesetRoot: string): AuthorityChangesetReceipt;
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FileEntry, SnapshotEntry } from "../domain/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface FileDatabasePort {
|
|
4
|
+
findProjectRoot(cwd: string): string;
|
|
5
|
+
pathExists(path: string): boolean;
|
|
6
|
+
isDirectory(path: string): boolean;
|
|
7
|
+
ensureDir(path: string): void;
|
|
8
|
+
assertSafeOutputDirectory(path: string): void;
|
|
9
|
+
copyPath(source: string, destination: string): void;
|
|
10
|
+
removePath(path: string): void;
|
|
11
|
+
readText(path: string): string;
|
|
12
|
+
writeText(path: string, value: string): void;
|
|
13
|
+
readJson<T>(path: string): T;
|
|
14
|
+
writeJson(path: string, value: unknown): void;
|
|
15
|
+
sha256File(path: string): string;
|
|
16
|
+
walkIndexableEntries(root: string): FileEntry[];
|
|
17
|
+
snapshotFiles(root: string, options: { ignoreAwbsViewManifest: boolean }): Map<string, SnapshotEntry>;
|
|
18
|
+
}
|
package/src/ports/git.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type GitCommandResult = {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
status: number | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export interface GitPort {
|
|
8
|
+
isRepository(root: string): boolean;
|
|
9
|
+
init(root: string): void;
|
|
10
|
+
headCommit(root: string): string | null;
|
|
11
|
+
requireHeadCommit(root: string): string;
|
|
12
|
+
refCommit(root: string, ref: string): string | null;
|
|
13
|
+
updateRef(root: string, ref: string, commit: string): void;
|
|
14
|
+
isAncestor(root: string, ancestor: string, descendant: string): boolean;
|
|
15
|
+
revList(root: string, range: string): string[];
|
|
16
|
+
statusPorcelain(root: string): string;
|
|
17
|
+
addAll(root: string, paths: string[]): void;
|
|
18
|
+
commit(root: string, message: string): void;
|
|
19
|
+
createDetachedWorktree(root: string, path: string, commit: string): void;
|
|
20
|
+
removeWorktree(root: string, path: string): void;
|
|
21
|
+
cloneAtCommit(sourceRoot: string, destination: string, commit: string): void;
|
|
22
|
+
diffNoIndex(baselineRoot: string, workspacePath: string): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IndexEntry, IndexStatus } from "../domain/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface IndexStorePort {
|
|
4
|
+
readIndex(indexFile: string): IndexEntry[];
|
|
5
|
+
writeIndex(indexFile: string, entries: IndexEntry[]): void;
|
|
6
|
+
queryIndex(indexFile: string, term: string | null, options: { status?: IndexStatus | "all" }): IndexEntry[];
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IndexKind, SummaryEntry } from "../domain/types.ts";
|
|
2
|
+
|
|
3
|
+
export type SummaryWriteInput = {
|
|
4
|
+
path: string;
|
|
5
|
+
kind: IndexKind | "unknown";
|
|
6
|
+
sha256: string | null;
|
|
7
|
+
commit: string | null;
|
|
8
|
+
summary: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface SummaryStorePort {
|
|
12
|
+
readSummaries(summaryFile: string): SummaryEntry[];
|
|
13
|
+
writeSummary(summaryFile: string, input: SummaryWriteInput): SummaryEntry;
|
|
14
|
+
findSummary(summaryFile: string, path: string, sha256: string | null): SummaryEntry | null;
|
|
15
|
+
fallbackSummary(absPath: string, relPath: string, kind: IndexKind): string;
|
|
16
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FileSummaryStoreAdapter } from "./adapters/file-summary-store.ts";
|
|
2
|
+
import { GitCliAdapter } from "./adapters/git-cli.ts";
|
|
3
|
+
import { LocalFileDatabaseAdapter } from "./adapters/local-file-database.ts";
|
|
4
|
+
import { LocalAuthoritySessionAdapter } from "./adapters/local-authority-session.ts";
|
|
5
|
+
import { SealedAuthorityAdapter } from "./adapters/sealed-authority.ts";
|
|
6
|
+
import { AutoAuthorityAdapter, SessionAuthorityClientAdapter } from "./adapters/session-authority-client.ts";
|
|
7
|
+
import { SqliteIndexStoreAdapter } from "./adapters/sqlite-index-store.ts";
|
|
8
|
+
import { createAuthorityUseCases, type AuthorityUseCases } from "./usecases/authority.ts";
|
|
9
|
+
import { createChangesetUseCases, type ChangesetUseCases } from "./usecases/changeset.ts";
|
|
10
|
+
import { createDbUseCases, type DbUseCases } from "./usecases/db.ts";
|
|
11
|
+
import { createIndexUseCases, type IndexUseCases } from "./usecases/index.ts";
|
|
12
|
+
import { createInitUseCases, type InitUseCases } from "./usecases/init.ts";
|
|
13
|
+
import { createLedgerUseCases, type LedgerUseCases } from "./usecases/ledger.ts";
|
|
14
|
+
import { createAuthoritySessionUseCases, type AuthoritySessionUseCases } from "./usecases/session.ts";
|
|
15
|
+
import { createViewUseCases, type ViewUseCases } from "./usecases/view.ts";
|
|
16
|
+
|
|
17
|
+
export type AwbsRuntime = {
|
|
18
|
+
usecases: {
|
|
19
|
+
init: InitUseCases;
|
|
20
|
+
index: IndexUseCases;
|
|
21
|
+
view: ViewUseCases;
|
|
22
|
+
changeset: ChangesetUseCases;
|
|
23
|
+
authority: AuthorityUseCases;
|
|
24
|
+
session: AuthoritySessionUseCases;
|
|
25
|
+
ledger: LedgerUseCases;
|
|
26
|
+
db: DbUseCases;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createDefaultRuntime(options: { authorityMode?: "local" | "auto" | "session"; controllerToken?: string; cliPath?: string } = {}): AwbsRuntime {
|
|
31
|
+
const files = new LocalFileDatabaseAdapter();
|
|
32
|
+
const git = new GitCliAdapter();
|
|
33
|
+
const index = new SqliteIndexStoreAdapter(files);
|
|
34
|
+
const summaries = new FileSummaryStoreAdapter(files);
|
|
35
|
+
const cliPath = options.cliPath ?? process.argv[1];
|
|
36
|
+
const authority =
|
|
37
|
+
options.authorityMode === "session"
|
|
38
|
+
? new SessionAuthorityClientAdapter(cliPath, { controllerToken: options.controllerToken })
|
|
39
|
+
: options.authorityMode === "auto"
|
|
40
|
+
? new AutoAuthorityAdapter(files, cliPath)
|
|
41
|
+
: new SealedAuthorityAdapter(files);
|
|
42
|
+
const session = new LocalAuthoritySessionAdapter(files, cliPath);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
usecases: {
|
|
46
|
+
init: createInitUseCases({ files, git, authority }),
|
|
47
|
+
index: createIndexUseCases({ files, git, index, summaries }),
|
|
48
|
+
view: createViewUseCases({ files, git, authority }),
|
|
49
|
+
changeset: createChangesetUseCases({ files, git, authority }),
|
|
50
|
+
authority: createAuthorityUseCases({ files, authority }),
|
|
51
|
+
session: createAuthoritySessionUseCases({ session }),
|
|
52
|
+
ledger: createLedgerUseCases({ files, git, authority }),
|
|
53
|
+
db: createDbUseCases({ files, git, authority })
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runAuthoritySessionDaemon, runAuthoritySessionRequest } from "./adapters/local-authority-session.ts";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AuthorityRepairReport, AuthorityVerifyReport } from "../domain/authority-types.ts";
|
|
2
|
+
import type { AuthorityPort } from "../ports/authority.ts";
|
|
3
|
+
import type { FileDatabasePort } from "../ports/file-database.ts";
|
|
4
|
+
|
|
5
|
+
export type AuthorityUseCases = {
|
|
6
|
+
verifyAuthority(cwd: string): AuthorityVerifyReport;
|
|
7
|
+
repairMirrors(cwd: string): AuthorityRepairReport;
|
|
8
|
+
formatVerifyReport(report: AuthorityVerifyReport): string;
|
|
9
|
+
formatRepairReport(report: AuthorityRepairReport): string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createAuthorityUseCases(deps: { files: FileDatabasePort; authority: AuthorityPort }): AuthorityUseCases {
|
|
13
|
+
return {
|
|
14
|
+
verifyAuthority(cwd: string): AuthorityVerifyReport {
|
|
15
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
16
|
+
return deps.authority.verify(root);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
repairMirrors(cwd: string): AuthorityRepairReport {
|
|
20
|
+
const root = deps.files.findProjectRoot(cwd);
|
|
21
|
+
return deps.authority.repairMirrors(root);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
formatVerifyReport(report: AuthorityVerifyReport): string {
|
|
25
|
+
const lines = [
|
|
26
|
+
`Authority: ${report.ok ? "ok" : "failed"}`,
|
|
27
|
+
`Views: ${report.catalog.views}`,
|
|
28
|
+
`Resources: ${report.catalog.resources}`,
|
|
29
|
+
`Mirror mismatches: ${report.mirrorMismatches.length}`
|
|
30
|
+
];
|
|
31
|
+
if (report.mirrorMismatches.length > 0) {
|
|
32
|
+
lines.push("", "Mismatched mirrors:");
|
|
33
|
+
for (const mirror of report.mirrorMismatches) {
|
|
34
|
+
lines.push(` ${mirror}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (report.errors.length > 0) {
|
|
38
|
+
lines.push("", "Errors:");
|
|
39
|
+
for (const error of report.errors) {
|
|
40
|
+
lines.push(` ${error}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
formatRepairReport(report: AuthorityRepairReport): string {
|
|
47
|
+
if (report.repairedMirrors.length === 0) {
|
|
48
|
+
return "Authority mirrors are already in sync.";
|
|
49
|
+
}
|
|
50
|
+
return [`Repaired ${report.repairedMirrors.length} mirror(s):`, ...report.repairedMirrors.map((mirror) => ` ${mirror}`)].join("\n");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|