@stream44.studio/t44 0.4.0-rc.24

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 (99) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yaml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yaml +13 -0
  4. package/.github/workflows/test.yaml +31 -0
  5. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  7. package/.o/GordianOpenIntegrity.yaml +21 -0
  8. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  9. package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
  10. package/.repo-identifier +1 -0
  11. package/DCO.md +34 -0
  12. package/LICENSE.txt +186 -0
  13. package/README.md +189 -0
  14. package/bin/activate +36 -0
  15. package/bin/activate.ts +30 -0
  16. package/bin/postinstall.sh +19 -0
  17. package/bin/shell +27 -0
  18. package/bin/t44 +27 -0
  19. package/caps/ConfigSchemaStruct.ts +55 -0
  20. package/caps/Home.ts +57 -0
  21. package/caps/HomeRegistry.ts +319 -0
  22. package/caps/HomeRegistryFile.ts +144 -0
  23. package/caps/JsonSchemas.ts +220 -0
  24. package/caps/OpenApiSchema.ts +67 -0
  25. package/caps/PackageDescriptor.ts +88 -0
  26. package/caps/ProjectCatalogs.ts +153 -0
  27. package/caps/ProjectDeployment.ts +426 -0
  28. package/caps/ProjectDevelopment.ts +257 -0
  29. package/caps/ProjectPublishing.ts +654 -0
  30. package/caps/ProjectPulling.ts +234 -0
  31. package/caps/ProjectRack.ts +155 -0
  32. package/caps/ProjectRepository.ts +332 -0
  33. package/caps/ProjectTest.ts +251 -0
  34. package/caps/ProjectTestLib.ts +257 -0
  35. package/caps/RootKey.ts +219 -0
  36. package/caps/SigningKey.ts +243 -0
  37. package/caps/TaskWorkflow.ts +192 -0
  38. package/caps/WorkspaceCli.ts +448 -0
  39. package/caps/WorkspaceConfig.ts +268 -0
  40. package/caps/WorkspaceConfig.yaml +87 -0
  41. package/caps/WorkspaceConfigFile.ts +902 -0
  42. package/caps/WorkspaceConnection.ts +329 -0
  43. package/caps/WorkspaceEntityConfig.ts +78 -0
  44. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  45. package/caps/WorkspaceEntityFact.ts +218 -0
  46. package/caps/WorkspaceInfo.ts +619 -0
  47. package/caps/WorkspaceInit.ts +30 -0
  48. package/caps/WorkspaceKey.ts +338 -0
  49. package/caps/WorkspaceModel.ts +373 -0
  50. package/caps/WorkspaceProjects.ts +636 -0
  51. package/caps/WorkspacePrompt.ts +430 -0
  52. package/caps/WorkspaceShell.sh +39 -0
  53. package/caps/WorkspaceShell.ts +104 -0
  54. package/caps/WorkspaceShell.yaml +64 -0
  55. package/caps/WorkspaceShellCli.ts +109 -0
  56. package/caps/patterns/README.md +2 -0
  57. package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
  58. package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
  59. package/docs/Overview.drawio +248 -0
  60. package/docs/Overview.svg +4 -0
  61. package/examples/01-Lifecycle/main.test.ts +223 -0
  62. package/lib/crypto.ts +53 -0
  63. package/lib/key.ts +381 -0
  64. package/lib/schema-console-renderer.ts +181 -0
  65. package/lib/schema-resolver.ts +349 -0
  66. package/lib/ucan.ts +137 -0
  67. package/package.json +91 -0
  68. package/standalone-rt.test.ts +150 -0
  69. package/standalone-rt.ts +140 -0
  70. package/structs/HomeRegistry.ts +55 -0
  71. package/structs/HomeRegistryConfig.ts +60 -0
  72. package/structs/ProjectCatalogsConfig.ts +53 -0
  73. package/structs/ProjectDeploymentConfig.ts +56 -0
  74. package/structs/ProjectDeploymentFact.ts +106 -0
  75. package/structs/ProjectPublishingConfig.ts +78 -0
  76. package/structs/ProjectPublishingFact.ts +68 -0
  77. package/structs/ProjectPullingConfig.ts +52 -0
  78. package/structs/ProjectRack.ts +51 -0
  79. package/structs/ProjectRackConfig.ts +56 -0
  80. package/structs/RepositoryOriginDescriptor.ts +51 -0
  81. package/structs/RootKeyConfig.ts +64 -0
  82. package/structs/SigningKeyConfig.ts +64 -0
  83. package/structs/Workspace.ts +56 -0
  84. package/structs/WorkspaceCatalogs.ts +56 -0
  85. package/structs/WorkspaceCliConfig.ts +53 -0
  86. package/structs/WorkspaceConfig.ts +64 -0
  87. package/structs/WorkspaceConfigFile.ts +50 -0
  88. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  89. package/structs/WorkspaceKey.ts +55 -0
  90. package/structs/WorkspaceKeyConfig.ts +56 -0
  91. package/structs/WorkspaceMappingsConfig.ts +56 -0
  92. package/structs/WorkspaceProject.ts +104 -0
  93. package/structs/WorkspaceProjectsConfig.ts +67 -0
  94. package/structs/WorkspaceShellConfig.ts +83 -0
  95. package/structs/patterns/README.md +2 -0
  96. package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
  97. package/tsconfig.json +33 -0
  98. package/workspace-rt.ts +152 -0
  99. package/workspace.yaml +3 -0
