trellis 1.0.8 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -83
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Git Reader
3
+ *
4
+ * Reads a Git repository's commit graph and file-level diffs
5
+ * by shelling out to `git`. No libgit2 dependency.
6
+ */
7
+
8
+ import { execSync } from 'child_process';
9
+ import { existsSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface GitCommit {
17
+ hash: string;
18
+ authorName: string;
19
+ authorEmail: string;
20
+ timestamp: string; // ISO 8601
21
+ message: string;
22
+ parentHashes: string[];
23
+ }
24
+
25
+ export interface GitFileChange {
26
+ status: 'A' | 'M' | 'D' | 'R'; // Added, Modified, Deleted, Renamed
27
+ path: string;
28
+ oldPath?: string; // only for renames
29
+ }
30
+
31
+ export interface GitCommitWithChanges extends GitCommit {
32
+ changes: GitFileChange[];
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Reader
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export class GitReader {
40
+ private repoPath: string;
41
+
42
+ constructor(repoPath: string) {
43
+ this.repoPath = repoPath;
44
+ }
45
+
46
+ /**
47
+ * Verifies this is a valid Git repository.
48
+ */
49
+ isGitRepo(): boolean {
50
+ return existsSync(join(this.repoPath, '.git'));
51
+ }
52
+
53
+ /**
54
+ * Returns all commits in topological order (oldest first).
55
+ */
56
+ readCommits(): GitCommit[] {
57
+ // Format: hash|authorName|authorEmail|isoTimestamp|parentHashes|subject
58
+ const SEP = '‖'; // unlikely to appear in commit messages
59
+ const format = `%H${SEP}%an${SEP}%ae${SEP}%aI${SEP}%P${SEP}%s`;
60
+
61
+ const raw = this.git(`log --all --reverse --format="${format}"`);
62
+ if (!raw.trim()) {
63
+ return [];
64
+ }
65
+
66
+ return raw
67
+ .trim()
68
+ .split('\n')
69
+ .map((line) => {
70
+ const parts = line.split(SEP);
71
+ return {
72
+ hash: parts[0],
73
+ authorName: parts[1],
74
+ authorEmail: parts[2],
75
+ timestamp: parts[3],
76
+ parentHashes: parts[4] ? parts[4].split(' ').filter(Boolean) : [],
77
+ message: parts[5] ?? '',
78
+ };
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Returns file changes for a specific commit.
84
+ * For the root commit (no parents), diffs against empty tree.
85
+ */
86
+ readChanges(commitHash: string, parentHash?: string): GitFileChange[] {
87
+ let raw: string;
88
+
89
+ if (parentHash) {
90
+ // Diff between parent and this commit
91
+ raw = this.git(
92
+ `diff-tree -r --name-status --no-commit-id -M ${parentHash} ${commitHash}`,
93
+ );
94
+ } else {
95
+ // Root commit: use --root flag to diff against empty tree
96
+ raw = this.git(
97
+ `diff-tree -r --root --name-status --no-commit-id -M ${commitHash}`,
98
+ );
99
+ }
100
+
101
+ if (!raw.trim()) {
102
+ return [];
103
+ }
104
+
105
+ return raw
106
+ .trim()
107
+ .split('\n')
108
+ .map((line) => {
109
+ const parts = line.split('\t');
110
+ const statusCode = parts[0].charAt(0) as 'A' | 'M' | 'D' | 'R';
111
+
112
+ if (statusCode === 'R') {
113
+ // Rename: R100\toldPath\tnewPath
114
+ return { status: 'R', oldPath: parts[1], path: parts[2] };
115
+ }
116
+
117
+ return { status: statusCode, path: parts[1] };
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Returns the full content of a file at a specific commit.
123
+ */
124
+ readFileContent(commitHash: string, filePath: string): Buffer | null {
125
+ try {
126
+ return Buffer.from(this.gitBuffer(`show ${commitHash}:${filePath}`));
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Reads all commits with their file changes in topological order.
134
+ * This is the main entry point for the import pipeline.
135
+ */
136
+ readFullHistory(): GitCommitWithChanges[] {
137
+ const commits = this.readCommits();
138
+
139
+ return commits.map((commit) => {
140
+ const parentHash = commit.parentHashes[0]; // first parent for changes
141
+ const changes = this.readChanges(commit.hash, parentHash);
142
+ return { ...commit, changes };
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Returns the total number of commits.
148
+ */
149
+ commitCount(): number {
150
+ const raw = this.git('rev-list --all --count');
151
+ return parseInt(raw.trim(), 10) || 0;
152
+ }
153
+
154
+ /**
155
+ * Returns the current branch name.
156
+ */
157
+ currentBranch(): string {
158
+ try {
159
+ return this.git('rev-parse --abbrev-ref HEAD').trim();
160
+ } catch {
161
+ return 'main';
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Returns all branch names.
167
+ */
168
+ branches(): string[] {
169
+ const raw = this.git('branch --format="%(refname:short)"');
170
+ return raw.trim().split('\n').filter(Boolean);
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Internals
175
+ // ---------------------------------------------------------------------------
176
+
177
+ private git(args: string): string {
178
+ return execSync(`git -C "${this.repoPath}" ${args}`, {
179
+ encoding: 'utf-8',
180
+ maxBuffer: 100 * 1024 * 1024, // 100MB for large repos
181
+ });
182
+ }
183
+
184
+ private gitBuffer(args: string): Buffer {
185
+ return execSync(`git -C "${this.repoPath}" ${args}`, {
186
+ maxBuffer: 100 * 1024 * 1024,
187
+ });
188
+ }
189
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Git Bridge — Public Surface
3
+ */
4
+
5
+ export { GitReader } from './git-reader.js';
6
+ export { importFromGit } from './git-importer.js';
7
+ export { exportToGit } from './git-exporter.js';
8
+ export type {
9
+ ImportOptions,
10
+ ImportResult,
11
+ ImportProgress,
12
+ } from './git-importer.js';
13
+ export type {
14
+ ExportOptions,
15
+ ExportResult,
16
+ ExportProgress,
17
+ } from './git-exporter.js';
18
+ export type {
19
+ GitCommit,
20
+ GitFileChange,
21
+ GitCommitWithChanges,
22
+ } from './git-reader.js';
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Governance Module
3
+ *
4
+ * DESIGN.md §6.3–6.4 — Policy nodes and governance enforcement.
5
+ *
6
+ * Policy rules are expressed as EAV entities. The governance engine evaluates
7
+ * ops against applicable policies before allowing them through.
8
+ */
9
+
10
+ import type { VcsOp } from '../vcs/types.js';
11
+ import type { IdentityResolver } from './signing-middleware.js';
12
+ import { verifySignature } from './identity.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface PolicyRule {
19
+ id: string;
20
+ /** What this policy protects. */
21
+ target: 'branch' | 'path' | 'entityType';
22
+ targetPattern: string;
23
+ /** What action requires authorization. */
24
+ action: 'push' | 'merge' | 'createMilestone' | 'deleteBranch';
25
+ /** Who is authorized (identity entity IDs). */
26
+ requiredSigners: string[];
27
+ /** Minimum number of valid signatures required. */
28
+ minSignatures: number;
29
+ /** Optional: require CI attestation. */
30
+ requireAttestation?: {
31
+ type: 'test-pass' | 'build-pass' | 'review-approved';
32
+ from: string;
33
+ };
34
+ /** Whether this policy is active. */
35
+ enabled: boolean;
36
+ }
37
+
38
+ export interface PolicyViolation {
39
+ policyId: string;
40
+ op: VcsOp;
41
+ reason: string;
42
+ }
43
+
44
+ export interface GovernanceResult {
45
+ allowed: boolean;
46
+ violations: PolicyViolation[];
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Op → action mapping
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Determine which governance action an op corresponds to.
55
+ */
56
+ function opToAction(op: VcsOp): string | null {
57
+ switch (op.kind) {
58
+ case 'vcs:branchDelete':
59
+ return 'deleteBranch';
60
+ case 'vcs:milestoneCreate':
61
+ return 'createMilestone';
62
+ case 'vcs:merge':
63
+ return 'merge';
64
+ case 'vcs:fileAdd':
65
+ case 'vcs:fileModify':
66
+ case 'vcs:fileDelete':
67
+ case 'vcs:fileRename':
68
+ case 'vcs:branchAdvance':
69
+ return 'push';
70
+ default:
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if an op's target matches a policy's target pattern.
77
+ */
78
+ function matchesTarget(op: VcsOp, policy: PolicyRule): boolean {
79
+ switch (policy.target) {
80
+ case 'branch': {
81
+ const branchName = op.vcs?.branchName ?? op.vcs?.sourceBranch;
82
+ if (!branchName) return false;
83
+ return matchGlob(branchName, policy.targetPattern);
84
+ }
85
+ case 'path': {
86
+ const filePath = op.vcs?.filePath;
87
+ if (!filePath) return false;
88
+ return matchGlob(filePath, policy.targetPattern);
89
+ }
90
+ case 'entityType': {
91
+ return op.kind.includes(policy.targetPattern.toLowerCase());
92
+ }
93
+ default:
94
+ return false;
95
+ }
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Policy evaluation
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Evaluate an op against a set of policies.
104
+ */
105
+ export function evaluatePolicy(
106
+ op: VcsOp,
107
+ policies: PolicyRule[],
108
+ resolver: IdentityResolver,
109
+ ): GovernanceResult {
110
+ const violations: PolicyViolation[] = [];
111
+ const action = opToAction(op);
112
+
113
+ if (!action) {
114
+ return { allowed: true, violations: [] };
115
+ }
116
+
117
+ const applicable = policies.filter(
118
+ (p) => p.enabled && p.action === action && matchesTarget(op, p),
119
+ );
120
+
121
+ for (const policy of applicable) {
122
+ // Check signature requirements
123
+ if (policy.minSignatures > 0) {
124
+ const validSigners = countValidSigners(op, policy, resolver);
125
+
126
+ if (validSigners < policy.minSignatures) {
127
+ violations.push({
128
+ policyId: policy.id,
129
+ op,
130
+ reason:
131
+ `Policy '${policy.id}' requires ${policy.minSignatures} signature(s) ` +
132
+ `from [${policy.requiredSigners.join(', ')}], got ${validSigners}.`,
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ return {
139
+ allowed: violations.length === 0,
140
+ violations,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Count how many valid required signers signed the op.
146
+ */
147
+ function countValidSigners(
148
+ op: VcsOp,
149
+ policy: PolicyRule,
150
+ resolver: IdentityResolver,
151
+ ): number {
152
+ if (!op.vcs?.signature || !op.vcs?.signedBy) return 0;
153
+
154
+ // Check if the signer is in the required signers list
155
+ if (!policy.requiredSigners.includes(op.vcs.signedBy)) return 0;
156
+
157
+ // Verify signature
158
+ const publicKey = resolver.resolvePublicKey(op.vcs.signedBy);
159
+ if (!publicKey) return 0;
160
+
161
+ const valid = verifySignature(op.hash, op.vcs.signature, publicKey);
162
+ return valid ? 1 : 0;
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Policy CRUD helpers
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Create a new policy rule.
171
+ */
172
+ export function createPolicy(opts: {
173
+ id: string;
174
+ target: PolicyRule['target'];
175
+ targetPattern: string;
176
+ action: PolicyRule['action'];
177
+ requiredSigners: string[];
178
+ minSignatures?: number;
179
+ }): PolicyRule {
180
+ return {
181
+ id: opts.id,
182
+ target: opts.target,
183
+ targetPattern: opts.targetPattern,
184
+ action: opts.action,
185
+ requiredSigners: opts.requiredSigners,
186
+ minSignatures: opts.minSignatures ?? 1,
187
+ enabled: true,
188
+ };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Helpers
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Simple glob matcher supporting * and ** patterns.
197
+ */
198
+ function matchGlob(value: string, pattern: string): boolean {
199
+ if (pattern === '*' || pattern === '**') return true;
200
+ if (pattern === value) return true;
201
+
202
+ // Convert glob to regex
203
+ const escaped = pattern
204
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
205
+ .replace(/\*\*/g, '{{DOUBLESTAR}}')
206
+ .replace(/\*/g, '[^/]*')
207
+ .replace(/\{\{DOUBLESTAR\}\}/g, '.*');
208
+
209
+ const regex = new RegExp(`^${escaped}$`);
210
+ return regex.test(value);
211
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Identity Module
3
+ *
4
+ * Ed25519 key pair generation, DID derivation, and local identity storage.
5
+ * DESIGN.md §6.1 — Every actor is an Identity entity with a cryptographic key pair.
6
+ *
7
+ * Private keys are stored locally in `.trellis/identity.json` (never synced).
8
+ * Public keys and DIDs are graph entities that get replicated to peers.
9
+ */
10
+
11
+ import {
12
+ generateKeyPairSync,
13
+ sign,
14
+ verify,
15
+ createPublicKey,
16
+ type KeyObject,
17
+ } from 'crypto';
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
19
+ import { join, dirname } from 'path';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface IdentityConfig {
26
+ displayName: string;
27
+ email?: string;
28
+ /** Ed25519 public key, base64-encoded. */
29
+ publicKey: string;
30
+ /** Ed25519 private key, base64-encoded (local only, never synced). */
31
+ privateKey: string;
32
+ /** did:key identifier derived from public key. */
33
+ did: string;
34
+ /** Entity ID for use in the EAV store. */
35
+ entityId: string;
36
+ /** ISO timestamp of creation. */
37
+ createdAt: string;
38
+ }
39
+
40
+ export interface PublicIdentity {
41
+ displayName: string;
42
+ email?: string;
43
+ publicKey: string;
44
+ did: string;
45
+ entityId: string;
46
+ createdAt: string;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Key generation
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Generate a new Ed25519 identity.
55
+ */
56
+ export function createIdentity(opts: {
57
+ displayName: string;
58
+ email?: string;
59
+ }): IdentityConfig {
60
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
61
+
62
+ const pubDer = publicKey.export({ type: 'spki', format: 'der' });
63
+ const privDer = privateKey.export({ type: 'pkcs8', format: 'der' });
64
+
65
+ // Extract raw 32-byte public key from SPKI DER (last 32 bytes)
66
+ const rawPub = pubDer.subarray(pubDer.length - 32);
67
+
68
+ const did = deriveDid(rawPub);
69
+ const entityId = `identity:${did}`;
70
+
71
+ return {
72
+ displayName: opts.displayName,
73
+ email: opts.email,
74
+ publicKey: pubDer.toString('base64'),
75
+ privateKey: privDer.toString('base64'),
76
+ did,
77
+ entityId,
78
+ createdAt: new Date().toISOString(),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Derive a did:key identifier from a raw Ed25519 public key.
84
+ * Format: did:key:z6Mk... (multicodec 0xed01 prefix + base58btc)
85
+ */
86
+ function deriveDid(rawPublicKey: Buffer | Uint8Array): string {
87
+ // Multicodec prefix for Ed25519 public key: 0xed 0x01
88
+ const multicodec = Buffer.concat([
89
+ Buffer.from([0xed, 0x01]),
90
+ Buffer.from(rawPublicKey),
91
+ ]);
92
+ // Base58btc encoding
93
+ const encoded = base58btcEncode(multicodec);
94
+ return `did:key:z${encoded}`;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Op signing / verification
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Sign a message (typically an op hash) with a private key.
103
+ */
104
+ export function signMessage(
105
+ message: string,
106
+ privateKeyBase64: string,
107
+ ): string {
108
+ const privDer = Buffer.from(privateKeyBase64, 'base64');
109
+ const privateKey = createPrivateKeyFromDer(privDer);
110
+ const sig = sign(null, Buffer.from(message, 'utf-8'), privateKey);
111
+ return sig.toString('base64');
112
+ }
113
+
114
+ /**
115
+ * Verify a signature against a message and public key.
116
+ */
117
+ export function verifySignature(
118
+ message: string,
119
+ signatureBase64: string,
120
+ publicKeyBase64: string,
121
+ ): boolean {
122
+ const pubDer = Buffer.from(publicKeyBase64, 'base64');
123
+ const publicKey = createPublicKey({
124
+ key: pubDer,
125
+ format: 'der',
126
+ type: 'spki',
127
+ });
128
+ const sig = Buffer.from(signatureBase64, 'base64');
129
+ return verify(null, Buffer.from(message, 'utf-8'), publicKey, sig);
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Local identity storage
134
+ // ---------------------------------------------------------------------------
135
+
136
+ const IDENTITY_FILE = 'identity.json';
137
+
138
+ /**
139
+ * Save an identity to the local .trellis directory.
140
+ */
141
+ export function saveIdentity(trellisDir: string, identity: IdentityConfig): void {
142
+ const filePath = join(trellisDir, IDENTITY_FILE);
143
+ if (!existsSync(dirname(filePath))) {
144
+ mkdirSync(dirname(filePath), { recursive: true });
145
+ }
146
+ writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
147
+ }
148
+
149
+ /**
150
+ * Load the local identity from .trellis/identity.json.
151
+ */
152
+ export function loadIdentity(trellisDir: string): IdentityConfig | null {
153
+ const filePath = join(trellisDir, IDENTITY_FILE);
154
+ if (!existsSync(filePath)) return null;
155
+ try {
156
+ return JSON.parse(readFileSync(filePath, 'utf-8')) as IdentityConfig;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Check if a local identity exists.
164
+ */
165
+ export function hasIdentity(trellisDir: string): boolean {
166
+ return existsSync(join(trellisDir, IDENTITY_FILE));
167
+ }
168
+
169
+ /**
170
+ * Extract the public (safe-to-share) portion of an identity.
171
+ */
172
+ export function toPublicIdentity(identity: IdentityConfig): PublicIdentity {
173
+ return {
174
+ displayName: identity.displayName,
175
+ email: identity.email,
176
+ publicKey: identity.publicKey,
177
+ did: identity.did,
178
+ entityId: identity.entityId,
179
+ createdAt: identity.createdAt,
180
+ };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Helpers
185
+ // ---------------------------------------------------------------------------
186
+
187
+ function createPrivateKeyFromDer(der: Buffer): KeyObject {
188
+ const { createPrivateKey } = require('crypto');
189
+ return createPrivateKey({
190
+ key: der,
191
+ format: 'der',
192
+ type: 'pkcs8',
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Base58btc encoding (Bitcoin alphabet).
198
+ */
199
+ function base58btcEncode(buf: Buffer | Uint8Array): string {
200
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
201
+
202
+ let num = BigInt(0);
203
+ for (const byte of buf) {
204
+ num = num * 256n + BigInt(byte);
205
+ }
206
+
207
+ let encoded = '';
208
+ while (num > 0n) {
209
+ const rem = Number(num % 58n);
210
+ num = num / 58n;
211
+ encoded = ALPHABET[rem] + encoded;
212
+ }
213
+
214
+ // Preserve leading zeros
215
+ for (const byte of buf) {
216
+ if (byte === 0) {
217
+ encoded = '1' + encoded;
218
+ } else {
219
+ break;
220
+ }
221
+ }
222
+
223
+ return encoded || '1';
224
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Identity Module — Public Surface
3
+ */
4
+
5
+ export {
6
+ createIdentity,
7
+ signMessage,
8
+ verifySignature,
9
+ saveIdentity,
10
+ loadIdentity,
11
+ hasIdentity,
12
+ toPublicIdentity,
13
+ } from './identity.js';
14
+
15
+ export type { IdentityConfig, PublicIdentity } from './identity.js';
16
+
17
+ export { signOp, verifyOp, verifyOpBatch } from './signing-middleware.js';
18
+
19
+ export type {
20
+ IdentityResolver,
21
+ SignatureVerificationResult,
22
+ } from './signing-middleware.js';
23
+
24
+ export { evaluatePolicy, createPolicy } from './governance.js';
25
+
26
+ export type {
27
+ PolicyRule,
28
+ PolicyViolation,
29
+ GovernanceResult,
30
+ } from './governance.js';