@gambi97/keel-cli 0.3.6 → 0.3.7

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
@@ -263,6 +263,12 @@ creating anything**.
263
263
  `ContainersFullAccess`, `ServerlessSQLDatabaseFullAccess`,
264
264
  `ContainerRegistryFullAccess`, plus `IAMManager` so Terraform can create
265
265
  the app's dedicated least-privilege database credential).
266
+ 5. **Organization security settings — API-key expiration must be unlimited**
267
+ (Console → Organization → Security → API keys). The app's database and
268
+ Object Storage credentials are non-expiring service credentials by design
269
+ (the container reads them at runtime); if your organization forces API
270
+ keys to expire, the first CI apply fails creating them. keel checks this
271
+ setting during validation and warns before anything is created.
266
272
 
267
273
  </details>
268
274
 
@@ -273,7 +279,9 @@ creating anything**.
273
279
  1. Create an account at [app.infisical.com](https://app.infisical.com) (or
274
280
  use a self-hosted instance).
275
281
  2. Create a **Machine Identity** with **Universal Auth**: you need its
276
- **client ID** and **client secret**.
282
+ **client ID** and **client secret**. Create the client secret with
283
+ **unlimited uses** (Max Number of Uses = 0): CI logs in with it on every
284
+ plan/apply, so any finite usage limit eventually runs out mid-pipeline.
277
285
  3. Give the identity permission to create and manage projects.
278
286
 
279
287
  </details>
@@ -30,8 +30,15 @@ export async function login(infisical) {
30
30
  const { host, clientId, clientSecret } = infisical;
31
31
  const { status, data } = await api(host, '/api/v1/auth/universal-auth/login', { method: 'POST', body: { clientId, clientSecret } });
32
32
  if (status !== 200 || !data.accessToken) {
33
- throw new InfisicalError(`Infisical Universal Auth login failed (HTTP ${status}${data.message ? `: ${data.message}` : ''}). ` +
34
- 'Check the machine identity client ID/secret and that Universal Auth is enabled.');
33
+ // A usage-limited client secret is a trap for this setup: besides the
34
+ // bootstrap, CI logs in with it on every plan/apply, so any finite limit
35
+ // eventually runs out. Name the fix instead of a generic "check secret".
36
+ const remediation = /usage limit/i.test(data.message ?? '')
37
+ ? 'This client secret was created with a usage limit and it is spent. Create a new one ' +
38
+ 'with unlimited uses (Max Number of Uses = 0): CI logs in with it on every plan/apply, ' +
39
+ 'so a limited secret will always run out.'
40
+ : 'Check the machine identity client ID/secret and that Universal Auth is enabled.';
41
+ throw new InfisicalError(`Infisical Universal Auth login failed (HTTP ${status}${data.message ? `: ${data.message}` : ''}). ${remediation}`);
35
42
  }
36
43
  return data.accessToken;
37
44
  }
@@ -7,10 +7,38 @@ export class ScalewayError extends Error {
7
7
  }
8
8
  }
9
9
  const API_BASE = 'https://api.scaleway.com';
