@quonfig/cli 0.0.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/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +9 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/api/get-environments.d.ts +10 -0
- package/dist/api/get-environments.js +11 -0
- package/dist/codegen/code-generators/base-generator.d.ts +11 -0
- package/dist/codegen/code-generators/base-generator.js +8 -0
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +17 -0
- package/dist/codegen/code-generators/base-typescript-generator.js +35 -0
- package/dist/codegen/code-generators/node-typescript-generator.d.ts +8 -0
- package/dist/codegen/code-generators/node-typescript-generator.js +96 -0
- package/dist/codegen/code-generators/react-typescript-generator.d.ts +11 -0
- package/dist/codegen/code-generators/react-typescript-generator.js +116 -0
- package/dist/codegen/config-downloader.d.ts +7 -0
- package/dist/codegen/config-downloader.js +26 -0
- package/dist/codegen/json-schema-to-zod.d.ts +3 -0
- package/dist/codegen/json-schema-to-zod.js +170 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.d.ts +4 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.js +55 -0
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-base-mapper.js +111 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +25 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +87 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +133 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +36 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +150 -0
- package/dist/codegen/mustache-extractor.d.ts +6 -0
- package/dist/codegen/mustache-extractor.js +83 -0
- package/dist/codegen/schema-evaluator.d.ts +37 -0
- package/dist/codegen/schema-evaluator.js +283 -0
- package/dist/codegen/schema-extractor.d.ts +24 -0
- package/dist/codegen/schema-extractor.js +265 -0
- package/dist/codegen/types.d.ts +44 -0
- package/dist/codegen/types.js +5 -0
- package/dist/codegen/zod-introspection.d.ts +85 -0
- package/dist/codegen/zod-introspection.js +205 -0
- package/dist/commands/create.d.ts +19 -0
- package/dist/commands/create.js +175 -0
- package/dist/commands/download.d.ts +13 -0
- package/dist/commands/download.js +63 -0
- package/dist/commands/generate-new-hex-key.d.ts +7 -0
- package/dist/commands/generate-new-hex-key.js +10 -0
- package/dist/commands/generate.d.ts +16 -0
- package/dist/commands/generate.js +220 -0
- package/dist/commands/get.d.ts +13 -0
- package/dist/commands/get.js +141 -0
- package/dist/commands/info.d.ts +21 -0
- package/dist/commands/info.js +464 -0
- package/dist/commands/interactive.d.ts +5 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/list.d.ts +13 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +145 -0
- package/dist/commands/override.d.ts +17 -0
- package/dist/commands/override.js +170 -0
- package/dist/commands/profile.d.ts +7 -0
- package/dist/commands/profile.js +47 -0
- package/dist/commands/schema.d.ts +21 -0
- package/dist/commands/schema.js +168 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +99 -0
- package/dist/commands/set-default.d.ts +20 -0
- package/dist/commands/set-default.js +273 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +48 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/commands/workspace.d.ts +7 -0
- package/dist/commands/workspace.js +38 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +10 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +112 -0
- package/dist/interactive-prompt.d.ts +2 -0
- package/dist/interactive-prompt.js +162 -0
- package/dist/quonfig.d.ts +15 -0
- package/dist/quonfig.js +75 -0
- package/dist/result.d.ts +35 -0
- package/dist/result.js +9 -0
- package/dist/services/mcp.d.ts +15 -0
- package/dist/services/mcp.js +59 -0
- package/dist/ui/get-confirmation.d.ts +10 -0
- package/dist/ui/get-confirmation.js +4 -0
- package/dist/ui/get-environment.d.ts +11 -0
- package/dist/ui/get-environment.js +42 -0
- package/dist/ui/get-key.d.ts +27 -0
- package/dist/ui/get-key.js +15 -0
- package/dist/ui/get-string.d.ts +5 -0
- package/dist/ui/get-string.js +8 -0
- package/dist/ui/get-value.d.ts +18 -0
- package/dist/ui/get-value.js +46 -0
- package/dist/util/autocomplete.d.ts +6 -0
- package/dist/util/autocomplete.js +25 -0
- package/dist/util/coerce.d.ts +7 -0
- package/dist/util/coerce.js +62 -0
- package/dist/util/color.d.ts +3 -0
- package/dist/util/color.js +4 -0
- package/dist/util/config-value-dto.d.ts +16 -0
- package/dist/util/config-value-dto.js +82 -0
- package/dist/util/context.d.ts +18 -0
- package/dist/util/context.js +10 -0
- package/dist/util/domain-urls.d.ts +3 -0
- package/dist/util/domain-urls.js +18 -0
- package/dist/util/encryption.d.ts +20 -0
- package/dist/util/encryption.js +153 -0
- package/dist/util/file-manager.d.ts +12 -0
- package/dist/util/file-manager.js +16 -0
- package/dist/util/get-client.d.ts +7 -0
- package/dist/util/get-client.js +127 -0
- package/dist/util/is-interactive.d.ts +5 -0
- package/dist/util/is-interactive.js +7 -0
- package/dist/util/json-maybe.d.ts +3 -0
- package/dist/util/json-maybe.js +14 -0
- package/dist/util/name-arg.d.ts +4 -0
- package/dist/util/name-arg.js +3 -0
- package/dist/util/oauth-client.d.ts +30 -0
- package/dist/util/oauth-client.js +99 -0
- package/dist/util/secret-flags.d.ts +13 -0
- package/dist/util/secret-flags.js +13 -0
- package/dist/util/token-storage.d.ts +25 -0
- package/dist/util/token-storage.js +90 -0
- package/dist/validations/value.d.ts +4 -0
- package/dist/validations/value.js +11 -0
- package/dist/verify/standalone.d.ts +14 -0
- package/dist/verify/standalone.js +133 -0
- package/dist/verify/validate.d.ts +43 -0
- package/dist/verify/validate.js +756 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/oclif.manifest.json +1429 -0
- package/package.json +104 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RequestResult } from '../result.js';
|
|
2
|
+
import { APICommand } from '../index.js';
|
|
3
|
+
import { Client } from '@quonfig/node';
|
|
4
|
+
export declare const resetClientCache: () => void;
|
|
5
|
+
declare const getClient: (command: APICommand, sdkKey?: string, profile?: string) => Promise<Client>;
|
|
6
|
+
export declare const unwrapRequest: (command: APICommand, promise: Promise<Response>) => Promise<RequestResult>;
|
|
7
|
+
export default getClient;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Client } from '@quonfig/node';
|
|
2
|
+
import { getApiUrl } from '../util/domain-urls.js';
|
|
3
|
+
import jsonMaybe from '../util/json-maybe.js';
|
|
4
|
+
import { decodeJWT, refreshAccessToken } from '../util/oauth-client.js';
|
|
5
|
+
import { getActiveProfile, loadAuthConfig, loadTokens, saveTokens } from '../util/token-storage.js';
|
|
6
|
+
import version from '../version.js';
|
|
7
|
+
let clientInstance;
|
|
8
|
+
let cachedWorkspaceId;
|
|
9
|
+
export const resetClientCache = () => {
|
|
10
|
+
clientInstance = undefined;
|
|
11
|
+
cachedWorkspaceId = undefined;
|
|
12
|
+
};
|
|
13
|
+
const getClient = async (command, sdkKey, profile) => {
|
|
14
|
+
// If client exists, still set workspaceId on command if available
|
|
15
|
+
if (clientInstance) {
|
|
16
|
+
if (cachedWorkspaceId) {
|
|
17
|
+
command.workspaceId = cachedWorkspaceId;
|
|
18
|
+
}
|
|
19
|
+
return clientInstance;
|
|
20
|
+
}
|
|
21
|
+
let jwt;
|
|
22
|
+
let workspaceId;
|
|
23
|
+
// If no SDK key provided, use WorkOS OAuth tokens
|
|
24
|
+
if (!sdkKey) {
|
|
25
|
+
const authConfig = await loadAuthConfig();
|
|
26
|
+
let tokens = await loadTokens();
|
|
27
|
+
command.verboseLog('OAuth auth', {
|
|
28
|
+
hasAuthConfig: Boolean(authConfig),
|
|
29
|
+
hasAccessToken: Boolean(tokens?.accessToken),
|
|
30
|
+
});
|
|
31
|
+
if (!authConfig || !tokens?.accessToken) {
|
|
32
|
+
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
33
|
+
}
|
|
34
|
+
// Check if token is expired and refresh if needed.
|
|
35
|
+
// Prefer the JWT's actual exp claim over the stored expiresAt.
|
|
36
|
+
let tokenExpired = tokens.expiresAt ? tokens.expiresAt < Date.now() : false;
|
|
37
|
+
try {
|
|
38
|
+
const payload = decodeJWT(tokens.accessToken);
|
|
39
|
+
if (typeof payload.exp === 'number') {
|
|
40
|
+
tokenExpired = payload.exp * 1000 < Date.now();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// If we can't decode, fall back to stored expiresAt
|
|
45
|
+
}
|
|
46
|
+
if (tokenExpired && tokens.refreshToken) {
|
|
47
|
+
command.verboseLog('Token expired, refreshing...');
|
|
48
|
+
try {
|
|
49
|
+
const refreshed = await refreshAccessToken(tokens.refreshToken);
|
|
50
|
+
// Use the JWT's actual exp claim for expiry, not a hardcoded duration
|
|
51
|
+
let expiresAt = Date.now() + 300 * 1000; // fallback: 5 minutes
|
|
52
|
+
try {
|
|
53
|
+
const payload = decodeJWT(refreshed.access_token);
|
|
54
|
+
if (typeof payload.exp === 'number') {
|
|
55
|
+
expiresAt = payload.exp * 1000; // convert seconds to ms
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Use fallback
|
|
60
|
+
}
|
|
61
|
+
tokens = {
|
|
62
|
+
...tokens,
|
|
63
|
+
accessToken: refreshed.access_token,
|
|
64
|
+
expiresAt,
|
|
65
|
+
refreshToken: refreshed.refresh_token,
|
|
66
|
+
};
|
|
67
|
+
await saveTokens(tokens);
|
|
68
|
+
command.verboseLog('Token refreshed successfully');
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
command.error('Session expired. Please run `qfg login` to re-authenticate.', { exit: 401 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Get the active profile to find the workspace
|
|
75
|
+
const activeProfile = getActiveProfile(profile);
|
|
76
|
+
const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
77
|
+
command.verboseLog('Profile lookup', {
|
|
78
|
+
activeProfile,
|
|
79
|
+
hasProfileData: Boolean(profileData),
|
|
80
|
+
workspaceId: profileData?.workspace,
|
|
81
|
+
});
|
|
82
|
+
if (profileData) {
|
|
83
|
+
workspaceId = profileData.workspace;
|
|
84
|
+
}
|
|
85
|
+
// Use the WorkOS access token directly as the JWT
|
|
86
|
+
jwt = tokens.accessToken;
|
|
87
|
+
// If still no JWT, user needs to login
|
|
88
|
+
if (!jwt) {
|
|
89
|
+
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Store workspaceId on the command for use in building URLs and cache it
|
|
93
|
+
if (workspaceId) {
|
|
94
|
+
command.workspaceId = workspaceId;
|
|
95
|
+
cachedWorkspaceId = workspaceId;
|
|
96
|
+
}
|
|
97
|
+
clientInstance = new Client({
|
|
98
|
+
jwt,
|
|
99
|
+
sdkKey,
|
|
100
|
+
apiUrl: getApiUrl(),
|
|
101
|
+
clientIdentifier: `cli-${version}`,
|
|
102
|
+
log: command.verboseLog,
|
|
103
|
+
});
|
|
104
|
+
return clientInstance;
|
|
105
|
+
};
|
|
106
|
+
export const unwrapRequest = async (command, promise) => {
|
|
107
|
+
const request = await promise;
|
|
108
|
+
if (request.status.toString().startsWith('2')) {
|
|
109
|
+
const json = (await request.json());
|
|
110
|
+
command.verboseLog('ApiClient', { response: json });
|
|
111
|
+
return { json, ok: true, status: request.status };
|
|
112
|
+
}
|
|
113
|
+
// Handle 403 with a user-friendly message
|
|
114
|
+
if (request.status === 403) {
|
|
115
|
+
return {
|
|
116
|
+
error: { error: 'You do not have permission to perform this action. Please check your workspace permissions.' },
|
|
117
|
+
ok: false,
|
|
118
|
+
status: request.status,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const error = jsonMaybe(await request.text());
|
|
122
|
+
if (typeof error === 'string') {
|
|
123
|
+
return { error: { error }, ok: false, status: request.status };
|
|
124
|
+
}
|
|
125
|
+
return { error, ok: false, status: request.status };
|
|
126
|
+
};
|
|
127
|
+
export default getClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Sometimes we get JSON from the server but if there's an exception, we might get a string.
|
|
2
|
+
// This function will try to parse the string as JSON and return the parsed object or the original string.
|
|
3
|
+
const jsonMaybe = (json) => {
|
|
4
|
+
if (typeof json === 'string') {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(json);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return json;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return json;
|
|
13
|
+
};
|
|
14
|
+
export default jsonMaybe;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface DeviceCodeResponse {
|
|
2
|
+
device_code: string;
|
|
3
|
+
expires_in: number;
|
|
4
|
+
interval: number;
|
|
5
|
+
user_code: string;
|
|
6
|
+
verification_uri: string;
|
|
7
|
+
verification_uri_complete: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkOSUser {
|
|
10
|
+
email: string;
|
|
11
|
+
email_verified: boolean;
|
|
12
|
+
first_name: string | null;
|
|
13
|
+
id: string;
|
|
14
|
+
last_name: string | null;
|
|
15
|
+
organization_id?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DeviceTokenResponse {
|
|
18
|
+
access_token: string;
|
|
19
|
+
authentication_method: string;
|
|
20
|
+
refresh_token: string;
|
|
21
|
+
user: WorkOSUser;
|
|
22
|
+
}
|
|
23
|
+
export interface TokenRefreshResponse {
|
|
24
|
+
access_token: string;
|
|
25
|
+
refresh_token: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const requestDeviceCode: () => Promise<DeviceCodeResponse>;
|
|
28
|
+
export declare const pollForToken: (deviceCode: string, interval: number, expiresIn: number, verbose?: boolean) => Promise<DeviceTokenResponse>;
|
|
29
|
+
export declare const refreshAccessToken: (refreshToken: string) => Promise<TokenRefreshResponse>;
|
|
30
|
+
export declare const decodeJWT: (token: string) => Record<string, unknown>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getDomain } from './domain-urls.js';
|
|
2
|
+
const WORKOS_BASE_URL = 'https://api.workos.com';
|
|
3
|
+
// WorkOS Client IDs for the CLI (public client — no secret needed)
|
|
4
|
+
const WORKOS_CLIENT_ID_PRODUCTION = 'client_01KKCCWMNB2K8HYQZQ96G1MXE0';
|
|
5
|
+
const WORKOS_CLIENT_ID_STAGING = 'client_01KKCCWM4K87JS0MKS8PHZ0ZY1';
|
|
6
|
+
const getClientId = () => {
|
|
7
|
+
if (process.env.WORKOS_CLIENT_ID) {
|
|
8
|
+
return process.env.WORKOS_CLIENT_ID;
|
|
9
|
+
}
|
|
10
|
+
const domain = getDomain();
|
|
11
|
+
if (domain === 'quonfig-staging.com') {
|
|
12
|
+
return WORKOS_CLIENT_ID_STAGING;
|
|
13
|
+
}
|
|
14
|
+
return WORKOS_CLIENT_ID_PRODUCTION;
|
|
15
|
+
};
|
|
16
|
+
// --- Device Code Flow ---
|
|
17
|
+
export const requestDeviceCode = async () => {
|
|
18
|
+
const response = await fetch(`${WORKOS_BASE_URL}/user_management/authorize/device`, {
|
|
19
|
+
body: new URLSearchParams({
|
|
20
|
+
client_id: getClientId(),
|
|
21
|
+
}),
|
|
22
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
23
|
+
method: 'POST',
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const errorText = await response.text();
|
|
27
|
+
throw new Error(`Failed to request device code: ${errorText}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
};
|
|
31
|
+
export const pollForToken = async (deviceCode, interval, expiresIn, verbose) => {
|
|
32
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
33
|
+
let pollInterval = interval * 1000;
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
// eslint-disable-next-line no-await-in-loop
|
|
36
|
+
await new Promise((resolve) => {
|
|
37
|
+
setTimeout(resolve, pollInterval);
|
|
38
|
+
});
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.error('[pollForToken] Polling for device authorization...');
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line no-await-in-loop
|
|
43
|
+
const response = await fetch(`${WORKOS_BASE_URL}/user_management/authenticate`, {
|
|
44
|
+
body: new URLSearchParams({
|
|
45
|
+
client_id: getClientId(),
|
|
46
|
+
device_code: deviceCode,
|
|
47
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
48
|
+
}),
|
|
49
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
50
|
+
method: 'POST',
|
|
51
|
+
});
|
|
52
|
+
if (response.ok) {
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line no-await-in-loop
|
|
56
|
+
const error = await response.json().catch(() => ({}));
|
|
57
|
+
const errorCode = error.error || error.code;
|
|
58
|
+
if (errorCode === 'authorization_pending') {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (errorCode === 'slow_down') {
|
|
62
|
+
pollInterval += 1000;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (errorCode === 'access_denied') {
|
|
66
|
+
throw new Error('Authorization was denied by the user.');
|
|
67
|
+
}
|
|
68
|
+
if (errorCode === 'expired_token') {
|
|
69
|
+
throw new Error('Device code expired. Please try logging in again.');
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Authentication failed: ${JSON.stringify(error)}`);
|
|
72
|
+
}
|
|
73
|
+
throw new Error('Device code expired. Please try logging in again.');
|
|
74
|
+
};
|
|
75
|
+
// --- Token Refresh ---
|
|
76
|
+
export const refreshAccessToken = async (refreshToken) => {
|
|
77
|
+
const response = await fetch(`${WORKOS_BASE_URL}/user_management/authenticate`, {
|
|
78
|
+
body: new URLSearchParams({
|
|
79
|
+
client_id: getClientId(),
|
|
80
|
+
grant_type: 'refresh_token',
|
|
81
|
+
refresh_token: refreshToken,
|
|
82
|
+
}),
|
|
83
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
84
|
+
method: 'POST',
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errorText = await response.text();
|
|
88
|
+
throw new Error(`Failed to refresh token: ${errorText}`);
|
|
89
|
+
}
|
|
90
|
+
return response.json();
|
|
91
|
+
};
|
|
92
|
+
// --- JWT Utilities ---
|
|
93
|
+
export const decodeJWT = (token) => {
|
|
94
|
+
const parts = token.split('.');
|
|
95
|
+
if (parts.length !== 3) {
|
|
96
|
+
throw new Error('Invalid JWT format');
|
|
97
|
+
}
|
|
98
|
+
return JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
|
|
99
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const secretFlags: (secretDescription: string) => {
|
|
2
|
+
secret: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
3
|
+
'secret-key-name': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
4
|
+
};
|
|
5
|
+
export type Secret = {
|
|
6
|
+
keyName: string;
|
|
7
|
+
selected: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const parsedSecretFlags: (flags: {
|
|
10
|
+
secret: boolean;
|
|
11
|
+
"secret-key-name": string;
|
|
12
|
+
}) => Secret;
|
|
13
|
+
export default secretFlags;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
const secretFlags = (secretDescription) => ({
|
|
3
|
+
secret: Flags.boolean({ default: false, description: secretDescription }),
|
|
4
|
+
'secret-key-name': Flags.string({
|
|
5
|
+
default: 'quonfig.secrets.encryption.key',
|
|
6
|
+
description: 'name of the secret key to use for encryption/decryption',
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
export const parsedSecretFlags = (flags) => ({
|
|
10
|
+
keyName: flags['secret-key-name'],
|
|
11
|
+
selected: flags.secret,
|
|
12
|
+
});
|
|
13
|
+
export default secretFlags;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface TokenStorageOptions {
|
|
2
|
+
quonfigDir?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface TokenData {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
userEmail?: string;
|
|
9
|
+
userId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthConfig {
|
|
12
|
+
defaultProfile?: string;
|
|
13
|
+
profiles: {
|
|
14
|
+
[profileName: string]: {
|
|
15
|
+
workspace: string;
|
|
16
|
+
workspaceName?: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare const saveTokens: (tokens: TokenData, options?: TokenStorageOptions) => Promise<void>;
|
|
21
|
+
export declare const loadTokens: (options?: TokenStorageOptions) => Promise<TokenData | null>;
|
|
22
|
+
export declare const saveAuthConfig: (config: AuthConfig, options?: TokenStorageOptions) => Promise<void>;
|
|
23
|
+
export declare const loadAuthConfig: (options?: TokenStorageOptions) => Promise<AuthConfig | null>;
|
|
24
|
+
export declare const getActiveProfile: (profileArg?: string) => string;
|
|
25
|
+
export declare const clearAuth: (options?: TokenStorageOptions) => Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const getQuonfigDir = (options) => options?.quonfigDir || path.join(os.homedir(), '.quonfig');
|
|
5
|
+
const getTokenFile = (options) => path.join(getQuonfigDir(options), 'tokens.json');
|
|
6
|
+
const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
|
|
7
|
+
const ensureQuonfigDir = async (options) => {
|
|
8
|
+
const quonfigDir = getQuonfigDir(options);
|
|
9
|
+
try {
|
|
10
|
+
await fs.promises.access(quonfigDir, fs.constants.F_OK);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
await fs.promises.mkdir(quonfigDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const saveTokens = async (tokens, options) => {
|
|
17
|
+
await ensureQuonfigDir(options);
|
|
18
|
+
await fs.promises.writeFile(getTokenFile(options), JSON.stringify(tokens, null, 2), 'utf8');
|
|
19
|
+
};
|
|
20
|
+
export const loadTokens = async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
const data = await fs.promises.readFile(getTokenFile(options), 'utf8');
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const saveAuthConfig = async (config, options) => {
|
|
30
|
+
await ensureQuonfigDir(options);
|
|
31
|
+
let configContent = '';
|
|
32
|
+
// Write default profile if specified
|
|
33
|
+
if (config.defaultProfile) {
|
|
34
|
+
configContent += `default_profile = ${config.defaultProfile}\n\n`;
|
|
35
|
+
}
|
|
36
|
+
// Write each profile
|
|
37
|
+
for (const [profileName, profileData] of Object.entries(config.profiles)) {
|
|
38
|
+
configContent += `[profile ${profileName}]\n`;
|
|
39
|
+
configContent += `workspace = ${profileData.workspace}`;
|
|
40
|
+
if (profileData.workspaceName) {
|
|
41
|
+
configContent += ` # ${profileData.workspaceName}`;
|
|
42
|
+
}
|
|
43
|
+
configContent += '\n\n';
|
|
44
|
+
}
|
|
45
|
+
await fs.promises.writeFile(getConfigFile(options), configContent, 'utf8');
|
|
46
|
+
};
|
|
47
|
+
export const loadAuthConfig = async (options) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = await fs.promises.readFile(getConfigFile(options), 'utf8');
|
|
50
|
+
const config = {
|
|
51
|
+
profiles: {},
|
|
52
|
+
};
|
|
53
|
+
// Parse default profile
|
|
54
|
+
const defaultMatch = data.match(/default_profile\s*=\s*(.+)/);
|
|
55
|
+
if (defaultMatch && defaultMatch[1]) {
|
|
56
|
+
config.defaultProfile = defaultMatch[1].trim();
|
|
57
|
+
}
|
|
58
|
+
// Parse profiles
|
|
59
|
+
const profileRegex = /\[profile\s+(\w+)]\s*\n\s*workspace\s*=\s*([^\s#]+)(?:\s*#\s*(.+))?/g;
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = profileRegex.exec(data)) !== null) {
|
|
62
|
+
const profileName = match[1];
|
|
63
|
+
const workspace = match[2];
|
|
64
|
+
const workspaceName = match[3]?.trim();
|
|
65
|
+
config.profiles[profileName] = {
|
|
66
|
+
workspace,
|
|
67
|
+
workspaceName,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(config.profiles).length > 0 ? config : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export const getActiveProfile = (profileArg) => profileArg || process.env.QUONFIG_PROFILE || 'default';
|
|
77
|
+
export const clearAuth = async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
await fs.promises.unlink(getTokenFile(options));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Ignore if file doesn't exist
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await fs.promises.unlink(getConfigFile(options));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Ignore if file doesn't exist
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { failure, success } from '../result.js';
|
|
2
|
+
const validateValue = (quonfig, key, value) => {
|
|
3
|
+
const config = quonfig.rawConfig(key);
|
|
4
|
+
if (!config) {
|
|
5
|
+
return failure(`Could not find config named ${key}`);
|
|
6
|
+
}
|
|
7
|
+
// In the new quonfig format, allowableValues are not yet supported at the config level.
|
|
8
|
+
// This validation is a placeholder — the server enforces schema constraints.
|
|
9
|
+
return success(value);
|
|
10
|
+
};
|
|
11
|
+
export default validateValue;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone workspace validator.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* qfg-verify <workspace-dir> Validate a workspace directory on disk
|
|
7
|
+
* qfg-verify --git-hook Run as git pre-receive hook (reads stdin for refs,
|
|
8
|
+
* uses git commands to read files from the pushed commit)
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 All checks pass
|
|
12
|
+
* 1 Validation errors found
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone workspace validator.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* qfg-verify <workspace-dir> Validate a workspace directory on disk
|
|
7
|
+
* qfg-verify --git-hook Run as git pre-receive hook (reads stdin for refs,
|
|
8
|
+
* uses git commands to read files from the pushed commit)
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 All checks pass
|
|
12
|
+
* 1 Validation errors found
|
|
13
|
+
*/
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import * as readline from "node:readline";
|
|
16
|
+
import { formatResult, validateFileMap, validateWorkspace } from "./validate.js";
|
|
17
|
+
async function main() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
if (args.includes("--git-hook")) {
|
|
20
|
+
await runGitHook();
|
|
21
|
+
}
|
|
22
|
+
else if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
printUsage();
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const dir = args[0] || ".";
|
|
27
|
+
runDiskValidation(dir);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function printUsage() {
|
|
31
|
+
console.log(`qfg-verify - Quonfig workspace validator
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
qfg-verify [path] Validate workspace directory (default: .)
|
|
35
|
+
qfg-verify --git-hook Run as git pre-receive hook
|
|
36
|
+
qfg-verify --help Show this help
|
|
37
|
+
|
|
38
|
+
Exit codes:
|
|
39
|
+
0 All checks pass
|
|
40
|
+
1 Validation errors found`);
|
|
41
|
+
}
|
|
42
|
+
function runDiskValidation(dir) {
|
|
43
|
+
const result = validateWorkspace(dir);
|
|
44
|
+
console.log(formatResult(result));
|
|
45
|
+
process.exit(result.valid ? 0 : 1);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Git pre-receive hook mode.
|
|
49
|
+
*
|
|
50
|
+
* Reads stdin for pushed refs (one line per ref: <old-oid> <new-oid> <ref-name>).
|
|
51
|
+
* For each new commit, lists all config files and validates them.
|
|
52
|
+
*/
|
|
53
|
+
async function runGitHook() {
|
|
54
|
+
const refs = [];
|
|
55
|
+
// Read all of stdin (ref lines). Use callback API for bun compatibility.
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
58
|
+
rl.on("line", (line) => {
|
|
59
|
+
const parts = line.trim().split(" ");
|
|
60
|
+
if (parts.length >= 3) {
|
|
61
|
+
refs.push({ oldOid: parts[0], newOid: parts[1], refName: parts[2] });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
rl.on("close", resolve);
|
|
65
|
+
});
|
|
66
|
+
if (refs.length === 0) {
|
|
67
|
+
console.log("qfg-verify: no refs received");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
// Validate the latest pushed commit for each ref
|
|
71
|
+
let hasErrors = false;
|
|
72
|
+
for (const ref of refs) {
|
|
73
|
+
// Skip deletions
|
|
74
|
+
if (ref.newOid === "0000000000000000000000000000000000000000")
|
|
75
|
+
continue;
|
|
76
|
+
console.log(`qfg-verify: validating ${ref.refName} (${ref.newOid.slice(0, 8)})`);
|
|
77
|
+
try {
|
|
78
|
+
const files = readFilesFromCommit(ref.newOid);
|
|
79
|
+
const result = validateFileMap(files);
|
|
80
|
+
console.log(formatResult(result));
|
|
81
|
+
if (!result.valid) {
|
|
82
|
+
hasErrors = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(`qfg-verify: error reading commit ${ref.newOid}: ${err.message}`);
|
|
87
|
+
hasErrors = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.exit(hasErrors ? 1 : 0);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read all JSON config files from a git commit using `git show`.
|
|
94
|
+
* Works in bare repos (no working tree needed).
|
|
95
|
+
*/
|
|
96
|
+
function readFilesFromCommit(commitOid) {
|
|
97
|
+
const files = new Map();
|
|
98
|
+
const dirs = ["configs", "feature-flags", "segments", "log-levels", "schemas", "schemas-protected"];
|
|
99
|
+
for (const dir of dirs) {
|
|
100
|
+
// List files in this directory at the given commit
|
|
101
|
+
let listing;
|
|
102
|
+
try {
|
|
103
|
+
listing = execSync(`git ls-tree --name-only ${commitOid} ${dir}/`, {
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Directory doesn't exist in this commit
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const filenames = listing.trim().split("\n").filter(Boolean);
|
|
113
|
+
for (const filePath of filenames) {
|
|
114
|
+
if (!filePath.endsWith(".json") || filePath.includes("/."))
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const content = execSync(`git show ${commitOid}:${filePath}`, {
|
|
118
|
+
encoding: "utf-8",
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
120
|
+
});
|
|
121
|
+
files.set(filePath, content);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// File can't be read, skip
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return files;
|
|
129
|
+
}
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
console.error(`qfg-verify: fatal: ${err.message}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace validation logic.
|
|
3
|
+
*
|
|
4
|
+
* This module validates a Quonfig workspace directory against the StoredConfig
|
|
5
|
+
* schema. It can be used from both the oclif CLI (`qfg verify`) and the
|
|
6
|
+
* standalone compiled binary (git pre-receive hook).
|
|
7
|
+
*
|
|
8
|
+
* Validation checks:
|
|
9
|
+
* - Valid JSON
|
|
10
|
+
* - Schema compliance (StoredConfigSchema from Zod)
|
|
11
|
+
* - Key matches filename
|
|
12
|
+
* - Key constraints (1-512 chars, no slashes, not "new")
|
|
13
|
+
* - Config type matches directory
|
|
14
|
+
* - Segment constraints (valueType=bool, sendToClientSdk=false)
|
|
15
|
+
* - Log level constraints (valueType=log_level)
|
|
16
|
+
* - Referential integrity (IN_SEG/NOT_IN_SEG reference existing segments)
|
|
17
|
+
* - schemaKey references existing schemas
|
|
18
|
+
* - Rule structure (criteria + value present)
|
|
19
|
+
* - Value type consistency
|
|
20
|
+
* - Weighted values non-empty and consistent
|
|
21
|
+
*/
|
|
22
|
+
export type Severity = "error" | "warning";
|
|
23
|
+
export interface ValidationIssue {
|
|
24
|
+
file: string;
|
|
25
|
+
message: string;
|
|
26
|
+
severity: Severity;
|
|
27
|
+
suggestion?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ValidationResult {
|
|
30
|
+
issues: ValidationIssue[];
|
|
31
|
+
filesChecked: number;
|
|
32
|
+
valid: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate an entire workspace directory on disk.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateWorkspace(workspaceDir: string): ValidationResult;
|
|
38
|
+
/**
|
|
39
|
+
* Validate configs provided as a map of { "dir/file.json": jsonString }.
|
|
40
|
+
* Used by the pre-receive hook which reads files from git objects.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateFileMap(files: Map<string, string>): ValidationResult;
|
|
43
|
+
export declare function formatResult(result: ValidationResult): string;
|