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
|
@@ -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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "t44",
|
|
3
|
+
"version": "0.4.0-rc.3",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Stream44/t44.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"t44": "./bin/t44"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./workspace-rt": "./workspace-rt.ts",
|
|
15
|
+
"./caps/Home": "./caps/Home.ts",
|
|
16
|
+
"./caps/HomeRegistry": "./caps/HomeRegistry.ts",
|
|
17
|
+
"./caps/OpenApiSchema": "./caps/OpenApiSchema.ts",
|
|
18
|
+
"./caps/JsonSchemas": "./caps/JsonSchemas.ts",
|
|
19
|
+
"./caps/ProjectDeployment": "./caps/ProjectDeployment.ts",
|
|
20
|
+
"./caps/ProjectDevelopment": "./caps/ProjectDevelopment.ts",
|
|
21
|
+
"./caps/ProjectPublishing": "./caps/ProjectPublishing.ts",
|
|
22
|
+
"./caps/ProjectRack": "./caps/ProjectRack.ts",
|
|
23
|
+
"./caps/WorkspaceCli": "./caps/WorkspaceCli.ts",
|
|
24
|
+
"./caps/WorkspaceConfig": "./caps/WorkspaceConfig.ts",
|
|
25
|
+
"./caps/WorkspaceConnection": "./caps/WorkspaceConnection.ts",
|
|
26
|
+
"./caps/WorkspaceEntityConfig": "./caps/WorkspaceEntityConfig.ts",
|
|
27
|
+
"./caps/WorkspaceEntityFact": "./caps/WorkspaceEntityFact.ts",
|
|
28
|
+
"./caps/WorkspaceInfo": "./caps/WorkspaceInfo.ts",
|
|
29
|
+
"./caps/WorkspaceInit": "./caps/WorkspaceInit.ts",
|
|
30
|
+
"./caps/RootKey": "./caps/RootKey.ts",
|
|
31
|
+
"./caps/WorkspaceKey": "./caps/WorkspaceKey.ts",
|
|
32
|
+
"./caps/WorkspaceProjects": "./caps/WorkspaceProjects.ts",
|
|
33
|
+
"./caps/WorkspacePrompt": "./caps/WorkspacePrompt.ts",
|
|
34
|
+
"./caps/WorkspaceShell": "./caps/WorkspaceShell.ts",
|
|
35
|
+
"./caps/WorkspaceShellCli": "./caps/WorkspaceShellCli.ts",
|
|
36
|
+
"./caps/WorkspaceTest": "./caps/WorkspaceTest.ts",
|
|
37
|
+
"./caps/providers/bunny.net/ProjectDeployment": "./caps/providers/bunny.net/ProjectDeployment.ts",
|
|
38
|
+
"./caps/providers/dynadot.com/ProjectDeployment": "./caps/providers/dynadot.com/ProjectDeployment.ts",
|
|
39
|
+
"./caps/providers/git-scm.com/ProjectPublishing": "./caps/providers/git-scm.com/ProjectPublishing.ts",
|
|
40
|
+
"./caps/providers/github.com/ProjectPublishing": "./caps/providers/github.com/ProjectPublishing.ts",
|
|
41
|
+
"./caps/providers/npmjs.com/ProjectPublishing": "./caps/providers/npmjs.com/ProjectPublishing.ts",
|
|
42
|
+
"./caps/providers/semver.org/ProjectPublishing": "./caps/providers/semver.org/ProjectPublishing.ts",
|
|
43
|
+
"./caps/providers/vercel.com/ProjectDeployment": "./caps/providers/vercel.com/ProjectDeployment.ts",
|
|
44
|
+
"./structs/HomeRegistryConfig": "./structs/HomeRegistryConfig.ts",
|
|
45
|
+
"./structs/RootKeyConfig": "./structs/RootKeyConfig.ts",
|
|
46
|
+
"./structs/ProjectDeploymentConfig": "./structs/ProjectDeploymentConfig.ts",
|
|
47
|
+
"./structs/ProjectDeploymentFact": "./structs/ProjectDeploymentFact.ts",
|
|
48
|
+
"./structs/ProjectPublishingFact": "./structs/ProjectPublishingFact.ts",
|
|
49
|
+
"./structs/ProjectRackConfig": "./structs/ProjectRackConfig.ts",
|
|
50
|
+
"./structs/WorkspaceCliConfig": "./structs/WorkspaceCliConfig.ts",
|
|
51
|
+
"./structs/WorkspaceConfig": "./structs/WorkspaceConfig.ts",
|
|
52
|
+
"./structs/WorkspaceKeyConfig": "./structs/WorkspaceKeyConfig.ts",
|
|
53
|
+
"./structs/WorkspaceMappingsConfig": "./structs/WorkspaceMappingsConfig.ts",
|
|
54
|
+
"./structs/WorkspaceProjectsConfig": "./structs/WorkspaceProjectsConfig.ts",
|
|
55
|
+
"./structs/WorkspacePublishingConfig": "./structs/WorkspacePublishingConfig.ts",
|
|
56
|
+
"./structs/WorkspaceShellConfig": "./structs/WorkspaceShellConfig.ts",
|
|
57
|
+
"./structs/providers/bunny.net/PullZoneFact": "./structs/providers/bunny.net/PullZoneFact.ts",
|
|
58
|
+
"./structs/providers/bunny.net/PullZoneListFact": "./structs/providers/bunny.net/PullZoneListFact.ts",
|
|
59
|
+
"./structs/providers/bunny.net/StorageZoneFact": "./structs/providers/bunny.net/StorageZoneFact.ts",
|
|
60
|
+
"./structs/providers/bunny.net/StorageZoneListFact": "./structs/providers/bunny.net/StorageZoneListFact.ts",
|
|
61
|
+
"./structs/providers/bunny.net/WorkspaceConnectionConfig": "./structs/providers/bunny.net/WorkspaceConnectionConfig.ts",
|
|
62
|
+
"./structs/providers/dynadot.com/DomainFact": "./structs/providers/dynadot.com/DomainFact.ts",
|
|
63
|
+
"./structs/providers/dynadot.com/WorkspaceConnectionConfig": "./structs/providers/dynadot.com/WorkspaceConnectionConfig.ts",
|
|
64
|
+
"./structs/providers/git-scm.com/ProjectPublishingFact": "./structs/providers/git-scm.com/ProjectPublishingFact.ts",
|
|
65
|
+
"./structs/providers/github.com/ProjectPublishingFact": "./structs/providers/github.com/ProjectPublishingFact.ts",
|
|
66
|
+
"./structs/providers/github.com/WorkspaceConnectionConfig": "./structs/providers/github.com/WorkspaceConnectionConfig.ts",
|
|
67
|
+
"./structs/providers/npmjs.com/ProjectPublishingFact": "./structs/providers/npmjs.com/ProjectPublishingFact.ts",
|
|
68
|
+
"./structs/providers/vercel.com/ProjectDeploymentFact": "./structs/providers/vercel.com/ProjectDeploymentFact.ts",
|
|
69
|
+
"./structs/providers/vercel.com/WorkspaceConnectionConfig": "./structs/providers/vercel.com/WorkspaceConnectionConfig.ts"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"ajv": "^8.17.1",
|
|
73
|
+
"ajv-formats": "^3.0.1",
|
|
74
|
+
"js-yaml": "^4.1.0",
|
|
75
|
+
"commander": "^14.0.0",
|
|
76
|
+
"chalk": "^5.6.2",
|
|
77
|
+
"dotenv": "^17.2.3",
|
|
78
|
+
"fast-glob": "^3.3.3",
|
|
79
|
+
"inquirer": "^12.4.0",
|
|
80
|
+
"upload-to-bunny": "^2.1.0",
|
|
81
|
+
"axios": "^1.13.4",
|
|
82
|
+
"vercel": "^50.4.9",
|
|
83
|
+
"turbo": "^2.7.5",
|
|
84
|
+
"@ucanto/principal": "^9.0.3",
|
|
85
|
+
"@ucanto/server": "^11.0.3",
|
|
86
|
+
"json-schema-ref-resolver": "^3.0.0",
|
|
87
|
+
"@stream44.studio/encapsulate": "^0.4.0-rc.3",
|
|
88
|
+
"@stream44.studio/dco": "^0.3.0-rc.3",
|
|
89
|
+
"@stream44.studio/t44-blockchaincommons.com": "^0.1.0-rc.3"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"@types/bun": "^1.3.4",
|
|
93
|
+
"@types/node": "^25.0.3",
|
|
94
|
+
"@types/js-yaml": "^4.0.9",
|
|
95
|
+
"bun-types": "^1.3.4"
|
|
96
|
+
},
|
|
97
|
+
"workspaces": [
|
|
98
|
+
"caps/providers/vercel.com"
|
|
99
|
+
],
|
|
100
|
+
"private": false
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
export async function capsule({
|
|
3
|
+
encapsulate,
|
|
4
|
+
CapsulePropertyTypes,
|
|
5
|
+
makeImportStack
|
|
6
|
+
}: any) {
|
|
7
|
+
return encapsulate({
|
|
8
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
9
|
+
'#t44/caps/ConfigSchemaStruct': {
|
|
10
|
+
as: 'schema',
|
|
11
|
+
options: {
|
|
12
|
+
'#': {
|
|
13
|
+
schema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
did: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The DID (Decentralized Identifier) of the home registry.'
|
|
19
|
+
},
|
|
20
|
+
privateKey: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The private key associated with the home registry DID.'
|
|
23
|
+
},
|
|
24
|
+
createdAt: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
format: 'date-time',
|
|
27
|
+
description: 'ISO 8601 timestamp of when the registry was created.'
|
|
28
|
+
},
|
|
29
|
+
rootDir: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Absolute path to the home registry root directory.'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['did', 'privateKey', 'createdAt', 'rootDir'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
description: 'Home registry identity containing the root DID and private key.'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
'#': {
|
|
42
|
+
capsuleName: {
|
|
43
|
+
type: CapsulePropertyTypes.Literal,
|
|
44
|
+
value: capsule['#']
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
extendsCapsule: 't44/caps/HomeRegistryFile',
|
|
50
|
+
importMeta: import.meta,
|
|
51
|
+
importStack: makeImportStack(),
|
|
52
|
+
capsuleName: capsule['#'],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
capsule['#'] = 't44/structs/HomeRegistry'
|