@occam-scaly/scaly-cli 0.2.7 → 0.2.8

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/bin/scaly.js CHANGED
@@ -487,28 +487,14 @@ async function sourceLocalEnv() {
487
487
  }
488
488
 
489
489
  async function importHelpers() {
490
- const {
491
- SecretsManagerClient,
492
- GetSecretValueCommand
493
- } = require('@aws-sdk/client-secrets-manager');
494
- const {
495
- CloudWatchLogsClient,
496
- DescribeLogStreamsCommand
497
- } = require('@aws-sdk/client-cloudwatch-logs');
498
- const {
499
- ECSClient,
500
- ListClustersCommand,
501
- ListServicesCommand,
502
- DescribeServicesCommand,
503
- ListTasksCommand,
504
- DescribeTasksCommand
505
- } = require('@aws-sdk/client-ecs');
506
- const {
507
- ElasticLoadBalancingV2Client,
508
- DescribeTargetGroupsCommand,
509
- DescribeTargetHealthCommand
510
- } = require('@aws-sdk/client-elastic-load-balancing-v2');
511
490
  const axios = require('axios');
491
+ const { resolveApiEndpoint } = require('../lib/scaly-api');
492
+ const { normalizeStage, readAuthStore } = require('../lib/scaly-auth');
493
+
494
+ // Ensure API endpoint is set for common commands (db/secrets/etc).
495
+ if (!process.env.API_ENDPOINT && !process.env.SCALY_API_URL) {
496
+ process.env.API_ENDPOINT = resolveApiEndpoint();
497
+ }
512
498
 
513
499
  const API = () => process.env.API_ENDPOINT;
514
500
  const deriveDomain = () => {
@@ -528,12 +514,48 @@ async function importHelpers() {
528
514
  })[String(r || '').toUpperCase()] || null;
529
515
 
530
516
  async function getToken() {
531
- // Prefer stored bearer token or env override to avoid aws-vault
517
+ // Prefer stored bearer token or env override.
532
518
  const t = getStoredBearer();
533
519
  if (t) return t;
534
520
  if (process.env.SCALY_API_BEARER) return process.env.SCALY_API_BEARER;
535
- if (!process.env.COGNITO_SECRET) throw new Error('COGNITO_SECRET not set');
521
+
522
+ // Prefer a Scaly session token from `scaly auth login`.
523
+ if (process.env.SCALY_OIDC_TOKEN) return process.env.SCALY_OIDC_TOKEN;
524
+ const found = readAuthStore();
525
+ if (found && found.session && found.session.access_token) {
526
+ const s = found.session;
527
+ const expMs = typeof s.expires_at === 'number' ? s.expires_at : null;
528
+ const stage = normalizeStage(
529
+ s.stage || process.env.SCALY_STAGE || 'prod'
530
+ );
531
+ if (!process.env.SCALY_STAGE) process.env.SCALY_STAGE = stage;
532
+ if (expMs && Date.now() >= expMs) {
533
+ const e = new Error(
534
+ `Your Scaly session token expired. Run: scaly auth login --stage ${stage}`
535
+ );
536
+ e.code = 'TOKEN_EXPIRED';
537
+ throw e;
538
+ }
539
+ return s.access_token;
540
+ }
541
+
542
+ // Internal-only fallback (dev tooling): Cognito client credentials via AWS Secrets Manager.
543
+ if (!process.env.COGNITO_SECRET) {
544
+ const stage = normalizeStage(process.env.SCALY_STAGE || 'prod');
545
+ const e = new Error(
546
+ `Not authenticated. Run: scaly auth login --stage ${stage}`
547
+ );
548
+ e.code = 'SCALY_AUTH_REQUIRED';
549
+ throw e;
550
+ }
536
551
  const region = process.env.COGNITO_SECRET_REGION || 'ca-central-1';
552
+ console.error(
553
+ '[warn] Using COGNITO_SECRET flow (internal dev only). Prefer: scaly auth login'
554
+ );
555
+ const {
556
+ SecretsManagerClient,
557
+ GetSecretValueCommand
558
+ } = require('@aws-sdk/client-secrets-manager');
537
559
  const sm = new SecretsManagerClient({ region });
538
560
  const res = await sm.send(
539
561
  new GetSecretValueCommand({ SecretId: process.env.COGNITO_SECRET })
@@ -571,9 +593,13 @@ async function importHelpers() {
571
593
  }
572
594
 
573
595
  function elbClient(region, creds) {
596
+ const {
597
+ ElasticLoadBalancingV2Client
598
+ } = require('@aws-sdk/client-elastic-load-balancing-v2');
574
599
  return new ElasticLoadBalancingV2Client({ region, credentials: creds });
575
600
  }
576
601
  function ecsClient(region, creds) {
602
+ const { ECSClient } = require('@aws-sdk/client-ecs');
577
603
  return new ECSClient({ region, credentials: creds });
578
604
  }
579
605
 
@@ -599,6 +625,10 @@ async function importHelpers() {
599
625
 
600
626
  async function checkCloudwatchLogs(accountId, appId, region) {
601
627
  try {
628
+ const {
629
+ CloudWatchLogsClient,
630
+ DescribeLogStreamsCommand
631
+ } = require('@aws-sdk/client-cloudwatch-logs');
602
632
  const cwl = new CloudWatchLogsClient({ region });
603
633
  const group = `/scaly/account/${accountId}`;
604
634
  const cmd = new DescribeLogStreamsCommand({
@@ -2284,6 +2314,36 @@ async function runSecrets(sub, rest) {
2284
2314
  const json = parseBool(f.json, false);
2285
2315
 
2286
2316
  // OIDC-only for build secrets.
2317
+ try {
2318
+ const { resolveApiEndpoint } = require('../lib/scaly-api');
2319
+ const { normalizeStage, readAuthStore } = require('../lib/scaly-auth');
2320
+ if (!process.env.API_ENDPOINT && !process.env.SCALY_API_URL) {
2321
+ process.env.API_ENDPOINT = resolveApiEndpoint();
2322
+ }
2323
+
2324
+ const found = readAuthStore();
2325
+ if (
2326
+ found &&
2327
+ found.session &&
2328
+ found.session.access_token &&
2329
+ !process.env.SCALY_API_BEARER
2330
+ ) {
2331
+ const expMs =
2332
+ typeof found.session.expires_at === 'number'
2333
+ ? found.session.expires_at
2334
+ : null;
2335
+ const stage = normalizeStage(
2336
+ found.session.stage || process.env.SCALY_STAGE || 'prod'
2337
+ );
2338
+ if (!process.env.SCALY_STAGE) process.env.SCALY_STAGE = stage;
2339
+ if (expMs && Date.now() >= expMs) {
2340
+ // Fall through to error message below.
2341
+ } else {
2342
+ process.env.SCALY_API_BEARER = found.session.access_token;
2343
+ }
2344
+ }
2345
+ } catch {}
2346
+
2287
2347
  try {
2288
2348
  const t = getStoredBearer();
2289
2349
  if (t && !process.env.SCALY_API_BEARER) process.env.SCALY_API_BEARER = t;
@@ -2295,8 +2355,8 @@ async function runSecrets(sub, rest) {
2295
2355
  process.env.API_BEARER;
2296
2356
  if (!bearer || !isProbablyJwt(bearer)) {
2297
2357
  const msg =
2298
- 'secrets commands require a Cognito OIDC access token (JWT). ' +
2299
- 'Run `scaly login --token <access_token_jwt>` first.';
2358
+ 'secrets commands require a Scaly session token (JWT). ' +
2359
+ 'Run `scaly auth login` first.';
2300
2360
  if (json)
2301
2361
  console.log(JSON.stringify({ ok: false, error: { message: msg } }));
2302
2362
  else console.error(msg);
@@ -2566,7 +2626,7 @@ async function mintDbProxyInfo(rest) {
2566
2626
  const bearer = await getToken();
2567
2627
  if (!isProbablyJwt(bearer)) {
2568
2628
  const e = new Error(
2569
- 'db commands require a Cognito OIDC access token (JWT) in Authorization: Bearer.'
2629
+ 'db commands require a Scaly session token (JWT). Run `scaly auth login` first.'
2570
2630
  );
2571
2631
  e.code = 'SCALY_DB_JWT_REQUIRED';
2572
2632
  throw e;
@@ -3059,6 +3119,17 @@ async function runDiagnoseApp(rest) {
3059
3119
  });
3060
3120
  const c = credsRes?.issueAwsCredentials;
3061
3121
  if (c?.accessKeyId) {
3122
+ const {
3123
+ DescribeTargetGroupsCommand,
3124
+ DescribeTargetHealthCommand
3125
+ } = require('@aws-sdk/client-elastic-load-balancing-v2');
3126
+ const {
3127
+ ListClustersCommand,
3128
+ ListServicesCommand,
3129
+ DescribeServicesCommand,
3130
+ ListTasksCommand,
3131
+ DescribeTasksCommand
3132
+ } = require('@aws-sdk/client-ecs');
3062
3133
  const creds = makeCreds(c);
3063
3134
  // Target group heuristic: scaly-<first8-of-stackId>
3064
3135
  const shortId = String(stackId).split('-')[0];
package/lib/scaly-api.js CHANGED
@@ -140,7 +140,7 @@ function buildHeaders(auth) {
140
140
  };
141
141
  }
142
142
 
143
- async function graphqlRequest(query, variables) {
143
+ async function graphqlRequest(query, variables, extraHeaders) {
144
144
  const endpoint = resolveApiEndpoint();
145
145
  const auth = resolveAuth();
146
146
  if (!auth) {
@@ -156,7 +156,10 @@ async function graphqlRequest(query, variables) {
156
156
  {
157
157
  headers: {
158
158
  'content-type': 'application/json',
159
- ...buildHeaders(auth)
159
+ ...buildHeaders(auth),
160
+ ...(extraHeaders && typeof extraHeaders === 'object'
161
+ ? extraHeaders
162
+ : {})
160
163
  },
161
164
  timeout: 60_000
162
165
  }
@@ -381,63 +384,111 @@ async function listAddOnsByType({ accountId, type, take = 10 }) {
381
384
  return data?.listAddOns || [];
382
385
  }
383
386
 
384
- async function createStack({ accountId, name, size, minIdle }) {
385
- const data = await graphqlRequest(MUTATIONS.createStack, {
386
- data: {
387
- name,
388
- size,
389
- minIdle: typeof minIdle === 'number' ? minIdle : undefined,
390
- account: { connect: { id: accountId } }
391
- }
392
- });
387
+ async function createStack({
388
+ accountId,
389
+ name,
390
+ size,
391
+ minIdle,
392
+ executionGroupId
393
+ }) {
394
+ const data = await graphqlRequest(
395
+ MUTATIONS.createStack,
396
+ {
397
+ data: {
398
+ name,
399
+ size,
400
+ minIdle: typeof minIdle === 'number' ? minIdle : undefined,
401
+ account: { connect: { id: accountId } }
402
+ }
403
+ },
404
+ executionGroupId
405
+ ? { 'x-scaly-execution-group-id': executionGroupId }
406
+ : undefined
407
+ );
393
408
  return data?.createStack;
394
409
  }
395
410
 
396
- async function updateStack({ id, name, size, minIdle }) {
397
- const data = await graphqlRequest(MUTATIONS.updateStack, {
398
- where: { id },
399
- data: {
400
- name: name || undefined,
401
- size: size || undefined,
402
- minIdle: typeof minIdle === 'number' ? minIdle : undefined
403
- }
404
- });
411
+ async function updateStack({ id, name, size, minIdle, executionGroupId }) {
412
+ const data = await graphqlRequest(
413
+ MUTATIONS.updateStack,
414
+ {
415
+ where: { id },
416
+ data: {
417
+ name: name || undefined,
418
+ size: size || undefined,
419
+ minIdle: typeof minIdle === 'number' ? minIdle : undefined
420
+ }
421
+ },
422
+ executionGroupId
423
+ ? { 'x-scaly-execution-group-id': executionGroupId }
424
+ : undefined
425
+ );
405
426
  return data?.updateStack;
406
427
  }
407
428
 
408
- async function createApp({ accountId, stackId, name, minIdle }) {
409
- const data = await graphqlRequest(MUTATIONS.createApp, {
410
- data: {
411
- name,
412
- minIdle: typeof minIdle === 'number' ? minIdle : undefined,
413
- account: { connect: { id: accountId } },
414
- stack: stackId ? { connect: { id: stackId } } : undefined
415
- }
416
- });
429
+ async function createApp({
430
+ accountId,
431
+ stackId,
432
+ name,
433
+ minIdle,
434
+ executionGroupId
435
+ }) {
436
+ const data = await graphqlRequest(
437
+ MUTATIONS.createApp,
438
+ {
439
+ data: {
440
+ name,
441
+ minIdle: typeof minIdle === 'number' ? minIdle : undefined,
442
+ account: { connect: { id: accountId } },
443
+ stack: stackId ? { connect: { id: stackId } } : undefined
444
+ }
445
+ },
446
+ executionGroupId
447
+ ? { 'x-scaly-execution-group-id': executionGroupId }
448
+ : undefined
449
+ );
417
450
  return data?.createApp;
418
451
  }
419
452
 
420
- async function updateApp({ id, stackId, name, minIdle }) {
421
- const data = await graphqlRequest(MUTATIONS.updateApp, {
422
- where: { id },
423
- data: {
424
- name: name || undefined,
425
- minIdle: typeof minIdle === 'number' ? minIdle : undefined,
426
- stack: stackId ? { connect: { id: stackId } } : undefined
427
- }
428
- });
453
+ async function updateApp({ id, stackId, name, minIdle, executionGroupId }) {
454
+ const data = await graphqlRequest(
455
+ MUTATIONS.updateApp,
456
+ {
457
+ where: { id },
458
+ data: {
459
+ name: name || undefined,
460
+ minIdle: typeof minIdle === 'number' ? minIdle : undefined,
461
+ stack: stackId ? { connect: { id: stackId } } : undefined
462
+ }
463
+ },
464
+ executionGroupId
465
+ ? { 'x-scaly-execution-group-id': executionGroupId }
466
+ : undefined
467
+ );
429
468
  return data?.updateApp;
430
469
  }
431
470
 
432
- async function createAddOn({ accountId, name, type, database }) {
433
- const data = await graphqlRequest(MUTATIONS.createAddOn, {
434
- data: {
435
- name,
436
- type,
437
- account: { connect: { id: accountId } },
438
- addOnDatabase: database ? { create: database } : undefined
439
- }
440
- });
471
+ async function createAddOn({
472
+ accountId,
473
+ name,
474
+ type,
475
+ database,
476
+ executionGroupId
477
+ }) {
478
+ const data = await graphqlRequest(
479
+ MUTATIONS.createAddOn,
480
+ {
481
+ data: {
482
+ name,
483
+ type,
484
+ account: { connect: { id: accountId } },
485
+ addOnDatabase: database ? { create: database } : undefined
486
+ }
487
+ },
488
+ executionGroupId
489
+ ? { 'x-scaly-execution-group-id': executionGroupId }
490
+ : undefined
491
+ );
441
492
  return data?.createAddOn;
442
493
  }
443
494
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('./scaly-api');
4
+ const { randomUUID } = require('crypto');
4
5
 
5
6
  async function applyPlan({ config, plan, autoApprove = false }) {
6
7
  const actionable = (plan.operations || []).filter(
@@ -8,6 +9,7 @@ async function applyPlan({ config, plan, autoApprove = false }) {
8
9
  );
9
10
 
10
11
  const results = [];
12
+ const executionGroupId = randomUUID();
11
13
 
12
14
  let createdStack = null;
13
15
  let stackId = null;
@@ -20,7 +22,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
20
22
  accountId: op.desired.accountId,
21
23
  name: op.desired.name,
22
24
  size: op.desired.size,
23
- minIdle: op.desired.minIdle
25
+ minIdle: op.desired.minIdle,
26
+ executionGroupId
24
27
  });
25
28
  createdStack = res;
26
29
  stackId = res?.id || stackId;
@@ -33,7 +36,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
33
36
  id: op.resource.id,
34
37
  name: op.desired.name,
35
38
  size: op.desired.size,
36
- minIdle: op.desired.minIdle
39
+ minIdle: op.desired.minIdle,
40
+ executionGroupId
37
41
  });
38
42
  stackId = res?.id || stackId;
39
43
  results.push({ op_id: op.id, ok: true, result: res });
@@ -48,7 +52,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
48
52
  const res = await api.createApp({
49
53
  accountId: op.desired.accountId,
50
54
  stackId,
51
- name: op.desired.name
55
+ name: op.desired.name,
56
+ executionGroupId
52
57
  });
53
58
  results.push({ op_id: op.id, ok: true, result: res });
54
59
  continue;
@@ -59,7 +64,8 @@ async function applyPlan({ config, plan, autoApprove = false }) {
59
64
  accountId: op.desired.accountId,
60
65
  name: op.desired.name,
61
66
  type: op.desired.type,
62
- database: op.desired.database || undefined
67
+ database: op.desired.database || undefined,
68
+ executionGroupId
63
69
  });
64
70
  results.push({ op_id: op.id, ok: true, result: res });
65
71
  continue;
@@ -76,6 +82,7 @@ async function applyPlan({ config, plan, autoApprove = false }) {
76
82
  return {
77
83
  ok,
78
84
  auto_approve: !!autoApprove,
85
+ execution_group_id: executionGroupId,
79
86
  applied: actionable.length,
80
87
  results,
81
88
  created_stack_id: createdStack?.id || null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occam-scaly/scaly-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Scaly CLI (auth + project config helpers)",
5
5
  "bin": {
6
6
  "scaly": "./bin/scaly.js"