@occam-scaly/scaly-cli 0.2.0 → 0.2.1

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.
Files changed (2) hide show
  1. package/lib/scaly-api.js +77 -2
  2. package/package.json +1 -1
package/lib/scaly-api.js CHANGED
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const axios = require('axios');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
4
7
 
5
8
  const STAGE_ENDPOINTS = {
6
9
  prod: 'https://api.scalyapps.io/graphql',
@@ -27,7 +30,74 @@ function normalizeStage(stage) {
27
30
  return s;
28
31
  }
29
32
 
33
+ function decodeBase64Url(input) {
34
+ const padLen = (4 - (input.length % 4)) % 4;
35
+ const padded =
36
+ input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(padLen);
37
+ return Buffer.from(padded, 'base64').toString('utf8');
38
+ }
39
+
40
+ function tryGetJwtExpSeconds(token) {
41
+ if (!token || typeof token !== 'string') return null;
42
+ const parts = token.split('.');
43
+ if (parts.length !== 3) return null;
44
+ try {
45
+ const payloadJson = decodeBase64Url(parts[1] || '');
46
+ const payload = JSON.parse(payloadJson);
47
+ const exp = payload && payload.exp;
48
+ return typeof exp === 'number' ? exp : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function isJwtExpired(token, nowMs = Date.now()) {
55
+ const expSeconds = tryGetJwtExpSeconds(token);
56
+ if (!expSeconds) return false;
57
+ return nowMs >= expSeconds * 1000;
58
+ }
59
+
60
+ function resolveAuthStorePath() {
61
+ if (process.env.SCALY_AUTH_STORE_PATH)
62
+ return String(process.env.SCALY_AUTH_STORE_PATH);
63
+ const base =
64
+ process.env.SCALY_CONFIG_DIR ||
65
+ process.env.XDG_CONFIG_HOME ||
66
+ path.join(os.homedir(), '.config');
67
+ const primary = path.join(base, 'scaly', 'auth.json');
68
+ if (fs.existsSync(primary)) return primary;
69
+ const legacy = path.join(os.homedir(), '.scaly', 'auth.json');
70
+ if (fs.existsSync(legacy)) return legacy;
71
+ return primary;
72
+ }
73
+
74
+ function tryReadAuthStoreToken() {
75
+ const p = resolveAuthStorePath();
76
+ try {
77
+ const text = fs.readFileSync(p, 'utf8');
78
+ const obj = JSON.parse(text);
79
+ const token =
80
+ obj && typeof obj.access_token === 'string' ? obj.access_token : null;
81
+ if (!token) return null;
82
+ const expiresAtMs =
83
+ typeof obj.expires_at === 'number' ? obj.expires_at : null;
84
+ if (expiresAtMs && Date.now() >= expiresAtMs) return null;
85
+ if (isJwtExpired(token)) return null;
86
+ return token;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
30
92
  function resolveAuth() {
93
+ const scalyApiKey = process.env.SCALY_API_KEY;
94
+
95
+ const oidcToken =
96
+ process.env.SCALY_OIDC_TOKEN || tryReadAuthStoreToken() || null;
97
+ if (oidcToken && scalyApiKey) {
98
+ return { mode: 'oidc', token: oidcToken, scalyApiKey };
99
+ }
100
+
31
101
  const bearer =
32
102
  process.env.SCALY_API_BEARER ||
33
103
  process.env.API_BEARER_TOKEN ||
@@ -36,7 +106,6 @@ function resolveAuth() {
36
106
  return { mode: 'bearer', token: bearer };
37
107
  }
38
108
 
39
- const scalyApiKey = process.env.SCALY_API_KEY;
40
109
  const stage = normalizeStage(process.env.SCALY_STAGE);
41
110
  const appSyncApiKey =
42
111
  process.env.SCALY_APPSYNC_KEY ||
@@ -59,6 +128,12 @@ function buildHeaders(auth) {
59
128
  if (auth.mode === 'bearer') {
60
129
  return { authorization: `Bearer ${auth.token}` };
61
130
  }
131
+ if (auth.mode === 'oidc') {
132
+ return {
133
+ authorization: `Bearer ${auth.token}`,
134
+ 'x-scaly-api-key': auth.scalyApiKey
135
+ };
136
+ }
62
137
  return {
63
138
  'x-api-key': auth.appSyncApiKey,
64
139
  'x-scaly-api-key': auth.scalyApiKey
@@ -70,7 +145,7 @@ async function graphqlRequest(query, variables) {
70
145
  const auth = resolveAuth();
71
146
  if (!auth) {
72
147
  const e = new Error(
73
- 'Missing auth. Provide SCALY_API_BEARER (or run `scaly login`) OR set SCALY_API_KEY.'
148
+ 'Missing auth. Set SCALY_API_KEY. For advanced planning (add-ons), run `scaly auth login` to create a session token.'
74
149
  );
75
150
  e.code = 'SCALY_AUTH_MISSING';
76
151
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occam-scaly/scaly-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Scaly CLI (auth + project config helpers)",
5
5
  "bin": {
6
6
  "scaly": "./bin/scaly.js"