@micropage-sh/cli 2.6.1 → 2.6.2
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/README.md +2 -0
- package/package.json +1 -1
- package/src/commands/projects.js +1 -0
- package/src/plan.js +26 -2
- package/src/supabase.js +22 -0
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ micropage whoami # show the currently logged-in user and subscription
|
|
|
32
32
|
micropage logout # clear the stored session
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
The CLI requires a paid plan (Pro or Pro+). Some features need a specific tier, such as deploy tokens (Pro+). The server enforces these limits. When a command needs a higher plan, the CLI prints the server's reason and an upgrade link.
|
|
36
|
+
|
|
35
37
|
## Project workflow
|
|
36
38
|
|
|
37
39
|
```bash
|
package/package.json
CHANGED
package/src/commands/projects.js
CHANGED
package/src/plan.js
CHANGED
|
@@ -2,13 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
|
|
4
4
|
|
|
5
|
+
const PRICING_URL = 'https://micropage.sh/pricing';
|
|
6
|
+
|
|
5
7
|
const UPGRADE_MESSAGE = [
|
|
6
8
|
'The micropage CLI is available on paid plans only.',
|
|
7
9
|
'Your account is currently on the free plan.',
|
|
8
10
|
'',
|
|
9
|
-
|
|
11
|
+
`Upgrade at: ${PRICING_URL}`,
|
|
10
12
|
].join('\n');
|
|
11
13
|
|
|
14
|
+
const TIER_LABELS = { pro: 'Pro', pro_plus: 'Pro+' };
|
|
15
|
+
|
|
16
|
+
// Message for a server refusal tagged PLAN_REQUIRED (see attachPlanRequired in
|
|
17
|
+
// supabase.js). Leads with the server's own explanation, then the upgrade hint.
|
|
18
|
+
function formatPlanRequiredMessage(err) {
|
|
19
|
+
const label = TIER_LABELS[err?.requiredTier];
|
|
20
|
+
const lines = [];
|
|
21
|
+
// Server messages written for the editor may end in a relative "Upgrade at /pricing.";
|
|
22
|
+
// drop it so the absolute link below is the only one.
|
|
23
|
+
const reason = (err?.message || '').replace(/\s*Upgrade at \S+$/i, '').trim();
|
|
24
|
+
if (reason) lines.push(reason, '');
|
|
25
|
+
lines.push(`This requires ${label ? `the ${label} plan` : 'a paid plan'}.`);
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(`Upgrade at: ${err?.upgradeUrl || PRICING_URL}`);
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The login gate below (and in whoami) is a product gate, not a security
|
|
32
|
+
// boundary: anyone can call the Supabase API directly with an editor session.
|
|
33
|
+
// The server enforces the per-feature gates (project limits via RLS, storage
|
|
34
|
+
// quota, custom domains, archives, posts, deploy-token creation and exchange)
|
|
35
|
+
// and answers with `code: "plan_required"` when a tier is missing.
|
|
12
36
|
function isPaidTier(tier) {
|
|
13
37
|
return tier === 'pro' || tier === 'pro_plus';
|
|
14
38
|
}
|
|
@@ -37,4 +61,4 @@ async function getPlanTierWithToken(userId, accessToken) {
|
|
|
37
61
|
}
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken };
|
|
64
|
+
module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken, formatPlanRequiredMessage };
|
package/src/supabase.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const { getSession, setSession, clearSession } = require('./auth');
|
|
15
15
|
const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
|
|
16
|
+
const { formatPlanRequiredMessage } = require('./plan');
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// JWT helpers
|
|
@@ -97,6 +98,18 @@ async function getValidAccessToken() {
|
|
|
97
98
|
// Base request
|
|
98
99
|
// ---------------------------------------------------------------------------
|
|
99
100
|
|
|
101
|
+
// Server tier refusals carry `code: "plan_required"` in the JSON body, reusing
|
|
102
|
+
// 402/403. Tag the error so callers can tell "upgrade needed" apart from
|
|
103
|
+
// "not yours" / "invalid token", which share those statuses.
|
|
104
|
+
function attachPlanRequired(err, data) {
|
|
105
|
+
if (data && typeof data === 'object' && data.code === 'plan_required') {
|
|
106
|
+
err.code = 'PLAN_REQUIRED';
|
|
107
|
+
err.requiredTier = data.required_tier || null;
|
|
108
|
+
err.upgradeUrl = data.upgrade_url || null;
|
|
109
|
+
}
|
|
110
|
+
return err;
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
async function requestWithToken(method, url, body, accessToken, extraHeaders = {}) {
|
|
101
114
|
const headers = {
|
|
102
115
|
'apikey': SUPABASE_ANON_KEY,
|
|
@@ -123,6 +136,7 @@ async function requestWithToken(method, url, body, accessToken, extraHeaders = {
|
|
|
123
136
|
const err = new Error(msg);
|
|
124
137
|
err.status = res.status;
|
|
125
138
|
err.data = data;
|
|
139
|
+
attachPlanRequired(err, data);
|
|
126
140
|
throw err;
|
|
127
141
|
}
|
|
128
142
|
|
|
@@ -172,6 +186,7 @@ async function request(method, url, body, extraHeaders = {}) {
|
|
|
172
186
|
err.status = res.status;
|
|
173
187
|
err.data = data;
|
|
174
188
|
if (res.status === 401) err.code = 'SESSION_EXPIRED';
|
|
189
|
+
attachPlanRequired(err, data);
|
|
175
190
|
throw err;
|
|
176
191
|
}
|
|
177
192
|
|
|
@@ -288,6 +303,10 @@ function handleAuthError(err) {
|
|
|
288
303
|
console.error(err.message);
|
|
289
304
|
process.exit(1);
|
|
290
305
|
}
|
|
306
|
+
if (err.code === 'PLAN_REQUIRED') {
|
|
307
|
+
console.error(formatPlanRequiredMessage(err));
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
291
310
|
}
|
|
292
311
|
|
|
293
312
|
// ---------------------------------------------------------------------------
|
|
@@ -670,6 +689,7 @@ async function exchangeDeployTokenForAccessToken(deployTokenPlaintext, projectUu
|
|
|
670
689
|
);
|
|
671
690
|
err.status = res.status;
|
|
672
691
|
err.data = data;
|
|
692
|
+
attachPlanRequired(err, data);
|
|
673
693
|
throw err;
|
|
674
694
|
}
|
|
675
695
|
if (!data?.access_token) {
|
|
@@ -709,6 +729,7 @@ async function invokePublishBuild(accessToken, projectId, buildId) {
|
|
|
709
729
|
);
|
|
710
730
|
err.status = res.status;
|
|
711
731
|
err.data = data;
|
|
732
|
+
attachPlanRequired(err, data);
|
|
712
733
|
throw err;
|
|
713
734
|
}
|
|
714
735
|
return data;
|
|
@@ -719,6 +740,7 @@ module.exports = {
|
|
|
719
740
|
fn,
|
|
720
741
|
getUserInfo,
|
|
721
742
|
handleAuthError,
|
|
743
|
+
attachPlanRequired,
|
|
722
744
|
getValidAccessToken,
|
|
723
745
|
getMaxDeployEventIdForBuild,
|
|
724
746
|
getMaxDeployEventIdForProject,
|