@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 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
- envVars: containerEnvVars,
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), `envVars` (Infisical credentials), `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`/`webhookSecret`/`detached`/`initialPrompt`/`localPort`/`absorbTasks`. See [Webhooks](#webhooks) and [Container lifecycle](#container-lifecycle) below. |
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
- envVars: {
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
- envVars: {
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",
@@ -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
- envVars: Record<string, string>;
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 buildContainerEnv(repo, envVars, extra = {}) {
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
- ...envVars,
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.envVars, extra);
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(config.envVars)) {
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
- * Returns a short-lived access token (default 30 day TTL).
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
- * Returns a key-value record of secret names to values.
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
- * Uses POST to create, falls back to PATCH if it already exists.
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
- * Returns a short-lived access token (default 30 day TTL).
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
- async function infisicalFetch(path, config) {
20
- const res = await fetch(`${INFISICAL_API_BASE}${path}`, {
21
- headers: {
22
- Authorization: `Bearer ${config.token}`,
23
- "Content-Type": "application/json",
24
- },
25
- });
26
- if (!res.ok) {
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
- * Returns a key-value record of secret names to values.
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
- workspaceId: config.projectId,
47
+ projectId: config.projectId,
39
48
  environment: config.environment,
40
49
  secretPath,
41
50
  });
42
- const res = await infisicalFetch(`/api/v3/secrets/raw?${params}`, config);
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
- * Uses POST to create, falls back to PATCH if it already exists.
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/v3/secrets/raw/${encodeURIComponent(name)}`;
127
+ const url = `${INFISICAL_API_BASE}/api/v4/secrets/${encodeURIComponent(name)}`;
69
128
  const body = {
70
- workspaceId: config.projectId,
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
- Authorization: `Bearer ${config.token}`,
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
- // If conflict (already exists), try PATCH to update
88
- if (res.status === 400 || res.status === 409) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio/app-building",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Library for managing agentic app-building containers",
5
5
  "type": "module",
6
6
  "exports": {