@replayio/app-building 1.17.0 → 1.18.0
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/README.md +4 -22
- package/dist/container.d.ts +3 -1
- package/dist/container.js +12 -5
- package/dist/secrets.d.ts +18 -3
- package/dist/secrets.js +110 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,18 +28,9 @@ import {
|
|
|
28
28
|
const orchestrationVars = loadDotEnv("/path/to/project");
|
|
29
29
|
const infisicalConfig = await getInfisicalConfig(orchestrationVars);
|
|
30
30
|
|
|
31
|
-
// Only pass Infisical credentials to the container — not actual secrets.
|
|
32
|
-
// The container fetches secrets from Infisical at startup and manages them
|
|
33
|
-
// via an internal secrets server. The agent never has direct access to secrets.
|
|
34
|
-
const containerEnvVars: Record<string, string> = {
|
|
35
|
-
INFISICAL_TOKEN: infisicalConfig.token,
|
|
36
|
-
INFISICAL_PROJECT_ID: infisicalConfig.projectId,
|
|
37
|
-
INFISICAL_ENVIRONMENT: infisicalConfig.environment,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
31
|
const config: ContainerConfig = {
|
|
41
32
|
projectRoot: "/path/to/project", // optional — only needed for local Docker operations
|
|
42
|
-
|
|
33
|
+
infisical: infisicalConfig,
|
|
43
34
|
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
44
35
|
flyToken: orchestrationVars.FLY_API_TOKEN,
|
|
45
36
|
flyApp: orchestrationVars.FLY_APP_NAME,
|
|
@@ -85,7 +76,7 @@ The agent can also run `list-secrets` to see which secrets are available, and `s
|
|
|
85
76
|
|
|
86
77
|
| Export | Description |
|
|
87
78
|
|---|---|
|
|
88
|
-
| `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `
|
|
79
|
+
| `ContainerConfig` | Interface bundling all external state: `infisical` (required `InfisicalConfig`), optional `projectRoot` (only needed for local Docker operations), `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`/`webhookSecret`/`detached`/`initialPrompt`/`localPort`/`absorbTasks`. See [Webhooks](#webhooks) and [Container lifecycle](#container-lifecycle) below. |
|
|
89
80
|
| `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
|
|
90
81
|
| `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
|
|
91
82
|
| `FileContainerRegistry` | Built-in file-backed implementation of `ContainerRegistry`, backed by a `.jsonl` file. |
|
|
@@ -164,13 +155,8 @@ The agent can also run `list-secrets` to see which secrets are available, and `s
|
|
|
164
155
|
const orchestrationVars = loadDotEnv(projectRoot);
|
|
165
156
|
const infisicalConfig = await getInfisicalConfig(orchestrationVars);
|
|
166
157
|
|
|
167
|
-
// Pass only Infisical credentials to the container
|
|
168
158
|
const config: ContainerConfig = {
|
|
169
|
-
|
|
170
|
-
INFISICAL_TOKEN: infisicalConfig.token,
|
|
171
|
-
INFISICAL_PROJECT_ID: infisicalConfig.projectId,
|
|
172
|
-
INFISICAL_ENVIRONMENT: infisicalConfig.environment,
|
|
173
|
-
},
|
|
159
|
+
infisical: infisicalConfig,
|
|
174
160
|
flyToken: orchestrationVars.FLY_API_TOKEN,
|
|
175
161
|
flyApp: orchestrationVars.FLY_APP_NAME,
|
|
176
162
|
...
|
|
@@ -269,11 +255,7 @@ const infisicalConfig = await getInfisicalConfig(orchestrationVars);
|
|
|
269
255
|
|
|
270
256
|
const config: ContainerConfig = {
|
|
271
257
|
projectRoot: "/path/to/project",
|
|
272
|
-
|
|
273
|
-
INFISICAL_TOKEN: infisicalConfig.token,
|
|
274
|
-
INFISICAL_PROJECT_ID: infisicalConfig.projectId,
|
|
275
|
-
INFISICAL_ENVIRONMENT: infisicalConfig.environment,
|
|
276
|
-
},
|
|
258
|
+
infisical: infisicalConfig,
|
|
277
259
|
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
278
260
|
webhookUrl: "https://example.com/hooks/container-events",
|
|
279
261
|
webhookSecret: "your-webhook-secret",
|
package/dist/container.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContainerRegistry } from "./container-registry";
|
|
2
|
+
import type { InfisicalConfig } from "./secrets";
|
|
2
3
|
export interface AgentState {
|
|
3
4
|
type: "local" | "remote";
|
|
4
5
|
containerName: string;
|
|
@@ -10,7 +11,8 @@ export interface AgentState {
|
|
|
10
11
|
}
|
|
11
12
|
export interface ContainerConfig {
|
|
12
13
|
projectRoot?: string;
|
|
13
|
-
|
|
14
|
+
/** Infisical credentials — required for all containers. */
|
|
15
|
+
infisical: InfisicalConfig;
|
|
14
16
|
registry: ContainerRegistry;
|
|
15
17
|
flyToken?: string;
|
|
16
18
|
flyApp?: string;
|
package/dist/container.js
CHANGED
|
@@ -73,7 +73,14 @@ function findFreePort() {
|
|
|
73
73
|
}
|
|
74
74
|
return port;
|
|
75
75
|
}
|
|
76
|
-
function
|
|
76
|
+
function infisicalEnvVars(config) {
|
|
77
|
+
return {
|
|
78
|
+
INFISICAL_TOKEN: config.token,
|
|
79
|
+
INFISICAL_PROJECT_ID: config.projectId,
|
|
80
|
+
INFISICAL_ENVIRONMENT: config.environment,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function buildContainerEnv(repo, infisical, extra = {}) {
|
|
77
84
|
const env = {
|
|
78
85
|
REPO_URL: repo.repoUrl,
|
|
79
86
|
CLONE_BRANCH: repo.cloneBranch,
|
|
@@ -83,7 +90,7 @@ function buildContainerEnv(repo, envVars, extra = {}) {
|
|
|
83
90
|
GIT_COMMITTER_NAME: "App Builder",
|
|
84
91
|
GIT_COMMITTER_EMAIL: "app-builder@localhost",
|
|
85
92
|
PLAYWRIGHT_BROWSERS_PATH: "/opt/playwright",
|
|
86
|
-
...
|
|
93
|
+
...infisicalEnvVars(infisical),
|
|
87
94
|
...extra,
|
|
88
95
|
};
|
|
89
96
|
if (process.env.DEBUG) {
|
|
@@ -99,7 +106,6 @@ export async function startContainer(config, repo) {
|
|
|
99
106
|
webhookUrl: config.webhookUrl,
|
|
100
107
|
detached: config.detached,
|
|
101
108
|
initialPrompt: config.initialPrompt ? `${config.initialPrompt.slice(0, 100)}...` : undefined,
|
|
102
|
-
envVarKeys: Object.keys(config.envVars),
|
|
103
109
|
});
|
|
104
110
|
debugLog("startContainer repo:", repo);
|
|
105
111
|
buildImage(config);
|
|
@@ -121,7 +127,7 @@ export async function startContainer(config, repo) {
|
|
|
121
127
|
extra.INITIAL_PROMPT = config.initialPrompt;
|
|
122
128
|
if (config.absorbTasks)
|
|
123
129
|
extra.ABSORB_TASKS = "1";
|
|
124
|
-
const containerEnv = buildContainerEnv(repo, config.
|
|
130
|
+
const containerEnv = buildContainerEnv(repo, config.infisical, extra);
|
|
125
131
|
// Build docker run args
|
|
126
132
|
const args = ["run", "-d", "--rm", "--name", containerName];
|
|
127
133
|
// Use explicit port mapping for macOS Docker Desktop compatibility
|
|
@@ -196,6 +202,7 @@ export function spawnTestContainer(config) {
|
|
|
196
202
|
ensureImageExists(config.projectRoot);
|
|
197
203
|
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
198
204
|
const containerName = `app-building-test-${uniqueId}`;
|
|
205
|
+
const infisicalVars = infisicalEnvVars(config.infisical);
|
|
199
206
|
const args = ["run", "-it", "--rm", "--name", containerName];
|
|
200
207
|
args.push("-v", `${config.projectRoot}:/repo`);
|
|
201
208
|
args.push("-w", "/repo");
|
|
@@ -203,7 +210,7 @@ export function spawnTestContainer(config) {
|
|
|
203
210
|
args.push("--user", `${process.getuid()}:${process.getgid()}`);
|
|
204
211
|
args.push("--env", "HOME=/repo/.agent-home");
|
|
205
212
|
args.push("--env", "PLAYWRIGHT_BROWSERS_PATH=/opt/playwright");
|
|
206
|
-
for (const [k, v] of Object.entries(
|
|
213
|
+
for (const [k, v] of Object.entries(infisicalVars)) {
|
|
207
214
|
args.push("--env", `${k}=${v}`);
|
|
208
215
|
}
|
|
209
216
|
args.push(IMAGE_NAME, "bash");
|
package/dist/secrets.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infisical secrets API.
|
|
3
|
+
*
|
|
4
|
+
* API versions used (per https://infisical.com/docs/api-reference):
|
|
5
|
+
* - Auth: POST /api/v1/auth/universal-auth/login
|
|
6
|
+
* - Secrets: GET/POST/PATCH /api/v4/secrets[/{secretName}]
|
|
7
|
+
* - Folders: POST /api/v2/folders
|
|
8
|
+
*/
|
|
1
9
|
export interface InfisicalConfig {
|
|
2
10
|
token: string;
|
|
3
11
|
projectId: string;
|
|
@@ -5,12 +13,14 @@ export interface InfisicalConfig {
|
|
|
5
13
|
}
|
|
6
14
|
/**
|
|
7
15
|
* Log in to Infisical using Universal Auth (Client ID + Client Secret).
|
|
8
|
-
*
|
|
16
|
+
* POST /api/v1/auth/universal-auth/login
|
|
17
|
+
* Returns a short-lived access token.
|
|
9
18
|
*/
|
|
10
19
|
export declare function infisicalLogin(clientId: string, clientSecret: string): Promise<string>;
|
|
11
20
|
/**
|
|
12
21
|
* Fetch secrets from an Infisical folder path.
|
|
13
|
-
*
|
|
22
|
+
* GET /api/v4/secrets?projectId=…&environment=…&secretPath=…
|
|
23
|
+
* Returns a key→value record.
|
|
14
24
|
*/
|
|
15
25
|
export declare function fetchInfisicalSecrets(config: InfisicalConfig, secretPath: string): Promise<Record<string, string>>;
|
|
16
26
|
/**
|
|
@@ -23,7 +33,12 @@ export declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Rec
|
|
|
23
33
|
export declare function fetchBranchSecrets(config: InfisicalConfig, branch: string): Promise<Record<string, string>>;
|
|
24
34
|
/**
|
|
25
35
|
* Create or update a branch secret in Infisical.
|
|
26
|
-
*
|
|
36
|
+
* Creates the folder path if it doesn't exist yet.
|
|
37
|
+
*
|
|
38
|
+
* POST /api/v4/secrets/{name} — create
|
|
39
|
+
* PATCH /api/v4/secrets/{name} — update (if secret already exists)
|
|
40
|
+
*
|
|
41
|
+
* Body: { projectId, environment, secretPath, secretValue, type }
|
|
27
42
|
*/
|
|
28
43
|
export declare function createBranchSecret(config: InfisicalConfig, branch: string, name: string, value: string): Promise<void>;
|
|
29
44
|
/**
|
package/dist/secrets.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infisical secrets API.
|
|
3
|
+
*
|
|
4
|
+
* API versions used (per https://infisical.com/docs/api-reference):
|
|
5
|
+
* - Auth: POST /api/v1/auth/universal-auth/login
|
|
6
|
+
* - Secrets: GET/POST/PATCH /api/v4/secrets[/{secretName}]
|
|
7
|
+
* - Folders: POST /api/v2/folders
|
|
8
|
+
*/
|
|
1
9
|
const INFISICAL_API_BASE = "https://app.infisical.com";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Auth
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
2
13
|
/**
|
|
3
14
|
* Log in to Infisical using Universal Auth (Client ID + Client Secret).
|
|
4
|
-
*
|
|
15
|
+
* POST /api/v1/auth/universal-auth/login
|
|
16
|
+
* Returns a short-lived access token.
|
|
5
17
|
*/
|
|
6
18
|
export async function infisicalLogin(clientId, clientSecret) {
|
|
7
19
|
const res = await fetch(`${INFISICAL_API_BASE}/api/v1/auth/universal-auth/login`, {
|
|
@@ -16,30 +28,33 @@ export async function infisicalLogin(clientId, clientSecret) {
|
|
|
16
28
|
const data = (await res.json());
|
|
17
29
|
return data.accessToken;
|
|
18
30
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const body = await res.text().catch(() => "");
|
|
28
|
-
throw new Error(`Infisical API GET ${path} → ${res.status}: ${body}`);
|
|
29
|
-
}
|
|
30
|
-
return res;
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
function authHeaders(config) {
|
|
35
|
+
return {
|
|
36
|
+
Authorization: `Bearer ${config.token}`,
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
};
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
41
|
* Fetch secrets from an Infisical folder path.
|
|
34
|
-
*
|
|
42
|
+
* GET /api/v4/secrets?projectId=…&environment=…&secretPath=…
|
|
43
|
+
* Returns a key→value record.
|
|
35
44
|
*/
|
|
36
45
|
export async function fetchInfisicalSecrets(config, secretPath) {
|
|
37
46
|
const params = new URLSearchParams({
|
|
38
|
-
|
|
47
|
+
projectId: config.projectId,
|
|
39
48
|
environment: config.environment,
|
|
40
49
|
secretPath,
|
|
41
50
|
});
|
|
42
|
-
const res = await
|
|
51
|
+
const res = await fetch(`${INFISICAL_API_BASE}/api/v4/secrets?${params}`, {
|
|
52
|
+
headers: authHeaders(config),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const body = await res.text().catch(() => "");
|
|
56
|
+
throw new Error(`Infisical GET secrets ${secretPath} → ${res.status}: ${body}`);
|
|
57
|
+
}
|
|
43
58
|
const data = (await res.json());
|
|
44
59
|
const secrets = {};
|
|
45
60
|
for (const s of data.secrets) {
|
|
@@ -59,24 +74,66 @@ export async function fetchGlobalSecrets(config) {
|
|
|
59
74
|
export async function fetchBranchSecrets(config, branch) {
|
|
60
75
|
return fetchInfisicalSecrets(config, `/branches/${branch}/`);
|
|
61
76
|
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Folders
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Ensure all folders in a path exist, creating any missing ones.
|
|
82
|
+
* POST /api/v2/folders — body: { projectId, environment, name, path }
|
|
83
|
+
*
|
|
84
|
+
* Infisical requires folders to exist before secrets can be written into them.
|
|
85
|
+
* We walk each segment of the path and issue a create; a 400 response means
|
|
86
|
+
* the folder already exists (the docs list 400 for "Bad Request" which
|
|
87
|
+
* Infisical returns for duplicate folder names).
|
|
88
|
+
*/
|
|
89
|
+
async function ensureFolder(config, folderPath) {
|
|
90
|
+
const segments = folderPath.split("/").filter(Boolean);
|
|
91
|
+
let parentPath = "/";
|
|
92
|
+
for (const segment of segments) {
|
|
93
|
+
const res = await fetch(`${INFISICAL_API_BASE}/api/v2/folders`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: authHeaders(config),
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
projectId: config.projectId,
|
|
98
|
+
environment: config.environment,
|
|
99
|
+
name: segment,
|
|
100
|
+
path: parentPath,
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
// 200 = created. 400 = folder already exists (Infisical returns 400 for
|
|
104
|
+
// duplicate folder names under the same parent).
|
|
105
|
+
if (res.ok || res.status === 400) {
|
|
106
|
+
parentPath += segment + "/";
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const text = await res.text().catch(() => "");
|
|
110
|
+
throw new Error(`Infisical create folder ${parentPath}${segment} → ${res.status}: ${text}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Secrets — write
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
62
116
|
/**
|
|
63
117
|
* Create or update a branch secret in Infisical.
|
|
64
|
-
*
|
|
118
|
+
* Creates the folder path if it doesn't exist yet.
|
|
119
|
+
*
|
|
120
|
+
* POST /api/v4/secrets/{name} — create
|
|
121
|
+
* PATCH /api/v4/secrets/{name} — update (if secret already exists)
|
|
122
|
+
*
|
|
123
|
+
* Body: { projectId, environment, secretPath, secretValue, type }
|
|
65
124
|
*/
|
|
66
125
|
export async function createBranchSecret(config, branch, name, value) {
|
|
67
126
|
const secretPath = `/branches/${branch}/`;
|
|
68
|
-
const url = `${INFISICAL_API_BASE}/api/
|
|
127
|
+
const url = `${INFISICAL_API_BASE}/api/v4/secrets/${encodeURIComponent(name)}`;
|
|
69
128
|
const body = {
|
|
70
|
-
|
|
129
|
+
projectId: config.projectId,
|
|
71
130
|
environment: config.environment,
|
|
72
131
|
secretPath,
|
|
73
132
|
secretValue: value,
|
|
74
133
|
type: "shared",
|
|
75
134
|
};
|
|
76
|
-
const headers =
|
|
77
|
-
|
|
78
|
-
"Content-Type": "application/json",
|
|
79
|
-
};
|
|
135
|
+
const headers = authHeaders(config);
|
|
136
|
+
// --- Try POST (create) ---------------------------------------------------
|
|
80
137
|
const res = await fetch(url, {
|
|
81
138
|
method: "POST",
|
|
82
139
|
headers,
|
|
@@ -84,8 +141,8 @@ export async function createBranchSecret(config, branch, name, value) {
|
|
|
84
141
|
});
|
|
85
142
|
if (res.ok)
|
|
86
143
|
return;
|
|
87
|
-
//
|
|
88
|
-
if (res.status === 400
|
|
144
|
+
// Secret already exists → PATCH to update
|
|
145
|
+
if (res.status === 400) {
|
|
89
146
|
const patchRes = await fetch(url, {
|
|
90
147
|
method: "PATCH",
|
|
91
148
|
headers,
|
|
@@ -96,9 +153,37 @@ export async function createBranchSecret(config, branch, name, value) {
|
|
|
96
153
|
const text = await patchRes.text().catch(() => "");
|
|
97
154
|
throw new Error(`Infisical PATCH ${name} → ${patchRes.status}: ${text}`);
|
|
98
155
|
}
|
|
156
|
+
// Folder doesn't exist → create folders then retry POST
|
|
157
|
+
if (res.status === 404) {
|
|
158
|
+
await ensureFolder(config, secretPath);
|
|
159
|
+
const retryRes = await fetch(url, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers,
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
});
|
|
164
|
+
if (retryRes.ok)
|
|
165
|
+
return;
|
|
166
|
+
// Retry may 400 if another process created it concurrently → try PATCH
|
|
167
|
+
if (retryRes.status === 400) {
|
|
168
|
+
const patchRes = await fetch(url, {
|
|
169
|
+
method: "PATCH",
|
|
170
|
+
headers,
|
|
171
|
+
body: JSON.stringify(body),
|
|
172
|
+
});
|
|
173
|
+
if (patchRes.ok)
|
|
174
|
+
return;
|
|
175
|
+
const text = await patchRes.text().catch(() => "");
|
|
176
|
+
throw new Error(`Infisical PATCH ${name} (after folder creation) → ${patchRes.status}: ${text}`);
|
|
177
|
+
}
|
|
178
|
+
const text = await retryRes.text().catch(() => "");
|
|
179
|
+
throw new Error(`Infisical POST ${name} (after folder creation) → ${retryRes.status}: ${text}`);
|
|
180
|
+
}
|
|
99
181
|
const text = await res.text().catch(() => "");
|
|
100
182
|
throw new Error(`Infisical POST ${name} → ${res.status}: ${text}`);
|
|
101
183
|
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Config helper
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
102
187
|
/**
|
|
103
188
|
* Extract Infisical config from environment variables and log in.
|
|
104
189
|
* Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
|