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,224 @@
|
|
|
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
|
+
domains
|
|
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
|
+
domains: {
|
|
34
|
+
type: CapsulePropertyTypes.Mapping,
|
|
35
|
+
value: './api-domains'
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
importMeta: import.meta,
|
|
41
|
+
importStack: makeImportStack(),
|
|
42
|
+
capsuleName: 't44/caps/providers/dynadot.com/api-domains.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 Domain API', function () {
|
|
52
|
+
|
|
53
|
+
let domainName: string
|
|
54
|
+
|
|
55
|
+
it('should return list of domains', async function () {
|
|
56
|
+
|
|
57
|
+
const result = await domains.list()
|
|
58
|
+
|
|
59
|
+
expect(result).toBeObject()
|
|
60
|
+
expect(result.code).toBe(200)
|
|
61
|
+
expect(result.data).toBeObject()
|
|
62
|
+
expect(result.data.domain_info).toBeArray()
|
|
63
|
+
expect(result.data.domain_info.length).toBeGreaterThan(0)
|
|
64
|
+
|
|
65
|
+
domainName = result.data.domain_info[0].domain_name
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return domain info for a specific domain', async function () {
|
|
69
|
+
|
|
70
|
+
const result = await domains.getInfo({ name: domainName })
|
|
71
|
+
|
|
72
|
+
expect(result).toBeObject()
|
|
73
|
+
expect(result.code).toBe(200)
|
|
74
|
+
expect(result.data).toBeObject()
|
|
75
|
+
expect(result.data.domain_info).toBeArray()
|
|
76
|
+
expect(result.data.domain_info[0].domain_name).toBe(domainName)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should return DNS records for a specific domain', async function () {
|
|
80
|
+
|
|
81
|
+
const result = await domains.getDns({ name: domainName })
|
|
82
|
+
|
|
83
|
+
expect(result).toBeObject()
|
|
84
|
+
expect(result.code).toBe(200)
|
|
85
|
+
expect(result.data).toBeObject()
|
|
86
|
+
expect(result.data.domain_info).toBeArray()
|
|
87
|
+
expect(result.data.domain_info[0].glue_info).toBeObject()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should return nameservers for a specific domain', async function () {
|
|
91
|
+
|
|
92
|
+
const result = await domains.getNameservers({ name: domainName })
|
|
93
|
+
|
|
94
|
+
expect(result).toBeObject()
|
|
95
|
+
expect(result.code).toBe(200)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should set an ANAME main domain record via v2 API', async function () {
|
|
99
|
+
|
|
100
|
+
// v2 API supports ANAME record type (v1 did not)
|
|
101
|
+
const setResult = await domains.setDns({
|
|
102
|
+
name: domainName,
|
|
103
|
+
records: [],
|
|
104
|
+
mainDomains: [{ recordType: 'aname', value: 'example.com' }],
|
|
105
|
+
})
|
|
106
|
+
expect(setResult).toBeObject()
|
|
107
|
+
expect(setResult.code).toBe(200)
|
|
108
|
+
|
|
109
|
+
// Verify ANAME was set
|
|
110
|
+
const verifyDns = await domains.getDns({ name: domainName })
|
|
111
|
+
expect(verifyDns.code).toBe(200)
|
|
112
|
+
const nsSettings = verifyDns.data.domain_info[0].glue_info?.name_server_settings
|
|
113
|
+
const mainRecords = nsSettings?.main_domains || []
|
|
114
|
+
const anameRecord = mainRecords.find((r: any) => r.record_type === 'aname')
|
|
115
|
+
expect(anameRecord).toBeDefined()
|
|
116
|
+
expect(anameRecord.value).toBe('example.com')
|
|
117
|
+
|
|
118
|
+
// Clean up: restore with a simple A record
|
|
119
|
+
await domains.setDns({
|
|
120
|
+
name: domainName,
|
|
121
|
+
records: [],
|
|
122
|
+
mainDomains: [{ recordType: 'a', value: '127.0.0.1' }]
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should preserve existing records when using addToCurrent', async function () {
|
|
127
|
+
|
|
128
|
+
// First set a baseline record
|
|
129
|
+
const baseResult = await domains.setDns({
|
|
130
|
+
name: domainName,
|
|
131
|
+
records: [{ subdomain: 't44-test-preserve-base', record_type: 'cname', value: 'base.example.com' }],
|
|
132
|
+
mainDomains: [{ recordType: 'a', value: '127.0.0.1' }]
|
|
133
|
+
})
|
|
134
|
+
expect(baseResult.code).toBe(200)
|
|
135
|
+
|
|
136
|
+
// Add another record using addToCurrent — should preserve the base record
|
|
137
|
+
const addResult = await domains.setDns({
|
|
138
|
+
name: domainName,
|
|
139
|
+
records: [{ subdomain: 't44-test-preserve-add', record_type: 'cname', value: 'add.example.com' }],
|
|
140
|
+
mainDomains: [],
|
|
141
|
+
addToCurrent: true
|
|
142
|
+
})
|
|
143
|
+
expect(addResult.code).toBe(200)
|
|
144
|
+
|
|
145
|
+
// Verify both records exist
|
|
146
|
+
const verifyDns = await domains.getDns({ name: domainName })
|
|
147
|
+
const nsSettings = verifyDns.data.domain_info[0].glue_info?.name_server_settings
|
|
148
|
+
const subDomains = nsSettings?.sub_domains || []
|
|
149
|
+
const baseRecord = subDomains.find((r: any) => r.sub_host === 't44-test-preserve-base')
|
|
150
|
+
const addRecord = subDomains.find((r: any) => r.sub_host === 't44-test-preserve-add')
|
|
151
|
+
expect(baseRecord).toBeDefined()
|
|
152
|
+
expect(addRecord).toBeDefined()
|
|
153
|
+
|
|
154
|
+
// Clean up
|
|
155
|
+
await domains.setDns({
|
|
156
|
+
name: domainName,
|
|
157
|
+
records: [],
|
|
158
|
+
mainDomains: [{ recordType: 'a', value: '127.0.0.1' }]
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should set DNS records, verify, and restore original', async function () {
|
|
163
|
+
|
|
164
|
+
const originalDns = await domains.getDns({ name: domainName })
|
|
165
|
+
expect(originalDns).toBeObject()
|
|
166
|
+
expect(originalDns.code).toBe(200)
|
|
167
|
+
|
|
168
|
+
const nsSettings = originalDns.data.domain_info[0].glue_info?.name_server_settings
|
|
169
|
+
const allOriginalRecords = nsSettings?.sub_domains || []
|
|
170
|
+
// Filter out any leftover test records to avoid duplicate subhost error
|
|
171
|
+
const originalRecords = allOriginalRecords.filter((r: any) => r.sub_host !== 't44-test-api-domains')
|
|
172
|
+
|
|
173
|
+
const testCnameRecord = {
|
|
174
|
+
record_type: 'cname',
|
|
175
|
+
subdomain: 't44-test-api-domains',
|
|
176
|
+
value: 'example.com'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const updatedRecords = [...originalRecords.map((r: any) => ({
|
|
180
|
+
record_type: r.record_type.toLowerCase(),
|
|
181
|
+
subdomain: r.sub_host,
|
|
182
|
+
value: r.value
|
|
183
|
+
})), testCnameRecord]
|
|
184
|
+
|
|
185
|
+
const setResult = await domains.setDns({
|
|
186
|
+
name: domainName,
|
|
187
|
+
records: updatedRecords
|
|
188
|
+
})
|
|
189
|
+
expect(setResult).toBeObject()
|
|
190
|
+
expect(setResult.code).toBe(200)
|
|
191
|
+
|
|
192
|
+
const verifyDns = await domains.getDns({ name: domainName })
|
|
193
|
+
expect(verifyDns).toBeObject()
|
|
194
|
+
expect(verifyDns.code).toBe(200)
|
|
195
|
+
const verifyNs = verifyDns.data.domain_info[0].glue_info?.name_server_settings
|
|
196
|
+
const verifyRecords = verifyNs?.sub_domains || []
|
|
197
|
+
expect(verifyRecords.length).toBe(originalRecords.length + 1)
|
|
198
|
+
|
|
199
|
+
const foundNewRecord = verifyRecords.find((r: any) => r.sub_host === 't44-test-api-domains')
|
|
200
|
+
expect(foundNewRecord).toBeDefined()
|
|
201
|
+
expect(foundNewRecord.record_type).toBe('cname')
|
|
202
|
+
expect(foundNewRecord.value).toBe('example.com')
|
|
203
|
+
|
|
204
|
+
// Restore original DNS by overwriting with original records
|
|
205
|
+
// If original had records, restore them; otherwise restore with just the original set
|
|
206
|
+
const restoreRecords = originalRecords.length > 0
|
|
207
|
+
? originalRecords.map((r: any) => ({
|
|
208
|
+
record_type: r.record_type.toLowerCase(),
|
|
209
|
+
subdomain: r.sub_host,
|
|
210
|
+
value: r.value
|
|
211
|
+
}))
|
|
212
|
+
: [] // empty sub_list will clear subdomain records when dns_main_list is provided
|
|
213
|
+
|
|
214
|
+
const restoreResult = await domains.setDns({
|
|
215
|
+
name: domainName,
|
|
216
|
+
records: restoreRecords,
|
|
217
|
+
// Provide a main domain record to satisfy the "at least one DNS record" requirement
|
|
218
|
+
mainDomains: [{ recordType: 'a', value: '127.0.0.1' }]
|
|
219
|
+
})
|
|
220
|
+
expect(restoreResult).toBeObject()
|
|
221
|
+
expect(restoreResult.code).toBe(200)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export async function capsule({
|
|
4
|
+
encapsulate,
|
|
5
|
+
CapsulePropertyTypes,
|
|
6
|
+
makeImportStack
|
|
7
|
+
}: {
|
|
8
|
+
encapsulate: any
|
|
9
|
+
CapsulePropertyTypes: any
|
|
10
|
+
makeImportStack: any
|
|
11
|
+
}) {
|
|
12
|
+
|
|
13
|
+
// Dynadot REST API v2
|
|
14
|
+
// Docs: https://www.dynadot.com/domain/api-document
|
|
15
|
+
// All operations use v2 (snake_case response: domain_info, glue_info, etc.)
|
|
16
|
+
// v2 supports ANAME record type
|
|
17
|
+
// DNS records: POST domains/{name}/records (body: { dns_main_list, sub_list, ttl, add_dns_to_current_setting })
|
|
18
|
+
// DNS read: embedded in domain info via glue_info.name_server_settings
|
|
19
|
+
return encapsulate({
|
|
20
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
21
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
22
|
+
'#t44/structs/providers/dynadot.com/DomainFact': {
|
|
23
|
+
as: '$DomainFact'
|
|
24
|
+
},
|
|
25
|
+
'#': {
|
|
26
|
+
|
|
27
|
+
api: {
|
|
28
|
+
type: CapsulePropertyTypes.Mapping,
|
|
29
|
+
value: './api-restful-v2'
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
list: {
|
|
33
|
+
type: CapsulePropertyTypes.Function,
|
|
34
|
+
value: async function (this: any) {
|
|
35
|
+
const result = await this.api.call({
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: 'domains',
|
|
38
|
+
operation: 'list'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await this.$DomainFact.set('list', result);
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
getInfo: {
|
|
48
|
+
type: CapsulePropertyTypes.Function,
|
|
49
|
+
value: async function (this: any, options: { name: string }) {
|
|
50
|
+
const result = await this.api.call({
|
|
51
|
+
method: 'GET',
|
|
52
|
+
path: `domains/${options.name}`,
|
|
53
|
+
operation: 'info'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await this.$DomainFact.set(options.name, result);
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
getDns: {
|
|
63
|
+
type: CapsulePropertyTypes.Function,
|
|
64
|
+
value: async function (this: any, options: { name: string }) {
|
|
65
|
+
// DNS info is embedded in domain info via glueInfo.name_server_settings
|
|
66
|
+
const result = await this.api.call({
|
|
67
|
+
method: 'GET',
|
|
68
|
+
path: `domains/${options.name}`,
|
|
69
|
+
operation: 'getDns'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await this.$DomainFact.set(options.name, result);
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
getNameservers: {
|
|
79
|
+
type: CapsulePropertyTypes.Function,
|
|
80
|
+
value: async function (this: any, options: { name: string }) {
|
|
81
|
+
const result = await this.api.call({
|
|
82
|
+
method: 'GET',
|
|
83
|
+
path: `domains/${options.name}/nameservers`,
|
|
84
|
+
operation: 'getNameservers'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await this.$DomainFact.set(options.name, result);
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
setDns: {
|
|
94
|
+
type: CapsulePropertyTypes.Function,
|
|
95
|
+
value: async function (this: any, options: {
|
|
96
|
+
name: string;
|
|
97
|
+
records: any[];
|
|
98
|
+
mainDomain?: { recordType: string; value: string; value2?: string };
|
|
99
|
+
mainDomains?: Array<{ recordType: string; value: string; value2?: string }>;
|
|
100
|
+
addToCurrent?: boolean;
|
|
101
|
+
}) {
|
|
102
|
+
const mainDomains = options.mainDomains || (options.mainDomain ? [options.mainDomain] : []);
|
|
103
|
+
|
|
104
|
+
// v1 REST API body format per docs:
|
|
105
|
+
// { dns_main_list, sub_list, ttl, add_dns_to_current_setting }
|
|
106
|
+
const body: Record<string, any> = {}
|
|
107
|
+
|
|
108
|
+
if (options.addToCurrent) {
|
|
109
|
+
body.add_dns_to_current_setting = true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// dns_main_list is required by the v2 API (at least one entry)
|
|
113
|
+
if (mainDomains.length > 0) {
|
|
114
|
+
body.dns_main_list = mainDomains.map(r => {
|
|
115
|
+
const record: any = {
|
|
116
|
+
record_type: r.recordType,
|
|
117
|
+
record_value1: r.value,
|
|
118
|
+
}
|
|
119
|
+
if (r.value2 !== undefined) {
|
|
120
|
+
record.record_value2 = r.value2
|
|
121
|
+
}
|
|
122
|
+
return record
|
|
123
|
+
})
|
|
124
|
+
} else {
|
|
125
|
+
// Fetch current main records to preserve them
|
|
126
|
+
const current = await this.api.call({
|
|
127
|
+
method: 'GET',
|
|
128
|
+
path: `domains/${options.name}`,
|
|
129
|
+
operation: 'getDns'
|
|
130
|
+
})
|
|
131
|
+
const ns = current?.data?.domain_info?.[0]?.glue_info?.name_server_settings || {}
|
|
132
|
+
const existingMain = ns.main_domains || []
|
|
133
|
+
body.dns_main_list = existingMain.length > 0
|
|
134
|
+
? existingMain.map((r: any) => ({
|
|
135
|
+
record_type: r.record_type,
|
|
136
|
+
record_value1: r.value || r.record_value1,
|
|
137
|
+
...(r.record_value2 ? { record_value2: r.record_value2 } : {}),
|
|
138
|
+
}))
|
|
139
|
+
: [{ record_type: 'a', record_value1: '127.0.0.1' }]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.records.length > 0) {
|
|
143
|
+
body.sub_list = options.records.map(r => ({
|
|
144
|
+
sub_host: r.subdomain,
|
|
145
|
+
record_type: r.record_type,
|
|
146
|
+
record_value1: r.value,
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// POST /restful/v2/domains/{domain_name}/records
|
|
151
|
+
return await this.api.call({
|
|
152
|
+
method: 'POST',
|
|
153
|
+
path: `domains/${options.name}/records`,
|
|
154
|
+
body,
|
|
155
|
+
operation: 'setDns'
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, {
|
|
163
|
+
importMeta: import.meta,
|
|
164
|
+
importStack: makeImportStack(),
|
|
165
|
+
capsuleName: capsule['#'],
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
capsule['#'] = 't44/caps/providers/dynadot.com/api-domains'
|
|
169
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
api: {
|
|
33
|
+
type: CapsulePropertyTypes.Mapping,
|
|
34
|
+
value: './api-restful-v1'
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, {
|
|
39
|
+
importMeta: import.meta,
|
|
40
|
+
importStack: makeImportStack(),
|
|
41
|
+
capsuleName: 't44/caps/providers/dynadot.com/api-restful-v1.test'
|
|
42
|
+
})
|
|
43
|
+
return { spine }
|
|
44
|
+
}, async ({ spine, apis }: any) => {
|
|
45
|
+
return apis[spine.capsuleSourceLineRef]
|
|
46
|
+
}, {
|
|
47
|
+
importMeta: import.meta
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('Dynadot REST API v1', function () {
|
|
51
|
+
|
|
52
|
+
let domainName: string
|
|
53
|
+
|
|
54
|
+
it('should list domains', async function () {
|
|
55
|
+
|
|
56
|
+
const result = await api.call({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: 'domains',
|
|
59
|
+
operation: 'list'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(result).toBeObject()
|
|
63
|
+
expect(result.code).toBe(200)
|
|
64
|
+
expect(result.data).toBeObject()
|
|
65
|
+
expect(result.data.domainInfo).toBeArray()
|
|
66
|
+
expect(result.data.domainInfo.length).toBeGreaterThan(0)
|
|
67
|
+
|
|
68
|
+
domainName = result.data.domainInfo[0].domainName
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should get domain info for first domain', async function () {
|
|
72
|
+
|
|
73
|
+
const result = await api.call({
|
|
74
|
+
method: 'GET',
|
|
75
|
+
path: `domains/${domainName}`,
|
|
76
|
+
operation: 'info'
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(result).toBeObject()
|
|
80
|
+
expect(result.code).toBe(200)
|
|
81
|
+
expect(result.data).toBeObject()
|
|
82
|
+
expect(result.data.domainInfo).toBeArray()
|
|
83
|
+
expect(result.data.domainInfo[0].domainName).toBe(domainName)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should set a TXT test record, verify, remove, and verify removal', async function () {
|
|
87
|
+
|
|
88
|
+
// 1. Get current DNS to snapshot existing records
|
|
89
|
+
const beforeDns = await api.call({
|
|
90
|
+
method: 'GET',
|
|
91
|
+
path: `domains/${domainName}`,
|
|
92
|
+
operation: 'getDns'
|
|
93
|
+
})
|
|
94
|
+
expect(beforeDns.code).toBe(200)
|
|
95
|
+
const nsBefore = beforeDns.data.domainInfo[0].glueInfo?.name_server_settings || {}
|
|
96
|
+
const existingMainRecords = nsBefore.main_records || nsBefore.dns_main_list || []
|
|
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
|
+
const setResult = await api.call({
|
|
101
|
+
method: 'POST',
|
|
102
|
+
path: `domains/${domainName}/records`,
|
|
103
|
+
body: {
|
|
104
|
+
add_dns_to_current_setting: true,
|
|
105
|
+
dns_main_list: [],
|
|
106
|
+
sub_list: [{
|
|
107
|
+
sub_host: 't44-v1-api-test',
|
|
108
|
+
record_type: 'txt',
|
|
109
|
+
record_value1: 'v1-test-value'
|
|
110
|
+
}]
|
|
111
|
+
},
|
|
112
|
+
operation: 'setDns'
|
|
113
|
+
})
|
|
114
|
+
expect(setResult).toBeObject()
|
|
115
|
+
expect(setResult.code).toBe(200)
|
|
116
|
+
|
|
117
|
+
// 3. Get DNS again — verify TXT record was added and existing records preserved
|
|
118
|
+
const afterSet = await api.call({
|
|
119
|
+
method: 'GET',
|
|
120
|
+
path: `domains/${domainName}`,
|
|
121
|
+
operation: 'getDns'
|
|
122
|
+
})
|
|
123
|
+
expect(afterSet.code).toBe(200)
|
|
124
|
+
const nsAfterSet = afterSet.data.domainInfo[0].glueInfo?.name_server_settings || {}
|
|
125
|
+
const subsAfterSet = nsAfterSet.sub_domains || []
|
|
126
|
+
|
|
127
|
+
const txtRecord = subsAfterSet.find((r: any) => r.sub_host === 't44-v1-api-test')
|
|
128
|
+
expect(txtRecord).toBeDefined()
|
|
129
|
+
expect(txtRecord.record_type).toBe('txt')
|
|
130
|
+
expect(txtRecord.value).toBe('v1-test-value')
|
|
131
|
+
|
|
132
|
+
// Verify existing subdomain records are still present
|
|
133
|
+
for (const orig of existingSubs) {
|
|
134
|
+
const found = subsAfterSet.find((r: any) => r.sub_host === orig.sub_host && r.record_type === orig.record_type)
|
|
135
|
+
expect(found).toBeDefined()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 4. Remove the TXT record by re-setting all records WITHOUT the test record
|
|
139
|
+
const subsWithoutTest = subsAfterSet
|
|
140
|
+
.filter((r: any) => r.sub_host !== 't44-v1-api-test')
|
|
141
|
+
.map((r: any) => ({
|
|
142
|
+
sub_host: r.sub_host,
|
|
143
|
+
record_type: r.record_type,
|
|
144
|
+
record_value1: r.value,
|
|
145
|
+
}))
|
|
146
|
+
|
|
147
|
+
const mainRecordsForRestore = (nsAfterSet.main_records || nsAfterSet.dns_main_list || []).map((r: any) => ({
|
|
148
|
+
record_type: r.record_type,
|
|
149
|
+
record_value1: r.value || r.record_value1,
|
|
150
|
+
...(r.record_value2 ? { record_value2: r.record_value2 } : {}),
|
|
151
|
+
}))
|
|
152
|
+
|
|
153
|
+
// Need at least one dns_main_list entry
|
|
154
|
+
const dnsMainList = mainRecordsForRestore.length > 0
|
|
155
|
+
? mainRecordsForRestore
|
|
156
|
+
: [{ record_type: 'a', record_value1: '127.0.0.1' }]
|
|
157
|
+
|
|
158
|
+
const removeResult = await api.call({
|
|
159
|
+
method: 'POST',
|
|
160
|
+
path: `domains/${domainName}/records`,
|
|
161
|
+
body: {
|
|
162
|
+
dns_main_list: dnsMainList,
|
|
163
|
+
sub_list: subsWithoutTest,
|
|
164
|
+
},
|
|
165
|
+
operation: 'setDns'
|
|
166
|
+
})
|
|
167
|
+
expect(removeResult).toBeObject()
|
|
168
|
+
expect(removeResult.code).toBe(200)
|
|
169
|
+
|
|
170
|
+
// 5. Get DNS again — verify TXT record is gone and original records restored
|
|
171
|
+
const afterRemove = await api.call({
|
|
172
|
+
method: 'GET',
|
|
173
|
+
path: `domains/${domainName}`,
|
|
174
|
+
operation: 'getDns'
|
|
175
|
+
})
|
|
176
|
+
expect(afterRemove.code).toBe(200)
|
|
177
|
+
const nsAfterRemove = afterRemove.data.domainInfo[0].glueInfo?.name_server_settings || {}
|
|
178
|
+
const subsAfterRemove = nsAfterRemove.sub_domains || []
|
|
179
|
+
|
|
180
|
+
const removedRecord = subsAfterRemove.find((r: any) => r.sub_host === 't44-v1-api-test')
|
|
181
|
+
expect(removedRecord).toBeUndefined()
|
|
182
|
+
|
|
183
|
+
// Verify original subdomain records are still present
|
|
184
|
+
for (const orig of existingSubs) {
|
|
185
|
+
const found = subsAfterRemove.find((r: any) => r.sub_host === orig.sub_host && r.record_type === orig.record_type)
|
|
186
|
+
expect(found).toBeDefined()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
})
|
|
@@ -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 v1
|
|
14
|
+
// Docs: https://www.dynadot.com/domain/api-document
|
|
15
|
+
// Reference impl: https://github.com/level23/dynadot-api
|
|
16
|
+
// URL format: https://api.dynadot.com/restful/v1/{resource}/{resource_identify}/{action}
|
|
17
|
+
// Auth: Bearer apiKey, HMAC-SHA256 signed with apiSecret
|
|
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/v1/'
|
|
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: sign with apiSecret, relative path
|
|
68
|
+
const stringToSign = `${apiKey}\n${relativePath}\n${requestId}\n${payloadJson}`
|
|
69
|
+
const signature = createHmac('sha256', apiSecret).update(stringToSign).digest('hex')
|
|
70
|
+
axiosOpts.headers['X-Signature'] = signature
|
|
71
|
+
|
|
72
|
+
const response = await axios(axiosOpts)
|
|
73
|
+
|
|
74
|
+
if (response.status >= 400 || (response.data?.code && response.data.code >= 400)) {
|
|
75
|
+
const errorData = response.data
|
|
76
|
+
const operationName = options.operation || options.path
|
|
77
|
+
const desc = errorData?.error?.description || errorData?.message || JSON.stringify(errorData)
|
|
78
|
+
throw new Error(`Dynadot API ${operationName} failed: ${response.status} - ${desc}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return response.data
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, {
|
|
88
|
+
importMeta: import.meta,
|
|
89
|
+
importStack: makeImportStack(),
|
|
90
|
+
capsuleName: capsule['#'],
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
capsule['#'] = 't44/caps/providers/dynadot.com/api-restful-v1'
|
|
94
|
+
|