@optima-chat/dev-skills 0.7.33 → 0.7.35
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 +2 -0
- package/bin/helpers/billing-http.ts +162 -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/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 +139 -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/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/specs/2026-05-24-marketplace-admin-cli-design.md +324 -0
- package/package.json +7 -5
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface CreateArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
plugins: string[];
|
|
6
|
+
type: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
refundWindowDays?: number;
|
|
10
|
+
refundProrateMaxDays?: number;
|
|
11
|
+
bundledPlanId?: string;
|
|
12
|
+
bundledDurationDays?: number;
|
|
13
|
+
revokeBundledOnRefund?: boolean;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
env: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv: string[]): CreateArgs {
|
|
19
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
20
|
+
console.log(`Usage: optima-product create --key <productKey> --plugins <slug1,slug2,...> --type <ProductType> [options]
|
|
21
|
+
|
|
22
|
+
Required:
|
|
23
|
+
--key <productKey> Unique slug
|
|
24
|
+
--plugins <slug1,slug2,...> Comma-separated, >=1 plugin slug
|
|
25
|
+
--type <ProductType> Only ONE_SHOT_SKILL accepted by v1 CLI policy
|
|
26
|
+
|
|
27
|
+
Optional:
|
|
28
|
+
--name "..." Convenience flag, folded into metadata.name
|
|
29
|
+
--description "..." Convenience flag, folded into metadata.description
|
|
30
|
+
--refund-window-days N
|
|
31
|
+
--refund-prorate-max-days N
|
|
32
|
+
--bundled-plan-id <planId> (joint with --bundled-duration-days)
|
|
33
|
+
--bundled-duration-days N
|
|
34
|
+
--revoke-bundled-on-refund true|false
|
|
35
|
+
--metadata '<json>' JSON object stored in product.metadata
|
|
36
|
+
--env stage|prod (default: stage)`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const out: Partial<CreateArgs> = { env: 'stage', revokeBundledOnRefund: undefined };
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
switch (a) {
|
|
45
|
+
case '--key': out.key = next; i++; break;
|
|
46
|
+
case '--plugins': out.plugins = next.split(',').map((s) => s.trim()).filter(Boolean); i++; break;
|
|
47
|
+
case '--type': out.type = next; i++; break;
|
|
48
|
+
case '--name': out.name = next; i++; break;
|
|
49
|
+
case '--description': out.description = next; i++; break;
|
|
50
|
+
case '--refund-window-days': out.refundWindowDays = parseInt(next, 10); i++; break;
|
|
51
|
+
case '--refund-prorate-max-days': out.refundProrateMaxDays = parseInt(next, 10); i++; break;
|
|
52
|
+
case '--bundled-plan-id': out.bundledPlanId = next; i++; break;
|
|
53
|
+
case '--bundled-duration-days': out.bundledDurationDays = parseInt(next, 10); i++; break;
|
|
54
|
+
case '--revoke-bundled-on-refund': out.revokeBundledOnRefund = next === 'true'; i++; break;
|
|
55
|
+
case '--metadata': out.metadata = JSON.parse(next); i++; break;
|
|
56
|
+
case '--env': out.env = next; i++; break;
|
|
57
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!out.key) throw new Error('--key required');
|
|
62
|
+
if (!out.plugins || out.plugins.length === 0) throw new Error('--plugins required (>=1 slug)');
|
|
63
|
+
if (!out.type) throw new Error('--type required');
|
|
64
|
+
if (out.type !== 'ONE_SHOT_SKILL') {
|
|
65
|
+
throw new Error(`v1 CLI accepts only --type ONE_SHOT_SKILL (got ${out.type}). See spec §5.1.`);
|
|
66
|
+
}
|
|
67
|
+
if ((out.bundledPlanId == null) !== (out.bundledDurationDays == null)) {
|
|
68
|
+
throw new Error('--bundled-plan-id and --bundled-duration-days must be set together');
|
|
69
|
+
}
|
|
70
|
+
return out as CreateArgs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runCreate(argv: string[]): Promise<void> {
|
|
74
|
+
const args = parseArgs(argv);
|
|
75
|
+
validateEnv(args.env);
|
|
76
|
+
|
|
77
|
+
const body: Record<string, unknown> = {
|
|
78
|
+
productKey: args.key,
|
|
79
|
+
type: args.type,
|
|
80
|
+
pluginSlugs: args.plugins,
|
|
81
|
+
};
|
|
82
|
+
if (args.name !== undefined) body.name = args.name;
|
|
83
|
+
if (args.description !== undefined) body.description = args.description;
|
|
84
|
+
if (args.refundWindowDays !== undefined) body.refundWindowDays = args.refundWindowDays;
|
|
85
|
+
if (args.refundProrateMaxDays !== undefined) body.refundProrateMaxDays = args.refundProrateMaxDays;
|
|
86
|
+
if (args.bundledPlanId !== undefined) body.bundledPlanId = args.bundledPlanId;
|
|
87
|
+
if (args.bundledDurationDays !== undefined) body.bundledDurationDays = args.bundledDurationDays;
|
|
88
|
+
if (args.revokeBundledOnRefund !== undefined) body.revokeBundledOnRefund = args.revokeBundledOnRefund;
|
|
89
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
90
|
+
|
|
91
|
+
console.log(`\n🎁 Creating product ${args.key} (${args.plugins.length} plugin(s)) on ${args.env.toUpperCase()}...`);
|
|
92
|
+
|
|
93
|
+
const res = await callBilling(args.env, 'POST', '/api/billing/admin/products', body);
|
|
94
|
+
console.log(`✓ Created Product (HTTP ${res.status}):`);
|
|
95
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface ShowArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
env: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv: string[]): ShowArgs {
|
|
9
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
10
|
+
console.log(`Usage: optima-product show --key <productKey> [options]
|
|
11
|
+
|
|
12
|
+
Required:
|
|
13
|
+
--key <productKey>
|
|
14
|
+
|
|
15
|
+
Optional:
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: returns the bare Product row only — productPlugins and channels arrays
|
|
19
|
+
are NOT included (billing's GET /api/internal/products/:key is a plain
|
|
20
|
+
findUnique with no include). To inspect channels/plugins today, query the DB
|
|
21
|
+
directly via optima-query-db. Tracked as follow-up in spec §7.`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const out: Partial<ShowArgs> = { env: 'stage' };
|
|
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 '--key': out.key = next; i++; break;
|
|
30
|
+
case '--env': out.env = next; i++; break;
|
|
31
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!out.key) throw new Error('--key required');
|
|
35
|
+
return out as ShowArgs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runShow(argv: string[]): Promise<void> {
|
|
39
|
+
const args = parseArgs(argv);
|
|
40
|
+
validateEnv(args.env);
|
|
41
|
+
const res = await callBilling(args.env, 'GET', `/api/internal/products/${encodeURIComponent(args.key)}`);
|
|
42
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface ToggleArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
env: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv: string[]): ToggleArgs {
|
|
11
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
12
|
+
console.log(`Usage: optima-product toggle-channel --key <productKey> --provider STRIPE --enabled true|false [options]
|
|
13
|
+
|
|
14
|
+
Required:
|
|
15
|
+
--key <productKey>
|
|
16
|
+
--provider STRIPE v1 CLI accepts only STRIPE
|
|
17
|
+
--enabled true|false
|
|
18
|
+
|
|
19
|
+
Optional:
|
|
20
|
+
--env stage|prod (default: stage)`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out: Partial<ToggleArgs> = { env: 'stage' };
|
|
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 '--key': out.key = next; i++; break;
|
|
29
|
+
case '--provider': out.provider = next; i++; break;
|
|
30
|
+
case '--enabled':
|
|
31
|
+
if (next !== 'true' && next !== 'false') throw new Error('--enabled must be true or false');
|
|
32
|
+
out.enabled = next === 'true'; i++; break;
|
|
33
|
+
case '--env': out.env = next; i++; break;
|
|
34
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!out.key) throw new Error('--key required');
|
|
38
|
+
if (!out.provider) throw new Error('--provider required');
|
|
39
|
+
if (out.provider !== 'STRIPE') throw new Error(`v1 CLI accepts only --provider STRIPE (got ${out.provider})`);
|
|
40
|
+
if (out.enabled === undefined) throw new Error('--enabled required');
|
|
41
|
+
return out as ToggleArgs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runToggleChannel(argv: string[]): Promise<void> {
|
|
45
|
+
const args = parseArgs(argv);
|
|
46
|
+
validateEnv(args.env);
|
|
47
|
+
console.log(`\n🔁 Setting ${args.provider} channel enabled=${args.enabled} on ${args.key} (${args.env.toUpperCase()})...`);
|
|
48
|
+
const res = await callBilling(
|
|
49
|
+
args.env,
|
|
50
|
+
'PATCH',
|
|
51
|
+
`/api/billing/admin/products/${encodeURIComponent(args.key)}/channels/${args.provider}`,
|
|
52
|
+
{ enabled: args.enabled },
|
|
53
|
+
);
|
|
54
|
+
console.log(`✓ Channel updated (HTTP ${res.status}):`);
|
|
55
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface UpdateArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
refundWindowDays?: number | null;
|
|
6
|
+
refundProrateMaxDays?: number | null;
|
|
7
|
+
bundledPlanId?: string | null;
|
|
8
|
+
bundledDurationDays?: number | null;
|
|
9
|
+
revokeBundledOnRefund?: boolean;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
env: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv: string[]): UpdateArgs {
|
|
15
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
16
|
+
console.log(`Usage: optima-product update --key <productKey> [options]
|
|
17
|
+
|
|
18
|
+
Required:
|
|
19
|
+
--key <productKey>
|
|
20
|
+
|
|
21
|
+
Optional (PATCH — only included fields are updated):
|
|
22
|
+
--refund-window-days N | null
|
|
23
|
+
--refund-prorate-max-days N | null
|
|
24
|
+
--bundled-plan-id <planId> | null (joint with --bundled-duration-days)
|
|
25
|
+
--bundled-duration-days N | null
|
|
26
|
+
--revoke-bundled-on-refund true|false
|
|
27
|
+
--metadata '<json>' FULL REPLACE — Prisma does not deep-merge JSON
|
|
28
|
+
--env stage|prod (default: stage)
|
|
29
|
+
|
|
30
|
+
Note: productKey, type, and pluginSlugs are immutable post-create. To change plugin
|
|
31
|
+
membership, create a new Product with a new key.`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const out: Partial<UpdateArgs> = { env: 'stage' };
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i];
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
const parseNullable = (v: string): number | null => v === 'null' ? null : parseInt(v, 10);
|
|
39
|
+
switch (a) {
|
|
40
|
+
case '--key': out.key = next; i++; break;
|
|
41
|
+
case '--refund-window-days': out.refundWindowDays = parseNullable(next); i++; break;
|
|
42
|
+
case '--refund-prorate-max-days': out.refundProrateMaxDays = parseNullable(next); i++; break;
|
|
43
|
+
case '--bundled-plan-id': out.bundledPlanId = next === 'null' ? null : next; i++; break;
|
|
44
|
+
case '--bundled-duration-days': out.bundledDurationDays = parseNullable(next); i++; break;
|
|
45
|
+
case '--revoke-bundled-on-refund': out.revokeBundledOnRefund = next === 'true'; i++; break;
|
|
46
|
+
case '--metadata': out.metadata = JSON.parse(next); i++; break;
|
|
47
|
+
case '--env': out.env = next; i++; break;
|
|
48
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!out.key) throw new Error('--key required');
|
|
52
|
+
return out as UpdateArgs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runUpdate(argv: string[]): Promise<void> {
|
|
56
|
+
const args = parseArgs(argv);
|
|
57
|
+
validateEnv(args.env);
|
|
58
|
+
const body: Record<string, unknown> = {};
|
|
59
|
+
if (args.refundWindowDays !== undefined) body.refundWindowDays = args.refundWindowDays;
|
|
60
|
+
if (args.refundProrateMaxDays !== undefined) body.refundProrateMaxDays = args.refundProrateMaxDays;
|
|
61
|
+
if (args.bundledPlanId !== undefined) body.bundledPlanId = args.bundledPlanId;
|
|
62
|
+
if (args.bundledDurationDays !== undefined) body.bundledDurationDays = args.bundledDurationDays;
|
|
63
|
+
if (args.revokeBundledOnRefund !== undefined) body.revokeBundledOnRefund = args.revokeBundledOnRefund;
|
|
64
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
65
|
+
|
|
66
|
+
if (Object.keys(body).length === 0) throw new Error('At least one updatable field must be passed');
|
|
67
|
+
|
|
68
|
+
console.log(`\n✏️ Updating product ${args.key} on ${args.env.toUpperCase()}...`);
|
|
69
|
+
const res = await callBilling(args.env, 'PATCH', `/api/billing/admin/products/${encodeURIComponent(args.key)}`, body);
|
|
70
|
+
console.log(`✓ Updated Product (HTTP ${res.status}):`);
|
|
71
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCreate } from './product/create';
|
|
4
|
+
import { runUpdate } from './product/update';
|
|
5
|
+
import { runAddChannel } from './product/add-channel';
|
|
6
|
+
import { runToggleChannel } from './product/toggle-channel';
|
|
7
|
+
import { runShow } from './product/show';
|
|
8
|
+
|
|
9
|
+
const SUBCOMMANDS = ['create', 'update', 'add-channel', 'toggle-channel', 'show'] as const;
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
console.log(`Usage: optima-product <subcommand> [options]
|
|
13
|
+
|
|
14
|
+
Subcommands:
|
|
15
|
+
create Create a Product bundling 1+ plugin slugs
|
|
16
|
+
update Patch refund policy / metadata on an existing Product
|
|
17
|
+
add-channel Attach a payment channel (Stripe Price ID) to a Product
|
|
18
|
+
toggle-channel Enable/disable an existing channel
|
|
19
|
+
show Show a Product's bare row (note: does NOT include plugins/channels)
|
|
20
|
+
|
|
21
|
+
Run 'optima-product <subcommand> --help' for subcommand-specific options.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
26
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') {
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case 'create': await runCreate(rest); break;
|
|
32
|
+
case 'update': await runUpdate(rest); break;
|
|
33
|
+
case 'add-channel': await runAddChannel(rest); break;
|
|
34
|
+
case 'toggle-channel': await runToggleChannel(rest); break;
|
|
35
|
+
case 'show': await runShow(rest); break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error(err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateEnv = validateEnv;
|
|
4
|
+
exports.getServiceToken = getServiceToken;
|
|
5
|
+
exports.callBilling = callBilling;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const infisical_secrets_1 = require("./infisical-secrets");
|
|
8
|
+
const db_utils_1 = require("./db-utils");
|
|
9
|
+
const USER_AUTH_URLS = {
|
|
10
|
+
stage: 'https://auth.stage.optima.onl',
|
|
11
|
+
prod: 'https://auth.optima.onl',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Validate the --env flag value at command entry, before any I/O.
|
|
15
|
+
*
|
|
16
|
+
* Without this, a typo like `--env staging` flows downstream and surfaces as
|
|
17
|
+
* a confusing error: entitlement subcommands hit resolveUserId first (SSH
|
|
18
|
+
* tunnel to RDS_HOSTS[undefined] → cryptic ssh failure), product subcommands
|
|
19
|
+
* reach getServiceToken (USER_AUTH_URLS[undefined] → "Unknown env"). All
|
|
20
|
+
* fail-closed (no wrong-env write), but the UX diverges per subcommand.
|
|
21
|
+
* Every runX handler calls this first so the error is uniform and immediate.
|
|
22
|
+
*/
|
|
23
|
+
function validateEnv(env) {
|
|
24
|
+
if (env !== 'stage' && env !== 'prod') {
|
|
25
|
+
throw new Error(`--env must be "stage" or "prod" (got: ${env})`);
|
|
26
|
+
}
|
|
27
|
+
return env;
|
|
28
|
+
}
|
|
29
|
+
// T1 discovered: client_id differs per env (stage=dev-skills-ubd3qz6n,
|
|
30
|
+
// prod=dev-skills-hinxa0rs). Both stored in Infisical alongside the
|
|
31
|
+
// secret at /shared-secrets/oauth-clients/.
|
|
32
|
+
const DEV_SKILLS_OAUTH_PATH = '/shared-secrets/oauth-clients';
|
|
33
|
+
const DEV_SKILLS_CLIENT_ID_KEY = 'DEV_SKILLS_OAUTH_CLIENT_ID';
|
|
34
|
+
const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
|
|
35
|
+
// ───── Cache (process-lifetime) ─────────────────────────────────────────────
|
|
36
|
+
// One CLI invocation does at most a handful of HTTP calls. We mint the M2M
|
|
37
|
+
// token once and reuse it. Cross-invocation re-mint is fine — JWT TTL is
|
|
38
|
+
// typically ≥1h, far longer than any single CLI run.
|
|
39
|
+
//
|
|
40
|
+
// NOT handled (acceptable for admin CLI):
|
|
41
|
+
// * Token expiry mid-invocation — a long-stalled revoke (list call + 5min
|
|
42
|
+
// pause + refund call) could in theory expire. Operator can retry.
|
|
43
|
+
// * Infisical 5xx retry — getInfisicalToken is sync execSync curl with no
|
|
44
|
+
// retry; any transient failure surfaces immediately. Re-run the CLI.
|
|
45
|
+
const tokenCache = {};
|
|
46
|
+
const billingUrlCache = {};
|
|
47
|
+
function getBillingUrl(env) {
|
|
48
|
+
if (billingUrlCache[env])
|
|
49
|
+
return billingUrlCache[env];
|
|
50
|
+
const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'BILLING_URL');
|
|
51
|
+
billingUrlCache[env] = url;
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
function getServiceToken(env) {
|
|
55
|
+
if (tokenCache[env])
|
|
56
|
+
return tokenCache[env];
|
|
57
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
58
|
+
const tok = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
59
|
+
// Fetch BOTH client_id and client_secret from Infisical — they differ per env.
|
|
60
|
+
const clientId = (0, infisical_secrets_1.fetchInfisicalSecret)(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_ID_KEY, cfg, tok);
|
|
61
|
+
const clientSecret = (0, infisical_secrets_1.fetchInfisicalSecret)(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_SECRET_KEY, cfg, tok);
|
|
62
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
63
|
+
if (!authUrl)
|
|
64
|
+
throw new Error(`Unknown env: ${env}`);
|
|
65
|
+
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`;
|
|
66
|
+
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' });
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(response);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`user-auth token endpoint returned non-JSON (${env}): ${response.slice(0, 200)}`);
|
|
73
|
+
}
|
|
74
|
+
if (!parsed.access_token) {
|
|
75
|
+
throw new Error(`user-auth token mint failed (${env}): ${response.slice(0, 200)}`);
|
|
76
|
+
}
|
|
77
|
+
tokenCache[env] = parsed.access_token;
|
|
78
|
+
return parsed.access_token;
|
|
79
|
+
}
|
|
80
|
+
function formatBillingError(status, statusText, body) {
|
|
81
|
+
let parsed = null;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(body);
|
|
84
|
+
}
|
|
85
|
+
catch { /* non-JSON */ }
|
|
86
|
+
// Dominant Wave 1.5 envelope: flat { error: "CODE_STRING", message: "..." }
|
|
87
|
+
// emitted by billing's global error handler (app.ts:99-118) for ALL
|
|
88
|
+
// BillingError throws + validation errors + internal errors. Most inline
|
|
89
|
+
// route returns also use this shape (admin-products.ts:110,144,184-185,etc).
|
|
90
|
+
if (parsed && typeof parsed.error === 'string') {
|
|
91
|
+
return `❌ Error [${status}] ${parsed.error}: ${parsed.message ?? '(no message)'}`;
|
|
92
|
+
}
|
|
93
|
+
// Less-common nested envelope: { error: { code, message } } — used by a
|
|
94
|
+
// few inline 400/404 returns in admin-products.ts toggle-channel handler
|
|
95
|
+
// (lines 92-93, 100-101, 114-117). Possibly extends to other routes
|
|
96
|
+
// post-Wave-1.5 as standardization lands.
|
|
97
|
+
if (parsed && typeof parsed.error === 'object' && parsed.error !== null) {
|
|
98
|
+
const code = parsed.error.code ?? 'UNKNOWN';
|
|
99
|
+
const msg = parsed.error.message ?? '(no message)';
|
|
100
|
+
return `❌ Error [${status}] ${code}: ${msg}`;
|
|
101
|
+
}
|
|
102
|
+
// Non-envelope fallback (raw 502 from upstream LB, crashed handler before
|
|
103
|
+
// error middleware, plain-text body, etc.)
|
|
104
|
+
return `❌ Error [${status}] ${statusText}\n Response body (first 500 bytes): ${body.slice(0, 500)}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Make an authenticated call to optima-billing. Returns `{status, body}` on
|
|
108
|
+
* 2xx; throws Error with formatted message on non-2xx. Single retry on 5xx
|
|
109
|
+
* (one-shot — no exponential backoff; admin CLI doesn't justify it).
|
|
110
|
+
*/
|
|
111
|
+
async function callBilling(env, method, path, body) {
|
|
112
|
+
const url = `${getBillingUrl(env)}${path}`;
|
|
113
|
+
const token = getServiceToken(env);
|
|
114
|
+
const doFetch = async () => fetch(url, {
|
|
115
|
+
method,
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Bearer ${token}`,
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
},
|
|
120
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
121
|
+
});
|
|
122
|
+
let res = await doFetch();
|
|
123
|
+
if (res.status >= 500) {
|
|
124
|
+
// One retry on 5xx
|
|
125
|
+
res = await doFetch();
|
|
126
|
+
}
|
|
127
|
+
const text = await res.text();
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw new Error(formatBillingError(res.status, res.statusText, text));
|
|
130
|
+
}
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = text ? JSON.parse(text) : undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error(`Billing returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
137
|
+
}
|
|
138
|
+
return { status: res.status, body: parsed };
|
|
139
|
+
}
|
|
@@ -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
|
+
}
|