@alienplatform/testing 0.1.0 → 1.3.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/LICENSE.md +105 -0
- package/README.md +61 -274
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/external-secrets.js +140 -0
- package/dist/external-secrets.js.map +1 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +458 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -14
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Alien Software, Inc.
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,320 +1,107 @@
|
|
|
1
1
|
# @alienplatform/testing
|
|
2
2
|
|
|
3
|
-
Testing framework for Alien applications.
|
|
3
|
+
Testing framework for Alien applications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @alienplatform/testing
|
|
8
|
+
npm install --save-dev @alienplatform/testing
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Local vs Cloud
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
import { deploy } from "@alienplatform/testing"
|
|
15
|
-
|
|
16
|
-
const deployment = await deploy({
|
|
17
|
-
app: "./my-app",
|
|
18
|
-
platform: "aws",
|
|
19
|
-
workspace: "my-workspace",
|
|
20
|
-
project: "my-project",
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// Test your deployment
|
|
24
|
-
const response = await fetch(`${deployment.url}/api/test`)
|
|
25
|
-
expect(response.status).toBe(200)
|
|
13
|
+
This package has two deployment modes:
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Authentication
|
|
32
|
-
|
|
33
|
-
### API Key
|
|
34
|
-
|
|
35
|
-
Set your Alien API key:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
export ALIEN_API_KEY="your-key"
|
|
39
|
-
# or
|
|
40
|
-
alien login
|
|
41
|
-
```
|
|
15
|
+
- local mode uses `alien dev`
|
|
16
|
+
- cloud mode uses the platform API
|
|
42
17
|
|
|
43
|
-
|
|
18
|
+
That split is intentional. Local mode stays centered on the shipped `alien` CLI binary instead of asking users to discover lower-level manager binaries.
|
|
44
19
|
|
|
45
|
-
|
|
20
|
+
## Local Mode
|
|
46
21
|
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
export AWS_ACCESS_KEY_ID="..."
|
|
50
|
-
export AWS_SECRET_ACCESS_KEY="..."
|
|
51
|
-
export AWS_REGION="us-east-1"
|
|
52
|
-
```
|
|
22
|
+
Local mode is the default:
|
|
53
23
|
|
|
54
|
-
Or pass explicitly:
|
|
55
24
|
```typescript
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
accessKeyId: "...",
|
|
59
|
-
secretAccessKey: "...",
|
|
60
|
-
region: "us-east-1",
|
|
61
|
-
}
|
|
62
|
-
```
|
|
25
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
|
26
|
+
import { deploy, type Deployment } from "@alienplatform/testing"
|
|
63
27
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json"
|
|
67
|
-
export GCP_PROJECT_ID="my-project"
|
|
68
|
-
```
|
|
28
|
+
describe("My App", () => {
|
|
29
|
+
let deployment: Deployment
|
|
69
30
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
platform: "gcp",
|
|
74
|
-
projectId: "my-project",
|
|
75
|
-
serviceAccountKeyPath: "/path/to/key.json",
|
|
76
|
-
// or
|
|
77
|
-
serviceAccountKeyJson: "{ ... }",
|
|
78
|
-
}
|
|
79
|
-
```
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
deployment = await deploy({ app: "./my-app" })
|
|
33
|
+
}, 300_000)
|
|
80
34
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export AZURE_TENANT_ID="..."
|
|
85
|
-
export AZURE_CLIENT_ID="..."
|
|
86
|
-
export AZURE_CLIENT_SECRET="..."
|
|
87
|
-
```
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await deployment?.destroy()
|
|
37
|
+
})
|
|
88
38
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
subscriptionId: "...",
|
|
94
|
-
tenantId: "...",
|
|
95
|
-
clientId: "...",
|
|
96
|
-
clientSecret: "...",
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
#### Kubernetes
|
|
101
|
-
```bash
|
|
102
|
-
export KUBECONFIG="~/.kube/config"
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Or pass explicitly:
|
|
106
|
-
```typescript
|
|
107
|
-
credentials: {
|
|
108
|
-
platform: "kubernetes",
|
|
109
|
-
kubeconfigPath: "~/.kube/config",
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Deployment Methods
|
|
114
|
-
|
|
115
|
-
### API (Default)
|
|
116
|
-
|
|
117
|
-
Fastest method. Agent Manager deploys directly:
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
const deployment = await deploy({
|
|
121
|
-
app: "./my-app",
|
|
122
|
-
platform: "aws",
|
|
123
|
-
workspace: "my-workspace",
|
|
124
|
-
project: "my-project",
|
|
125
|
-
method: "api", // default
|
|
39
|
+
it("responds to requests", async () => {
|
|
40
|
+
const response = await fetch(`${deployment.url}/api/hello`)
|
|
41
|
+
expect(response.status).toBe(200)
|
|
42
|
+
})
|
|
126
43
|
})
|
|
127
44
|
```
|
|
128
45
|
|
|
129
|
-
|
|
46
|
+
Under the hood the package:
|
|
130
47
|
|
|
131
|
-
|
|
48
|
+
1. finds the `alien` binary
|
|
49
|
+
2. spawns `alien dev --status-file ...`
|
|
50
|
+
3. waits for the status file to report readiness
|
|
51
|
+
4. reads the public URL and commands URL from that file
|
|
132
52
|
|
|
133
|
-
|
|
134
|
-
const deployment = await deploy({
|
|
135
|
-
app: "./my-app",
|
|
136
|
-
platform: "aws",
|
|
137
|
-
workspace: "my-workspace",
|
|
138
|
-
project: "my-project",
|
|
139
|
-
method: "cli",
|
|
140
|
-
})
|
|
141
|
-
```
|
|
53
|
+
The local machine contract is the `DevStatus` JSON written by `alien dev`, not terminal output.
|
|
142
54
|
|
|
143
|
-
|
|
55
|
+
## Cloud Mode
|
|
144
56
|
|
|
145
|
-
|
|
57
|
+
Cloud mode targets real infrastructure:
|
|
146
58
|
|
|
147
59
|
```typescript
|
|
148
60
|
const deployment = await deploy({
|
|
149
61
|
app: "./my-app",
|
|
150
62
|
platform: "aws",
|
|
151
|
-
workspace: "my-workspace",
|
|
152
|
-
project: "my-project",
|
|
153
|
-
method: "terraform",
|
|
154
63
|
})
|
|
155
64
|
```
|
|
156
65
|
|
|
157
|
-
|
|
66
|
+
Supported cloud platforms:
|
|
158
67
|
|
|
159
|
-
|
|
68
|
+
- `aws`
|
|
69
|
+
- `gcp`
|
|
70
|
+
- `azure`
|
|
160
71
|
|
|
161
|
-
|
|
162
|
-
const deployment = await deploy({
|
|
163
|
-
app: "./my-app",
|
|
164
|
-
platform: "aws",
|
|
165
|
-
workspace: "my-workspace",
|
|
166
|
-
project: "my-project",
|
|
167
|
-
method: "cloudformation",
|
|
168
|
-
})
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Helm
|
|
172
|
-
|
|
173
|
-
Tests Helm chart deployment (Kubernetes only):
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
const deployment = await deploy({
|
|
177
|
-
app: "./my-app",
|
|
178
|
-
platform: "kubernetes",
|
|
179
|
-
workspace: "my-workspace",
|
|
180
|
-
project: "my-project",
|
|
181
|
-
method: "helm",
|
|
182
|
-
valuesYaml: "./values.yaml",
|
|
183
|
-
namespace: "default",
|
|
184
|
-
})
|
|
185
|
-
```
|
|
72
|
+
Cloud mode requires `ALIEN_API_KEY`.
|
|
186
73
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
Tests pull-mode deployment via Docker operator:
|
|
74
|
+
## API
|
|
190
75
|
|
|
191
76
|
```typescript
|
|
192
|
-
|
|
193
|
-
app:
|
|
194
|
-
platform
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Query Logs
|
|
202
|
-
|
|
203
|
-
Query deployment logs using DeepStore:
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
const deployment = await deploy({
|
|
207
|
-
app: "./my-app",
|
|
208
|
-
platform: "test",
|
|
209
|
-
workspace: "my-workspace",
|
|
210
|
-
project: "my-project",
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// Configure log querying
|
|
214
|
-
const logsConfig = {
|
|
215
|
-
managerUrl: "http://localhost:3000",
|
|
216
|
-
deepstoreServerUrl: "http://localhost:8080",
|
|
217
|
-
databaseId: "db_123",
|
|
218
|
-
agentToken: "token_123",
|
|
77
|
+
interface DeployOptions {
|
|
78
|
+
app: string
|
|
79
|
+
platform?: "local" | "aws" | "gcp" | "azure"
|
|
80
|
+
config?: string
|
|
81
|
+
environmentVariables?: EnvironmentVariable[]
|
|
82
|
+
verbose?: boolean
|
|
219
83
|
}
|
|
220
|
-
|
|
221
|
-
const logs = await deployment.queryLogs({
|
|
222
|
-
query: "level:ERROR",
|
|
223
|
-
startTime: new Date(Date.now() - 3600_000), // 1 hour ago
|
|
224
|
-
endTime: new Date(),
|
|
225
|
-
maxHits: 100,
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
console.log(`Found ${logs.num_hits} logs`)
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Environment Variables
|
|
232
|
-
|
|
233
|
-
Pass environment variables to your deployment:
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
const deployment = await deploy({
|
|
237
|
-
app: "./my-app",
|
|
238
|
-
platform: "aws",
|
|
239
|
-
workspace: "my-workspace",
|
|
240
|
-
project: "my-project",
|
|
241
|
-
environmentVariables: [
|
|
242
|
-
{ name: "DATABASE_URL", value: "postgres://...", type: "plaintext" },
|
|
243
|
-
{ name: "API_KEY", value: "secret", type: "secret" },
|
|
244
|
-
],
|
|
245
|
-
})
|
|
246
84
|
```
|
|
247
85
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Customize deployment behavior:
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
const deployment = await deploy({
|
|
254
|
-
app: "./my-app",
|
|
255
|
-
platform: "aws",
|
|
256
|
-
workspace: "my-workspace",
|
|
257
|
-
project: "my-project",
|
|
258
|
-
stackSettings: {
|
|
259
|
-
deploymentModel: "push",
|
|
260
|
-
heartbeats: "on",
|
|
261
|
-
telemetry: "auto",
|
|
262
|
-
updates: "auto",
|
|
263
|
-
},
|
|
264
|
-
})
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
## Example Test
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
import { describe, it, expect } from "vitest"
|
|
271
|
-
import { deploy } from "@alienplatform/testing"
|
|
272
|
-
|
|
273
|
-
describe("my app", () => {
|
|
274
|
-
it("should deploy and respond", async () => {
|
|
275
|
-
const deployment = await deploy({
|
|
276
|
-
app: "./fixtures/my-app",
|
|
277
|
-
platform: "test",
|
|
278
|
-
workspace: "test-workspace",
|
|
279
|
-
project: "test-project",
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const response = await fetch(`${deployment.url}/api/hello`)
|
|
284
|
-
const data = await response.json()
|
|
285
|
-
|
|
286
|
-
expect(response.status).toBe(200)
|
|
287
|
-
expect(data.message).toBe("Hello, World!")
|
|
288
|
-
} finally {
|
|
289
|
-
await deployment.destroy()
|
|
290
|
-
}
|
|
291
|
-
}, 180_000) // 3 min timeout
|
|
292
|
-
})
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## API Reference
|
|
296
|
-
|
|
297
|
-
### `deploy(options: DeployOptions): Promise<Deployment>`
|
|
298
|
-
|
|
299
|
-
Deploy an Alien application for testing.
|
|
300
|
-
|
|
301
|
-
### `Deployment`
|
|
86
|
+
The returned `Deployment` supports:
|
|
302
87
|
|
|
303
|
-
|
|
88
|
+
- `deployment.url`
|
|
89
|
+
- `deployment.invokeCommand(name, params)`
|
|
90
|
+
- `deployment.setExternalSecret(vault, key, value)`
|
|
91
|
+
- `deployment.upgrade(options)` for cloud mode
|
|
92
|
+
- `deployment.destroy()`
|
|
304
93
|
|
|
305
|
-
|
|
306
|
-
- `id: string` - Agent ID
|
|
307
|
-
- `name: string` - Agent name
|
|
308
|
-
- `url: string` - Deployment URL
|
|
309
|
-
- `platform: Platform` - Target platform
|
|
310
|
-
- `status: AgentStatus` - Current status
|
|
94
|
+
## Configuration
|
|
311
95
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
96
|
+
| Variable | Description | Default |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `ALIEN_API_KEY` | Platform API key for cloud mode | — |
|
|
99
|
+
| `ALIEN_API_URL` | Platform API base URL | `https://api.alien.dev` |
|
|
100
|
+
| `ALIEN_CLI_PATH` | Path to the `alien` binary | auto-detected |
|
|
101
|
+
| `VERBOSE` | Show detailed child-process logs | `false` |
|
|
317
102
|
|
|
318
|
-
##
|
|
103
|
+
## Notes
|
|
319
104
|
|
|
320
|
-
|
|
105
|
+
- local mode needs only the `alien` binary
|
|
106
|
+
- cloud mode needs `ALIEN_API_KEY`
|
|
107
|
+
- repo-internal low-level tests can still use standalone manager helpers directly; this package is the higher-level product-facing layer
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AlienError, defineError } from "@alienplatform/core";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
const TestingOperationFailedError = defineError({
|
|
5
|
+
code: "TESTING_OPERATION_FAILED",
|
|
6
|
+
context: z.object({
|
|
7
|
+
operation: z.string(),
|
|
8
|
+
message: z.string(),
|
|
9
|
+
details: z.record(z.string(), z.unknown()).optional()
|
|
10
|
+
}),
|
|
11
|
+
message: ({ operation, message }) => `Testing operation '${operation}' failed: ${message}`,
|
|
12
|
+
retryable: false,
|
|
13
|
+
internal: false,
|
|
14
|
+
httpStatusCode: 500
|
|
15
|
+
});
|
|
16
|
+
const TestingUnsupportedPlatformError = defineError({
|
|
17
|
+
code: "TESTING_UNSUPPORTED_PLATFORM",
|
|
18
|
+
context: z.object({
|
|
19
|
+
platform: z.string(),
|
|
20
|
+
operation: z.string()
|
|
21
|
+
}),
|
|
22
|
+
message: ({ platform, operation }) => `Unsupported platform '${platform}' for testing operation '${operation}'`,
|
|
23
|
+
retryable: false,
|
|
24
|
+
internal: false,
|
|
25
|
+
httpStatusCode: 400
|
|
26
|
+
});
|
|
27
|
+
async function withTestingContext(error, operation, message, details) {
|
|
28
|
+
return (await AlienError.from(error)).withContext(TestingOperationFailedError.create({
|
|
29
|
+
operation,
|
|
30
|
+
message,
|
|
31
|
+
details
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { TestingUnsupportedPlatformError as n, withTestingContext as r, TestingOperationFailedError as t };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { AlienError, defineError } from \"@alienplatform/core\"\nimport * as z from \"zod/v4\"\n\nexport const TestingOperationFailedError = defineError({\n code: \"TESTING_OPERATION_FAILED\",\n context: z.object({\n operation: z.string(),\n message: z.string(),\n details: z.record(z.string(), z.unknown()).optional(),\n }),\n message: ({ operation, message }) => `Testing operation '${operation}' failed: ${message}`,\n retryable: false,\n internal: false,\n httpStatusCode: 500,\n})\n\nexport const TestingUnsupportedPlatformError = defineError({\n code: \"TESTING_UNSUPPORTED_PLATFORM\",\n context: z.object({\n platform: z.string(),\n operation: z.string(),\n }),\n message: ({ platform, operation }) =>\n `Unsupported platform '${platform}' for testing operation '${operation}'`,\n retryable: false,\n internal: false,\n httpStatusCode: 400,\n})\n\nexport async function withTestingContext(\n error: unknown,\n operation: string,\n message: string,\n details?: Record<string, unknown>,\n): Promise<AlienError<any>> {\n return (await AlienError.from(error)).withContext(\n TestingOperationFailedError.create({\n operation,\n message,\n details,\n }),\n )\n}\n"],"mappings":";;;AAGA,MAAa,8BAA8B,YAAY;CACrD,MAAM;CACN,SAAS,EAAE,OAAO;EAChB,WAAW,EAAE,QAAQ;EACrB,SAAS,EAAE,QAAQ;EACnB,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC,UAAU;EACtD,CAAC;CACF,UAAU,EAAE,WAAW,cAAc,sBAAsB,UAAU,YAAY;CACjF,WAAW;CACX,UAAU;CACV,gBAAgB;CACjB,CAAC;AAEF,MAAa,kCAAkC,YAAY;CACzD,MAAM;CACN,SAAS,EAAE,OAAO;EAChB,UAAU,EAAE,QAAQ;EACpB,WAAW,EAAE,QAAQ;EACtB,CAAC;CACF,UAAU,EAAE,UAAU,gBACpB,yBAAyB,SAAS,2BAA2B,UAAU;CACzE,WAAW;CACX,UAAU;CACV,gBAAgB;CACjB,CAAC;AAEF,eAAsB,mBACpB,OACA,WACA,SACA,SAC0B;AAC1B,SAAQ,MAAM,WAAW,KAAK,MAAM,EAAE,YACpC,4BAA4B,OAAO;EACjC;EACA;EACA;EACD,CAAC,CACH"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { n as TestingUnsupportedPlatformError, r as withTestingContext, t as TestingOperationFailedError } from "./errors.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { AlienError } from "@alienplatform/core";
|
|
5
|
+
import { PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
6
|
+
import { ClientSecretCredential } from "@azure/identity";
|
|
7
|
+
import { SecretClient } from "@azure/keyvault-secrets";
|
|
8
|
+
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
|
9
|
+
//#region src/external-secrets.ts
|
|
10
|
+
/**
|
|
11
|
+
* External secrets - platform-native secret management
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Set an external secret using platform-native tools
|
|
15
|
+
*
|
|
16
|
+
* Falls back to environment variables for cloud provider credentials.
|
|
17
|
+
*/
|
|
18
|
+
async function setExternalSecret(platform, resourcePrefix, vaultName, secretKey, secretValue, _namespace, stateDir, deploymentId) {
|
|
19
|
+
try {
|
|
20
|
+
switch (platform) {
|
|
21
|
+
case "aws":
|
|
22
|
+
await setAWSSecret(resourcePrefix, vaultName, secretKey, secretValue);
|
|
23
|
+
break;
|
|
24
|
+
case "gcp":
|
|
25
|
+
await setGCPSecret(resourcePrefix, vaultName, secretKey, secretValue);
|
|
26
|
+
break;
|
|
27
|
+
case "azure":
|
|
28
|
+
await setAzureSecret(resourcePrefix, vaultName, secretKey, secretValue);
|
|
29
|
+
break;
|
|
30
|
+
case "local":
|
|
31
|
+
await setLocalSecret(vaultName, secretKey, secretValue, stateDir, deploymentId);
|
|
32
|
+
break;
|
|
33
|
+
default: {
|
|
34
|
+
const exhaustive = platform;
|
|
35
|
+
throw new AlienError(TestingUnsupportedPlatformError.create({
|
|
36
|
+
platform: String(exhaustive),
|
|
37
|
+
operation: "setExternalSecret"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw await withTestingContext(error, "setExternalSecret", "Failed to set external secret", {
|
|
43
|
+
platform,
|
|
44
|
+
resourcePrefix,
|
|
45
|
+
vaultName,
|
|
46
|
+
secretKey
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Set AWS SSM Parameter Store secret
|
|
52
|
+
*
|
|
53
|
+
* Uses environment variables for credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION).
|
|
54
|
+
*/
|
|
55
|
+
async function setAWSSecret(resourcePrefix, vaultName, secretKey, secretValue) {
|
|
56
|
+
const client = new SSMClient({ region: process.env.AWS_REGION });
|
|
57
|
+
const parameterName = `/${resourcePrefix}-${vaultName}-${secretKey}`;
|
|
58
|
+
await client.send(new PutParameterCommand({
|
|
59
|
+
Name: parameterName,
|
|
60
|
+
Value: secretValue,
|
|
61
|
+
Type: "SecureString",
|
|
62
|
+
Overwrite: true
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Set GCP Secret Manager secret
|
|
67
|
+
*
|
|
68
|
+
* Uses environment variables for credentials (GOOGLE_APPLICATION_CREDENTIALS, GCP_PROJECT_ID).
|
|
69
|
+
*/
|
|
70
|
+
async function setGCPSecret(resourcePrefix, vaultName, secretKey, secretValue) {
|
|
71
|
+
const client = new SecretManagerServiceClient();
|
|
72
|
+
const projectId = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT;
|
|
73
|
+
if (!projectId) throw new AlienError(TestingOperationFailedError.create({
|
|
74
|
+
operation: "setGCPSecret",
|
|
75
|
+
message: "GCP project ID is required (set GCP_PROJECT_ID or GOOGLE_CLOUD_PROJECT env var)"
|
|
76
|
+
}));
|
|
77
|
+
const secretName = `${resourcePrefix}-${vaultName}-${secretKey}`;
|
|
78
|
+
const parent = `projects/${projectId}`;
|
|
79
|
+
const secretPath = `${parent}/secrets/${secretName}`;
|
|
80
|
+
try {
|
|
81
|
+
await client.createSecret({
|
|
82
|
+
parent,
|
|
83
|
+
secretId: secretName,
|
|
84
|
+
secret: { replication: { automatic: {} } }
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!error.message?.includes("ALREADY_EXISTS")) throw await withTestingContext(error, "setGCPSecret", "Failed to create GCP secret");
|
|
88
|
+
}
|
|
89
|
+
await client.addSecretVersion({
|
|
90
|
+
parent: secretPath,
|
|
91
|
+
payload: { data: Buffer.from(secretValue, "utf8") }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Set Azure Key Vault secret
|
|
96
|
+
*
|
|
97
|
+
* Uses environment variables for credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET).
|
|
98
|
+
*/
|
|
99
|
+
async function setAzureSecret(resourcePrefix, vaultName, secretKey, secretValue) {
|
|
100
|
+
const vaultUrl = `https://${`${resourcePrefix}-${vaultName}`}.vault.azure.net`;
|
|
101
|
+
const tenantId = process.env.AZURE_TENANT_ID;
|
|
102
|
+
const clientId = process.env.AZURE_CLIENT_ID;
|
|
103
|
+
const clientSecret = process.env.AZURE_CLIENT_SECRET;
|
|
104
|
+
if (!tenantId || !clientId || !clientSecret) throw new AlienError(TestingOperationFailedError.create({
|
|
105
|
+
operation: "setAzureSecret",
|
|
106
|
+
message: "Azure credentials are required (set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET env vars)"
|
|
107
|
+
}));
|
|
108
|
+
const client = new SecretClient(vaultUrl, new ClientSecretCredential(tenantId, clientId, clientSecret));
|
|
109
|
+
const azureSecretKey = secretKey.replace(/_/g, "-");
|
|
110
|
+
await client.setSecret(azureSecretKey, secretValue);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Set local dev secret by writing directly to the vault's secrets.json file.
|
|
114
|
+
*
|
|
115
|
+
* The local vault binding (LocalVault) reads from:
|
|
116
|
+
* {stateDir}/{deploymentId}/vault/{vaultName}/secrets.json
|
|
117
|
+
*
|
|
118
|
+
* We write to the same path so the running function can read it immediately.
|
|
119
|
+
*/
|
|
120
|
+
async function setLocalSecret(vaultName, secretKey, secretValue, stateDir, deploymentId) {
|
|
121
|
+
if (!stateDir) throw new AlienError(TestingOperationFailedError.create({
|
|
122
|
+
operation: "setLocalSecret",
|
|
123
|
+
message: "stateDir is required for local vault set"
|
|
124
|
+
}));
|
|
125
|
+
if (!deploymentId) throw new AlienError(TestingOperationFailedError.create({
|
|
126
|
+
operation: "setLocalSecret",
|
|
127
|
+
message: "deploymentId is required for local vault set"
|
|
128
|
+
}));
|
|
129
|
+
const vaultDir = join(stateDir, deploymentId, "vault", vaultName);
|
|
130
|
+
const secretsFile = join(vaultDir, "secrets.json");
|
|
131
|
+
let secrets = {};
|
|
132
|
+
if (existsSync(secretsFile)) secrets = JSON.parse(readFileSync(secretsFile, "utf-8"));
|
|
133
|
+
secrets[secretKey] = secretValue;
|
|
134
|
+
mkdirSync(vaultDir, { recursive: true });
|
|
135
|
+
writeFileSync(secretsFile, JSON.stringify(secrets, null, 2));
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { setExternalSecret };
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=external-secrets.js.map
|