@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,329 @@
1
+
2
+ import { join } from 'path'
3
+
4
+ // IMPORTANT: Connection config files contain encrypted credentials.
5
+ // NEVER delete these files programmatically. If decryption fails,
6
+ // log a clear error and exit so the user can investigate. The user must manually delete and re-enter
7
+ // credentials if the workspace key has changed.
8
+
9
+ // Track which connection setup titles and descriptions have been shown
10
+ const shownConnectionTitles = new Set<string>()
11
+ const shownDescriptions = new Set<string>()
12
+
13
+ // Cache for in-flight getStoredConfig promises to prevent parallel decryption race conditions
14
+ const storedConfigCache = new Map<string, Promise<Record<string, any> | null>>()
15
+
16
+ // TODO: Remove after all workspaces have been migrated.
17
+ // Maps new capsule name prefixes to old capsule name prefixes for credential file migration.
18
+ const structRelocations: Record<string, string> = {
19
+ '@stream44.studio/t44-bunny.net/structs/': '@stream44.studio/t44/structs/providers/bunny.net/',
20
+ '@stream44.studio/t44-vercel.com/structs/': '@stream44.studio/t44/structs/providers/vercel.com/',
21
+ '@stream44.studio/t44-github.com/structs/': '@stream44.studio/t44/structs/providers/github.com/',
22
+ '@stream44.studio/t44-dynadot.com/structs/': '@stream44.studio/t44/structs/providers/dynadot.com/',
23
+ '@stream44.studio/t44-npmjs.com/structs/': '@stream44.studio/t44/structs/providers/npmjs.com/',
24
+ }
25
+
26
+ export async function capsule({
27
+ encapsulate,
28
+ CapsulePropertyTypes,
29
+ makeImportStack
30
+ }: {
31
+ encapsulate: any
32
+ CapsulePropertyTypes: any
33
+ makeImportStack: any
34
+ }) {
35
+ return encapsulate({
36
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
37
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
38
+ '#': {
39
+ Home: {
40
+ type: CapsulePropertyTypes.Mapping,
41
+ value: '@stream44.studio/t44/caps/Home'
42
+ },
43
+ WorkspaceConfig: {
44
+ type: CapsulePropertyTypes.Mapping,
45
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
46
+ },
47
+ WorkspacePrompt: {
48
+ type: CapsulePropertyTypes.Mapping,
49
+ value: '@stream44.studio/t44/caps/WorkspacePrompt'
50
+ },
51
+ WorkspaceKey: {
52
+ type: CapsulePropertyTypes.Mapping,
53
+ value: '@stream44.studio/t44/caps/WorkspaceKey'
54
+ },
55
+ HomeRegistry: {
56
+ type: CapsulePropertyTypes.Mapping,
57
+ value: '@stream44.studio/t44/caps/HomeRegistry'
58
+ },
59
+ JsonSchema: {
60
+ type: CapsulePropertyTypes.Mapping,
61
+ value: '@stream44.studio/t44/caps/JsonSchemas'
62
+ },
63
+ RegisterSchemas: {
64
+ type: CapsulePropertyTypes.StructInit,
65
+ value: async function (this: any): Promise<void> {
66
+ if (this.schema?.schema) {
67
+ const version = this.schemaMinorVersion || '0'
68
+ await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
69
+ }
70
+ }
71
+ },
72
+ getFilepath: {
73
+ type: CapsulePropertyTypes.Function,
74
+ value: async function (this: any): Promise<string> {
75
+ const registryDir = await this.Home.registryDir
76
+ const workspaceConfig = await this.WorkspaceConfig.config
77
+ const workspaceConfigStruct = workspaceConfig?.['#@stream44.studio/t44/structs/WorkspaceConfig'] || {}
78
+ const workspaceName = workspaceConfigStruct.name
79
+ const connectionType = this.capsuleName.replace(/\//g, '~')
80
+ return join(registryDir, '@t44.sh~t44~caps~WorkspaceConnection', workspaceName, `${connectionType}.json`)
81
+ }
82
+ },
83
+ getRelativeFilepath: {
84
+ type: CapsulePropertyTypes.Function,
85
+ value: async function (this: any): Promise<string> {
86
+ const fullPath = await this.getFilepath()
87
+ return this.Home.relativePath(fullPath)
88
+ }
89
+ },
90
+ getStoredConfig: {
91
+ type: CapsulePropertyTypes.Function,
92
+ value: async function (this: any): Promise<Record<string, any> | null> {
93
+ const filepath = await this.getFilepath()
94
+
95
+ // TODO: Remove after all workspaces have been migrated.
96
+ // Migrate credential files from old struct paths to new package paths.
97
+ await this._migrateCredentialFile(filepath)
98
+
99
+ // Use cached promise if already in-flight to prevent parallel decryption race conditions
100
+ if (storedConfigCache.has(filepath)) {
101
+ return storedConfigCache.get(filepath)!
102
+ }
103
+
104
+ const promise = this._getStoredConfigImpl(filepath)
105
+ storedConfigCache.set(filepath, promise)
106
+
107
+ try {
108
+ return await promise
109
+ } finally {
110
+ // Clear cache after completion so next call gets fresh data
111
+ storedConfigCache.delete(filepath)
112
+ }
113
+ }
114
+ },
115
+ // TODO: Remove after all workspaces have been migrated.
116
+ _migrateCredentialFile: {
117
+ type: CapsulePropertyTypes.Function,
118
+ value: async function (this: any, newFilepath: string): Promise<void> {
119
+ const { access, rename, mkdir } = await import('fs/promises')
120
+ const { dirname } = await import('path')
121
+
122
+ // Check if the new file already exists — nothing to migrate
123
+ try {
124
+ await access(newFilepath)
125
+ return
126
+ } catch { }
127
+
128
+ // Determine if this capsule name matches a known relocation
129
+ let oldCapsuleName: string | null = null
130
+ for (const [newPrefix, oldPrefix] of Object.entries(structRelocations)) {
131
+ if (this.capsuleName.startsWith(newPrefix)) {
132
+ oldCapsuleName = oldPrefix + this.capsuleName.slice(newPrefix.length)
133
+ break
134
+ }
135
+ }
136
+ if (!oldCapsuleName) return
137
+
138
+ // Build old filepath using the same pattern as getFilepath
139
+ const oldConnectionType = oldCapsuleName.replace(/\//g, '~')
140
+ const dir = dirname(newFilepath)
141
+ const oldFilepath = join(dir, `${oldConnectionType}.json`)
142
+
143
+ // Check if old file exists and move it
144
+ try {
145
+ await access(oldFilepath)
146
+ await mkdir(dirname(newFilepath), { recursive: true })
147
+ await rename(oldFilepath, newFilepath)
148
+ } catch { }
149
+ }
150
+ },
151
+ _getStoredConfigImpl: {
152
+ type: CapsulePropertyTypes.Function,
153
+ value: async function (this: any, filepath: string): Promise<Record<string, any> | null> {
154
+ const { readFile } = await import('fs/promises')
155
+
156
+ try {
157
+ const content = await readFile(filepath, 'utf-8')
158
+ const parsed = JSON.parse(content)
159
+ const config = parsed.config || {}
160
+
161
+ // Handle legacy encryptedConfig format (migrate to per-value encryption)
162
+ if (parsed.encryptedConfig) {
163
+ const decrypted = await this.WorkspaceKey.decryptString(parsed.encryptedConfig)
164
+ const legacyConfig = JSON.parse(decrypted)
165
+ // Re-save with per-value encryption
166
+ await this.setStoredConfig(legacyConfig)
167
+ return legacyConfig
168
+ }
169
+
170
+ const result: Record<string, any> = {}
171
+ let needsMigration = false
172
+
173
+ for (const [key, value] of Object.entries(config)) {
174
+ if (typeof value === 'string' && value.startsWith('aes-256-gcm:')) {
175
+ // Encrypted value: <algo>:<keyName>-<did>:<enc value>
176
+ // Also supports legacy format: <algo>:<keyName>:<enc value>
177
+ // Use lastIndexOf since DID contains colons but base64 does not
178
+ const lastColon = value.lastIndexOf(':')
179
+ if (lastColon > 'aes-256-gcm:'.length) {
180
+ const encryptedValue = value.substring(lastColon + 1)
181
+ const keyIdentifier = value.substring('aes-256-gcm:'.length, lastColon)
182
+ try {
183
+ const decrypted = await this.WorkspaceKey.decryptString(encryptedValue)
184
+ result[key] = JSON.parse(decrypted)
185
+ } catch (decryptErr: any) {
186
+ const chalk = (await import('chalk')).default
187
+ let currentKeyId = 'unknown'
188
+ try {
189
+ const keyConfig = await this.WorkspaceKey.ensureKey()
190
+ const did = await this.WorkspaceKey.getDid()
191
+ currentKeyId = `${keyConfig.keyName}-${did}`
192
+ } catch { }
193
+ console.error(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
194
+ console.error(chalk.red(`│ ✗ Connection Credential Decryption Failed │`))
195
+ console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
196
+ console.error(chalk.red(`│ │`))
197
+ console.error(chalk.red(`│ Cannot decrypt '${key}' in ${this.capsuleName}`))
198
+ console.error(chalk.red(`│ │`))
199
+ console.error(chalk.red(`│ This credential was encrypted with a different workspace key. │`))
200
+ console.error(chalk.red(`│ Encrypted with: ${keyIdentifier}`))
201
+ console.error(chalk.red(`│ Current key: ${currentKeyId}`))
202
+ console.error(chalk.red(`│ │`))
203
+ console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
204
+ console.error(chalk.red(`│ To fix, delete the connection config and re-enter credentials: │`))
205
+ console.error(chalk.red(`│ │`))
206
+ console.error(chalk.red(`│ rm ${filepath}`))
207
+ console.error(chalk.red(`│ │`))
208
+ console.error(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
209
+ process.exit(1)
210
+ }
211
+ }
212
+ } else {
213
+ // Plain text value - needs migration
214
+ result[key] = value
215
+ needsMigration = true
216
+ }
217
+ }
218
+
219
+ // Auto-migrate plain text values to encrypted
220
+ if (needsMigration) {
221
+ await this.setStoredConfig(result)
222
+ }
223
+
224
+ return Object.keys(result).length > 0 ? result : null
225
+ } catch (err: any) {
226
+ // If file doesn't exist, that's normal - user hasn't configured this provider yet
227
+ if (err?.code === 'ENOENT') {
228
+ return null
229
+ }
230
+
231
+ // For other errors (permission issues, malformed JSON, etc.), log and exit
232
+ const chalk = (await import('chalk')).default
233
+ console.error(chalk.red(`\n\u2717 Failed to read connection config for '${this.capsuleName}'\n`))
234
+ console.error(chalk.red(` File: ${filepath}`))
235
+ console.error(chalk.red(` Error: ${err?.message || err}\n`))
236
+ process.exit(1)
237
+ }
238
+ }
239
+ },
240
+ setStoredConfig: {
241
+ type: CapsulePropertyTypes.Function,
242
+ value: async function (this: any, config: Record<string, any>): Promise<void> {
243
+ const { mkdir, writeFile } = await import('fs/promises')
244
+ const { dirname } = await import('path')
245
+ const filepath = await this.getFilepath()
246
+ const dir = dirname(filepath)
247
+
248
+ await mkdir(dir, { recursive: true })
249
+
250
+ // Ensure workspace key exists and get key name + DID
251
+ const { keyName } = await this.WorkspaceKey.ensureKey()
252
+ const did = await this.WorkspaceKey.getDid()
253
+
254
+ // Encrypt each value separately with prefix format
255
+ const encryptedConfig: Record<string, string> = {}
256
+ for (const [key, value] of Object.entries(config)) {
257
+ const valueJson = JSON.stringify(value)
258
+ const encrypted = await this.WorkspaceKey.encryptString(valueJson)
259
+ // Format: <algo>:<keyName>-<did>:<enc value>
260
+ encryptedConfig[key] = `aes-256-gcm:${keyName}-${did}:${encrypted}`
261
+ }
262
+
263
+ const output = {
264
+ config: encryptedConfig
265
+ }
266
+
267
+ await writeFile(filepath, JSON.stringify(output, null, 4), { mode: 0o600 })
268
+ }
269
+ },
270
+ getConfigValue: {
271
+ type: CapsulePropertyTypes.Function,
272
+ value: async function (this: any, key: string): Promise<any> {
273
+ const storedConfig = await this.getStoredConfig() || {}
274
+
275
+ if (storedConfig[key] !== undefined) {
276
+ return storedConfig[key]
277
+ }
278
+
279
+ // Value not set, need to prompt user
280
+ const propertySchema = this.schema?.schema?.properties?.[key]
281
+ if (!propertySchema) {
282
+ throw new Error(`No schema defined for config key "${key}" in ${this.capsuleName} connection config`)
283
+ }
284
+
285
+ // Create promptFactId for deduplication
286
+ const promptFactId = `${this.capsuleName}:${key}`
287
+
288
+ // Show title once per capsuleName
289
+ const chalk = (await import('chalk')).default
290
+ if (!shownConnectionTitles.has(this.capsuleName)) {
291
+ console.log(chalk.cyan(`\n🔑 ${this.capsuleName} Connection Setup\n`))
292
+ shownConnectionTitles.add(this.capsuleName)
293
+ }
294
+
295
+ // Show description once per promptFactId
296
+ if (propertySchema.description && !shownDescriptions.has(promptFactId)) {
297
+ console.log(chalk.gray(` ${propertySchema.description}\n`))
298
+ shownDescriptions.add(promptFactId)
299
+ }
300
+
301
+ const value = await this.WorkspacePrompt.input({
302
+ message: `${propertySchema.title || key}:`,
303
+ validate: (input: string) => {
304
+ if (!input || input.trim().length === 0) {
305
+ return `${propertySchema.title || key} cannot be empty`
306
+ }
307
+ return true
308
+ },
309
+ promptFactId
310
+ })
311
+
312
+ // Store the value
313
+ storedConfig[key] = value
314
+ await this.setStoredConfig(storedConfig)
315
+
316
+ console.log(chalk.green(`\n ✓ ${propertySchema.title || key} saved to connection config\n`))
317
+
318
+ return value
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }, {
324
+ importMeta: import.meta,
325
+ importStack: makeImportStack(),
326
+ capsuleName: capsule['#'],
327
+ })
328
+ }
329
+ capsule['#'] = '@stream44.studio/t44/caps/WorkspaceConnection'
@@ -0,0 +1,78 @@
1
+
2
+ export async function capsule({
3
+ encapsulate,
4
+ CapsulePropertyTypes,
5
+ makeImportStack
6
+ }: {
7
+ encapsulate: any
8
+ CapsulePropertyTypes: any
9
+ makeImportStack: any
10
+ }) {
11
+ return encapsulate({
12
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
13
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
14
+ '#': {
15
+ WorkspaceConfig: {
16
+ type: CapsulePropertyTypes.Mapping,
17
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
18
+ },
19
+ JsonSchema: {
20
+ type: CapsulePropertyTypes.Mapping,
21
+ value: '@stream44.studio/t44/caps/JsonSchemas'
22
+ },
23
+ config: {
24
+ type: CapsulePropertyTypes.GetterFunction,
25
+ value: async function (this: any): Promise<void> {
26
+
27
+ const config = await this.WorkspaceConfig.config
28
+
29
+ const configKey = '#' + this.capsuleName
30
+
31
+ const entityConfig = config[configKey] || undefined
32
+
33
+ return entityConfig
34
+ }
35
+ },
36
+ setConfigValue: {
37
+ type: CapsulePropertyTypes.Function,
38
+ value: async function (this: any, path: string[], value: any): Promise<void> {
39
+
40
+ const configKey = '#' + this.capsuleName
41
+
42
+ const now = new Date().toISOString()
43
+ await this.WorkspaceConfig.ensureEntityTimestamps(
44
+ { entityName: this.capsuleName },
45
+ configKey, now
46
+ )
47
+
48
+ const changed = await this.WorkspaceConfig.setConfigValueForEntity(
49
+ { entityName: this.capsuleName, schema: this.schema },
50
+ [configKey, ...path], value
51
+ )
52
+
53
+ if (changed) {
54
+ await this.WorkspaceConfig.setConfigValueForEntity(
55
+ { entityName: this.capsuleName, schema: this.schema },
56
+ [configKey, 'updatedAt'], new Date().toISOString()
57
+ )
58
+ }
59
+ }
60
+ },
61
+ RegisterSchemas: {
62
+ type: CapsulePropertyTypes.StructInit,
63
+ value: async function (this: any): Promise<void> {
64
+ if (this.schema?.schema) {
65
+ const version = this.schemaMinorVersion || '0'
66
+ await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
67
+ }
68
+ }
69
+ },
70
+ }
71
+ }
72
+ }, {
73
+ importMeta: import.meta,
74
+ importStack: makeImportStack(),
75
+ capsuleName: capsule['#'],
76
+ })
77
+ }
78
+ capsule['#'] = '@stream44.studio/t44/caps/WorkspaceEntityConfig'
@@ -0,0 +1,77 @@
1
+
2
+ export async function capsule({
3
+ encapsulate,
4
+ CapsulePropertyTypes,
5
+ makeImportStack
6
+ }: {
7
+ encapsulate: any
8
+ CapsulePropertyTypes: any
9
+ makeImportStack: any
10
+ }) {
11
+ return encapsulate({
12
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
13
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
14
+ '#': {
15
+ WorkspaceConfig: {
16
+ type: CapsulePropertyTypes.Mapping,
17
+ value: '@stream44.studio/t44/caps/WorkspaceConfig.v0'
18
+ },
19
+ JsonSchema: {
20
+ type: CapsulePropertyTypes.Mapping,
21
+ value: '@stream44.studio/t44/caps/JsonSchemas.v0'
22
+ },
23
+ config: {
24
+ type: CapsulePropertyTypes.GetterFunction,
25
+ value: async function (this: any): Promise<void> {
26
+
27
+ const config = await this.WorkspaceConfig.config
28
+
29
+ const configKey = '#' + this.capsuleName
30
+
31
+ const entityConfig = config[configKey] || undefined
32
+
33
+ return entityConfig
34
+ }
35
+ },
36
+ setConfigValue: {
37
+ type: CapsulePropertyTypes.Function,
38
+ value: async function (this: any, path: string[], value: any): Promise<void> {
39
+
40
+ const configKey = '#' + this.capsuleName
41
+
42
+ await this.WorkspaceConfig.setConfigValueForEntity(
43
+ { entityName: this.capsuleName, schema: this.schema },
44
+ [configKey, 'createdAt'], new Date().toISOString(), { ifAbsent: true }
45
+ )
46
+
47
+ const changed = await this.WorkspaceConfig.setConfigValueForEntity(
48
+ { entityName: this.capsuleName, schema: this.schema },
49
+ [configKey, ...path], value
50
+ )
51
+
52
+ if (changed) {
53
+ await this.WorkspaceConfig.setConfigValueForEntity(
54
+ { entityName: this.capsuleName, schema: this.schema },
55
+ [configKey, 'updatedAt'], new Date().toISOString()
56
+ )
57
+ }
58
+ }
59
+ },
60
+ RegisterSchemas: {
61
+ type: CapsulePropertyTypes.StructInit,
62
+ value: async function (this: any): Promise<void> {
63
+ if (this.schema?.schema) {
64
+ const version = this.schemaMinorVersion || '0'
65
+ await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
66
+ }
67
+ }
68
+ },
69
+ }
70
+ }
71
+ }, {
72
+ importMeta: import.meta,
73
+ importStack: makeImportStack(),
74
+ capsuleName: capsule['#'],
75
+ })
76
+ }
77
+ capsule['#'] = '@stream44.studio/t44/caps/WorkspaceEntityConfig.v0'