@@ -0,0 +1,338 @@
1
+ export async function capsule({
2
+ encapsulate,
3
+ CapsulePropertyTypes,
4
+ makeImportStack
5
+ }: {
6
+ encapsulate: any
7
+ CapsulePropertyTypes: any
8
+ makeImportStack: any
9
+ }) {
10
+ return encapsulate({
11
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
12
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
13
+ '#@stream44.studio/t44/structs/WorkspaceConfig': {
14
+ as: '$WorkspaceConfig'
15
+ },
16
+ '#@stream44.studio/t44/structs/WorkspaceKeyConfig': {
17
+ as: '$WorkspaceKeyConfig'
18
+ },
19
+ '#': {
20
+ WorkspacePrompt: {
21
+ type: CapsulePropertyTypes.Mapping,
22
+ value: '@stream44.studio/t44/caps/WorkspacePrompt'
23
+ },
24
+ HomeRegistry: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: '@stream44.studio/t44/caps/HomeRegistry'
27
+ },
28
+ RootKey: {
29
+ type: CapsulePropertyTypes.Mapping,
30
+ value: '@stream44.studio/t44/caps/RootKey'
31
+ },
32
+ ensureKey: {
33
+ type: CapsulePropertyTypes.Function,
34
+ value: async function (this: any): Promise<{ keyName: string; keyPath: string }> {
35
+ const workspaceConfig = await this.$WorkspaceConfig.config
36
+ const keyConfig = await this.$WorkspaceKeyConfig.config
37
+
38
+ // Check if key is already set in config (object format: { name, identifier })
39
+ if (keyConfig?.name && keyConfig?.identifier) {
40
+ const keyExists = await this.HomeRegistry.keyExists(keyConfig.name)
41
+
42
+ if (keyExists) {
43
+ // Migrate legacy: encrypt raw privateKey if not yet encrypted
44
+ const keyData = await this.HomeRegistry.getKey(keyConfig.name)
45
+ if (keyData?.privateKey && !keyData?.encryptedPrivateKey) {
46
+ const passphrase = await this._derivePassphrase(keyConfig.name)
47
+ const { encryptString: encryptFn } = await import('../lib/crypto.js')
48
+ const chalk = (await import('chalk')).default
49
+ keyData.encryptedPrivateKey = encryptFn(keyData.privateKey, passphrase)
50
+ delete keyData.privateKey
51
+ await this.HomeRegistry.setKey(keyConfig.name, keyData)
52
+ console.log(chalk.green(` ✓ Private key encrypted with root key\n`))
53
+ }
54
+
55
+ // Verify decryption works with current root key derivation
56
+ if (keyData?.encryptedPrivateKey) {
57
+ const passphrase = await this._derivePassphrase(keyConfig.name)
58
+ const { decryptString: decryptFn } = await import('../lib/crypto.js')
59
+ try {
60
+ decryptFn(keyData.encryptedPrivateKey, passphrase)
61
+ } catch {
62
+ const chalk = (await import('chalk')).default
63
+ const { stat: statFile } = await import('fs/promises')
64
+ const keyPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
65
+
66
+ // Run diagnostics
67
+ const rootKeyPath = await this.RootKey.getKeyPath()
68
+ let rootKeyFound = false
69
+ let rootKeyFingerprint = ''
70
+ if (rootKeyPath) {
71
+ try {
72
+ await statFile(rootKeyPath)
73
+ rootKeyFound = true
74
+ const { execSync } = await import('child_process')
75
+ const fpOutput = execSync(`ssh-keygen -lf ${JSON.stringify(rootKeyPath)}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
76
+ const match = fpOutput.match(/(SHA256:\S+)/)
77
+ rootKeyFingerprint = match ? match[1] : fpOutput
78
+ } catch { }
79
+ }
80
+
81
+ console.error(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
82
+ console.error(chalk.red(`│ ✗ Workspace Key Decryption Failed │`))
83
+ console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
84
+ console.error(chalk.red(`│ │`))
85
+ console.error(chalk.red(`│ The workspace key's encrypted private key cannot be decrypted │`))
86
+ console.error(chalk.red(`│ with the current root key. This typically happens when the │`))
87
+ console.error(chalk.red(`│ root SSH key has been changed or regenerated since the │`))
88
+ console.error(chalk.red(`│ workspace key was first created. │`))
89
+ console.error(chalk.red(`│ │`))
90
+ console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
91
+ console.error(chalk.red(`│ Diagnostics: │`))
92
+ console.error(chalk.red(`│ │`))
93
+ console.error(chalk.red(`│ ${rootKeyFound ? '✓' : '✗'} Root SSH key: ${rootKeyPath || '(not configured)'}`))
94
+ if (rootKeyFound && rootKeyFingerprint) {
95
+ console.error(chalk.red(`│ Fingerprint: ${rootKeyFingerprint}`))
96
+ }
97
+ console.error(chalk.red(`│ ✓ Workspace key file: ${keyPath}`))
98
+ console.error(chalk.red(`│ DID: ${keyData.did}`))
99
+ console.error(chalk.red(`│ Created: ${keyData.createdAt}`))
100
+ console.error(chalk.red(`│ ✗ Passphrase derivation: mismatch`))
101
+ console.error(chalk.red(`│ │`))
102
+ console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
103
+ console.error(chalk.red(`│ To fix, delete the workspace key file and re-run: │`))
104
+ console.error(chalk.red(`│ │`))
105
+ console.error(chalk.red(`│ rm ${keyPath}`))
106
+ console.error(chalk.red(`│ │`))
107
+ console.error(chalk.red(`│ A new workspace key will be generated automatically. │`))
108
+ console.error(chalk.red(`│ You will need to re-enter any saved connection credentials │`))
109
+ console.error(chalk.red(`│ (e.g. GitHub tokens) as they were encrypted with the old key. │`))
110
+ console.error(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
111
+ process.exit(1)
112
+ }
113
+ }
114
+
115
+ const keyPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
116
+ return { keyName: keyConfig.name, keyPath }
117
+ } else {
118
+ const chalk = (await import('chalk')).default
119
+ const keyPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
120
+ console.log(chalk.yellow(`\n⚠️ Workspace key '${keyConfig.name}' is configured but key file not found at:`))
121
+ console.log(chalk.yellow(` ${keyPath}\n`))
122
+ // Fall through to generate the key
123
+ }
124
+ }
125
+
126
+ let keyName: string
127
+
128
+ const keyConfigStructKey = '#@stream44.studio/t44/structs/WorkspaceKeyConfig'
129
+ if (!keyConfig?.name) {
130
+ const chalk = (await import('chalk')).default
131
+
132
+ console.log(chalk.cyan(`\n🔐 Workspace Key Setup\n`))
133
+ console.log(chalk.gray(` Workspace: ${workspaceConfig?.name || 'unknown'}`))
134
+ console.log(chalk.gray(` Root: ${workspaceConfig?.rootDir || 'unknown'}`))
135
+ console.log(chalk.gray(''))
136
+ console.log(chalk.gray(' All credentials in this workspace are encrypted with a workspace key.'))
137
+ console.log(chalk.gray(' You can select an existing key or create a new one.'))
138
+ console.log(chalk.gray(''))
139
+
140
+ // List existing workspace keys from registry
141
+ const existingKeys = await this.HomeRegistry.listKeys()
142
+
143
+ // Build choices
144
+ const choices: Array<{ name: string; value: any }> = []
145
+
146
+ for (const key of existingKeys) {
147
+ choices.push({
148
+ name: `${key.name} ${chalk.gray(key.did ? key.did.substring(0, 50) + '...' : '')}`,
149
+ value: { type: 'existing', name: key.name }
150
+ })
151
+ }
152
+
153
+ choices.push({
154
+ name: chalk.yellow('+ Create a new workspace key'),
155
+ value: { type: 'create' }
156
+ })
157
+
158
+ const selected = await this.WorkspacePrompt.select({
159
+ message: 'Select a workspace key:',
160
+ choices,
161
+ defaultValue: { type: 'create' },
162
+ pageSize: 15
163
+ })
164
+
165
+ if (selected.type === 'existing') {
166
+ keyName = selected.name
167
+ } else {
168
+ // Prompt for key name
169
+ keyName = await this.WorkspacePrompt.input({
170
+ message: 'Enter a name for the new workspace key:',
171
+ defaultValue: workspaceConfig?.name || 'genesis',
172
+ validate: (input: string) => {
173
+ if (!input || input.trim().length === 0) {
174
+ return 'Key name cannot be empty'
175
+ }
176
+ if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
177
+ return 'Key name can only contain letters, numbers, underscores, and hyphens'
178
+ }
179
+ return true
180
+ }
181
+ })
182
+ }
183
+ } else {
184
+ keyName = keyConfig.name
185
+ }
186
+
187
+ // Check if key already exists in registry
188
+ let keyData = await this.HomeRegistry.getKey(keyName)
189
+
190
+ // Derive passphrase for encrypting the private key
191
+ const passphrase = await this._derivePassphrase(keyName)
192
+ const { encryptString: encryptFn } = await import('../lib/crypto.js')
193
+
194
+ if (!keyData) {
195
+ const chalk = (await import('chalk')).default
196
+ // Generate Ed25519 key pair using UCAN library
197
+ console.log(chalk.cyan(`\n Generating Ed25519 key '${keyName}'...\n`))
198
+
199
+ const { generateKeypair } = await import('../lib/ucan.js')
200
+ const { did, privateKey } = await generateKeypair()
201
+
202
+ // Encrypt the private key before storing
203
+ const encryptedPrivateKey = encryptFn(privateKey, passphrase)
204
+
205
+ keyData = {
206
+ did,
207
+ encryptedPrivateKey,
208
+ createdAt: new Date().toISOString()
209
+ }
210
+
211
+ const keyPath = await this.HomeRegistry.setKey(keyName, keyData)
212
+
213
+ console.log(chalk.green(` ✓ Key generated and saved to:`))
214
+ console.log(chalk.green(` ${keyPath}`))
215
+ console.log(chalk.green(` ✓ DID: ${keyData.did}\n`))
216
+ } else {
217
+ const chalk = (await import('chalk')).default
218
+ const keyPath = await this.HomeRegistry.getKeyPath(keyName)
219
+
220
+ // Migrate legacy: encrypt raw privateKey and remove it
221
+ if (keyData.privateKey && !keyData.encryptedPrivateKey) {
222
+ const encryptedPrivateKey = encryptFn(keyData.privateKey, passphrase)
223
+ keyData.encryptedPrivateKey = encryptedPrivateKey
224
+ delete keyData.privateKey
225
+ await this.HomeRegistry.setKey(keyName, keyData)
226
+ console.log(chalk.green(`\n ✓ Using existing key at:`))
227
+ console.log(chalk.green(` ${keyPath}`))
228
+ console.log(chalk.green(` ✓ DID: ${keyData.did}`))
229
+ console.log(chalk.green(` ✓ Private key encrypted with root key\n`))
230
+ } else {
231
+ console.log(chalk.green(`\n ✓ Using existing key at:`))
232
+ console.log(chalk.green(` ${keyPath}`))
233
+ console.log(chalk.green(` ✓ DID: ${keyData.did}\n`))
234
+ }
235
+ }
236
+
237
+ // Store key as object { name, identifier } in key config struct
238
+ await this.$WorkspaceKeyConfig.setConfigValue(['name'], keyName)
239
+ await this.$WorkspaceKeyConfig.setConfigValue(['identifier'], keyData.did)
240
+
241
+ const keyPath = await this.HomeRegistry.getKeyPath(keyName)
242
+ return { keyName, keyPath }
243
+ }
244
+ },
245
+ getKeyPath: {
246
+ type: CapsulePropertyTypes.Function,
247
+ value: async function (this: any): Promise<string | null> {
248
+ const keyConfig = await this.$WorkspaceKeyConfig.config
249
+
250
+ if (!keyConfig?.name) {
251
+ return null
252
+ }
253
+
254
+ return this.HomeRegistry.getKeyPath(keyConfig.name)
255
+ }
256
+ },
257
+ getKey: {
258
+ type: CapsulePropertyTypes.Function,
259
+ value: async function (this: any): Promise<{ did: string; privateKey: string }> {
260
+ const keyConfig = await this.$WorkspaceKeyConfig.config
261
+
262
+ if (!keyConfig?.name) {
263
+ throw new Error('No workspace key configured. Run ensureKey() first.')
264
+ }
265
+
266
+ const keyData = await this.HomeRegistry.getKey(keyConfig.name)
267
+
268
+ if (!keyData) {
269
+ throw new Error(`Workspace key '${keyConfig.name}' not found in registry. Run ensureKey() first.`)
270
+ }
271
+
272
+ // Decrypt encryptedPrivateKey at runtime
273
+ if (keyData.encryptedPrivateKey) {
274
+ const passphrase = await this._derivePassphrase(keyConfig.name)
275
+ const { decryptString: decryptFn } = await import('../lib/crypto.js')
276
+ const privateKey = decryptFn(keyData.encryptedPrivateKey, passphrase)
277
+ return { did: keyData.did, privateKey }
278
+ }
279
+
280
+ // Legacy fallback: raw privateKey
281
+ if (keyData.privateKey) {
282
+ return { did: keyData.did, privateKey: keyData.privateKey }
283
+ }
284
+
285
+ throw new Error(`Workspace key '${keyConfig.name}' has no private key data.`)
286
+ }
287
+ },
288
+ getDid: {
289
+ type: CapsulePropertyTypes.Function,
290
+ value: async function (this: any): Promise<string> {
291
+ const { did } = await this.getKey()
292
+ return did
293
+ }
294
+ },
295
+ _derivePassphrase: {
296
+ type: CapsulePropertyTypes.Function,
297
+ value: async function (this: any, keyName: string): Promise<string> {
298
+ const crypto = await import('crypto')
299
+ const { readFile } = await import('fs/promises')
300
+
301
+ const rootKeyPath = await this.RootKey.getKeyPath()
302
+ if (!rootKeyPath) {
303
+ throw new Error('Root key not configured. Run RootKey.ensureKey() first.')
304
+ }
305
+
306
+ const privateKeyData = await readFile(rootKeyPath, 'utf-8')
307
+ const hash = crypto.createHmac('sha256', privateKeyData)
308
+ .update(keyName)
309
+ .digest('base64url')
310
+
311
+ return hash
312
+ }
313
+ },
314
+ encryptString: {
315
+ type: CapsulePropertyTypes.Function,
316
+ value: async function (this: any, plaintext: string): Promise<string> {
317
+ const { privateKey } = await this.getKey()
318
+ const { encryptString } = await import('../lib/crypto.js')
319
+ return encryptString(plaintext, privateKey)
320
+ }
321
+ },
322
+ decryptString: {
323
+ type: CapsulePropertyTypes.Function,
324
+ value: async function (this: any, ciphertext: string): Promise<string> {
325
+ const { privateKey } = await this.getKey()
326
+ const { decryptString } = await import('../lib/crypto.js')
327
+ return decryptString(ciphertext, privateKey)
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }, {
333
+ importMeta: import.meta,
334
+ importStack: makeImportStack(),
335
+ capsuleName: capsule['#'],
336
+ })
337
+ }
338
+ capsule['#'] = '@stream44.studio/t44/caps/WorkspaceKey'