@soulbatical/tetra-dev-toolkit 1.21.1 → 1.22.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.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Tetra Dev Toolkit - License Key CLI
5
+ *
6
+ * Generate and verify Tetra license keys.
7
+ *
8
+ * Usage:
9
+ * tetra-license generate --customer "Acme BV" --order ORD-2026-001 --plan pro --expiry 2027-04-09
10
+ * tetra-license verify <key>
11
+ * tetra-license verify # reads TETRA_LICENSE_KEY env var
12
+ * tetra-license decode <key> # show payload without HMAC validation
13
+ */
14
+
15
+ import { program } from 'commander'
16
+ import chalk from 'chalk'
17
+ import crypto from 'node:crypto'
18
+
19
+ // ─── Inline license logic (dev-toolkit has no tetra-core dependency) ───
20
+
21
+ const KEY_VERSION = 1
22
+ const KEY_SEPARATOR = '.'
23
+ const HMAC_ALGORITHM = 'sha256'
24
+ const SIGNING_SECRET = 'tetra-license-v1-soulbatical-bv-2026'
25
+
26
+ const PLAN_HIERARCHY = { starter: 0, pro: 1, enterprise: 2 }
27
+
28
+ function generateKey({ orderId, customer, plan, scope, model, expiryDate }) {
29
+ const payload = {
30
+ orderId,
31
+ customer,
32
+ plan,
33
+ scope: scope || ['core', 'ui'],
34
+ model: model || 'perpetual',
35
+ expiry: new Date(expiryDate).getTime(),
36
+ keyVersion: KEY_VERSION,
37
+ }
38
+ if (isNaN(payload.expiry)) throw new Error(`Invalid expiry date: ${expiryDate}`)
39
+ const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url')
40
+ const hmac = crypto.createHmac(HMAC_ALGORITHM, SIGNING_SECRET).update(payloadBase64).digest('hex')
41
+ return `${hmac}${KEY_SEPARATOR}${payloadBase64}`
42
+ }
43
+
44
+ function decodeKey(key) {
45
+ try {
46
+ const sep = key.indexOf(KEY_SEPARATOR)
47
+ if (sep === -1) return null
48
+ const json = Buffer.from(key.slice(sep + 1), 'base64url').toString('utf8')
49
+ return JSON.parse(json)
50
+ } catch { return null }
51
+ }
52
+
53
+ function verifyKey(key) {
54
+ const sep = key.indexOf(KEY_SEPARATOR)
55
+ if (sep === -1) return { valid: false, reason: 'Invalid format' }
56
+ const hmacProvided = key.slice(0, sep)
57
+ const payloadBase64 = key.slice(sep + 1)
58
+ const hmacComputed = crypto.createHmac(HMAC_ALGORITHM, SIGNING_SECRET).update(payloadBase64).digest('hex')
59
+ try {
60
+ if (!crypto.timingSafeEqual(Buffer.from(hmacProvided, 'hex'), Buffer.from(hmacComputed, 'hex'))) {
61
+ return { valid: false, reason: 'HMAC mismatch — key has been tampered with' }
62
+ }
63
+ } catch {
64
+ return { valid: false, reason: 'HMAC mismatch — invalid key' }
65
+ }
66
+ const payload = decodeKey(key)
67
+ if (!payload) return { valid: false, reason: 'Payload decode failed' }
68
+ return { valid: true, payload }
69
+ }
70
+
71
+ // ─── Commands ────────────────────────────────────────────────
72
+
73
+ program
74
+ .name('tetra-license')
75
+ .description('Generate and verify Tetra license keys')
76
+ .version('1.0.0')
77
+
78
+ program
79
+ .command('generate')
80
+ .description('Generate a new license key')
81
+ .requiredOption('--customer <name>', 'Customer name')
82
+ .requiredOption('--order <id>', 'Order ID (e.g., ORD-2026-001)')
83
+ .requiredOption('--plan <plan>', 'Plan tier: starter, pro, enterprise')
84
+ .requiredOption('--expiry <date>', 'Expiry date (ISO format, e.g., 2027-04-09)')
85
+ .option('--scope <scopes>', 'Comma-separated scopes (default: core,ui)', 'core,ui')
86
+ .option('--model <model>', 'Licensing model: perpetual or subscription (default: perpetual)', 'perpetual')
87
+ .action((options) => {
88
+ try {
89
+ if (!['starter', 'pro', 'enterprise'].includes(options.plan)) {
90
+ console.error(chalk.red(`Invalid plan: ${options.plan}. Must be starter, pro, or enterprise.`))
91
+ process.exit(1)
92
+ }
93
+ if (!['perpetual', 'subscription'].includes(options.model)) {
94
+ console.error(chalk.red(`Invalid model: ${options.model}. Must be perpetual or subscription.`))
95
+ process.exit(1)
96
+ }
97
+
98
+ const key = generateKey({
99
+ orderId: options.order,
100
+ customer: options.customer,
101
+ plan: options.plan,
102
+ scope: options.scope.split(','),
103
+ model: options.model,
104
+ expiryDate: options.expiry,
105
+ })
106
+
107
+ const payload = decodeKey(key)
108
+ const expiryDate = new Date(payload.expiry).toISOString().split('T')[0]
109
+
110
+ console.log()
111
+ console.log(chalk.green.bold('✓ License key generated'))
112
+ console.log()
113
+ console.log(chalk.dim('─'.repeat(60)))
114
+ console.log(chalk.bold('Key: '), key)
115
+ console.log(chalk.dim('─'.repeat(60)))
116
+ console.log(chalk.bold('Customer: '), payload.customer)
117
+ console.log(chalk.bold('Order: '), payload.orderId)
118
+ console.log(chalk.bold('Plan: '), chalk.cyan(payload.plan))
119
+ console.log(chalk.bold('Scope: '), payload.scope.join(', '))
120
+ console.log(chalk.bold('Model: '), payload.model)
121
+ console.log(chalk.bold('Expiry: '), expiryDate)
122
+ console.log(chalk.bold('Version: '), `v${payload.keyVersion}`)
123
+ console.log()
124
+ console.log(chalk.dim('Set this in the customer\'s environment:'))
125
+ console.log(chalk.yellow(` TETRA_LICENSE_KEY=${key}`))
126
+ console.log(chalk.dim('For frontend (Next.js):'))
127
+ console.log(chalk.yellow(` NEXT_PUBLIC_TETRA_LICENSE_KEY=${key}`))
128
+ console.log()
129
+ } catch (err) {
130
+ console.error(chalk.red(`Error: ${err.message}`))
131
+ process.exit(1)
132
+ }
133
+ })
134
+
135
+ program
136
+ .command('verify [key]')
137
+ .description('Verify a license key (reads TETRA_LICENSE_KEY if no key given)')
138
+ .action((keyArg) => {
139
+ const key = keyArg || process.env.TETRA_LICENSE_KEY
140
+ if (!key) {
141
+ console.error(chalk.red('No key provided. Pass as argument or set TETRA_LICENSE_KEY.'))
142
+ process.exit(1)
143
+ }
144
+
145
+ const result = verifyKey(key)
146
+ console.log()
147
+
148
+ if (!result.valid) {
149
+ console.log(chalk.red.bold('✗ Invalid license key'))
150
+ console.log(chalk.red(` Reason: ${result.reason}`))
151
+ console.log()
152
+ process.exit(1)
153
+ }
154
+
155
+ const { payload } = result
156
+ const now = Date.now()
157
+ const daysRemaining = Math.floor((payload.expiry - now) / (1000 * 60 * 60 * 24))
158
+ const expired = payload.model === 'subscription' ? now > payload.expiry : false
159
+ const expiryDate = new Date(payload.expiry).toISOString().split('T')[0]
160
+
161
+ console.log(expired ? chalk.red.bold('✗ License EXPIRED') : chalk.green.bold('✓ License valid'))
162
+ console.log()
163
+ console.log(chalk.bold('Customer: '), payload.customer)
164
+ console.log(chalk.bold('Order: '), payload.orderId)
165
+ console.log(chalk.bold('Plan: '), chalk.cyan(payload.plan))
166
+ console.log(chalk.bold('Scope: '), payload.scope.join(', '))
167
+ console.log(chalk.bold('Model: '), payload.model)
168
+ console.log(chalk.bold('Expiry: '), expired ? chalk.red(expiryDate) : expiryDate)
169
+ console.log(chalk.bold('Remaining: '), daysRemaining > 0
170
+ ? (daysRemaining <= 30 ? chalk.yellow(`${daysRemaining} days`) : chalk.green(`${daysRemaining} days`))
171
+ : chalk.red(`expired ${Math.abs(daysRemaining)} days ago`))
172
+ console.log(chalk.bold('Version: '), `v${payload.keyVersion}`)
173
+ console.log()
174
+ })
175
+
176
+ program
177
+ .command('decode [key]')
178
+ .description('Decode a license key payload (no HMAC validation)')
179
+ .action((keyArg) => {
180
+ const key = keyArg || process.env.TETRA_LICENSE_KEY
181
+ if (!key) {
182
+ console.error(chalk.red('No key provided.'))
183
+ process.exit(1)
184
+ }
185
+
186
+ const payload = decodeKey(key)
187
+ if (!payload) {
188
+ console.error(chalk.red('Could not decode key payload.'))
189
+ process.exit(1)
190
+ }
191
+
192
+ console.log()
193
+ console.log(chalk.bold('Decoded payload:'))
194
+ console.log(JSON.stringify(payload, null, 2))
195
+ console.log()
196
+ })
197
+
198
+ program.parse()
@@ -28,7 +28,7 @@ const ALLOWED_ROOT_MD = new Set([
28
28
 
29
29
  const IGNORED_DIRS = new Set([
30
30
  'node_modules', 'dist', 'build', '.git', '.next', '.cache', '.turbo',
31
- 'coverage', '.nyc_output', '.playwright', 'test-results'
31
+ 'coverage', '.nyc_output', '.playwright', 'test-results', '.netlify'
32
32
  ])
33
33
 
34
34
  const ALLOWED_SCRIPT_DIRS = new Set([
@@ -37,7 +37,7 @@ const ALLOWED_SCRIPT_DIRS = new Set([
37
37
 
38
38
  /** Directories whose .md content is always allowed (tooling config) */
39
39
  const ALLOWED_MD_DIRS = new Set([
40
- 'docs', '.ralph', '.claude', '.agents', 'e2e', 'tests'
40
+ 'docs', '.ralph', '.claude', '.agents', 'e2e', 'tests', 'shell'
41
41
  ])
42
42
 
43
43
  /** Directories where .yml/.yaml config files are allowed */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.21.1",
3
+ "version": "1.22.1",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },
@@ -45,7 +45,8 @@
45
45
  "tetra-check-views": "./bin/tetra-check-views.js",
46
46
  "tetra-doctor": "./bin/tetra-doctor.js",
47
47
  "tetra-style-audit": "./bin/tetra-style-audit.js",
48
- "tetra-init-smoke": "./bin/tetra-init-smoke.js"
48
+ "tetra-init-smoke": "./bin/tetra-init-smoke.js",
49
+ "tetra-license": "./bin/tetra-license.js"
49
50
  },
50
51
  "files": [
51
52
  "bin/",