@replayio/app-building 1.24.1 → 1.26.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/dist/fly.js DELETED
@@ -1,199 +0,0 @@
1
- const API_BASE = "https://api.machines.dev/v1";
2
- async function flyFetch(path, token, opts = {}) {
3
- const res = await fetch(`${API_BASE}${path}`, {
4
- ...opts,
5
- headers: {
6
- Authorization: `Bearer ${token}`,
7
- "Content-Type": "application/json",
8
- ...(opts.headers ?? {}),
9
- },
10
- });
11
- if (!res.ok) {
12
- const body = await res.text().catch(() => "");
13
- throw new Error(`Fly API ${opts.method ?? "GET"} ${path} → ${res.status}: ${body}`);
14
- }
15
- return res;
16
- }
17
- /**
18
- * Create a Fly app via the Machines API and allocate IPs so .fly.dev DNS works.
19
- */
20
- export async function createApp(token, name, org) {
21
- await flyFetch("/apps", token, {
22
- method: "POST",
23
- body: JSON.stringify({ app_name: name, org_slug: org ?? "personal" }),
24
- });
25
- // Allocate shared IPv4 and IPv6 via GraphQL so the app gets a .fly.dev domain
26
- const gqlFetch = async (query, variables) => {
27
- const res = await fetch("https://api.fly.io/graphql", {
28
- method: "POST",
29
- headers: {
30
- Authorization: `Bearer ${token}`,
31
- "Content-Type": "application/json",
32
- },
33
- body: JSON.stringify({ query, variables }),
34
- });
35
- if (!res.ok) {
36
- const body = await res.text().catch(() => "");
37
- throw new Error(`Fly GraphQL error ${res.status}: ${body}`);
38
- }
39
- const data = await res.json();
40
- if (data.errors?.length) {
41
- throw new Error(`Fly GraphQL: ${data.errors[0].message}`);
42
- }
43
- };
44
- const allocateMutation = `
45
- mutation($input: AllocateIPAddressInput!) {
46
- allocateIpAddress(input: $input) {
47
- ipAddress { id address type }
48
- }
49
- }
50
- `;
51
- await gqlFetch(allocateMutation, { input: { appId: name, type: "shared_v4" } });
52
- await gqlFetch(allocateMutation, { input: { appId: name, type: "v6" } });
53
- }
54
- /**
55
- * Create a Fly Volume in the given region.
56
- * Returns the volume ID.
57
- */
58
- export async function createVolume(app, token, name, region, sizeGb = 50) {
59
- const res = await flyFetch(`/apps/${app}/volumes`, token, {
60
- method: "POST",
61
- body: JSON.stringify({
62
- name,
63
- region,
64
- size_gb: sizeGb,
65
- encrypted: true,
66
- require_unique_zone: false,
67
- }),
68
- });
69
- const data = (await res.json());
70
- return data.id;
71
- }
72
- /**
73
- * Delete a Fly Volume.
74
- */
75
- export async function deleteVolume(app, token, volumeId) {
76
- await flyFetch(`/apps/${app}/volumes/${volumeId}`, token, {
77
- method: "DELETE",
78
- });
79
- }
80
- /**
81
- * Create a Fly Machine with the given image and env vars.
82
- * Creates a volume mounted at /repo for storage.
83
- * Returns the machine ID and volume ID.
84
- */
85
- export async function createMachine(app, token, image, env, name) {
86
- const volumeName = `repo_${name.replace(/-/g, "_")}`.slice(0, 30);
87
- // Regions to try in order. dfw and iad have the most reliable capacity for
88
- // performance machines. Fall back to ord and sjc if needed.
89
- const regions = ["dfw", "iad", "ord", "sjc"];
90
- // Delete unattached volumes in parallel with creating the new machine.
91
- let cleanupDone;
92
- for (const region of regions) {
93
- console.log(`Creating machine in region ${region}...`);
94
- const volumeId = await createVolume(app, token, volumeName, region, 50);
95
- // Start cleanup on first attempt only
96
- if (!cleanupDone) {
97
- cleanupDone = listVolumes(app, token).then(vols => Promise.all(vols.map(async ({ id, attached_machine_id }) => {
98
- if (attached_machine_id || id === volumeId)
99
- return;
100
- await deleteVolume(app, token, id).catch(() => { });
101
- })));
102
- }
103
- try {
104
- const res = await flyFetch(`/apps/${app}/machines`, token, {
105
- method: "POST",
106
- body: JSON.stringify({
107
- name,
108
- region,
109
- config: {
110
- image,
111
- env,
112
- auto_destroy: true,
113
- restart: { policy: "on-failure", max_retries: 3 },
114
- guest: {
115
- cpu_kind: "performance",
116
- cpus: 16,
117
- memory_mb: 32768,
118
- },
119
- mounts: [{ volume: volumeId, path: "/repo" }],
120
- services: [
121
- {
122
- ports: [{ port: 443, handlers: ["tls", "http"] }],
123
- protocol: "tcp",
124
- internal_port: 3000,
125
- autostart: false,
126
- autostop: "off",
127
- },
128
- ],
129
- },
130
- }),
131
- });
132
- const data = (await res.json());
133
- await cleanupDone;
134
- return { machineId: data.id, volumeId };
135
- }
136
- catch (err) {
137
- await deleteVolume(app, token, volumeId).catch(() => { });
138
- const msg = err instanceof Error ? err.message : String(err);
139
- if (msg.includes("412") || msg.includes("insufficient")) {
140
- console.log(`Insufficient resources in ${region}, trying next region...`);
141
- continue;
142
- }
143
- throw new Error(`Failed to create machine in region ${region}: ${msg}`);
144
- }
145
- }
146
- throw new Error(`Failed to create machine in any region (tried ${regions.join(", ")}): insufficient resources`);
147
- }
148
- /**
149
- * Wait for a Fly Machine to reach the "started" state.
150
- */
151
- export async function waitForMachine(app, token, machineId, timeoutMs = 180000) {
152
- const start = Date.now();
153
- let lastLogTime = 0;
154
- while (Date.now() - start < timeoutMs) {
155
- try {
156
- await flyFetch(`/apps/${app}/machines/${machineId}/wait?state=started&timeout=60`, token);
157
- return;
158
- }
159
- catch (e) {
160
- const now = Date.now();
161
- const elapsed = Math.round((now - start) / 1000);
162
- // Only log at most once every 10 seconds
163
- if (now - lastLogTime >= 10000) {
164
- console.log(`Still waiting for machine to start (${elapsed}s elapsed): ${e instanceof Error ? e.message : e}`);
165
- lastLogTime = now;
166
- }
167
- // Wait before retrying
168
- await new Promise((r) => setTimeout(r, 5000));
169
- }
170
- }
171
- throw new Error(`Machine ${machineId} did not reach started state within ${timeoutMs / 1000}s`);
172
- }
173
- /**
174
- * Destroy a Fly Machine (force) and its attached volume.
175
- */
176
- export async function destroyMachine(app, token, machineId, volumeId) {
177
- await flyFetch(`/apps/${app}/machines/${machineId}?force=true`, token, {
178
- method: "DELETE",
179
- });
180
- if (volumeId) {
181
- await deleteVolume(app, token, volumeId).catch((err) => {
182
- console.log(`Warning: failed to delete volume ${volumeId}: ${err instanceof Error ? err.message : err}`);
183
- });
184
- }
185
- }
186
- /**
187
- * List all machines for a Fly app.
188
- */
189
- export async function listMachines(app, token) {
190
- const res = await flyFetch(`/apps/${app}/machines`, token);
191
- return (await res.json());
192
- }
193
- /**
194
- * List all volumes for a Fly app.
195
- */
196
- export async function listVolumes(app, token) {
197
- const res = await flyFetch(`/apps/${app}/volumes`, token);
198
- return (await res.json());
199
- }
@@ -1,6 +0,0 @@
1
- export interface HttpOptions {
2
- timeout?: number;
3
- headers?: Record<string, string>;
4
- }
5
- export declare function httpGet(url: string, opts?: HttpOptions): Promise<any>;
6
- export declare function httpPost(url: string, body?: unknown, opts?: HttpOptions): Promise<any>;
@@ -1,30 +0,0 @@
1
- const DEFAULT_TIMEOUT = 30000;
2
- const MAX_RETRIES = 4;
3
- const RETRY_DELAY_MS = 2000;
4
- async function fetchWithRetry(url, init, timeout) {
5
- for (let attempt = 0;; attempt++) {
6
- try {
7
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeout) });
8
- if (!res.ok)
9
- throw new Error(`${init.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`);
10
- return res;
11
- }
12
- catch (err) {
13
- if (attempt >= MAX_RETRIES)
14
- throw err;
15
- await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
16
- }
17
- }
18
- }
19
- export async function httpGet(url, opts = {}) {
20
- const res = await fetchWithRetry(url, { headers: opts.headers }, opts.timeout ?? DEFAULT_TIMEOUT);
21
- return res.json();
22
- }
23
- export async function httpPost(url, body, opts = {}) {
24
- const res = await fetchWithRetry(url, {
25
- method: "POST",
26
- headers: { "Content-Type": "application/json", ...opts.headers },
27
- body: body !== undefined ? JSON.stringify(body) : undefined,
28
- }, opts.timeout ?? DEFAULT_TIMEOUT);
29
- return res.json();
30
- }
@@ -1 +0,0 @@
1
- export declare function getImageRef(): string;
package/dist/image-ref.js DELETED
@@ -1,4 +0,0 @@
1
- const DEFAULT_IMAGE_REF = "ghcr.io/replayio/app-building:latest";
2
- export function getImageRef() {
3
- return process.env.CONTAINER_IMAGE_REF ?? DEFAULT_IMAGE_REF;
4
- }
package/dist/secrets.d.ts DELETED
@@ -1,50 +0,0 @@
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
- */
9
- export interface InfisicalConfig {
10
- token: string;
11
- projectId: string;
12
- environment: string;
13
- }
14
- /**
15
- * Log in to Infisical using Universal Auth (Client ID + Client Secret).
16
- * POST /api/v1/auth/universal-auth/login
17
- * Returns a short-lived access token.
18
- */
19
- export declare function infisicalLogin(clientId: string, clientSecret: string): Promise<string>;
20
- /**
21
- * Fetch secrets from an Infisical folder path.
22
- * GET /api/v4/secrets?projectId=…&environment=…&secretPath=…
23
- * Returns a key→value record.
24
- */
25
- export declare function fetchInfisicalSecrets(config: InfisicalConfig, secretPath: string): Promise<Record<string, string>>;
26
- /**
27
- * Fetch global build secrets from `/global/`.
28
- */
29
- export declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Record<string, string>>;
30
- /**
31
- * Fetch per-branch deployment secrets from `/branches/<branch>/`.
32
- */
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>;
44
- /**
45
- * Extract Infisical config from environment variables and log in.
46
- * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
47
- * and INFISICAL_ENVIRONMENT from the env vars.
48
- * Throws if any required var is missing.
49
- */
50
- export declare function getInfisicalConfig(envVars: Record<string, string>): Promise<InfisicalConfig>;
package/dist/secrets.js DELETED
@@ -1,209 +0,0 @@
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
- */
9
- const INFISICAL_API_BASE = "https://app.infisical.com";
10
- // ---------------------------------------------------------------------------
11
- // Auth
12
- // ---------------------------------------------------------------------------
13
- /**
14
- * Log in to Infisical using Universal Auth (Client ID + Client Secret).
15
- * POST /api/v1/auth/universal-auth/login
16
- * Returns a short-lived access token.
17
- */
18
- export async function infisicalLogin(clientId, clientSecret) {
19
- const res = await fetch(`${INFISICAL_API_BASE}/api/v1/auth/universal-auth/login`, {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json" },
22
- body: JSON.stringify({ clientId, clientSecret }),
23
- });
24
- if (!res.ok) {
25
- const body = await res.text().catch(() => "");
26
- throw new Error(`Infisical login failed → ${res.status}: ${body}`);
27
- }
28
- const data = (await res.json());
29
- return data.accessToken;
30
- }
31
- // ---------------------------------------------------------------------------
32
- // Helpers
33
- // ---------------------------------------------------------------------------
34
- function authHeaders(config) {
35
- return {
36
- Authorization: `Bearer ${config.token}`,
37
- "Content-Type": "application/json",
38
- };
39
- }
40
- /**
41
- * Fetch secrets from an Infisical folder path.
42
- * GET /api/v4/secrets?projectId=…&environment=…&secretPath=…
43
- * Returns a key→value record.
44
- */
45
- export async function fetchInfisicalSecrets(config, secretPath) {
46
- const params = new URLSearchParams({
47
- projectId: config.projectId,
48
- environment: config.environment,
49
- secretPath,
50
- });
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
- }
58
- const data = (await res.json());
59
- const secrets = {};
60
- for (const s of data.secrets) {
61
- secrets[s.secretKey] = s.secretValue;
62
- }
63
- return secrets;
64
- }
65
- /**
66
- * Fetch global build secrets from `/global/`.
67
- */
68
- export async function fetchGlobalSecrets(config) {
69
- return fetchInfisicalSecrets(config, "/global/");
70
- }
71
- /**
72
- * Fetch per-branch deployment secrets from `/branches/<branch>/`.
73
- */
74
- export async function fetchBranchSecrets(config, branch) {
75
- return fetchInfisicalSecrets(config, `/branches/${branch}/`);
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
- // ---------------------------------------------------------------------------
187
- /**
188
- * Extract Infisical config from environment variables and log in.
189
- * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
190
- * and INFISICAL_ENVIRONMENT from the env vars.
191
- * Throws if any required var is missing.
192
- */
193
- export async function getInfisicalConfig(envVars) {
194
- const clientId = envVars.INFISICAL_CLIENT_ID;
195
- const clientSecret = envVars.INFISICAL_CLIENT_SECRET;
196
- const projectId = envVars.INFISICAL_PROJECT_ID;
197
- const environment = envVars.INFISICAL_ENVIRONMENT;
198
- const missing = [
199
- !clientId && "INFISICAL_CLIENT_ID",
200
- !clientSecret && "INFISICAL_CLIENT_SECRET",
201
- !projectId && "INFISICAL_PROJECT_ID",
202
- !environment && "INFISICAL_ENVIRONMENT",
203
- ].filter(Boolean);
204
- if (missing.length > 0) {
205
- throw new Error(`Missing Infisical config in .env: ${missing.join(", ")}`);
206
- }
207
- const token = await infisicalLogin(clientId, clientSecret);
208
- return { token, projectId: projectId, environment: environment };
209
- }