@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.
- package/lib/scaly-api.js +77 -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.
|
|
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;
|