@replayio/app-building 1.25.0 → 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/README.md +3 -3
- package/dist/index.d.ts +188 -7
- package/dist/index.js +799 -7
- package/package.json +6 -2
- package/dist/container-registry.d.ts +0 -25
- package/dist/container-registry.js +0 -78
- package/dist/container-utils.d.ts +0 -5
- package/dist/container-utils.js +0 -30
- package/dist/container.d.ts +0 -58
- package/dist/container.js +0 -361
- package/dist/fly.d.ts +0 -55
- package/dist/fly.js +0 -199
- package/dist/http-client.d.ts +0 -6
- package/dist/http-client.js +0 -30
- package/dist/image-ref.d.ts +0 -1
- package/dist/image-ref.js +0 -4
- package/dist/secrets.d.ts +0 -50
- package/dist/secrets.js +0 -209
- package/dist/tasks.d.ts +0 -24
- package/dist/tasks.js +0 -53
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
|
-
}
|
package/dist/http-client.d.ts
DELETED
|
@@ -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>;
|
package/dist/http-client.js
DELETED
|
@@ -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
|
-
}
|
package/dist/image-ref.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getImageRef(): string;
|
package/dist/image-ref.js
DELETED
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
|
-
}
|
package/dist/tasks.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task dependency resolution helpers.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions that take task arrays and return ready tasks — no file I/O.
|
|
5
|
-
*/
|
|
6
|
-
export interface TaskInfo {
|
|
7
|
-
id?: string;
|
|
8
|
-
requiredTaskIds?: string[];
|
|
9
|
-
parentTaskId?: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Find the first task in `pendingTasks` whose dependencies are all satisfied.
|
|
13
|
-
* Returns the task, or null if all tasks are blocked.
|
|
14
|
-
*
|
|
15
|
-
* A required task is "fully complete" when:
|
|
16
|
-
* 1. It appears in `completedTasks`, AND
|
|
17
|
-
* 2. Every task in `completedTasks` that has it as `parentTaskId` is also
|
|
18
|
-
* fully complete (recursive — children's children must also be done), AND
|
|
19
|
-
* 3. No task in `pendingTasks` has it as `parentTaskId`.
|
|
20
|
-
*
|
|
21
|
-
* @param pendingTasks - Tasks waiting to run (ordered by priority).
|
|
22
|
-
* @param completedTasks - Tasks that have finished (need id and parentTaskId).
|
|
23
|
-
*/
|
|
24
|
-
export declare function findReadyTask<T extends TaskInfo>(pendingTasks: T[], completedTasks: TaskInfo[]): T | null;
|
package/dist/tasks.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task dependency resolution helpers.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions that take task arrays and return ready tasks — no file I/O.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Find the first task in `pendingTasks` whose dependencies are all satisfied.
|
|
8
|
-
* Returns the task, or null if all tasks are blocked.
|
|
9
|
-
*
|
|
10
|
-
* A required task is "fully complete" when:
|
|
11
|
-
* 1. It appears in `completedTasks`, AND
|
|
12
|
-
* 2. Every task in `completedTasks` that has it as `parentTaskId` is also
|
|
13
|
-
* fully complete (recursive — children's children must also be done), AND
|
|
14
|
-
* 3. No task in `pendingTasks` has it as `parentTaskId`.
|
|
15
|
-
*
|
|
16
|
-
* @param pendingTasks - Tasks waiting to run (ordered by priority).
|
|
17
|
-
* @param completedTasks - Tasks that have finished (need id and parentTaskId).
|
|
18
|
-
*/
|
|
19
|
-
export function findReadyTask(pendingTasks, completedTasks) {
|
|
20
|
-
const completedIds = new Set(completedTasks.map((t) => t.id).filter(Boolean));
|
|
21
|
-
// Parents that still have pending children.
|
|
22
|
-
const pendingChildParents = new Set();
|
|
23
|
-
for (const t of pendingTasks) {
|
|
24
|
-
if (t.parentTaskId)
|
|
25
|
-
pendingChildParents.add(t.parentTaskId);
|
|
26
|
-
}
|
|
27
|
-
// Cache for recursive full-completion check.
|
|
28
|
-
const cache = new Map();
|
|
29
|
-
function isFullyComplete(taskId) {
|
|
30
|
-
if (cache.has(taskId))
|
|
31
|
-
return cache.get(taskId);
|
|
32
|
-
// Prevent infinite recursion on cycles.
|
|
33
|
-
cache.set(taskId, false);
|
|
34
|
-
if (!completedIds.has(taskId))
|
|
35
|
-
return false;
|
|
36
|
-
if (pendingChildParents.has(taskId))
|
|
37
|
-
return false;
|
|
38
|
-
// Check completed children are also fully complete.
|
|
39
|
-
for (const child of completedTasks) {
|
|
40
|
-
if (child.parentTaskId === taskId && child.id && !isFullyComplete(child.id)) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
cache.set(taskId, true);
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
const idx = pendingTasks.findIndex((task) => {
|
|
48
|
-
if (!task.requiredTaskIds || task.requiredTaskIds.length === 0)
|
|
49
|
-
return true;
|
|
50
|
-
return task.requiredTaskIds.every((id) => isFullyComplete(id));
|
|
51
|
-
});
|
|
52
|
-
return idx === -1 ? null : pendingTasks[idx];
|
|
53
|
-
}
|