@replayio/app-building 1.16.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,
@@ -77,7 +68,7 @@ exec-secrets NETLIFY_AUTH_TOKEN NETLIFY_ACCOUNT_SLUG -- netlify deploy --prod
77
68
 
78
69
  The secrets server spawns the command with the requested secrets in its environment and **redacts all secret values** from the output.
79
70
 
80
- The agent can also run `list-secrets` to see which secrets are available.
71
+ The agent can also run `list-secrets` to see which secrets are available, and `set-branch-secret` to store new branch-level secrets (e.g., `DATABASE_URL` created at deploy time). The server rejects values that have already appeared in logs.
81
72
 
82
73
  ## Exported API
83
74
 
@@ -85,7 +76,7 @@ The agent can also run `list-secrets` to see which secrets are available.
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. |
@@ -149,9 +140,11 @@ The agent can also run `list-secrets` to see which secrets are available.
149
140
 
150
141
  | Export | Description |
151
142
  |---|---|
143
+ | `infisicalLogin(clientId, clientSecret)` | Log in via Universal Auth, returns a short-lived access token. |
152
144
  | `getInfisicalConfig(envVars)` | Extract `InfisicalConfig` from env vars and log in. Requires `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`, `INFISICAL_PROJECT_ID`, `INFISICAL_ENVIRONMENT`. |
153
145
  | `fetchGlobalSecrets(config)` | Fetch secrets from the `/global/` path. |
154
146
  | `fetchBranchSecrets(config, branch)` | Fetch secrets from `/branches/<branch>/`. |
147
+ | `createBranchSecret(config, branch, name, value)` | Create or update a secret in `/branches/<branch>/`. |
155
148
  | `fetchInfisicalSecrets(config, path)` | Raw fetch from any Infisical folder path. |
156
149
 
157
150
  **Types:** `InfisicalConfig`
@@ -162,13 +155,8 @@ The agent can also run `list-secrets` to see which secrets are available.
162
155
  const orchestrationVars = loadDotEnv(projectRoot);
163
156
  const infisicalConfig = await getInfisicalConfig(orchestrationVars);
164
157
 
165
- // Pass only Infisical credentials to the container
166
158
  const config: ContainerConfig = {
167
- envVars: {
168
- INFISICAL_TOKEN: infisicalConfig.token,
169
- INFISICAL_PROJECT_ID: infisicalConfig.projectId,
170
- INFISICAL_ENVIRONMENT: infisicalConfig.environment,
171
- },
159
+ infisical: infisicalConfig,
172
160
  flyToken: orchestrationVars.FLY_API_TOKEN,
173
161
  flyApp: orchestrationVars.FLY_APP_NAME,
174
162
  ...
@@ -267,11 +255,7 @@ const infisicalConfig = await getInfisicalConfig(orchestrationVars);
267
255
 
268
256
  const config: ContainerConfig = {
269
257
  projectRoot: "/path/to/project",
270
- envVars: {
271
- INFISICAL_TOKEN: infisicalConfig.token,
272
- INFISICAL_PROJECT_ID: infisicalConfig.projectId,
273
- INFISICAL_ENVIRONMENT: infisicalConfig.environment,
274
- },
258
+ infisical: infisicalConfig,
275
259
  registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
276
260
  webhookUrl: "https://example.com/hooks/container-events",
277
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
  /**
@@ -21,6 +31,16 @@ export declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Rec
21
31
  * Fetch per-branch deployment secrets from `/branches/<branch>/`.
22
32
  */
23
33
  export declare function fetchBranchSecrets(config: InfisicalConfig, branch: string): Promise<Record<string, string>>;
34
+ /**
35
+ * Create or update a branch secret in Infisical.
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 }
42
+ */
43
+ export declare function createBranchSecret(config: InfisicalConfig, branch: string, name: string, value: string): Promise<void>;
24
44
  /**
25
45
  * Extract Infisical config from environment variables and log in.
26
46
  * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
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,6 +74,116 @@ 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
+ // ---------------------------------------------------------------------------
116
+ /**
117
+ * Create or update a branch secret in Infisical.
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 }
124
+ */
125
+ export async function createBranchSecret(config, branch, name, value) {
126
+ const secretPath = `/branches/${branch}/`;
127
+ const url = `${INFISICAL_API_BASE}/api/v4/secrets/${encodeURIComponent(name)}`;
128
+ const body = {
129
+ projectId: config.projectId,
130
+ environment: config.environment,
131
+ secretPath,
132
+ secretValue: value,
133
+ type: "shared",
134
+ };
135
+ const headers = authHeaders(config);
136
+ // --- Try POST (create) ---------------------------------------------------
137
+ const res = await fetch(url, {
138
+ method: "POST",
139
+ headers,
140
+ body: JSON.stringify(body),
141
+ });
142
+ if (res.ok)
143
+ return;
144
+ // Secret already exists → PATCH to update
145
+ if (res.status === 400) {
146
+ const patchRes = await fetch(url, {
147
+ method: "PATCH",
148
+ headers,
149
+ body: JSON.stringify(body),
150
+ });
151
+ if (patchRes.ok)
152
+ return;
153
+ const text = await patchRes.text().catch(() => "");
154
+ throw new Error(`Infisical PATCH ${name} → ${patchRes.status}: ${text}`);
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
+ }
181
+ const text = await res.text().catch(() => "");
182
+ throw new Error(`Infisical POST ${name} → ${res.status}: ${text}`);
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // Config helper
186
+ // ---------------------------------------------------------------------------
62
187
  /**
63
188
  * Extract Infisical config from environment variables and log in.
64
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.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Library for managing agentic app-building containers",
5
5
  "type": "module",
6
6
  "exports": {