@optima-chat/dev-skills 0.7.33 → 0.7.36
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/AGENTS.md +3 -0
- package/bin/helpers/billing-http.ts +191 -0
- package/bin/helpers/confirm-prompt.ts +23 -0
- package/bin/helpers/entitlement/grant.ts +70 -0
- package/bin/helpers/entitlement/list.ts +77 -0
- package/bin/helpers/entitlement/revoke.ts +116 -0
- package/bin/helpers/entitlement.ts +32 -0
- package/bin/helpers/infisical-secrets.ts +41 -0
- package/bin/helpers/plugin/set-default.ts +65 -0
- package/bin/helpers/plugin/set-paid.ts +69 -0
- package/bin/helpers/plugin/show.ts +42 -0
- package/bin/helpers/plugin.ts +32 -0
- package/bin/helpers/product/add-channel.ts +78 -0
- package/bin/helpers/product/create.ts +96 -0
- package/bin/helpers/product/show.ts +43 -0
- package/bin/helpers/product/toggle-channel.ts +56 -0
- package/bin/helpers/product/update.ts +72 -0
- package/bin/helpers/product.ts +46 -0
- package/dist/bin/helpers/billing-http.js +153 -0
- package/dist/bin/helpers/confirm-prompt.js +54 -0
- package/dist/bin/helpers/entitlement/grant.js +73 -0
- package/dist/bin/helpers/entitlement/list.js +62 -0
- package/dist/bin/helpers/entitlement/revoke.js +105 -0
- package/dist/bin/helpers/entitlement.js +39 -0
- package/dist/bin/helpers/infisical-secrets.js +33 -0
- package/dist/bin/helpers/plugin/set-default.js +61 -0
- package/dist/bin/helpers/plugin/set-paid.js +65 -0
- package/dist/bin/helpers/plugin/show.js +45 -0
- package/dist/bin/helpers/plugin.js +39 -0
- package/dist/bin/helpers/product/add-channel.js +95 -0
- package/dist/bin/helpers/product/create.js +124 -0
- package/dist/bin/helpers/product/show.js +46 -0
- package/dist/bin/helpers/product/toggle-channel.js +61 -0
- package/dist/bin/helpers/product/update.js +92 -0
- package/dist/bin/helpers/product.js +53 -0
- package/docs/superpowers/plans/2026-05-24-marketplace-admin-cli-impl.md +1973 -0
- package/docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md +700 -0
- package/docs/superpowers/specs/2026-05-24-marketplace-admin-cli-design.md +324 -0
- package/docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md +156 -0
- package/package.json +8 -5
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateEnv = validateEnv;
|
|
4
|
+
exports.getServiceToken = getServiceToken;
|
|
5
|
+
exports.callBilling = callBilling;
|
|
6
|
+
exports.callSkills = callSkills;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const infisical_secrets_1 = require("./infisical-secrets");
|
|
9
|
+
const db_utils_1 = require("./db-utils");
|
|
10
|
+
const USER_AUTH_URLS = {
|
|
11
|
+
stage: 'https://auth.stage.optima.onl',
|
|
12
|
+
prod: 'https://auth.optima.onl',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validate the --env flag value at command entry, before any I/O.
|
|
16
|
+
*
|
|
17
|
+
* Without this, a typo like `--env staging` flows downstream and surfaces as
|
|
18
|
+
* a confusing error: entitlement subcommands hit resolveUserId first (SSH
|
|
19
|
+
* tunnel to RDS_HOSTS[undefined] → cryptic ssh failure), product subcommands
|
|
20
|
+
* reach getServiceToken (USER_AUTH_URLS[undefined] → "Unknown env"). All
|
|
21
|
+
* fail-closed (no wrong-env write), but the UX diverges per subcommand.
|
|
22
|
+
* Every runX handler calls this first so the error is uniform and immediate.
|
|
23
|
+
*/
|
|
24
|
+
function validateEnv(env) {
|
|
25
|
+
if (env !== 'stage' && env !== 'prod') {
|
|
26
|
+
throw new Error(`--env must be "stage" or "prod" (got: ${env})`);
|
|
27
|
+
}
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
// T1 discovered: client_id differs per env (stage=dev-skills-ubd3qz6n,
|
|
31
|
+
// prod=dev-skills-hinxa0rs). Both stored in Infisical alongside the
|
|
32
|
+
// secret at /shared-secrets/oauth-clients/.
|
|
33
|
+
const DEV_SKILLS_OAUTH_PATH = '/shared-secrets/oauth-clients';
|
|
34
|
+
const DEV_SKILLS_CLIENT_ID_KEY = 'DEV_SKILLS_OAUTH_CLIENT_ID';
|
|
35
|
+
const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
|
|
36
|
+
// ───── Cache (process-lifetime) ─────────────────────────────────────────────
|
|
37
|
+
// One CLI invocation does at most a handful of HTTP calls. We mint the M2M
|
|
38
|
+
// token once and reuse it. Cross-invocation re-mint is fine — JWT TTL is
|
|
39
|
+
// typically ≥1h, far longer than any single CLI run.
|
|
40
|
+
//
|
|
41
|
+
// NOT handled (acceptable for admin CLI):
|
|
42
|
+
// * Token expiry mid-invocation — a long-stalled revoke (list call + 5min
|
|
43
|
+
// pause + refund call) could in theory expire. Operator can retry.
|
|
44
|
+
// * Infisical 5xx retry — getInfisicalToken is sync execSync curl with no
|
|
45
|
+
// retry; any transient failure surfaces immediately. Re-run the CLI.
|
|
46
|
+
const tokenCache = {};
|
|
47
|
+
const billingUrlCache = {};
|
|
48
|
+
const skillsUrlCache = {};
|
|
49
|
+
function getBillingUrl(env) {
|
|
50
|
+
if (billingUrlCache[env])
|
|
51
|
+
return billingUrlCache[env];
|
|
52
|
+
const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'BILLING_URL');
|
|
53
|
+
billingUrlCache[env] = url;
|
|
54
|
+
return url;
|
|
55
|
+
}
|
|
56
|
+
function getSkillsUrl(env) {
|
|
57
|
+
if (skillsUrlCache[env])
|
|
58
|
+
return skillsUrlCache[env];
|
|
59
|
+
const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'SKILLS_REGISTRY_URL');
|
|
60
|
+
skillsUrlCache[env] = url;
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
function getServiceToken(env) {
|
|
64
|
+
if (tokenCache[env])
|
|
65
|
+
return tokenCache[env];
|
|
66
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
67
|
+
const tok = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
68
|
+
// Fetch BOTH client_id and client_secret from Infisical — they differ per env.
|
|
69
|
+
const clientId = (0, infisical_secrets_1.fetchInfisicalSecret)(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_ID_KEY, cfg, tok);
|
|
70
|
+
const clientSecret = (0, infisical_secrets_1.fetchInfisicalSecret)(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_SECRET_KEY, cfg, tok);
|
|
71
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
72
|
+
if (!authUrl)
|
|
73
|
+
throw new Error(`Unknown env: ${env}`);
|
|
74
|
+
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`;
|
|
75
|
+
const response = (0, child_process_1.execSync)(`curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`, { encoding: 'utf-8' });
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(response);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error(`user-auth token endpoint returned non-JSON (${env}): ${response.slice(0, 200)}`);
|
|
82
|
+
}
|
|
83
|
+
if (!parsed.access_token) {
|
|
84
|
+
throw new Error(`user-auth token mint failed (${env}): ${response.slice(0, 200)}`);
|
|
85
|
+
}
|
|
86
|
+
tokenCache[env] = parsed.access_token;
|
|
87
|
+
return parsed.access_token;
|
|
88
|
+
}
|
|
89
|
+
function formatServiceError(status, statusText, body) {
|
|
90
|
+
let parsed = null;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(body);
|
|
93
|
+
}
|
|
94
|
+
catch { /* non-JSON */ }
|
|
95
|
+
// Dominant Wave 1.5 envelope: flat { error: "CODE_STRING", message: "..." }
|
|
96
|
+
// emitted by billing's global error handler (app.ts:99-118) for ALL
|
|
97
|
+
// BillingError throws + validation errors + internal errors. Most inline
|
|
98
|
+
// route returns also use this shape (admin-products.ts:110,144,184-185,etc).
|
|
99
|
+
if (parsed && typeof parsed.error === 'string') {
|
|
100
|
+
return `❌ Error [${status}] ${parsed.error}: ${parsed.message ?? '(no message)'}`;
|
|
101
|
+
}
|
|
102
|
+
// Less-common nested envelope: { error: { code, message } } — used by a
|
|
103
|
+
// few inline 400/404 returns in admin-products.ts toggle-channel handler
|
|
104
|
+
// (lines 92-93, 100-101, 114-117). Possibly extends to other routes
|
|
105
|
+
// post-Wave-1.5 as standardization lands.
|
|
106
|
+
if (parsed && typeof parsed.error === 'object' && parsed.error !== null) {
|
|
107
|
+
const code = parsed.error.code ?? 'UNKNOWN';
|
|
108
|
+
const msg = parsed.error.message ?? '(no message)';
|
|
109
|
+
return `❌ Error [${status}] ${code}: ${msg}`;
|
|
110
|
+
}
|
|
111
|
+
// Non-envelope fallback (raw 502 from upstream LB, crashed handler before
|
|
112
|
+
// error middleware, plain-text body, etc.)
|
|
113
|
+
return `❌ Error [${status}] ${statusText}\n Response body (first 500 bytes): ${body.slice(0, 500)}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Authenticated call to an Optima service (billing or skills — same dev-skills
|
|
117
|
+
* M2M token works for both). Returns `{status, body}` on 2xx; throws Error with
|
|
118
|
+
* formatted message on non-2xx. Single retry on 5xx (no backoff — admin CLI).
|
|
119
|
+
*/
|
|
120
|
+
async function callService(baseUrl, env, method, path, body) {
|
|
121
|
+
const url = `${baseUrl}${path}`;
|
|
122
|
+
const token = getServiceToken(env);
|
|
123
|
+
const doFetch = async () => fetch(url, {
|
|
124
|
+
method,
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${token}`,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
130
|
+
});
|
|
131
|
+
let res = await doFetch();
|
|
132
|
+
if (res.status >= 500) {
|
|
133
|
+
res = await doFetch();
|
|
134
|
+
}
|
|
135
|
+
const text = await res.text();
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
138
|
+
}
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = text ? JSON.parse(text) : undefined;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error(`Service returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
145
|
+
}
|
|
146
|
+
return { status: res.status, body: parsed };
|
|
147
|
+
}
|
|
148
|
+
async function callBilling(env, method, path, body) {
|
|
149
|
+
return callService(getBillingUrl(env), env, method, path, body);
|
|
150
|
+
}
|
|
151
|
+
async function callSkills(env, method, path, body) {
|
|
152
|
+
return callService(getSkillsUrl(env), env, method, path, body);
|
|
153
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.confirmIfProd = confirmIfProd;
|
|
37
|
+
const readline = __importStar(require("readline"));
|
|
38
|
+
/**
|
|
39
|
+
* On prod, print the resolved action and require typing "yes" to proceed.
|
|
40
|
+
* No-op on stage or when --yes was passed. Exits 1 if user declines.
|
|
41
|
+
*/
|
|
42
|
+
async function confirmIfProd(env, actionDescription, skipFlag) {
|
|
43
|
+
if (env !== 'prod' || skipFlag)
|
|
44
|
+
return;
|
|
45
|
+
console.log(`\n⚠️ About to perform on PROD:\n${actionDescription}\n`);
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
const answer = await new Promise((resolve) => {
|
|
48
|
+
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
49
|
+
});
|
|
50
|
+
if (answer !== 'yes') {
|
|
51
|
+
console.error('❌ Aborted by user.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runGrant = runGrant;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
const db_utils_1 = require("../db-utils");
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
|
+
console.log(`Usage: optima-entitlement grant --email <user> --product-key <slug> --justification "..." [options]
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
--email <user-email> Resolved to userId via user-auth DB
|
|
13
|
+
--product-key <productKey>
|
|
14
|
+
--justification "..." Required by billing (400 otherwise); stored on entitlement.justification
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
+
--env stage|prod (default: stage)
|
|
19
|
+
|
|
20
|
+
Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<clientId>.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out = { env: 'stage', yes: false };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
switch (a) {
|
|
28
|
+
case '--email':
|
|
29
|
+
out.email = next;
|
|
30
|
+
i++;
|
|
31
|
+
break;
|
|
32
|
+
case '--product-key':
|
|
33
|
+
out.productKey = next;
|
|
34
|
+
i++;
|
|
35
|
+
break;
|
|
36
|
+
case '--justification':
|
|
37
|
+
out.justification = next;
|
|
38
|
+
i++;
|
|
39
|
+
break;
|
|
40
|
+
case '--yes':
|
|
41
|
+
out.yes = true;
|
|
42
|
+
break;
|
|
43
|
+
case '--env':
|
|
44
|
+
out.env = next;
|
|
45
|
+
i++;
|
|
46
|
+
break;
|
|
47
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!out.email)
|
|
51
|
+
throw new Error('--email required');
|
|
52
|
+
if (!out.productKey)
|
|
53
|
+
throw new Error('--product-key required');
|
|
54
|
+
if (!out.justification)
|
|
55
|
+
throw new Error('--justification required (billing returns 400 otherwise)');
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
async function runGrant(argv) {
|
|
59
|
+
const args = parseArgs(argv);
|
|
60
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
61
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
62
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
63
|
+
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
64
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: GRANT product '${args.productKey}' to user ${args.email} (userId=${userId}) on ${args.env.toUpperCase()}\nJustification: ${args.justification}`, args.yes);
|
|
65
|
+
console.log(`\n🎁 Granting ${args.productKey} to ${args.email}...`);
|
|
66
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/grant-entitlement', {
|
|
67
|
+
userId,
|
|
68
|
+
productKey: args.productKey,
|
|
69
|
+
justification: args.justification,
|
|
70
|
+
});
|
|
71
|
+
console.log(`✓ Granted entitlement (HTTP ${res.status}):`);
|
|
72
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
73
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runList = runList;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const db_utils_1 = require("../db-utils");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-entitlement list --email <user-email> [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--email <user-email> Resolved to userId via user-auth DB
|
|
12
|
+
|
|
13
|
+
Optional:
|
|
14
|
+
--env stage|prod (default: stage)`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const out = { env: 'stage' };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
const next = argv[i + 1];
|
|
21
|
+
switch (a) {
|
|
22
|
+
case '--email':
|
|
23
|
+
out.email = next;
|
|
24
|
+
i++;
|
|
25
|
+
break;
|
|
26
|
+
case '--env':
|
|
27
|
+
out.env = next;
|
|
28
|
+
i++;
|
|
29
|
+
break;
|
|
30
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!out.email)
|
|
34
|
+
throw new Error('--email required');
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
async function runList(argv) {
|
|
38
|
+
const args = parseArgs(argv);
|
|
39
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
40
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
41
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
42
|
+
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
43
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
44
|
+
const rows = res.body.entitlements ?? [];
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
console.log(`(no entitlements for ${args.email} on ${args.env})`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Newest first per spec
|
|
50
|
+
rows.sort((a, b) => b.purchasedAt.localeCompare(a.purchasedAt));
|
|
51
|
+
console.log(`${rows.length} entitlement(s) for ${args.email}:\n`);
|
|
52
|
+
console.log('id'.padEnd(38) + ' | ' + 'productKey'.padEnd(32) + ' | ' + 'status'.padEnd(9) + ' | ' + 'source'.padEnd(12) + ' | purchasedAt | refundedAt');
|
|
53
|
+
console.log('-'.repeat(140));
|
|
54
|
+
for (const r of rows) {
|
|
55
|
+
console.log(r.id.padEnd(38) + ' | ' +
|
|
56
|
+
r.productKey.padEnd(32) + ' | ' +
|
|
57
|
+
r.status.padEnd(9) + ' | ' +
|
|
58
|
+
r.source.padEnd(12) + ' | ' +
|
|
59
|
+
r.purchasedAt.padEnd(24) + ' | ' +
|
|
60
|
+
(r.refundedAt ?? ''));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRevoke = runRevoke;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
const db_utils_1 = require("../db-utils");
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
|
+
console.log(`Usage: optima-entitlement revoke --email <user> --product-key <slug> --reason "..." [options]
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
--email <user-email> Resolved to userId via user-auth DB
|
|
13
|
+
--product-key <productKey>
|
|
14
|
+
--reason "..." Required by billing (400 otherwise); stored on entitlement.refundReason
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
+
--env stage|prod (default: stage)
|
|
19
|
+
|
|
20
|
+
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
21
|
+
error pointing to the right reversal flow.`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const out = { env: 'stage', yes: false };
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
switch (a) {
|
|
29
|
+
case '--email':
|
|
30
|
+
out.email = next;
|
|
31
|
+
i++;
|
|
32
|
+
break;
|
|
33
|
+
case '--product-key':
|
|
34
|
+
out.productKey = next;
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
case '--reason':
|
|
38
|
+
out.reason = next;
|
|
39
|
+
i++;
|
|
40
|
+
break;
|
|
41
|
+
case '--yes':
|
|
42
|
+
out.yes = true;
|
|
43
|
+
break;
|
|
44
|
+
case '--env':
|
|
45
|
+
out.env = next;
|
|
46
|
+
i++;
|
|
47
|
+
break;
|
|
48
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!out.email)
|
|
52
|
+
throw new Error('--email required');
|
|
53
|
+
if (!out.productKey)
|
|
54
|
+
throw new Error('--product-key required');
|
|
55
|
+
if (!out.reason)
|
|
56
|
+
throw new Error('--reason required (billing returns 400 otherwise)');
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
const PAYMENT_REFUSAL = `refusing to revoke a PAYMENT-source entitlement via CLI; this would leave the customer charged but unentitled. Use the Stripe refund flow which calls Stripe refund API + records refundedAmountCents + emits webhook. Manual psql is the escape hatch if absolutely necessary.`;
|
|
60
|
+
const PARTNER_REFUSAL = `refusing to revoke a PARTNER-source entitlement via CLI; PARTNER grants are issued out-of-band and must be reversed via the partner contract / process that issued them. Manual psql is the escape hatch if absolutely necessary.`;
|
|
61
|
+
async function runRevoke(argv) {
|
|
62
|
+
const args = parseArgs(argv);
|
|
63
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
64
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
65
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
66
|
+
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
67
|
+
// Step 1: Fetch user's entitlements
|
|
68
|
+
const listRes = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
69
|
+
const all = listRes.body.entitlements ?? [];
|
|
70
|
+
// Step 2: Filter for ACTIVE + matching productKey
|
|
71
|
+
const matches = all.filter((e) => e.status === 'ACTIVE' && e.productKey === args.productKey);
|
|
72
|
+
// Step 3: Validate count (partial unique index enforces ≤1 ACTIVE)
|
|
73
|
+
if (matches.length === 0) {
|
|
74
|
+
throw new Error(`no active entitlement for (user=${args.email}, product=${args.productKey}) on ${args.env}`);
|
|
75
|
+
}
|
|
76
|
+
if (matches.length > 1) {
|
|
77
|
+
throw new Error(`unexpected: ${matches.length} ACTIVE entitlements for (user, product) — partial unique constraint should prevent this. Inspect with: optima-entitlement list --email ${args.email}`);
|
|
78
|
+
}
|
|
79
|
+
const target = matches[0];
|
|
80
|
+
// Step 4: Validate source
|
|
81
|
+
if (target.source === 'PAYMENT')
|
|
82
|
+
throw new Error(PAYMENT_REFUSAL);
|
|
83
|
+
if (target.source === 'PARTNER')
|
|
84
|
+
throw new Error(PARTNER_REFUSAL);
|
|
85
|
+
if (target.source !== 'ADMIN_GRANT')
|
|
86
|
+
throw new Error(`unknown entitlement source: ${target.source}`);
|
|
87
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: REVOKE entitlement ${target.id} (productKey=${target.productKey}) for user ${args.email} (userId=${userId}) on ${args.env.toUpperCase()}\nReason: ${args.reason}`, args.yes);
|
|
88
|
+
// Step 5: Refund
|
|
89
|
+
// refundAmountCents=0 is always correct for ADMIN_GRANT (priceCents=0,
|
|
90
|
+
// no upstream charge). Pass explicitly so billing's auto-compute
|
|
91
|
+
// (which requires refundWindowDays on the Product) doesn't 400 with
|
|
92
|
+
// MANUAL_REFUND_AMOUNT_REQUIRED for products that lack a refund policy.
|
|
93
|
+
// PAYMENT/PARTNER sources are already refused in step 4 above, so
|
|
94
|
+
// this branch only ever runs for ADMIN_GRANT (priceCents=0) — Stripe
|
|
95
|
+
// refund path in billing (admin-products.ts:296-307) is gated on
|
|
96
|
+
// source=PAYMENT and won't trigger here.
|
|
97
|
+
console.log(`\n♻️ Revoking entitlement ${target.id} for ${args.email}...`);
|
|
98
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/refund-entitlement', {
|
|
99
|
+
entitlementId: target.id,
|
|
100
|
+
refundReason: args.reason,
|
|
101
|
+
refundAmountCents: 0,
|
|
102
|
+
});
|
|
103
|
+
console.log(`✓ Revoked entitlement (HTTP ${res.status}):`);
|
|
104
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const grant_1 = require("./entitlement/grant");
|
|
5
|
+
const list_1 = require("./entitlement/list");
|
|
6
|
+
const revoke_1 = require("./entitlement/revoke");
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`Usage: optima-entitlement <subcommand> [options]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
grant Admin-grant a product entitlement to a user
|
|
12
|
+
revoke Revoke an admin-granted entitlement (refuses PAYMENT / PARTNER sources)
|
|
13
|
+
list List a user's entitlements, newest first
|
|
14
|
+
|
|
15
|
+
Run 'optima-entitlement <subcommand> --help' for subcommand-specific options.`);
|
|
16
|
+
}
|
|
17
|
+
async function main() {
|
|
18
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
19
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') {
|
|
20
|
+
printHelp();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
switch (subcommand) {
|
|
24
|
+
case 'list':
|
|
25
|
+
await (0, list_1.runList)(rest);
|
|
26
|
+
break;
|
|
27
|
+
case 'grant':
|
|
28
|
+
await (0, grant_1.runGrant)(rest);
|
|
29
|
+
break;
|
|
30
|
+
case 'revoke':
|
|
31
|
+
await (0, revoke_1.runRevoke)(rest);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
35
|
+
printHelp();
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
main().catch((err) => { console.error(err.message); process.exit(1); });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchInfisicalSecret = fetchInfisicalSecret;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const db_utils_1 = require("./db-utils");
|
|
6
|
+
/**
|
|
7
|
+
* Fetch a single secret value from Infisical given env + path + name.
|
|
8
|
+
*
|
|
9
|
+
* env mapping: 'stage' → Infisical env slug 'staging'; 'prod' → 'prod'
|
|
10
|
+
* (matches dev-skills convention documented at
|
|
11
|
+
* ~/.claude/projects/-mnt-d-work-projects-optima/memory/optima_infisical_env_naming.md).
|
|
12
|
+
*
|
|
13
|
+
* Returns the raw secretValue string. Throws if the secret is missing.
|
|
14
|
+
*/
|
|
15
|
+
function fetchInfisicalSecret(env, secretPath, secretName, config, token) {
|
|
16
|
+
const cfg = config ?? (0, db_utils_1.getInfisicalConfig)();
|
|
17
|
+
const tok = token ?? (0, db_utils_1.getInfisicalToken)(cfg);
|
|
18
|
+
const envSlug = env === 'stage' ? 'staging' : env;
|
|
19
|
+
const encodedPath = encodeURIComponent(secretPath);
|
|
20
|
+
const encodedName = encodeURIComponent(secretName);
|
|
21
|
+
const response = (0, child_process_1.execSync)(`curl -s "${cfg.url}/api/v3/secrets/raw/${encodedName}?workspaceId=${cfg.projectId}&environment=${envSlug}&secretPath=${encodedPath}" -H "Authorization: Bearer ${tok}"`, { encoding: 'utf-8' });
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(response);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Infisical raw secret fetch returned non-JSON for ${secretPath}/${secretName} (${envSlug}): ${response.slice(0, 200)}`);
|
|
28
|
+
}
|
|
29
|
+
if (!parsed.secret?.secretValue) {
|
|
30
|
+
throw new Error(`Infisical secret not found: env=${envSlug} path=${secretPath} name=${secretName} (response: ${response.slice(0, 200)})`);
|
|
31
|
+
}
|
|
32
|
+
return parsed.secret.secretValue;
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSetDefault = runSetDefault;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-plugin set-default --slug <slug> --default true|false [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--slug <slug>
|
|
12
|
+
--default true|false Sets Plugin.defaultForUser
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: no skill-sync broadcast — changes what NEW user syncs receive; does not
|
|
19
|
+
retroactively add/remove the plugin for existing users until their next sync.`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const out = { env: 'stage', yes: false };
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
const next = argv[i + 1];
|
|
26
|
+
switch (a) {
|
|
27
|
+
case '--slug':
|
|
28
|
+
out.slug = next;
|
|
29
|
+
i++;
|
|
30
|
+
break;
|
|
31
|
+
case '--default':
|
|
32
|
+
if (next !== 'true' && next !== 'false')
|
|
33
|
+
throw new Error('--default must be true or false');
|
|
34
|
+
out.default = next === 'true';
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
case '--yes':
|
|
38
|
+
out.yes = true;
|
|
39
|
+
break;
|
|
40
|
+
case '--env':
|
|
41
|
+
out.env = next;
|
|
42
|
+
i++;
|
|
43
|
+
break;
|
|
44
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!out.slug)
|
|
48
|
+
throw new Error('--slug required');
|
|
49
|
+
if (out.default === undefined)
|
|
50
|
+
throw new Error('--default required (true|false)');
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
async function runSetDefault(argv) {
|
|
54
|
+
const args = parseArgs(argv);
|
|
55
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
56
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: set defaultForUser=${args.default} on plugin '${args.slug}' (${args.env.toUpperCase()})`, args.yes);
|
|
57
|
+
console.log(`\n🔧 Setting defaultForUser=${args.default} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
58
|
+
const res = await (0, billing_http_1.callSkills)(args.env, 'PATCH', `/api/admin/plugins/${encodeURIComponent(args.slug)}`, { defaultForUser: args.default });
|
|
59
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
60
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
61
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSetPaid = runSetPaid;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-plugin set-paid --slug <slug> --paid true|false [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--slug <slug>
|
|
12
|
+
--paid true|false Sets Plugin.isPaid (the user-facing paid/free gate)
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: salesUrl is NOT settable here (skills PATCH is strict; salesUrl is
|
|
19
|
+
publish-time-only via plugin.json metadata). When isPaid=true and salesUrl is
|
|
20
|
+
null, the 402 falls back to sales.optima.onl.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out = { env: 'stage', yes: false };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
switch (a) {
|
|
28
|
+
case '--slug':
|
|
29
|
+
out.slug = next;
|
|
30
|
+
i++;
|
|
31
|
+
break;
|
|
32
|
+
case '--paid':
|
|
33
|
+
if (next !== 'true' && next !== 'false')
|
|
34
|
+
throw new Error('--paid must be true or false');
|
|
35
|
+
out.paid = next === 'true';
|
|
36
|
+
i++;
|
|
37
|
+
break;
|
|
38
|
+
case '--yes':
|
|
39
|
+
out.yes = true;
|
|
40
|
+
break;
|
|
41
|
+
case '--env':
|
|
42
|
+
out.env = next;
|
|
43
|
+
i++;
|
|
44
|
+
break;
|
|
45
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!out.slug)
|
|
49
|
+
throw new Error('--slug required');
|
|
50
|
+
if (out.paid === undefined)
|
|
51
|
+
throw new Error('--paid required (true|false)');
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async function runSetPaid(argv) {
|
|
55
|
+
const args = parseArgs(argv);
|
|
56
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
57
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: set isPaid=${args.paid} on plugin '${args.slug}' (${args.env.toUpperCase()})`, args.yes);
|
|
58
|
+
console.log(`\n💰 Setting isPaid=${args.paid} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
59
|
+
const res = await (0, billing_http_1.callSkills)(args.env, 'PATCH', `/api/admin/plugins/${encodeURIComponent(args.slug)}`, { isPaid: args.paid });
|
|
60
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
61
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
62
|
+
if (args.paid) {
|
|
63
|
+
console.log(`\nℹ️ Reminder: ensure a billing Product + channel exists for '${args.slug}' (optima-product) or users will 402 with no purchase path. salesUrl is publish-time-only (currently shown above).`);
|
|
64
|
+
}
|
|
65
|
+
}
|