10
+ /**
11
+ * The generated stack creates non-expiring service credentials (the app's
12
+ * database and Object Storage API keys are read by the container at runtime).
13
+ * An organization security setting that forces API-key expiration makes the
14
+ * very first CI apply fail — hours after the bootstrap looked green. Read the
15
+ * setting here (read-only) so the run can warn upfront, while it is still a
16
+ * 30-second console fix. Returns undefined when the key cannot read it.
17
+ */
18
+ async function apiKeyExpirationPolicyWarning(secretKey, organizationId) {
19
+ try {
20
+ const response = await fetch(`${API_BASE}/iam/v1alpha1/organizations/${organizationId}/security-settings`, { headers: { 'X-Auth-Token': secretKey } });
21
+ if (!response.ok)
22
+ return undefined;
23
+ const settings = (await response.json());
24
+ const seconds = Number.parseInt(settings.max_api_key_expiration_duration ?? '0', 10);
25
+ if (!Number.isFinite(seconds) || seconds <= 0)
26
+ return undefined; // "0s" = unlimited
27
+ return ('Your Scaleway organization requires API keys to expire ' +
28
+ `(max ${Math.round(seconds / 86400)} days). The database/storage credentials the ` +
29
+ 'generated stack creates are non-expiring service credentials, so the first CI apply ' +
30
+ 'WILL fail on them. Disable the requirement first (Console → Organization → Security → ' +
31
+ 'API keys), then merge or re-run the apply.');
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
10
37
  /**
11
38
  * Validate credentials with a harmless read call before creating anything.
12
39
  * Also confirms the project belongs to the given organization. Read-only: it
13
- * proves the key can see the project, not that it can create resources.
40
+ * proves the key can see the project, not that it can create resources. The
41
+ * returned warning flags an org policy that would break the first CI apply.
14
42
  */
15
43
  export async function validateScalewayCredentials(scaleway) {
16
44
  const { secretKey, projectId, organizationId } = scaleway;
@@ -32,6 +60,8 @@ export async function validateScalewayCredentials(scaleway) {
32
60
  throw new ScalewayError(`Scaleway project ${projectId} belongs to organization ${project.organization_id}, ` +
33
61
  `not ${organizationId}. Check --scw-organization-id.`, 'organization');
34
62
  }
63
+ const warning = await apiKeyExpirationPolicyWarning(secretKey, organizationId);
64
+ return warning ? { warning } : {};
35
65
  }
36
66
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
67
  /**
package/dist/index.js CHANGED
@@ -298,8 +298,9 @@ async function runBootstrap(answers, state, options) {
298
298
  else {
299
299
  // All three credentials are checked before anything is created anywhere,
300
300
  // so a bad token cannot leave a half-bootstrapped account behind.
301
+ let scalewayWarning;
301
302
  await withSpinner('Validating Scaleway, Infisical and GitHub credentials', async () => {
302
- await validateScalewayCredentials(answers.scaleway);
303
+ scalewayWarning = (await validateScalewayCredentials(answers.scaleway)).warning;
303
304
  await validateInfisical(answers.infisical);
304
305
  ctx = await createContext(answers.github);
305
306
  // A repo with commits would make the push fail after the bucket and the
@@ -310,6 +311,8 @@ async function runBootstrap(answers, state, options) {
310
311
  assertRepoUsable(ctx, repoState);
311
312
  }
312
313
  });
314
+ if (scalewayWarning)
315
+ log.warn(scalewayWarning);
313
316
  }
314
317
  for (const step of BOOTSTRAP_STEPS) {
315
318
  if (isDone(state, step.name)) {
package/dist/prompts.js CHANGED
@@ -358,11 +358,16 @@ async function askScaleway(out) {
358
358
  out.scaleway.organizationId = await text('Scaleway organization ID');
359
359
  }
360
360
  try {
361
- await validateScalewayCredentials({
361
+ const { warning } = await validateScalewayCredentials({
362
362
  secretKey: out.scaleway.secretKey,
363
363
  projectId: out.scaleway.projectId,
364
364
  organizationId: out.scaleway.organizationId,
365
365
  });
366
+ // e.g. an org policy that would make the first CI apply fail: better a
367
+ // loud heads-up now, while it is a 30-second console fix, than a red
368
+ // pipeline hours after everything here looked green.
369
+ if (warning)
370
+ log.warn(warning);
366
371
  p.outro('Scaleway connected — credentials valid and project reachable.');
367
372
  return;
368
373
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gambi97/keel-cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "One command from zero to a production-shaped serverless infrastructure on Scaleway (Terraform + GitHub Actions + Infisical). Only Node required.",
5
5
  "keywords": [
6
6
  "keel",
@@ -209,6 +209,10 @@ Avoid local applies: they race against CI on the same state.
209
209
  match a Scaleway key with Object Storage access.
210
210
  - **Plan fails reading Infisical secrets**: check `INFISICAL_PROJECT_ID` and
211
211
  that the machine identity has access to the project and every environment.
212
+ If the error mentions a **usage limit**, the machine identity's client
213
+ secret was created with a maximum number of uses and it is spent: create a
214
+ new client secret with unlimited uses (the pipeline logs in on every
215
+ plan/apply) and update the `INFISICAL_CLIENT_SECRET` Actions secret.
212
216
  - **Apply green but no container**: expected until `container_image` is set.
213
217
  - **App can't reach the database**: `DATABASE_URL` is only synced after an
214
218
  apply; check the value in Infisical for that environment. It must contain