@replayio/app-building 1.11.0 → 1.13.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 +47 -8
- package/dist/container.d.ts +3 -0
- package/dist/container.js +6 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/secrets.d.ts +29 -0
- package/dist/secrets.js +95 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npm install @replayio/app-building
|
|
|
14
14
|
import {
|
|
15
15
|
loadDotEnv,
|
|
16
16
|
FileContainerRegistry,
|
|
17
|
+
getInfisicalConfig,
|
|
18
|
+
resolveContainerSecrets,
|
|
17
19
|
createMachine,
|
|
18
20
|
destroyMachine,
|
|
19
21
|
type ContainerConfig,
|
|
@@ -23,14 +25,17 @@ import {
|
|
|
23
25
|
httpOptsFor,
|
|
24
26
|
} from "@replayio/app-building";
|
|
25
27
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
+
// Load orchestration vars from .env, then fetch build secrets from Infisical
|
|
29
|
+
const orchestrationVars = loadDotEnv("/path/to/project");
|
|
30
|
+
const infisicalConfig = getInfisicalConfig(orchestrationVars);
|
|
31
|
+
const containerSecrets = await resolveContainerSecrets(infisicalConfig);
|
|
32
|
+
|
|
28
33
|
const config: ContainerConfig = {
|
|
29
34
|
projectRoot: "/path/to/project", // optional — only needed for local Docker operations
|
|
30
|
-
envVars,
|
|
35
|
+
envVars: containerSecrets,
|
|
31
36
|
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
32
|
-
flyToken:
|
|
33
|
-
flyApp:
|
|
37
|
+
flyToken: orchestrationVars.FLY_API_TOKEN,
|
|
38
|
+
flyApp: orchestrationVars.FLY_APP_NAME,
|
|
34
39
|
};
|
|
35
40
|
|
|
36
41
|
// Create a Fly machine (automatically provisions a volume)
|
|
@@ -54,7 +59,7 @@ await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
|
|
|
54
59
|
|
|
55
60
|
| Export | Description |
|
|
56
61
|
|---|---|
|
|
57
|
-
| `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars
|
|
62
|
+
| `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars` (build secrets from Infisical), `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`/`webhookSecret`/`detached`/`initialPrompt`/`localPort`. See [Webhooks](#webhooks) and [Container lifecycle](#container-lifecycle) below. |
|
|
58
63
|
| `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
|
|
59
64
|
| `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
|
|
60
65
|
| `FileContainerRegistry` | Built-in file-backed implementation of `ContainerRegistry`, backed by a `.jsonl` file. |
|
|
@@ -114,6 +119,35 @@ await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
|
|
|
114
119
|
|
|
115
120
|
**Types:** `FlyMachineInfo`, `FlyVolumeInfo`, `CreateMachineResult`
|
|
116
121
|
|
|
122
|
+
### Secrets (Infisical)
|
|
123
|
+
|
|
124
|
+
| Export | Description |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `getInfisicalConfig(envVars)` | Extract `InfisicalConfig` from env vars. Returns `null` if any required var is missing (enables fallback to raw `.env`). |
|
|
127
|
+
| `resolveContainerSecrets(config)` | Fetch global build secrets from Infisical and merge with Infisical config vars. Returns a `Record<string, string>` suitable for `ContainerConfig.envVars`. |
|
|
128
|
+
| `fetchGlobalSecrets(config)` | Fetch secrets from the `/global/` path. |
|
|
129
|
+
| `fetchBranchSecrets(config, branch)` | Fetch secrets from `/branches/<branch>/`. |
|
|
130
|
+
| `fetchInfisicalSecrets(config, path)` | Raw fetch from any Infisical folder path. |
|
|
131
|
+
|
|
132
|
+
**Types:** `InfisicalConfig`
|
|
133
|
+
|
|
134
|
+
**Usage pattern** (orchestration scripts):
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const orchestrationVars = loadDotEnv(projectRoot);
|
|
138
|
+
const infisicalConfig = getInfisicalConfig(orchestrationVars);
|
|
139
|
+
const containerSecrets = infisicalConfig
|
|
140
|
+
? await resolveContainerSecrets(infisicalConfig)
|
|
141
|
+
: orchestrationVars; // fallback for local dev without Infisical
|
|
142
|
+
|
|
143
|
+
const config: ContainerConfig = {
|
|
144
|
+
envVars: containerSecrets,
|
|
145
|
+
flyToken: orchestrationVars.FLY_API_TOKEN,
|
|
146
|
+
flyApp: orchestrationVars.FLY_APP_NAME,
|
|
147
|
+
...
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
117
151
|
### Image ref
|
|
118
152
|
|
|
119
153
|
| Export | Description |
|
|
@@ -154,7 +188,7 @@ so that interactive users can send follow-up messages at any time.
|
|
|
154
188
|
|
|
155
189
|
## Webhooks
|
|
156
190
|
|
|
157
|
-
Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs JSON to that URL on key events (no retries; failures are logged to stderr).
|
|
191
|
+
Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs JSON to that URL on key events (no retries; failures are logged to stderr). Set `webhookSecret` to include a `Bearer` token in the `Authorization` header for authenticating webhook requests.
|
|
158
192
|
|
|
159
193
|
### Payload format
|
|
160
194
|
|
|
@@ -195,10 +229,15 @@ Every POST body has this shape:
|
|
|
195
229
|
### Example
|
|
196
230
|
|
|
197
231
|
```ts
|
|
232
|
+
const orchestrationVars = loadDotEnv("/path/to/project");
|
|
233
|
+
const infisicalConfig = getInfisicalConfig(orchestrationVars);
|
|
234
|
+
const containerSecrets = await resolveContainerSecrets(infisicalConfig);
|
|
235
|
+
|
|
198
236
|
const config: ContainerConfig = {
|
|
199
237
|
projectRoot: "/path/to/project",
|
|
200
|
-
envVars:
|
|
238
|
+
envVars: containerSecrets,
|
|
201
239
|
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
202
240
|
webhookUrl: "https://example.com/hooks/container-events",
|
|
241
|
+
webhookSecret: "your-webhook-secret",
|
|
203
242
|
};
|
|
204
243
|
```
|
package/dist/container.d.ts
CHANGED
|
@@ -16,10 +16,13 @@ export interface ContainerConfig {
|
|
|
16
16
|
flyApp?: string;
|
|
17
17
|
imageRef?: string;
|
|
18
18
|
webhookUrl?: string;
|
|
19
|
+
webhookSecret?: string;
|
|
19
20
|
/** Start the container in detached mode. It will exit after processing all messages and tasks. */
|
|
20
21
|
detached?: boolean;
|
|
21
22
|
/** Initial prompt to queue at container startup (before the HTTP server accepts external requests). */
|
|
22
23
|
initialPrompt?: string;
|
|
24
|
+
/** Override the host port for local containers (default: auto-selected). */
|
|
25
|
+
localPort?: number;
|
|
23
26
|
}
|
|
24
27
|
export interface RepoOptions {
|
|
25
28
|
repoUrl: string;
|
package/dist/container.js
CHANGED
|
@@ -105,13 +105,16 @@ export async function startContainer(config, repo) {
|
|
|
105
105
|
buildImage(config);
|
|
106
106
|
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
107
107
|
const containerName = `app-building-${uniqueId}`;
|
|
108
|
-
const
|
|
108
|
+
const containerPort = 3000;
|
|
109
|
+
const hostPort = config.localPort ?? findFreePort();
|
|
109
110
|
const extra = {
|
|
110
|
-
PORT: String(
|
|
111
|
+
PORT: String(containerPort),
|
|
111
112
|
CONTAINER_NAME: containerName,
|
|
112
113
|
};
|
|
113
114
|
if (config.webhookUrl)
|
|
114
115
|
extra.WEBHOOK_URL = config.webhookUrl;
|
|
116
|
+
if (config.webhookSecret)
|
|
117
|
+
extra.WEBHOOK_SECRET = config.webhookSecret;
|
|
115
118
|
if (config.detached)
|
|
116
119
|
extra.DETACHED = "1";
|
|
117
120
|
if (config.initialPrompt)
|
|
@@ -121,7 +124,7 @@ export async function startContainer(config, repo) {
|
|
|
121
124
|
const args = ["run", "-d", "--rm", "--name", containerName];
|
|
122
125
|
// Use explicit port mapping for macOS Docker Desktop compatibility
|
|
123
126
|
// (--network host only works on Linux)
|
|
124
|
-
args.push("-p", `${hostPort}:${
|
|
127
|
+
args.push("-p", `${hostPort}:${containerPort}`);
|
|
125
128
|
for (const [k, v] of Object.entries(containerEnv)) {
|
|
126
129
|
args.push("--env", `${k}=${v}`);
|
|
127
130
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface InfisicalConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
environment: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fetch secrets from an Infisical folder path.
|
|
8
|
+
* Returns a key-value record of secret names to values.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchInfisicalSecrets(config: InfisicalConfig, secretPath: string): Promise<Record<string, string>>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetch global build secrets from `/global/`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch per-branch deployment secrets from `/branches/<branch>/`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchBranchSecrets(config: InfisicalConfig, branch: string): Promise<Record<string, string>>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the full set of secrets to inject into a container:
|
|
21
|
+
* global build secrets + Infisical config vars (so the container can fetch branch secrets).
|
|
22
|
+
* Throws if any required secret from .env.example is missing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveContainerSecrets(config: InfisicalConfig): Promise<Record<string, string>>;
|
|
25
|
+
/**
|
|
26
|
+
* Extract Infisical config from environment variables.
|
|
27
|
+
* Throws if any required Infisical var is missing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getInfisicalConfig(envVars: Record<string, string>): InfisicalConfig;
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const INFISICAL_API_BASE = "https://app.infisical.com";
|
|
6
|
+
/** Keys listed in src/package/.env.example that must be present in container secrets. */
|
|
7
|
+
function getRequiredSecretKeys() {
|
|
8
|
+
const examplePath = resolve(__dirname, ".env.example");
|
|
9
|
+
return readFileSync(examplePath, "utf-8")
|
|
10
|
+
.split("\n")
|
|
11
|
+
.map((l) => l.split("#")[0].trim())
|
|
12
|
+
.filter((l) => l && l.includes("="))
|
|
13
|
+
.map((l) => l.split("=")[0].trim());
|
|
14
|
+
}
|
|
15
|
+
async function infisicalFetch(path, config) {
|
|
16
|
+
const res = await fetch(`${INFISICAL_API_BASE}${path}`, {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${config.token}`,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const body = await res.text().catch(() => "");
|
|
24
|
+
throw new Error(`Infisical API GET ${path} → ${res.status}: ${body}`);
|
|
25
|
+
}
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch secrets from an Infisical folder path.
|
|
30
|
+
* Returns a key-value record of secret names to values.
|
|
31
|
+
*/
|
|
32
|
+
export async function fetchInfisicalSecrets(config, secretPath) {
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
workspaceId: config.projectId,
|
|
35
|
+
environment: config.environment,
|
|
36
|
+
secretPath,
|
|
37
|
+
});
|
|
38
|
+
const res = await infisicalFetch(`/api/v3/secrets/raw?${params}`, config);
|
|
39
|
+
const data = (await res.json());
|
|
40
|
+
const secrets = {};
|
|
41
|
+
for (const s of data.secrets) {
|
|
42
|
+
secrets[s.secretKey] = s.secretValue;
|
|
43
|
+
}
|
|
44
|
+
return secrets;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fetch global build secrets from `/global/`.
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchGlobalSecrets(config) {
|
|
50
|
+
return fetchInfisicalSecrets(config, "/global/");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fetch per-branch deployment secrets from `/branches/<branch>/`.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchBranchSecrets(config, branch) {
|
|
56
|
+
return fetchInfisicalSecrets(config, `/branches/${branch}/`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the full set of secrets to inject into a container:
|
|
60
|
+
* global build secrets + Infisical config vars (so the container can fetch branch secrets).
|
|
61
|
+
* Throws if any required secret from .env.example is missing.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveContainerSecrets(config) {
|
|
64
|
+
const globals = await fetchGlobalSecrets(config);
|
|
65
|
+
const secrets = {
|
|
66
|
+
...globals,
|
|
67
|
+
INFISICAL_TOKEN: config.token,
|
|
68
|
+
INFISICAL_PROJECT_ID: config.projectId,
|
|
69
|
+
INFISICAL_ENVIRONMENT: config.environment,
|
|
70
|
+
};
|
|
71
|
+
const required = getRequiredSecretKeys();
|
|
72
|
+
const missing = required.filter((k) => !secrets[k]);
|
|
73
|
+
if (missing.length > 0) {
|
|
74
|
+
throw new Error(`Missing required secrets in Infisical /global/: ${missing.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
return secrets;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract Infisical config from environment variables.
|
|
80
|
+
* Throws if any required Infisical var is missing.
|
|
81
|
+
*/
|
|
82
|
+
export function getInfisicalConfig(envVars) {
|
|
83
|
+
const token = envVars.INFISICAL_TOKEN;
|
|
84
|
+
const projectId = envVars.INFISICAL_PROJECT_ID;
|
|
85
|
+
const environment = envVars.INFISICAL_ENVIRONMENT;
|
|
86
|
+
const missing = [
|
|
87
|
+
!token && "INFISICAL_TOKEN",
|
|
88
|
+
!projectId && "INFISICAL_PROJECT_ID",
|
|
89
|
+
!environment && "INFISICAL_ENVIRONMENT",
|
|
90
|
+
].filter(Boolean);
|
|
91
|
+
if (missing.length > 0) {
|
|
92
|
+
throw new Error(`Missing Infisical config in .env: ${missing.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
return { token: token, projectId: projectId, environment: environment };
|
|
95
|
+
}
|