@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,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
+ }
@@ -0,0 +1,349 @@
1
+
2
+ import { join, relative } from 'path'
3
+ import { readdir, readFile } from 'fs/promises'
4
+ import { RefResolver } from 'json-schema-ref-resolver'
5
+ import Ajv from 'ajv'
6
+ import addFormats from 'ajv-formats'
7
+
8
+ export interface ResolvedEntity {
9
+ schemaId: string
10
+ name: string
11
+ data: any
12
+ filePath: string
13
+ relPath: string
14
+ line?: number
15
+ valid: boolean
16
+ errors: any[]
17
+ }
18
+
19
+ export interface ResolverContext {
20
+ workspaceRootDir: string
21
+ workspaceName: string
22
+ schemasDir: string
23
+ factsDir: string
24
+ metaCacheDir: string
25
+ homeRegistryConnectionsDir: string
26
+ }
27
+
28
+ export interface WorkspaceResolverResult {
29
+ schemas: Map<string, any>
30
+ entities: Map<string, ResolvedEntity[]>
31
+ configEntities: Map<string, any>
32
+ factEntities: Map<string, ResolvedEntity[]>
33
+ connectionEntities: Map<string, ResolvedEntity[]>
34
+ refResolver: RefResolver
35
+ }
36
+
37
+ function createAjvInstance(): Ajv {
38
+ const ajv = new Ajv({
39
+ allErrors: true,
40
+ strict: false,
41
+ validateFormats: true,
42
+ logger: false
43
+ })
44
+ addFormats(ajv)
45
+ return ajv
46
+ }
47
+
48
+ export function Resolver(context: ResolverContext) {
49
+ const refResolver = new RefResolver()
50
+ const ajv = createAjvInstance()
51
+ const schemas = new Map<string, any>()
52
+
53
+ return {
54
+ async loadSchemas(): Promise<Map<string, any>> {
55
+ await loadSchemasInternal(context.schemasDir, refResolver, schemas)
56
+ return schemas
57
+ },
58
+
59
+ async loadConfigEntities(): Promise<{ configEntities: Map<string, any>; entities: Map<string, ResolvedEntity[]> }> {
60
+ const configEntities = new Map<string, any>()
61
+ const entities = new Map<string, ResolvedEntity[]>()
62
+ await loadConfigFileEntities(context.metaCacheDir, ajv, schemas, configEntities, entities)
63
+ return { configEntities, entities }
64
+ },
65
+
66
+ async loadFactEntities(): Promise<{ factEntities: Map<string, ResolvedEntity[]>; entities: Map<string, ResolvedEntity[]> }> {
67
+ const factEntities = new Map<string, ResolvedEntity[]>()
68
+ const entities = new Map<string, ResolvedEntity[]>()
69
+ await loadEntityFiles({
70
+ entityDir: context.factsDir,
71
+ workspaceRootDir: context.workspaceRootDir,
72
+ ajv,
73
+ schemas,
74
+ entityMap: factEntities,
75
+ allEntities: entities
76
+ })
77
+ return { factEntities, entities }
78
+ },
79
+
80
+ async loadConnectionEntities(): Promise<{ connectionEntities: Map<string, ResolvedEntity[]>; entities: Map<string, ResolvedEntity[]> }> {
81
+ const connectionEntities = new Map<string, ResolvedEntity[]>()
82
+ const entities = new Map<string, ResolvedEntity[]>()
83
+ await loadEntityFiles({
84
+ entityDir: context.homeRegistryConnectionsDir,
85
+ workspaceRootDir: context.workspaceRootDir,
86
+ ajv,
87
+ schemas,
88
+ entityMap: connectionEntities,
89
+ allEntities: entities,
90
+ isFlat: true
91
+ })
92
+ return { connectionEntities, entities }
93
+ },
94
+
95
+ getRefResolver(): RefResolver {
96
+ return refResolver
97
+ },
98
+
99
+ getSchemas(): Map<string, any> {
100
+ return schemas
101
+ }
102
+ }
103
+ }
104
+
105
+ async function loadSchemasInternal(
106
+ schemasDir: string,
107
+ refResolver: RefResolver,
108
+ schemas: Map<string, any>
109
+ ): Promise<void> {
110
+ let files: string[]
111
+ try {
112
+ files = await readdir(schemasDir)
113
+ } catch {
114
+ return
115
+ }
116
+
117
+ for (const file of files) {
118
+ if (!file.endsWith('.json')) continue
119
+ const filePath = join(schemasDir, file)
120
+ try {
121
+ const content = await readFile(filePath, 'utf-8')
122
+ const schema = JSON.parse(content)
123
+ if (schema.$id) {
124
+ refResolver.addSchema(schema)
125
+ // Store by $id (with version)
126
+ schemas.set(schema.$id, schema)
127
+ // Also store by entity name (without version) for easier lookup
128
+ const entityName = schema.$id.replace(/\.v\d+$/, '')
129
+ schemas.set(entityName, schema)
130
+ }
131
+ } catch {
132
+ // Skip files that can't be parsed
133
+ }
134
+ }
135
+ }
136
+
137
+ async function loadConfigFileEntities(
138
+ metaCacheDir: string,
139
+ ajv: Ajv,
140
+ schemas: Map<string, any>,
141
+ configEntities: Map<string, any>,
142
+ entities: Map<string, ResolvedEntity[]>
143
+ ): Promise<void> {
144
+
145
+ let metaFiles: string[]
146
+ try {
147
+ metaFiles = await readdir(metaCacheDir)
148
+ } catch {
149
+ return
150
+ }
151
+
152
+ for (const metaFile of metaFiles) {
153
+ if (!metaFile.endsWith('.json')) continue
154
+
155
+ try {
156
+ const metaPath = join(metaCacheDir, metaFile)
157
+ const metaContent = await readFile(metaPath, 'utf-8')
158
+ const metadata = JSON.parse(metaContent)
159
+
160
+ if (!metadata.entities || typeof metadata.entities !== 'object') continue
161
+
162
+ for (const [entityKey, entityMeta] of Object.entries(metadata.entities as Record<string, any>)) {
163
+ if (!entityKey.startsWith('#')) continue
164
+ const entityName = entityKey.substring(1)
165
+
166
+ configEntities.set(entityKey, entityMeta.data)
167
+
168
+ // Try to validate against schema
169
+ const schemaId = entityName + '.v0'
170
+ const schema = schemas.get(schemaId)
171
+
172
+ const resolved: ResolvedEntity = {
173
+ schemaId,
174
+ name: entityName,
175
+ data: entityMeta.data,
176
+ filePath: metadata.filePath,
177
+ relPath: metadata.relPath,
178
+ line: entityMeta.line,
179
+ valid: true,
180
+ errors: []
181
+ }
182
+
183
+ if (schema) {
184
+ try {
185
+ const validate = ajv.compile(schema)
186
+ if (!validate(entityMeta.data)) {
187
+ resolved.valid = false
188
+ resolved.errors = validate.errors?.map(e => ({
189
+ path: e.instancePath || '/',
190
+ message: e.message,
191
+ keyword: e.keyword
192
+ })) || []
193
+ }
194
+ } catch {
195
+ // Schema compilation failed
196
+ }
197
+ }
198
+
199
+ if (!entities.has(entityKey)) entities.set(entityKey, [])
200
+ entities.get(entityKey)!.push(resolved)
201
+ }
202
+ } catch {
203
+ // Skip unparseable metadata files
204
+ }
205
+ }
206
+ }
207
+
208
+ interface LoadEntityFilesOptions {
209
+ entityDir: string
210
+ workspaceRootDir: string
211
+ ajv: Ajv
212
+ schemas: Map<string, any>
213
+ entityMap: Map<string, ResolvedEntity[]>
214
+ allEntities: Map<string, ResolvedEntity[]>
215
+ isFlat?: boolean // true for connections (flat .json files), false for facts (subdirectories)
216
+ }
217
+
218
+ async function loadEntityFiles(options: LoadEntityFilesOptions): Promise<void> {
219
+ const { entityDir, workspaceRootDir, ajv, schemas, entityMap, allEntities, isFlat = false } = options
220
+
221
+ let items: string[]
222
+ try {
223
+ items = await readdir(entityDir)
224
+ } catch {
225
+ return
226
+ }
227
+
228
+ if (isFlat) {
229
+ // Flat structure: files are directly in entityDir (e.g., connections)
230
+ for (const file of items) {
231
+ if (!file.startsWith('@') || !file.endsWith('.json')) continue
232
+
233
+ const entityTypeDir = file.replace(/\.json$/, '')
234
+ const entityName = entityTypeDir.replace(/~/g, '/')
235
+ const schema = schemas.get(entityName)
236
+ const filePath = join(entityDir, file)
237
+ const fileName = file.replace(/\.json$/, '')
238
+
239
+ try {
240
+ const content = await readFile(filePath, 'utf-8')
241
+ const data = JSON.parse(content)
242
+ const cleanData = { ...data }
243
+ delete cleanData.$schema
244
+ delete cleanData.$defs
245
+ delete cleanData.$id
246
+
247
+ const resolved: ResolvedEntity = {
248
+ schemaId: entityName,
249
+ name: fileName,
250
+ data: cleanData,
251
+ filePath,
252
+ relPath: relative(workspaceRootDir, filePath),
253
+ valid: true,
254
+ errors: []
255
+ }
256
+
257
+ if (schema) {
258
+ try {
259
+ const validate = ajv.compile(schema)
260
+ if (!validate(cleanData)) {
261
+ resolved.valid = false
262
+ resolved.errors = validate.errors?.map(e => ({
263
+ path: e.instancePath || '/',
264
+ message: e.message,
265
+ keyword: e.keyword
266
+ })) || []
267
+ }
268
+ } catch {
269
+ // Schema compilation failed
270
+ }
271
+ }
272
+
273
+ if (!entityMap.has(entityName)) entityMap.set(entityName, [])
274
+ entityMap.get(entityName)!.push(resolved)
275
+
276
+ const entityKey = `#${entityName}`
277
+ if (!allEntities.has(entityKey)) allEntities.set(entityKey, [])
278
+ allEntities.get(entityKey)!.push(resolved)
279
+ } catch {
280
+ // Skip unparseable files
281
+ }
282
+ }
283
+ } else {
284
+ // Nested structure: subdirectories contain entity files (e.g., facts)
285
+ for (const entityTypeDir of items) {
286
+ if (!entityTypeDir.startsWith('@')) continue
287
+ const entityTypePath = join(entityDir, entityTypeDir)
288
+ const entityName = entityTypeDir.replace(/~/g, '/')
289
+ const schema = schemas.get(entityName)
290
+
291
+ let entityFiles: string[]
292
+ try {
293
+ entityFiles = await readdir(entityTypePath)
294
+ } catch {
295
+ continue
296
+ }
297
+
298
+ for (const entityFile of entityFiles) {
299
+ if (!entityFile.endsWith('.json')) continue
300
+ const entityFilePath = join(entityTypePath, entityFile)
301
+ const entityFileName = entityFile.slice(0, -5) // strip .json
302
+
303
+ try {
304
+ const content = await readFile(entityFilePath, 'utf-8')
305
+ const data = JSON.parse(content)
306
+ const cleanData = { ...data }
307
+ delete cleanData.$schema
308
+ delete cleanData.$defs
309
+ delete cleanData.$id
310
+
311
+ const resolved: ResolvedEntity = {
312
+ schemaId: entityName,
313
+ name: entityFileName,
314
+ data: cleanData,
315
+ filePath: entityFilePath,
316
+ relPath: relative(workspaceRootDir, entityFilePath),
317
+ valid: true,
318
+ errors: []
319
+ }
320
+
321
+ if (schema) {
322
+ try {
323
+ const validate = ajv.compile(schema)
324
+ if (!validate(cleanData)) {
325
+ resolved.valid = false
326
+ resolved.errors = validate.errors?.map(e => ({
327
+ path: e.instancePath || '/',
328
+ message: e.message,
329
+ keyword: e.keyword
330
+ })) || []
331
+ }
332
+ } catch {
333
+ // Schema compilation failed
334
+ }
335
+ }
336
+
337
+ if (!entityMap.has(entityName)) entityMap.set(entityName, [])
338
+ entityMap.get(entityName)!.push(resolved)
339
+
340
+ const entityKey = `#${entityName}`
341
+ if (!allEntities.has(entityKey)) allEntities.set(entityKey, [])
342
+ allEntities.get(entityKey)!.push(resolved)
343
+ } catch {
344
+ // Skip unparseable files
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
package/lib/ucan.ts ADDED
@@ -0,0 +1,137 @@
1
+
2
+ import { ed25519 } from '@ucanto/principal';
3
+ import * as Server from '@ucanto/server';
4
+
5
+
6
+ export async function generateKeypair(): Promise<{ did: string; privateKey: string }> {
7
+ const principal = await ed25519.generate();
8
+ // The principal itself is a Uint8Array containing the private key
9
+ const privateKeyBytes = new Uint8Array(principal as any);
10
+ return {
11
+ did: principal.did(),
12
+ privateKey: Buffer.from(privateKeyBytes).toString('base64'),
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Extract DID from a base64-encoded private key
18
+ * @param privateKeyBase64 - Base64-encoded private key
19
+ * @returns The DID string
20
+ */
21
+ export function didForPrivateKey(privateKeyBase64: string): string {
22
+ const keyBytes = Buffer.from(privateKeyBase64, 'base64');
23
+ const principal = ed25519.decode(new Uint8Array(keyBytes));
24
+ return principal.did();
25
+ }
26
+
27
+ /**
28
+ * Issue a UCAN capability delegation
29
+ * @param options.issuerPrivateKey - Base64-encoded private key of the issuer
30
+ * @param options.audienceDID - DID of the audience (recipient)
31
+ * @param options.capabilities - Array of capabilities to delegate
32
+ * @param options.expiresIn - Expiration time in seconds from now (default: 1 year)
33
+ * @returns Base64-encoded UCAN token
34
+ */
35
+ export async function issueCapability(options: {
36
+ issuerPrivateKey: string;
37
+ audienceDID: string;
38
+ capabilities: Array<{ with: string; can: string }>;
39
+ expiresIn?: number;
40
+ }): Promise<string> {
41
+ // Decode the issuer's private key
42
+ const keyBytes = Buffer.from(options.issuerPrivateKey, 'base64');
43
+ const issuer = ed25519.decode(new Uint8Array(keyBytes));
44
+
45
+ // Parse the audience DID
46
+ const audience = ed25519.Verifier.parse(options.audienceDID);
47
+
48
+ const expiresIn = options.expiresIn || 365 * 24 * 60 * 60; // 1 year
49
+ const expiration = Math.floor(Date.now() / 1000) + expiresIn;
50
+
51
+ const delegation = await Server.delegate({
52
+ issuer,
53
+ audience,
54
+ capabilities: options.capabilities,
55
+ expiration,
56
+ });
57
+
58
+ const archive = await delegation.archive();
59
+ if (!archive.ok) {
60
+ throw new Error('Failed to archive delegation');
61
+ }
62
+ return Buffer.from(archive.ok).toString('base64');
63
+ };
64
+
65
+ /**
66
+ * Validate a UCAN capability delegation
67
+ * @param options.ucanToken - Base64-encoded UCAN token
68
+ * @param options.issuerDID - Expected DID of the issuer
69
+ * @param options.expectedCapability - Expected capability (optional)
70
+ * @returns Validation result with delegation details
71
+ */
72
+ export async function validateCapability(options: {
73
+ ucanToken: string;
74
+ issuerDID: string;
75
+ expectedCapability?: { can: string };
76
+ }): Promise<{
77
+ valid: boolean;
78
+ error?: string;
79
+ issuer?: string;
80
+ audience?: string;
81
+ capabilities?: Array<{ with: string; can: string }>;
82
+ expiration?: number;
83
+ }> {
84
+ try {
85
+ // Parse the UCAN from the token
86
+ const carBytes = Buffer.from(options.ucanToken, 'base64');
87
+ const result: any = await Server.Delegation.extract(carBytes);
88
+ if (!result.ok) {
89
+ return {
90
+ valid: false,
91
+ error: `Failed to parse UCAN: ${result.error.message}`,
92
+ };
93
+ }
94
+ const delegation: any = result.ok;
95
+
96
+ // Basic validation: check expiration
97
+ const now = Math.floor(Date.now() / 1000);
98
+ if (delegation.expiration && delegation.expiration < now) {
99
+ return {
100
+ valid: false,
101
+ error: 'UCAN has expired',
102
+ };
103
+ }
104
+
105
+ // Verify issuer matches expected
106
+ if (delegation.issuer.did() !== options.issuerDID) {
107
+ return {
108
+ valid: false,
109
+ error: `UCAN not issued by expected issuer. Expected: ${options.issuerDID}, Got: ${delegation.issuer.did()}`,
110
+ };
111
+ }
112
+
113
+ // Validate capability if specified
114
+ if (options.expectedCapability) {
115
+ const capability = delegation.capabilities[0];
116
+ if (capability.can !== options.expectedCapability.can) {
117
+ return {
118
+ valid: false,
119
+ error: `Invalid capability: expected '${options.expectedCapability.can}', got '${capability.can}'`,
120
+ };
121
+ }
122
+ }
123
+
124
+ return {
125
+ valid: true,
126
+ issuer: delegation.issuer.did(),
127
+ audience: delegation.audience.did(),
128
+ capabilities: delegation.capabilities,
129
+ expiration: delegation.expiration,
130
+ };
131
+ } catch (error: any) {
132
+ return {
133
+ valid: false,
134
+ error: `Failed to validate UCAN: ${error.message}`,
135
+ };
136
+ }
137
+ }