clishop 0.2.0 → 0.3.0

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/SECURITY.md ADDED
@@ -0,0 +1,25 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security issue in CLISHOP CLI, please report it privately:
6
+
7
+ - Open a private security advisory on GitHub (preferred), or
8
+ - Contact the maintainers via repository security contact.
9
+
10
+ Please include:
11
+ - A clear description of the issue
12
+ - Reproduction steps / proof of concept
13
+ - Potential impact
14
+ - Suggested remediation (if available)
15
+
16
+ ## Supported Versions
17
+
18
+ Until a stable `1.0.0` release is published, only the latest release line is considered supported for security fixes.
19
+
20
+ ## Security Expectations
21
+
22
+ - Never commit secrets or tokens.
23
+ - Use OS keychain-backed credentials where available.
24
+ - Prefer secure non-interactive auth methods over plaintext CLI arguments.
25
+ - Keep dependencies up to date and monitor advisories.
@@ -0,0 +1,192 @@
1
+ import {
2
+ getApiBaseUrl
3
+ } from "./chunk-ZF3JIQRK.js";
4
+
5
+ // src/auth.ts
6
+ import keytar from "keytar";
7
+ import axios from "axios";
8
+ var SERVICE_NAME = "clishop";
9
+ var ACCOUNT_TOKEN = "auth-token";
10
+ var ACCOUNT_REFRESH = "refresh-token";
11
+ var ACCOUNT_USER = "user-info";
12
+ function assertAuthResponse(data) {
13
+ const payload = data;
14
+ if (!payload || typeof payload !== "object") {
15
+ throw new Error("Invalid auth response from server.");
16
+ }
17
+ if (!payload.token || typeof payload.token !== "string") {
18
+ throw new Error("Auth response missing access token.");
19
+ }
20
+ if (payload.refreshToken !== void 0 && typeof payload.refreshToken !== "string") {
21
+ throw new Error("Auth response has an invalid refresh token.");
22
+ }
23
+ if (!payload.user || typeof payload.user !== "object") {
24
+ throw new Error("Auth response missing user profile.");
25
+ }
26
+ if (!payload.user.id || !payload.user.email || !payload.user.name) {
27
+ throw new Error("Auth response user profile is incomplete.");
28
+ }
29
+ return payload;
30
+ }
31
+ async function storeToken(token) {
32
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_TOKEN, token);
33
+ }
34
+ async function storeRefreshToken(token) {
35
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_REFRESH, token);
36
+ }
37
+ async function storeUserInfo(user) {
38
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_USER, JSON.stringify(user));
39
+ }
40
+ async function getToken() {
41
+ return keytar.getPassword(SERVICE_NAME, ACCOUNT_TOKEN);
42
+ }
43
+ async function getRefreshToken() {
44
+ return keytar.getPassword(SERVICE_NAME, ACCOUNT_REFRESH);
45
+ }
46
+ async function getUserInfo() {
47
+ const raw = await keytar.getPassword(SERVICE_NAME, ACCOUNT_USER);
48
+ if (!raw) return null;
49
+ try {
50
+ return JSON.parse(raw);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function clearAuth() {
56
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_TOKEN);
57
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_REFRESH);
58
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_USER);
59
+ }
60
+ async function isLoggedIn() {
61
+ const token = await getToken();
62
+ return !!token;
63
+ }
64
+ async function login(email, password) {
65
+ const baseUrl = getApiBaseUrl();
66
+ const res = await axios.post(`${baseUrl}/auth/login`, { email, password });
67
+ const { token, refreshToken, user } = assertAuthResponse(res.data);
68
+ await storeToken(token);
69
+ if (refreshToken) await storeRefreshToken(refreshToken);
70
+ await storeUserInfo(user);
71
+ return user;
72
+ }
73
+ async function register(email, password, name, monthlySpendingLimitInCents) {
74
+ const baseUrl = getApiBaseUrl();
75
+ const body = { email, password, name };
76
+ if (monthlySpendingLimitInCents !== void 0) {
77
+ body.monthlySpendingLimitInCents = monthlySpendingLimitInCents;
78
+ }
79
+ const res = await axios.post(`${baseUrl}/auth/register`, body);
80
+ const { token, refreshToken, user } = assertAuthResponse(res.data);
81
+ await storeToken(token);
82
+ if (refreshToken) await storeRefreshToken(refreshToken);
83
+ await storeUserInfo(user);
84
+ return user;
85
+ }
86
+ async function logout() {
87
+ const baseUrl = getApiBaseUrl();
88
+ const token = await getToken();
89
+ if (token) {
90
+ try {
91
+ await axios.post(`${baseUrl}/auth/logout`, {}, {
92
+ headers: { Authorization: `Bearer ${token}` }
93
+ });
94
+ } catch {
95
+ }
96
+ }
97
+ await clearAuth();
98
+ }
99
+
100
+ // src/api.ts
101
+ import axios2 from "axios";
102
+ import chalk from "chalk";
103
+ var client = null;
104
+ function assertRefreshResponse(data) {
105
+ const payload = data;
106
+ if (!payload || typeof payload !== "object" || typeof payload.token !== "string") {
107
+ throw new Error("Invalid refresh response from server.");
108
+ }
109
+ return { token: payload.token };
110
+ }
111
+ function getApiClient() {
112
+ if (client) return client;
113
+ const baseUrl = getApiBaseUrl();
114
+ if (!baseUrl.startsWith("https://")) {
115
+ console.warn(chalk.yellow(`
116
+ \u26A0 Using a non-HTTPS API URL: ${baseUrl}
117
+ `));
118
+ }
119
+ client = axios2.create({
120
+ baseURL: baseUrl,
121
+ timeout: 3e4,
122
+ headers: {
123
+ "Content-Type": "application/json"
124
+ }
125
+ });
126
+ client.interceptors.request.use(async (reqConfig) => {
127
+ const token = await getToken();
128
+ if (token) {
129
+ reqConfig.headers.Authorization = `Bearer ${token}`;
130
+ }
131
+ return reqConfig;
132
+ });
133
+ client.interceptors.response.use(
134
+ (res) => res,
135
+ async (error) => {
136
+ if (error.response?.status === 401) {
137
+ const refreshToken = await getRefreshToken();
138
+ if (refreshToken) {
139
+ try {
140
+ const res = await axios2.post(`${baseUrl}/auth/refresh`, { refreshToken });
141
+ const { token } = assertRefreshResponse(res.data);
142
+ await storeToken(token);
143
+ if (error.config) {
144
+ error.config.headers.Authorization = `Bearer ${token}`;
145
+ return axios2(error.config);
146
+ }
147
+ } catch {
148
+ }
149
+ }
150
+ console.error(chalk.red("\n\u2717 Session expired. Please login again: clishop login\n"));
151
+ process.exit(1);
152
+ }
153
+ throw error;
154
+ }
155
+ );
156
+ return client;
157
+ }
158
+ function handleApiError(error) {
159
+ if (axios2.isAxiosError(error)) {
160
+ const data = error.response?.data;
161
+ const message = data?.message || data?.error || error.message;
162
+ const status = error.response?.status;
163
+ if (status === 422 && data?.errors) {
164
+ console.error(chalk.red("\n\u2717 Validation errors:"));
165
+ for (const [field, msgs] of Object.entries(data.errors)) {
166
+ console.error(chalk.red(` ${field}: ${msgs.join(", ")}`));
167
+ }
168
+ } else if (status === 404) {
169
+ console.error(chalk.red(`
170
+ \u2717 Not found: ${message}`));
171
+ } else {
172
+ console.error(chalk.red(`
173
+ \u2717 API error (${status || "network"}): ${message}`));
174
+ }
175
+ } else if (error instanceof Error) {
176
+ console.error(chalk.red(`
177
+ \u2717 ${error.message}`));
178
+ } else {
179
+ console.error(chalk.red("\n\u2717 An unexpected error occurred."));
180
+ }
181
+ process.exit(1);
182
+ }
183
+
184
+ export {
185
+ getUserInfo,
186
+ isLoggedIn,
187
+ login,
188
+ register,
189
+ logout,
190
+ getApiClient,
191
+ handleApiError
192
+ };
@@ -0,0 +1,108 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/config.ts
8
+ import Conf from "conf";
9
+ var DEFAULT_AGENT = {
10
+ name: "default",
11
+ requireConfirmation: true,
12
+ maxOrderAmount: 500,
13
+ allowedCategories: [],
14
+ blockedCategories: []
15
+ };
16
+ var DEFAULT_API_BASE_URL = "https://clishop-backend.vercel.app/api";
17
+ var config = new Conf({
18
+ projectName: "clishop",
19
+ defaults: {
20
+ activeAgent: "default",
21
+ agents: {
22
+ default: DEFAULT_AGENT
23
+ },
24
+ apiBaseUrl: DEFAULT_API_BASE_URL,
25
+ outputFormat: "human",
26
+ setupCompleted: false
27
+ }
28
+ });
29
+ function getConfig() {
30
+ return config;
31
+ }
32
+ function getApiBaseUrl() {
33
+ const envOverride = process.env.CLISHOP_API_URL?.trim();
34
+ if (envOverride) return envOverride;
35
+ return config.get("apiBaseUrl") || DEFAULT_API_BASE_URL;
36
+ }
37
+ function getActiveAgent() {
38
+ const cfg = config.store;
39
+ const override = process.env.__CLISHOP_AGENT_OVERRIDE;
40
+ if (override && cfg.agents[override]) {
41
+ return cfg.agents[override];
42
+ }
43
+ return cfg.agents[cfg.activeAgent] || cfg.agents["default"];
44
+ }
45
+ function getAgent(name) {
46
+ return config.store.agents[name];
47
+ }
48
+ function setActiveAgent(name) {
49
+ if (!config.store.agents[name]) {
50
+ throw new Error(`Agent "${name}" does not exist. Create it first with: clishop agent create ${name}`);
51
+ }
52
+ config.set("activeAgent", name);
53
+ }
54
+ function createAgent(name, opts = {}) {
55
+ if (config.store.agents[name]) {
56
+ throw new Error(`Agent "${name}" already exists.`);
57
+ }
58
+ const agent = {
59
+ name,
60
+ requireConfirmation: true,
61
+ maxOrderAmount: 500,
62
+ allowedCategories: [],
63
+ blockedCategories: [],
64
+ ...opts
65
+ };
66
+ config.set(`agents.${name}`, agent);
67
+ return agent;
68
+ }
69
+ function updateAgent(name, opts) {
70
+ const existing = config.store.agents[name];
71
+ if (!existing) {
72
+ throw new Error(`Agent "${name}" does not exist.`);
73
+ }
74
+ const updated = { ...existing, ...opts, name };
75
+ config.set(`agents.${name}`, updated);
76
+ return updated;
77
+ }
78
+ function deleteAgent(name) {
79
+ if (name === "default") {
80
+ throw new Error('Cannot delete the "default" agent.');
81
+ }
82
+ if (!config.store.agents[name]) {
83
+ throw new Error(`Agent "${name}" does not exist.`);
84
+ }
85
+ const agents = { ...config.store.agents };
86
+ delete agents[name];
87
+ config.set("agents", agents);
88
+ if (config.store.activeAgent === name) {
89
+ config.set("activeAgent", "default");
90
+ }
91
+ }
92
+ function listAgents() {
93
+ return Object.values(config.store.agents);
94
+ }
95
+
96
+ export {
97
+ __export,
98
+ DEFAULT_API_BASE_URL,
99
+ getConfig,
100
+ getApiBaseUrl,
101
+ getActiveAgent,
102
+ getAgent,
103
+ setActiveAgent,
104
+ createAgent,
105
+ updateAgent,
106
+ deleteAgent,
107
+ listAgents
108
+ };
@@ -0,0 +1,24 @@
1
+ import {
2
+ DEFAULT_API_BASE_URL,
3
+ createAgent,
4
+ deleteAgent,
5
+ getActiveAgent,
6
+ getAgent,
7
+ getApiBaseUrl,
8
+ getConfig,
9
+ listAgents,
10
+ setActiveAgent,
11
+ updateAgent
12
+ } from "./chunk-ZF3JIQRK.js";
13
+ export {
14
+ DEFAULT_API_BASE_URL,
15
+ createAgent,
16
+ deleteAgent,
17
+ getActiveAgent,
18
+ getAgent,
19
+ getApiBaseUrl,
20
+ getConfig,
21
+ listAgents,
22
+ setActiveAgent,
23
+ updateAgent
24
+ };