t44 0.4.0-rc.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/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +203 -0
- package/README.md +183 -0
- package/bin/activate +36 -0
- package/bin/activate.ts +30 -0
- package/bin/postinstall.sh +19 -0
- package/bin/shell +27 -0
- package/bin/t44 +27 -0
- package/caps/ConfigSchemaStruct.ts +55 -0
- package/caps/Home.ts +51 -0
- package/caps/HomeRegistry.ts +313 -0
- package/caps/HomeRegistryFile.ts +144 -0
- package/caps/JsonSchemas.ts +220 -0
- package/caps/OpenApiSchema.ts +67 -0
- package/caps/PackageDescriptor.ts +88 -0
- package/caps/ProjectCatalogs.ts +153 -0
- package/caps/ProjectDeployment.ts +363 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +522 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +322 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/WorkspaceCli.ts +442 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +71 -0
- package/caps/WorkspaceConfigFile.ts +799 -0
- package/caps/WorkspaceConnection.ts +249 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +595 -0
- package/caps/WorkspaceInit.ts +30 -0
- package/caps/WorkspaceKey.ts +338 -0
- package/caps/WorkspaceModel.ts +373 -0
- package/caps/WorkspaceProjects.ts +636 -0
- package/caps/WorkspacePrompt.ts +406 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.ts +104 -0
- package/caps/WorkspaceShell.yaml +64 -0
- package/caps/WorkspaceShellCli.ts +109 -0
- package/caps/WorkspaceTest.ts +167 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
- package/caps/providers/bunny.net/api-pull.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.ts +164 -0
- package/caps/providers/bunny.net/api-storage.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.ts +248 -0
- package/caps/providers/bunny.net/api.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
- package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
- package/caps/providers/dynadot.com/api-domains.ts +169 -0
- package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
- package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
- package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
- package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
- package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
- package/caps/providers/github.com/ProjectPublishing.ts +118 -0
- package/caps/providers/github.com/api.ts +115 -0
- package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
- package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
- package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
- package/caps/providers/vercel.com/api.test.ts +67 -0
- package/caps/providers/vercel.com/api.ts +132 -0
- package/caps/providers/vercel.com/bun.lock +194 -0
- package/caps/providers/vercel.com/package.json +10 -0
- package/caps/providers/vercel.com/project.test.ts +108 -0
- package/caps/providers/vercel.com/project.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +365 -0
- package/lib/schema-console-renderer.ts +181 -0
- package/lib/schema-resolver.ts +349 -0
- package/lib/ucan.ts +137 -0
- package/package.json +101 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +56 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectRack.ts +51 -0
- package/structs/ProjectRackConfig.ts +56 -0
- package/structs/RepositoryOriginDescriptor.ts +51 -0
- package/structs/RootKeyConfig.ts +64 -0
- package/structs/SigningKeyConfig.ts +64 -0
- package/structs/Workspace.ts +56 -0
- package/structs/WorkspaceCatalogs.ts +56 -0
- package/structs/WorkspaceCliConfig.ts +53 -0
- package/structs/WorkspaceConfig.ts +64 -0
- package/structs/WorkspaceConfigFile.ts +50 -0
- package/structs/WorkspaceConfigFileMeta.ts +70 -0
- package/structs/WorkspaceKey.ts +55 -0
- package/structs/WorkspaceKeyConfig.ts +56 -0
- package/structs/WorkspaceMappingsConfig.ts +56 -0
- package/structs/WorkspaceProject.ts +104 -0
- package/structs/WorkspaceProjectsConfig.ts +67 -0
- package/structs/WorkspacePublishingConfig.ts +65 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
- package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/dynadot.com/DomainFact.ts +46 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
- package/tests/01-Lifecycle/main.test.ts +173 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +3 -0
package/lib/crypto.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import * as crypto from 'crypto'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encrypt a string using AES-256-GCM with a key derived from the private key
|
|
6
|
+
*/
|
|
7
|
+
export function encryptString(plaintext: string, privateKeyBase64: string): string {
|
|
8
|
+
// Derive a 32-byte symmetric key from the private key using SHA-256
|
|
9
|
+
const keyBytes = Buffer.from(privateKeyBase64, 'base64')
|
|
10
|
+
const symmetricKey = crypto.createHash('sha256').update(keyBytes).digest()
|
|
11
|
+
|
|
12
|
+
// Generate a random IV
|
|
13
|
+
const iv = crypto.randomBytes(16)
|
|
14
|
+
|
|
15
|
+
// Encrypt using AES-256-GCM
|
|
16
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, iv)
|
|
17
|
+
const encrypted = Buffer.concat([
|
|
18
|
+
cipher.update(plaintext, 'utf8'),
|
|
19
|
+
cipher.final()
|
|
20
|
+
])
|
|
21
|
+
const authTag = cipher.getAuthTag()
|
|
22
|
+
|
|
23
|
+
// Combine IV + authTag + encrypted data and encode as base64
|
|
24
|
+
const combined = Buffer.concat([iv, authTag, encrypted])
|
|
25
|
+
return combined.toString('base64')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decrypt a string using AES-256-GCM with a key derived from the private key
|
|
30
|
+
*/
|
|
31
|
+
export function decryptString(ciphertext: string, privateKeyBase64: string): string {
|
|
32
|
+
// Derive the same symmetric key from the private key
|
|
33
|
+
const keyBytes = Buffer.from(privateKeyBase64, 'base64')
|
|
34
|
+
const symmetricKey = crypto.createHash('sha256').update(keyBytes).digest()
|
|
35
|
+
|
|
36
|
+
// Decode the combined data
|
|
37
|
+
const combined = Buffer.from(ciphertext, 'base64')
|
|
38
|
+
|
|
39
|
+
// Extract IV (16 bytes), authTag (16 bytes), and encrypted data
|
|
40
|
+
const iv = combined.subarray(0, 16)
|
|
41
|
+
const authTag = combined.subarray(16, 32)
|
|
42
|
+
const encrypted = combined.subarray(32)
|
|
43
|
+
|
|
44
|
+
// Decrypt using AES-256-GCM
|
|
45
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', symmetricKey, iv)
|
|
46
|
+
decipher.setAuthTag(authTag)
|
|
47
|
+
const decrypted = Buffer.concat([
|
|
48
|
+
decipher.update(encrypted),
|
|
49
|
+
decipher.final()
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
return decrypted.toString('utf8')
|
|
53
|
+
}
|
package/lib/key.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { readdir, readFile, stat } from 'fs/promises'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
|
|
6
|
+
export interface Ed25519KeyInfo {
|
|
7
|
+
name: string
|
|
8
|
+
privateKeyPath: string
|
|
9
|
+
publicKey: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface KeyConfig {
|
|
13
|
+
name: string
|
|
14
|
+
privateKeyPath: string
|
|
15
|
+
publicKey: string
|
|
16
|
+
keyFingerprint: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract the SHA256 fingerprint from ssh-keygen -lf output.
|
|
21
|
+
* Output format: "256 SHA256:xxx comment (ED25519)"
|
|
22
|
+
*/
|
|
23
|
+
export function extractFingerprint(sshKeygenOutput: string): string {
|
|
24
|
+
const match = sshKeygenOutput.match(/(SHA256:\S+)/)
|
|
25
|
+
return match ? match[1] : sshKeygenOutput
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute the SHA256 fingerprint of a key file.
|
|
30
|
+
*/
|
|
31
|
+
export function computeFingerprint(privateKeyPath: string): string {
|
|
32
|
+
return extractFingerprint(execSync(
|
|
33
|
+
`ssh-keygen -lf ${JSON.stringify(privateKeyPath)}`,
|
|
34
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
35
|
+
).trim())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check whether an Ed25519 private key has a passphrase set.
|
|
40
|
+
*/
|
|
41
|
+
export function hasPassphrase(privateKeyPath: string): boolean {
|
|
42
|
+
try {
|
|
43
|
+
execSync(
|
|
44
|
+
`ssh-keygen -y -P "" -f ${JSON.stringify(privateKeyPath)}`,
|
|
45
|
+
{ stdio: 'pipe' }
|
|
46
|
+
)
|
|
47
|
+
return false // empty passphrase worked → no passphrase
|
|
48
|
+
} catch {
|
|
49
|
+
return true // empty passphrase failed → has passphrase
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Discover existing Ed25519 keys in the given SSH directory.
|
|
55
|
+
*/
|
|
56
|
+
export async function discoverEd25519Keys(sshDir: string): Promise<Ed25519KeyInfo[]> {
|
|
57
|
+
const dir = sshDir
|
|
58
|
+
const keys: Ed25519KeyInfo[] = []
|
|
59
|
+
|
|
60
|
+
let entries: string[]
|
|
61
|
+
try {
|
|
62
|
+
entries = await readdir(dir)
|
|
63
|
+
} catch {
|
|
64
|
+
return keys
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pubFiles = entries.filter(f => f.endsWith('.pub'))
|
|
68
|
+
|
|
69
|
+
for (const pubFile of pubFiles) {
|
|
70
|
+
try {
|
|
71
|
+
const pubPath = join(dir, pubFile)
|
|
72
|
+
const content = await readFile(pubPath, 'utf-8')
|
|
73
|
+
const trimmed = content.trim()
|
|
74
|
+
|
|
75
|
+
if (!trimmed.startsWith('ssh-ed25519 ')) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const privateName = pubFile.replace(/\.pub$/, '')
|
|
80
|
+
const privatePath = join(dir, privateName)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await stat(privatePath)
|
|
84
|
+
} catch {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
keys.push({
|
|
89
|
+
name: privateName,
|
|
90
|
+
privateKeyPath: privatePath,
|
|
91
|
+
publicKey: trimmed
|
|
92
|
+
})
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip unreadable files
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return keys
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a key file exists at the given path.
|
|
103
|
+
*/
|
|
104
|
+
export async function keyFileExists(privateKeyPath: string): Promise<boolean> {
|
|
105
|
+
try {
|
|
106
|
+
const s = await stat(privateKeyPath)
|
|
107
|
+
return s.isFile()
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if the key is loaded in the ssh-agent by fingerprint.
|
|
115
|
+
*/
|
|
116
|
+
export function isKeyInAgent(privateKeyPath: string): boolean {
|
|
117
|
+
let keyFingerprint: string
|
|
118
|
+
try {
|
|
119
|
+
const fpOutput = execSync(
|
|
120
|
+
`ssh-keygen -lf ${JSON.stringify(privateKeyPath)}`,
|
|
121
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
122
|
+
).trim()
|
|
123
|
+
const match = fpOutput.match(/(SHA256:\S+)/)
|
|
124
|
+
keyFingerprint = match ? match[1] : ''
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!keyFingerprint) return false
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const agentKeys = execSync('ssh-add -l', {
|
|
133
|
+
encoding: 'utf-8',
|
|
134
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
135
|
+
})
|
|
136
|
+
return agentKeys.includes(keyFingerprint)
|
|
137
|
+
} catch {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Add a key to the macOS ssh-agent with Keychain storage.
|
|
144
|
+
* Returns true if added successfully, false otherwise.
|
|
145
|
+
*/
|
|
146
|
+
export function addKeyToAgent(privateKeyPath: string): boolean {
|
|
147
|
+
try {
|
|
148
|
+
execSync(
|
|
149
|
+
`ssh-add --apple-use-keychain ${JSON.stringify(privateKeyPath)}`,
|
|
150
|
+
{ stdio: 'inherit' }
|
|
151
|
+
)
|
|
152
|
+
return true
|
|
153
|
+
} catch {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Ensure a key is loaded in the ssh-agent. Adds it if not already present.
|
|
160
|
+
* Logs status messages using chalk.
|
|
161
|
+
*/
|
|
162
|
+
export async function ensureKeyInAgent(privateKeyPath: string, keyName: string, keyLabel: string): Promise<void> {
|
|
163
|
+
const chalk = (await import('chalk')).default
|
|
164
|
+
|
|
165
|
+
if (isKeyInAgent(privateKeyPath)) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(chalk.gray(`\n Adding ${keyLabel} '${keyName}' to ssh-agent (macOS Keychain) ...`))
|
|
170
|
+
if (addKeyToAgent(privateKeyPath)) {
|
|
171
|
+
console.log(chalk.green(` ✓ ${keyLabel} added to ssh-agent`))
|
|
172
|
+
console.log(chalk.gray(` Passphrase stored in macOS Keychain.\n`))
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.yellow(`\n ⚠ Could not add key to ssh-agent`))
|
|
175
|
+
console.log(chalk.yellow(` You may need to enter the passphrase manually when the key is used.\n`))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Prompt for a passphrase using inquirer (password input with confirmation).
|
|
181
|
+
*/
|
|
182
|
+
export async function promptPassphrase(): Promise<string | null> {
|
|
183
|
+
const inquirer = await import('inquirer')
|
|
184
|
+
const chalk = (await import('chalk')).default
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const { passphrase } = await inquirer.default.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'password',
|
|
190
|
+
name: 'passphrase',
|
|
191
|
+
message: 'Enter a passphrase for the key:',
|
|
192
|
+
mask: '*',
|
|
193
|
+
validate: (input: string) => {
|
|
194
|
+
if (!input || input.length === 0) {
|
|
195
|
+
return 'Passphrase cannot be empty'
|
|
196
|
+
}
|
|
197
|
+
if (input.length < 5) {
|
|
198
|
+
return 'Passphrase must be at least 5 characters'
|
|
199
|
+
}
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
const { confirm } = await inquirer.default.prompt([
|
|
206
|
+
{
|
|
207
|
+
type: 'password',
|
|
208
|
+
name: 'confirm',
|
|
209
|
+
message: 'Confirm passphrase:',
|
|
210
|
+
mask: '*',
|
|
211
|
+
validate: (input: string) => {
|
|
212
|
+
if (input !== passphrase) {
|
|
213
|
+
return 'Passphrases do not match'
|
|
214
|
+
}
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
return passphrase
|
|
221
|
+
} catch (error: any) {
|
|
222
|
+
if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
|
|
223
|
+
console.log(chalk.red('\n\nABORTED\n'))
|
|
224
|
+
process.exit(0)
|
|
225
|
+
}
|
|
226
|
+
throw error
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Ensure a key has a passphrase. If not, prompt the user to set one.
|
|
232
|
+
*/
|
|
233
|
+
export async function ensurePassphrase(privateKeyPath: string, keyName: string, keyLabel: string): Promise<boolean> {
|
|
234
|
+
const chalk = (await import('chalk')).default
|
|
235
|
+
|
|
236
|
+
if (hasPassphrase(privateKeyPath)) {
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(chalk.yellow(`\n ⚠ ${keyLabel} '${keyName}' has no passphrase`))
|
|
241
|
+
console.log(chalk.gray(` A passphrase is required to protect the key. The passphrase will be stored`))
|
|
242
|
+
console.log(chalk.gray(` in the macOS Keychain so you won't need to enter it again.\n`))
|
|
243
|
+
|
|
244
|
+
const passphrase = await promptPassphrase()
|
|
245
|
+
if (!passphrase) {
|
|
246
|
+
console.log(chalk.red(`\n ✗ A passphrase is required for the ${keyLabel.toLowerCase()}.\n`))
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(chalk.gray(`\n Setting passphrase on ${privateKeyPath} ...`))
|
|
251
|
+
try {
|
|
252
|
+
execSync(
|
|
253
|
+
`ssh-keygen -p -f ${JSON.stringify(privateKeyPath)} -P "" -N ${JSON.stringify(passphrase)}`,
|
|
254
|
+
{ stdio: 'pipe' }
|
|
255
|
+
)
|
|
256
|
+
} catch (error: any) {
|
|
257
|
+
console.log(chalk.red(`\n ✗ Failed to set passphrase: ${error.message}\n`))
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(chalk.green(` ✓ Passphrase set on ${keyLabel.toLowerCase()} '${keyName}'\n`))
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate a new Ed25519 SSH key with a passphrase.
|
|
267
|
+
* Returns the key info or null on failure.
|
|
268
|
+
*/
|
|
269
|
+
export async function generateEd25519Key(
|
|
270
|
+
privateKeyPath: string,
|
|
271
|
+
passphrase: string,
|
|
272
|
+
comment: string
|
|
273
|
+
): Promise<{ publicKey: string; keyFingerprint: string } | null> {
|
|
274
|
+
const chalk = (await import('chalk')).default
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
execSync(
|
|
278
|
+
`ssh-keygen -t ed25519 -f ${JSON.stringify(privateKeyPath)} -N ${JSON.stringify(passphrase)} -C ${JSON.stringify(comment)}`,
|
|
279
|
+
{ stdio: 'pipe' }
|
|
280
|
+
)
|
|
281
|
+
} catch (error: any) {
|
|
282
|
+
console.log(chalk.red(`\n ✗ Failed to generate key: ${error.message}\n`))
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pubKeyContent = await readFile(privateKeyPath + '.pub', 'utf-8')
|
|
287
|
+
const publicKey = pubKeyContent.trim()
|
|
288
|
+
const keyFingerprint = computeFingerprint(privateKeyPath)
|
|
289
|
+
|
|
290
|
+
return { publicKey, keyFingerprint }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validate that a configured key exists and its fingerprint matches.
|
|
295
|
+
* Returns true if valid, false otherwise (with error messages logged).
|
|
296
|
+
*/
|
|
297
|
+
export async function validateConfiguredKey(
|
|
298
|
+
keyConfig: KeyConfig,
|
|
299
|
+
keyLabel: string
|
|
300
|
+
): Promise<boolean> {
|
|
301
|
+
const chalk = (await import('chalk')).default
|
|
302
|
+
|
|
303
|
+
const exists = await keyFileExists(keyConfig.privateKeyPath)
|
|
304
|
+
if (!exists) {
|
|
305
|
+
console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
|
|
306
|
+
console.log(chalk.red(`│ ✗ ${keyLabel} Error${' '.repeat(Math.max(0, 56 - keyLabel.length))}│`))
|
|
307
|
+
console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
|
|
308
|
+
console.log(chalk.red(`│ The configured private key file is missing: │`))
|
|
309
|
+
console.log(chalk.red(`│ ${keyConfig.privateKeyPath}`))
|
|
310
|
+
console.log(chalk.red(`│ │`))
|
|
311
|
+
console.log(chalk.red(`│ The private key '${keyConfig.name}' has been removed or moved.`))
|
|
312
|
+
console.log(chalk.red(`│ Please restore the key file to the path above to proceed. │`))
|
|
313
|
+
console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let currentFingerprint: string
|
|
318
|
+
try {
|
|
319
|
+
currentFingerprint = computeFingerprint(keyConfig.privateKeyPath)
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
|
|
322
|
+
console.log(chalk.red(`│ ✗ ${keyLabel} Error${' '.repeat(Math.max(0, 56 - keyLabel.length))}│`))
|
|
323
|
+
console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
|
|
324
|
+
console.log(chalk.red(`│ Failed to compute fingerprint for the private key at: │`))
|
|
325
|
+
console.log(chalk.red(`│ ${keyConfig.privateKeyPath}`))
|
|
326
|
+
console.log(chalk.red(`│ │`))
|
|
327
|
+
console.log(chalk.red(`│ The key file may be corrupted. Please restore the original │`))
|
|
328
|
+
console.log(chalk.red(`│ key file to proceed. │`))
|
|
329
|
+
console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (currentFingerprint !== keyConfig.keyFingerprint) {
|
|
334
|
+
console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
|
|
335
|
+
console.log(chalk.red(`│ ✗ ${keyLabel} Mismatch${' '.repeat(Math.max(0, 53 - keyLabel.length))}│`))
|
|
336
|
+
console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
|
|
337
|
+
console.log(chalk.red(`│ The private key at the configured path does not match the │`))
|
|
338
|
+
console.log(chalk.red(`│ fingerprint stored in the workspace config. │`))
|
|
339
|
+
console.log(chalk.red(`│ │`))
|
|
340
|
+
console.log(chalk.red(`│ Key name: ${keyConfig.name}`))
|
|
341
|
+
console.log(chalk.red(`│ Path: ${keyConfig.privateKeyPath}`))
|
|
342
|
+
console.log(chalk.red(`│ │`))
|
|
343
|
+
console.log(chalk.red(`│ The private key has changed. Please restore the original │`))
|
|
344
|
+
console.log(chalk.red(`│ private key that matches the configured fingerprint to proceed.│`))
|
|
345
|
+
console.log(chalk.red(`│ │`))
|
|
346
|
+
console.log(chalk.red(`│ Expected fingerprint: │`))
|
|
347
|
+
console.log(chalk.red(`│ ${keyConfig.keyFingerprint}`))
|
|
348
|
+
console.log(chalk.red(`│ │`))
|
|
349
|
+
console.log(chalk.red(`│ Current fingerprint: │`))
|
|
350
|
+
console.log(chalk.red(`│ ${currentFingerprint}`))
|
|
351
|
+
console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!process.env.T44_KEYS_PASSPHRASE) {
|
|
356
|
+
const passphraseOk = await ensurePassphrase(keyConfig.privateKeyPath, keyConfig.name, keyLabel)
|
|
357
|
+
if (!passphraseOk) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await ensureKeyInAgent(keyConfig.privateKeyPath, keyConfig.name, keyLabel)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export interface RenderOptions {
|
|
4
|
+
indent?: number
|
|
5
|
+
maxDepth?: number
|
|
6
|
+
currentDepth?: number
|
|
7
|
+
showTypes?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generic schema-based console renderer that formats data based on JSON Schema properties
|
|
12
|
+
*/
|
|
13
|
+
export class SchemaConsoleRenderer {
|
|
14
|
+
/**
|
|
15
|
+
* Render entity data based on its schema
|
|
16
|
+
*/
|
|
17
|
+
static renderEntity(
|
|
18
|
+
data: any,
|
|
19
|
+
schema: any,
|
|
20
|
+
options: RenderOptions = {}
|
|
21
|
+
): string {
|
|
22
|
+
const {
|
|
23
|
+
indent = 0,
|
|
24
|
+
maxDepth = 5,
|
|
25
|
+
currentDepth = 0,
|
|
26
|
+
showTypes = false
|
|
27
|
+
} = options
|
|
28
|
+
|
|
29
|
+
// maxDepth of -1 means unlimited depth
|
|
30
|
+
if (maxDepth !== -1 && currentDepth >= maxDepth) {
|
|
31
|
+
return chalk.gray(' '.repeat(indent) + '[max depth reached]')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
const properties = schema?.properties || {}
|
|
36
|
+
const required = new Set(schema?.required || [])
|
|
37
|
+
|
|
38
|
+
// If data is primitive, render directly
|
|
39
|
+
if (typeof data !== 'object' || data === null) {
|
|
40
|
+
return chalk.yellow(String(data))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Render each property based on schema
|
|
44
|
+
for (const [key, value] of Object.entries(data)) {
|
|
45
|
+
const propSchema = properties[key]
|
|
46
|
+
const isRequired = required.has(key)
|
|
47
|
+
const prefix = ' '.repeat(indent)
|
|
48
|
+
|
|
49
|
+
// Format key with required indicator
|
|
50
|
+
const keyDisplay = isRequired
|
|
51
|
+
? chalk.bold.cyan(key)
|
|
52
|
+
: chalk.cyan(key)
|
|
53
|
+
|
|
54
|
+
// Add type annotation if requested
|
|
55
|
+
const typeAnnotation = showTypes && propSchema?.type
|
|
56
|
+
? chalk.gray(` (${propSchema.type})`)
|
|
57
|
+
: ''
|
|
58
|
+
|
|
59
|
+
// Render value based on type
|
|
60
|
+
if (value === null || value === undefined) {
|
|
61
|
+
lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${chalk.gray('null')}`)
|
|
62
|
+
} else if (Array.isArray(value)) {
|
|
63
|
+
lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${chalk.gray(`[${value.length} items]`)}`)
|
|
64
|
+
|
|
65
|
+
// Render array items if not too deep
|
|
66
|
+
if ((maxDepth === -1 || currentDepth < maxDepth - 1) && value.length > 0) {
|
|
67
|
+
const itemSchema = propSchema?.items
|
|
68
|
+
value.forEach((item, idx) => {
|
|
69
|
+
if (typeof item === 'object' && item !== null) {
|
|
70
|
+
lines.push(`${prefix} ${chalk.gray(`${idx}`)}:`)
|
|
71
|
+
lines.push(this.renderEntity(item, itemSchema, {
|
|
72
|
+
indent: indent + 3,
|
|
73
|
+
maxDepth,
|
|
74
|
+
currentDepth: currentDepth + 1,
|
|
75
|
+
showTypes
|
|
76
|
+
}))
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(`${prefix} ${chalk.gray(`${idx}`)}: ${this.formatValue(item, propSchema)}`)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
} else if (typeof value === 'object') {
|
|
83
|
+
const description = propSchema?.description
|
|
84
|
+
const descSuffix = description ? chalk.gray(` // ${description}`) : ''
|
|
85
|
+
lines.push(`${prefix}${keyDisplay}${typeAnnotation}:${descSuffix}`)
|
|
86
|
+
|
|
87
|
+
// Recursively render nested object
|
|
88
|
+
lines.push(this.renderEntity(value, propSchema, {
|
|
89
|
+
indent: indent + 1,
|
|
90
|
+
maxDepth,
|
|
91
|
+
currentDepth: currentDepth + 1,
|
|
92
|
+
showTypes
|
|
93
|
+
}))
|
|
94
|
+
} else {
|
|
95
|
+
const description = propSchema?.description
|
|
96
|
+
const descSuffix = description ? chalk.gray(` // ${description}`) : ''
|
|
97
|
+
lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${this.formatValue(value, propSchema)}${descSuffix}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join('\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Format a primitive value based on its schema
|
|
106
|
+
*/
|
|
107
|
+
private static formatValue(value: any, schema?: any): string {
|
|
108
|
+
if (value === null || value === undefined) {
|
|
109
|
+
return chalk.gray('null')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const type = schema?.type || typeof value
|
|
113
|
+
const format = schema?.format
|
|
114
|
+
|
|
115
|
+
switch (type) {
|
|
116
|
+
case 'string':
|
|
117
|
+
if (format === 'date-time') {
|
|
118
|
+
return chalk.green(value)
|
|
119
|
+
} else if (format === 'uri' || format === 'url') {
|
|
120
|
+
return chalk.blue.underline(value)
|
|
121
|
+
} else if (schema?.enum) {
|
|
122
|
+
return chalk.magenta(value)
|
|
123
|
+
}
|
|
124
|
+
return chalk.yellow(JSON.stringify(value))
|
|
125
|
+
|
|
126
|
+
case 'number':
|
|
127
|
+
case 'integer':
|
|
128
|
+
return chalk.cyan(String(value))
|
|
129
|
+
|
|
130
|
+
case 'boolean':
|
|
131
|
+
return value ? chalk.green('true') : chalk.red('false')
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
return chalk.yellow(JSON.stringify(value))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Render a summary line for an entity
|
|
140
|
+
*/
|
|
141
|
+
static renderSummary(
|
|
142
|
+
entityName: string,
|
|
143
|
+
data: any,
|
|
144
|
+
schema: any
|
|
145
|
+
): string {
|
|
146
|
+
const properties = schema?.properties || {}
|
|
147
|
+
const parts: string[] = []
|
|
148
|
+
|
|
149
|
+
// Try to find key identifying properties
|
|
150
|
+
const identifiers = ['name', 'identifier', 'id', 'title']
|
|
151
|
+
for (const key of identifiers) {
|
|
152
|
+
if (data[key] && properties[key]) {
|
|
153
|
+
parts.push(chalk.yellow(data[key]))
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Add status if present
|
|
159
|
+
if (data.status && properties.status) {
|
|
160
|
+
const statusColor = data.status === 'READY' ? chalk.green :
|
|
161
|
+
data.status === 'ERROR' ? chalk.red :
|
|
162
|
+
chalk.yellow
|
|
163
|
+
parts.push(statusColor(`[${data.status}]`))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parts.length > 0 ? parts.join(' ') : ''
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Render validation errors
|
|
171
|
+
*/
|
|
172
|
+
static renderErrors(errors: any[]): string {
|
|
173
|
+
if (errors.length === 0) return ''
|
|
174
|
+
|
|
175
|
+
const lines: string[] = [chalk.red.bold('Validation Errors:')]
|
|
176
|
+
for (const err of errors) {
|
|
177
|
+
lines.push(chalk.red(` ${err.path}: ${err.message}`))
|
|
178
|
+
}
|
|
179
|
+
return lines.join('\n')
|
|
180
|
+
}
|
|
181
|
+
}
|