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.
Files changed (125) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  4. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity.yaml +25 -0
  6. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  7. package/DCO.md +34 -0
  8. package/LICENSE.md +203 -0
  9. package/README.md +183 -0
  10. package/bin/activate +36 -0
  11. package/bin/activate.ts +30 -0
  12. package/bin/postinstall.sh +19 -0
  13. package/bin/shell +27 -0
  14. package/bin/t44 +27 -0
  15. package/caps/ConfigSchemaStruct.ts +55 -0
  16. package/caps/Home.ts +51 -0
  17. package/caps/HomeRegistry.ts +313 -0
  18. package/caps/HomeRegistryFile.ts +144 -0
  19. package/caps/JsonSchemas.ts +220 -0
  20. package/caps/OpenApiSchema.ts +67 -0
  21. package/caps/PackageDescriptor.ts +88 -0
  22. package/caps/ProjectCatalogs.ts +153 -0
  23. package/caps/ProjectDeployment.ts +363 -0
  24. package/caps/ProjectDevelopment.ts +257 -0
  25. package/caps/ProjectPublishing.ts +522 -0
  26. package/caps/ProjectRack.ts +155 -0
  27. package/caps/ProjectRepository.ts +322 -0
  28. package/caps/RootKey.ts +219 -0
  29. package/caps/SigningKey.ts +243 -0
  30. package/caps/WorkspaceCli.ts +442 -0
  31. package/caps/WorkspaceConfig.ts +268 -0
  32. package/caps/WorkspaceConfig.yaml +71 -0
  33. package/caps/WorkspaceConfigFile.ts +799 -0
  34. package/caps/WorkspaceConnection.ts +249 -0
  35. package/caps/WorkspaceEntityConfig.ts +78 -0
  36. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  37. package/caps/WorkspaceEntityFact.ts +218 -0
  38. package/caps/WorkspaceInfo.ts +595 -0
  39. package/caps/WorkspaceInit.ts +30 -0
  40. package/caps/WorkspaceKey.ts +338 -0
  41. package/caps/WorkspaceModel.ts +373 -0
  42. package/caps/WorkspaceProjects.ts +636 -0
  43. package/caps/WorkspacePrompt.ts +406 -0
  44. package/caps/WorkspaceShell.sh +39 -0
  45. package/caps/WorkspaceShell.ts +104 -0
  46. package/caps/WorkspaceShell.yaml +64 -0
  47. package/caps/WorkspaceShellCli.ts +109 -0
  48. package/caps/WorkspaceTest.ts +167 -0
  49. package/caps/providers/README.md +2 -0
  50. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  51. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  52. package/caps/providers/bunny.net/api-pull.ts +164 -0
  53. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  54. package/caps/providers/bunny.net/api-storage.ts +248 -0
  55. package/caps/providers/bunny.net/api.ts +95 -0
  56. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  57. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  58. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  59. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  61. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  63. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  64. package/caps/providers/github.com/ProjectPublishing.ts +118 -0
  65. package/caps/providers/github.com/api.ts +115 -0
  66. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  67. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  68. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  69. package/caps/providers/vercel.com/api.test.ts +67 -0
  70. package/caps/providers/vercel.com/api.ts +132 -0
  71. package/caps/providers/vercel.com/bun.lock +194 -0
  72. package/caps/providers/vercel.com/package.json +10 -0
  73. package/caps/providers/vercel.com/project.test.ts +108 -0
  74. package/caps/providers/vercel.com/project.ts +150 -0
  75. package/caps/providers/vercel.com/tsconfig.json +28 -0
  76. package/docs/Overview.drawio +248 -0
  77. package/docs/Overview.svg +4 -0
  78. package/lib/crypto.ts +53 -0
  79. package/lib/key.ts +365 -0
  80. package/lib/schema-console-renderer.ts +181 -0
  81. package/lib/schema-resolver.ts +349 -0
  82. package/lib/ucan.ts +137 -0
  83. package/package.json +101 -0
  84. package/structs/HomeRegistry.ts +55 -0
  85. package/structs/HomeRegistryConfig.ts +56 -0
  86. package/structs/ProjectCatalogsConfig.ts +53 -0
  87. package/structs/ProjectDeploymentConfig.ts +56 -0
  88. package/structs/ProjectDeploymentFact.ts +106 -0
  89. package/structs/ProjectPublishingFact.ts +68 -0
  90. package/structs/ProjectRack.ts +51 -0
  91. package/structs/ProjectRackConfig.ts +56 -0
  92. package/structs/RepositoryOriginDescriptor.ts +51 -0
  93. package/structs/RootKeyConfig.ts +64 -0
  94. package/structs/SigningKeyConfig.ts +64 -0
  95. package/structs/Workspace.ts +56 -0
  96. package/structs/WorkspaceCatalogs.ts +56 -0
  97. package/structs/WorkspaceCliConfig.ts +53 -0
  98. package/structs/WorkspaceConfig.ts +64 -0
  99. package/structs/WorkspaceConfigFile.ts +50 -0
  100. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  101. package/structs/WorkspaceKey.ts +55 -0
  102. package/structs/WorkspaceKeyConfig.ts +56 -0
  103. package/structs/WorkspaceMappingsConfig.ts +56 -0
  104. package/structs/WorkspaceProject.ts +104 -0
  105. package/structs/WorkspaceProjectsConfig.ts +67 -0
  106. package/structs/WorkspacePublishingConfig.ts +65 -0
  107. package/structs/WorkspaceShellConfig.ts +83 -0
  108. package/structs/providers/README.md +2 -0
  109. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  110. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  111. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  112. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  113. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  114. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  115. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  116. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  117. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  118. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  119. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  121. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  122. package/tests/01-Lifecycle/main.test.ts +173 -0
  123. package/tsconfig.json +28 -0
  124. package/workspace-rt.ts +134 -0
  125. package/workspace.yaml +3 -0
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bun test
2
+
3
+ export const testConfig = {
4
+ group: 'vendor',
5
+ runOnAll: false,
6
+ }
7
+
8
+ import * as bunTest from 'bun:test'
9
+ import { run } from 't44/workspace-rt'
10
+
11
+ const {
12
+ test: { describe, it, expect },
13
+ api
14
+ } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
15
+ const spine = await encapsulate({
16
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
17
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
18
+ '#': {
19
+ test: {
20
+ type: CapsulePropertyTypes.Mapping,
21
+ value: 't44/caps/WorkspaceTest',
22
+ options: {
23
+ '#': {
24
+ bunTest,
25
+ env: {
26
+ DYNADOT_API_KEY: { factReference: 't44/structs/providers/dynadot.com/WorkspaceConnectionConfig:apiKey' },
27
+ DYNADOT_API_SECRET: { factReference: 't44/structs/providers/dynadot.com/WorkspaceConnectionConfig:apiSecret' },
28
+ DYNADOT_API_KEY_TRANSACTION_SECRET: { factReference: 't44/structs/providers/dynadot.com/WorkspaceConnectionConfig:apiKeyTransactionSecret' }
29
+ }
30
+ }
31
+ }
32
+ },
33
+ api: {
34
+ type: CapsulePropertyTypes.Mapping,
35
+ value: './api-restful-v2'
36
+ },
37
+ }
38
+ }
39
+ }, {
40
+ importMeta: import.meta,
41
+ importStack: makeImportStack(),
42
+ capsuleName: 't44/caps/providers/dynadot.com/api-restful-v2.test'
43
+ })
44
+ return { spine }
45
+ }, async ({ spine, apis }: any) => {
46
+ return apis[spine.capsuleSourceLineRef]
47
+ }, {
48
+ importMeta: import.meta
49
+ })
50
+
51
+ describe('Dynadot REST API v2', function () {
52
+
53
+ let domainName: string
54
+
55
+ it('should list domains', async function () {
56
+
57
+ const result = await api.call({
58
+ method: 'GET',
59
+ path: 'domains',
60
+ operation: 'list'
61
+ })
62
+
63
+ expect(result).toBeObject()
64
+ expect(result.code).toBe(200)
65
+ expect(result.data).toBeObject()
66
+ expect(result.data.domain_info).toBeArray()
67
+ expect(result.data.domain_info.length).toBeGreaterThan(0)
68
+
69
+ domainName = result.data.domain_info[0].domain_name
70
+ })
71
+
72
+ it('should get domain info for first domain', async function () {
73
+
74
+ const result = await api.call({
75
+ method: 'GET',
76
+ path: `domains/${domainName}`,
77
+ operation: 'info'
78
+ })
79
+
80
+ expect(result).toBeObject()
81
+ expect(result.code).toBe(200)
82
+ expect(result.data).toBeObject()
83
+ expect(result.data.domain_info).toBeArray()
84
+ expect(result.data.domain_info[0].domain_name).toBe(domainName)
85
+ })
86
+
87
+ it('should set a TXT test record, verify, remove, and verify removal', async function () {
88
+
89
+ // 1. Get current DNS to snapshot existing records
90
+ const beforeDns = await api.call({
91
+ method: 'GET',
92
+ path: `domains/${domainName}`,
93
+ operation: 'getDns'
94
+ })
95
+ expect(beforeDns.code).toBe(200)
96
+ const nsBefore = beforeDns.data.domain_info[0].glue_info?.name_server_settings || {}
97
+ const existingSubs = nsBefore.sub_domains || []
98
+
99
+ // 2. Add a TXT test record using add_dns_to_current_setting to preserve all existing records
100
+ // v2 requires at least one dns_main_list entry
101
+ const existingMainDomains = nsBefore.main_domains || []
102
+ const mainListForAdd = existingMainDomains.length > 0
103
+ ? existingMainDomains.map((r: any) => ({
104
+ record_type: r.record_type,
105
+ record_value1: r.value || r.record_value1,
106
+ ...(r.record_value2 ? { record_value2: r.record_value2 } : {}),
107
+ }))
108
+ : [{ record_type: 'a', record_value1: '127.0.0.1' }]
109
+
110
+ const setResult = await api.call({
111
+ method: 'POST',
112
+ path: `domains/${domainName}/records`,
113
+ body: {
114
+ add_dns_to_current_setting: true,
115
+ dns_main_list: mainListForAdd,
116
+ sub_list: [{
117
+ sub_host: 't44-v2-api-test',
118
+ record_type: 'txt',
119
+ record_value1: 'v2-test-value'
120
+ }]
121
+ },
122
+ operation: 'setDns'
123
+ })
124
+ expect(setResult).toBeObject()
125
+ expect(setResult.code).toBe(200)
126
+
127
+ // 3. Get DNS again — verify TXT record was added and existing records preserved
128
+ const afterSet = await api.call({
129
+ method: 'GET',
130
+ path: `domains/${domainName}`,
131
+ operation: 'getDns'
132
+ })
133
+ expect(afterSet.code).toBe(200)
134
+ const nsAfterSet = afterSet.data.domain_info[0].glue_info?.name_server_settings || {}
135
+ const subsAfterSet = nsAfterSet.sub_domains || []
136
+
137
+ const txtRecord = subsAfterSet.find((r: any) => r.sub_host === 't44-v2-api-test')
138
+ expect(txtRecord).toBeDefined()
139
+ expect(txtRecord.record_type).toBe('txt')
140
+ expect(txtRecord.value).toBe('v2-test-value')
141
+
142
+ // Verify existing subdomain records are still present
143
+ for (const orig of existingSubs) {
144
+ const found = subsAfterSet.find((r: any) => r.sub_host === orig.sub_host && r.record_type === orig.record_type)
145
+ expect(found).toBeDefined()
146
+ }
147
+
148
+ // 4. Remove the TXT record by re-setting all records WITHOUT the test record
149
+ const subsWithoutTest = subsAfterSet
150
+ .filter((r: any) => r.sub_host !== 't44-v2-api-test')
151
+ .map((r: any) => ({
152
+ sub_host: r.sub_host,
153
+ record_type: r.record_type,
154
+ record_value1: r.value,
155
+ }))
156
+
157
+ const mainRecordsForRestore = (nsAfterSet.main_domains || []).map((r: any) => ({
158
+ record_type: r.record_type,
159
+ record_value1: r.value || r.record_value1,
160
+ ...(r.record_value2 ? { record_value2: r.record_value2 } : {}),
161
+ }))
162
+
163
+ // Need at least one dns_main_list entry
164
+ const dnsMainList = mainRecordsForRestore.length > 0
165
+ ? mainRecordsForRestore
166
+ : [{ record_type: 'a', record_value1: '127.0.0.1' }]
167
+
168
+ const removeResult = await api.call({
169
+ method: 'POST',
170
+ path: `domains/${domainName}/records`,
171
+ body: {
172
+ dns_main_list: dnsMainList,
173
+ sub_list: subsWithoutTest,
174
+ },
175
+ operation: 'setDns'
176
+ })
177
+ expect(removeResult).toBeObject()
178
+ expect(removeResult.code).toBe(200)
179
+
180
+ // 5. Get DNS again — verify TXT record is gone and original records restored
181
+ const afterRemove = await api.call({
182
+ method: 'GET',
183
+ path: `domains/${domainName}`,
184
+ operation: 'getDns'
185
+ })
186
+ expect(afterRemove.code).toBe(200)
187
+ const nsAfterRemove = afterRemove.data.domain_info[0].glue_info?.name_server_settings || {}
188
+ const subsAfterRemove = nsAfterRemove.sub_domains || []
189
+
190
+ const removedRecord = subsAfterRemove.find((r: any) => r.sub_host === 't44-v2-api-test')
191
+ expect(removedRecord).toBeUndefined()
192
+
193
+ // Verify original subdomain records are still present
194
+ for (const orig of existingSubs) {
195
+ const found = subsAfterRemove.find((r: any) => r.sub_host === orig.sub_host && r.record_type === orig.record_type)
196
+ expect(found).toBeDefined()
197
+ }
198
+ })
199
+
200
+ })
@@ -0,0 +1,94 @@
1
+
2
+ import axios from 'axios';
3
+
4
+ export async function capsule({
5
+ encapsulate,
6
+ CapsulePropertyTypes,
7
+ makeImportStack
8
+ }: {
9
+ encapsulate: any
10
+ CapsulePropertyTypes: any
11
+ makeImportStack: any
12
+ }) {
13
+ // Dynadot REST API v2
14
+ // Docs: https://www.dynadot.com/domain/api-document
15
+ // URL format: https://api.dynadot.com/restful/v2/{resource}/{resource_identify}/{action}
16
+ // Auth: Bearer apiKey, HMAC-SHA256 signed with apiSecret
17
+ // Signing: stringToSign uses full path /restful/v2/... (per v2 docs)
18
+ return encapsulate({
19
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
20
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
21
+ '#t44/structs/providers/dynadot.com/WorkspaceConnectionConfig': {
22
+ as: '$ConnectionConfig'
23
+ },
24
+ '#': {
25
+
26
+ call: {
27
+ type: CapsulePropertyTypes.Function,
28
+ value: async function (this: any, options: {
29
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
30
+ path: string;
31
+ query?: Record<string, string>;
32
+ body?: any;
33
+ operation?: string;
34
+ }) {
35
+ const { createHmac, randomUUID } = await import('crypto')
36
+ const apiKey = await this.$ConnectionConfig.getConfigValue('apiKey')
37
+ const apiSecret = await this.$ConnectionConfig.getConfigValue('apiSecret')
38
+
39
+ const baseUrl = 'https://api.dynadot.com/restful/v2/'
40
+ const relativePath = options.path.replace(/^\//, '')
41
+ const requestId = randomUUID()
42
+
43
+ // Build request
44
+ const axiosOpts: any = {
45
+ method: options.method,
46
+ url: `${baseUrl}${relativePath}`,
47
+ headers: {
48
+ 'Authorization': `Bearer ${apiKey}`,
49
+ 'X-Request-Id': requestId,
50
+ 'Accept': 'application/json',
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ validateStatus: () => true,
54
+ }
55
+
56
+ // For GET: params as query string, empty payload
57
+ // For POST/PUT: JSON body as payload
58
+ // Send pre-serialized string to ensure signature matches body exactly
59
+ let payloadJson = ''
60
+ if (options.method === 'GET' && options.query) {
61
+ axiosOpts.params = options.query
62
+ } else if (options.body) {
63
+ payloadJson = JSON.stringify(options.body)
64
+ axiosOpts.data = payloadJson
65
+ }
66
+
67
+ // HMAC-SHA256 signature: full path + apiSecret + base64 encoding
68
+ const fullPath = `/restful/v2/${relativePath}`
69
+ const stringToSign = `${apiKey}\n${fullPath}\n${requestId}\n${payloadJson}`
70
+ const signature = createHmac('sha256', apiSecret).update(stringToSign).digest('base64')
71
+ axiosOpts.headers['X-Signature'] = signature
72
+
73
+ const response = await axios(axiosOpts)
74
+
75
+ if (response.status >= 400 || (response.data?.code && response.data.code >= 400)) {
76
+ const errorData = response.data
77
+ const operationName = options.operation || options.path
78
+ const desc = errorData?.error?.description || errorData?.message || JSON.stringify(errorData)
79
+ throw new Error(`Dynadot API v2 ${operationName} failed: ${response.status} - ${desc}`)
80
+ }
81
+
82
+ return response.data
83
+ }
84
+ }
85
+
86
+ }
87
+ }
88
+ }, {
89
+ importMeta: import.meta,
90
+ importStack: makeImportStack(),
91
+ capsuleName: capsule['#'],
92
+ })
93
+ }
94
+ capsule['#'] = 't44/caps/providers/dynadot.com/api-restful-v2'