chrome-ext-ci-cd 1.0.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/lib/init.js ADDED
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { parseArgs } = require('node:util');
6
+
7
+ // --- Constants ---
8
+
9
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
10
+
11
+ const FILES_TO_SCAFFOLD = [
12
+ { src: 'release.yml', dest: '.github/workflows/release.yml', label: 'GitHub Actions workflow' },
13
+ { src: 'extensionignore', dest: '.extensionignore', label: 'Extension ignore patterns' },
14
+ { src: 'nvmrc', dest: '.nvmrc', label: 'Node version file' },
15
+ ];
16
+
17
+ const SCRIPTS_TO_INJECT = {
18
+ 'ext:validate': 'chrome-ext-ci-cd validate',
19
+ 'ext:package': 'chrome-ext-ci-cd package',
20
+ 'ext:release:patch': 'chrome-ext-ci-cd release patch',
21
+ 'ext:release:minor': 'chrome-ext-ci-cd release minor',
22
+ 'ext:release:major': 'chrome-ext-ci-cd release major',
23
+ };
24
+
25
+ // --- Pure Init Function ---
26
+
27
+ function init(projectDir, options) {
28
+ var dir = projectDir || process.cwd();
29
+ var opts = options || {};
30
+ var errors = [];
31
+ var warnings = [];
32
+ var created = [];
33
+ var skipped = [];
34
+ var scriptsAdded = [];
35
+ var scriptsSkipped = [];
36
+
37
+ // --- File scaffolding ---
38
+ for (var i = 0; i < FILES_TO_SCAFFOLD.length; i++) {
39
+ var file = FILES_TO_SCAFFOLD[i];
40
+ var srcPath = path.join(TEMPLATE_DIR, file.src);
41
+ var destPath = path.join(dir, file.dest);
42
+
43
+ // Verify template source exists
44
+ if (!fs.existsSync(srcPath)) {
45
+ errors.push('Template not found: ' + file.src + '. Package may be corrupted.');
46
+ continue;
47
+ }
48
+
49
+ // Check if dest already exists
50
+ if (fs.existsSync(destPath) && !opts.force) {
51
+ skipped.push(file.dest);
52
+ warnings.push('Skipped: ' + file.dest + ' (already exists)');
53
+ continue;
54
+ }
55
+
56
+ // Create parent directories and copy
57
+ try {
58
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
59
+ fs.copyFileSync(srcPath, destPath);
60
+ created.push(file.dest);
61
+ } catch (e) {
62
+ errors.push('Failed to create ' + file.dest + ': ' + e.message);
63
+ }
64
+ }
65
+
66
+ // --- npm scripts injection ---
67
+ var packageJsonPath = path.join(dir, 'package.json');
68
+ if (!fs.existsSync(packageJsonPath)) {
69
+ warnings.push('No package.json found. Skipping npm scripts injection. Run \'npm init\' first.');
70
+ } else {
71
+ try {
72
+ var pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
73
+ if (!pkg.scripts) {
74
+ pkg.scripts = {};
75
+ }
76
+
77
+ var keys = Object.keys(SCRIPTS_TO_INJECT);
78
+ for (var j = 0; j < keys.length; j++) {
79
+ var key = keys[j];
80
+ if (pkg.scripts[key] !== undefined && !opts.force) {
81
+ scriptsSkipped.push(key);
82
+ warnings.push('Skipped script: ' + key + ' (already defined)');
83
+ } else {
84
+ pkg.scripts[key] = SCRIPTS_TO_INJECT[key];
85
+ scriptsAdded.push(key);
86
+ }
87
+ }
88
+
89
+ // Only write if scripts were added
90
+ if (scriptsAdded.length > 0) {
91
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
92
+ }
93
+ } catch (e) {
94
+ errors.push('Failed to update package.json: ' + e.message);
95
+ }
96
+ }
97
+
98
+ return {
99
+ success: errors.length === 0,
100
+ errors: errors,
101
+ warnings: warnings,
102
+ data: {
103
+ created: created,
104
+ skipped: skipped,
105
+ scriptsAdded: scriptsAdded,
106
+ scriptsSkipped: scriptsSkipped
107
+ }
108
+ };
109
+ }
110
+
111
+ // --- Help ---
112
+
113
+ function printHelp() {
114
+ console.log('Usage: chrome-ext-ci-cd init [options]');
115
+ console.log('');
116
+ console.log('Scaffold CI/CD files into your Chrome extension project.');
117
+ console.log('');
118
+ console.log('Creates:');
119
+ console.log(' .github/workflows/release.yml GitHub Actions workflow');
120
+ console.log(' .extensionignore Extension ignore patterns');
121
+ console.log(' .nvmrc Node version file');
122
+ console.log('');
123
+ console.log('Injects npm scripts into package.json:');
124
+ console.log(' ext:validate, ext:package, ext:release:patch,');
125
+ console.log(' ext:release:minor, ext:release:major');
126
+ console.log('');
127
+ console.log('Options:');
128
+ console.log(' --force, -f Overwrite existing files');
129
+ console.log(' --help, -h Show this help message');
130
+ console.log('');
131
+ console.log('Examples:');
132
+ console.log(' chrome-ext-ci-cd init');
133
+ console.log(' chrome-ext-ci-cd init --force');
134
+ }
135
+
136
+ // --- CLI Runner ---
137
+
138
+ function run(args) {
139
+ var values;
140
+ try {
141
+ var parsed = parseArgs({
142
+ args: args,
143
+ options: {
144
+ force: { type: 'boolean', short: 'f', default: false },
145
+ help: { type: 'boolean', short: 'h', default: false },
146
+ },
147
+ allowPositionals: false,
148
+ });
149
+ values = parsed.values;
150
+ } catch (err) {
151
+ if (err.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
152
+ console.error('Unknown option: ' + err.message);
153
+ console.error('');
154
+ printHelp();
155
+ return 1;
156
+ }
157
+ throw err;
158
+ }
159
+
160
+ if (values.help) {
161
+ printHelp();
162
+ return 0;
163
+ }
164
+
165
+ var result = init(process.cwd(), { force: values.force });
166
+
167
+ // Print summary
168
+ console.log('chrome-ext-ci-cd init');
169
+ console.log('');
170
+
171
+ if (result.data.created.length > 0) {
172
+ console.log(' Created:');
173
+ for (var i = 0; i < FILES_TO_SCAFFOLD.length; i++) {
174
+ var file = FILES_TO_SCAFFOLD[i];
175
+ if (result.data.created.indexOf(file.dest) !== -1) {
176
+ console.log(' ' + file.dest.padEnd(38) + '(' + file.label + ')');
177
+ }
178
+ }
179
+ console.log('');
180
+ }
181
+
182
+ if (result.data.skipped.length > 0) {
183
+ console.log(' Skipped (already exist):');
184
+ for (var j = 0; j < result.data.skipped.length; j++) {
185
+ console.log(' ' + result.data.skipped[j]);
186
+ }
187
+ console.log('');
188
+ }
189
+
190
+ if (result.data.scriptsAdded.length > 0) {
191
+ console.log(' npm scripts added to package.json:');
192
+ console.log(' ' + result.data.scriptsAdded.join(', '));
193
+ console.log('');
194
+ }
195
+
196
+ if (result.data.scriptsSkipped.length > 0) {
197
+ console.log(' npm scripts skipped (already defined):');
198
+ console.log(' ' + result.data.scriptsSkipped.join(', '));
199
+ console.log('');
200
+ }
201
+
202
+ // Nothing to do
203
+ if (result.data.created.length === 0 && result.data.scriptsAdded.length === 0 && result.errors.length === 0) {
204
+ console.log(' Nothing to do. All files already exist. Use --force to overwrite.');
205
+ console.log('');
206
+ }
207
+
208
+ // Warnings
209
+ if (result.warnings.length > 0) {
210
+ for (var k = 0; k < result.warnings.length; k++) {
211
+ if (result.warnings[k].indexOf('Skipped') === -1) {
212
+ console.log(' WARN: ' + result.warnings[k]);
213
+ }
214
+ }
215
+ }
216
+
217
+ // Errors
218
+ if (result.errors.length > 0) {
219
+ for (var m = 0; m < result.errors.length; m++) {
220
+ console.error(' ERROR: ' + result.errors[m]);
221
+ }
222
+ return 1;
223
+ }
224
+
225
+ if (result.data.skipped.length > 0 && result.data.created.length > 0) {
226
+ console.log(' Tip: Use --force to overwrite existing files.');
227
+ }
228
+
229
+ return 0;
230
+ }
231
+
232
+ module.exports = { init, run };
package/lib/keygen.js ADDED
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const { parseArgs } = require('node:util');
4
+ const license = require('./license');
5
+
6
+ // --- Help ---
7
+
8
+ function printHelp() {
9
+ console.log('Usage: chrome-ext-ci-cd keygen [options]');
10
+ console.log('');
11
+ console.log('Generate a license key. Owner-only command.');
12
+ console.log('Requires KEYGEN_SECRET environment variable.');
13
+ console.log('');
14
+ console.log('Options:');
15
+ console.log(' --tier <name> License tier (e.g. starter, pro, team)');
16
+ console.log(' --customer <name> Customer or company name');
17
+ console.log(' --expires <YYYY-MM> Expiry month (e.g. 2027-03)');
18
+ console.log(' --help, -h Show this help message');
19
+ console.log('');
20
+ console.log('Examples:');
21
+ console.log(" KEYGEN_SECRET=... chrome-ext-ci-cd keygen --tier pro --customer 'Acme Corp' --expires 2027-06");
22
+ }
23
+
24
+ // --- CLI Runner ---
25
+
26
+ function run(args) {
27
+ var values;
28
+ try {
29
+ var parsed = parseArgs({
30
+ args: args,
31
+ options: {
32
+ tier: { type: 'string' },
33
+ customer: { type: 'string' },
34
+ expires: { type: 'string' },
35
+ help: { type: 'boolean', short: 'h', default: false },
36
+ },
37
+ allowPositionals: false,
38
+ });
39
+ values = parsed.values;
40
+ } catch (err) {
41
+ if (err.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
42
+ console.error('Unknown option: ' + err.message);
43
+ console.error('');
44
+ printHelp();
45
+ return 1;
46
+ }
47
+ throw err;
48
+ }
49
+
50
+ if (values.help) {
51
+ printHelp();
52
+ return 0;
53
+ }
54
+
55
+ // Check KEYGEN_SECRET env var
56
+ var secret = process.env.KEYGEN_SECRET;
57
+ if (!secret) {
58
+ console.error('KEYGEN_SECRET environment variable is required.');
59
+ console.error('Set it to the signing secret before generating keys.');
60
+ return 1;
61
+ }
62
+
63
+ // Validate secret matches the embedded HMAC_SECRET
64
+ if (secret !== license.HMAC_SECRET) {
65
+ console.error('KEYGEN_SECRET does not match the expected signing secret.');
66
+ return 1;
67
+ }
68
+
69
+ // Validate required flags
70
+ if (!values.tier) {
71
+ console.error('Missing --tier flag. Specify the license tier (e.g. --tier pro).');
72
+ return 1;
73
+ }
74
+
75
+ if (!values.customer) {
76
+ console.error('Missing --customer flag. Specify the customer name (e.g. --customer "Acme Corp").');
77
+ return 1;
78
+ }
79
+
80
+ if (!values.expires) {
81
+ console.error('Missing --expires flag. Specify expiry month (e.g. --expires 2027-03).');
82
+ return 1;
83
+ }
84
+
85
+ // Validate expires format
86
+ if (!/^\d{4}-\d{2}$/.test(values.expires)) {
87
+ console.error('Invalid --expires format. Use YYYY-MM (e.g. 2027-03).');
88
+ return 1;
89
+ }
90
+
91
+ // Generate key
92
+ var key = license.generateKey(secret, values.tier, values.customer, values.expires);
93
+
94
+ // Self-validate
95
+ var check = license.verifyKey(key, secret);
96
+ if (!check.valid) {
97
+ console.error('Internal error: generated key failed self-validation. ' + check.error);
98
+ return 1;
99
+ }
100
+
101
+ console.log(key);
102
+ return 0;
103
+ }
104
+
105
+ // --- Exports ---
106
+
107
+ module.exports = { run: run };
package/lib/license.js ADDED
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+
8
+ // --- Constants ---
9
+
10
+ var HMAC_SECRET = 'cecicd-v1-hmac-key';
11
+
12
+ // --- Config Paths ---
13
+
14
+ function getConfigDir() {
15
+ var base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
16
+ return path.join(base, 'chrome-ext-ci-cd');
17
+ }
18
+
19
+ // --- Key Operations ---
20
+
21
+ function generateKey(secret, tier, customer, expiresYYYYMM) {
22
+ var payload = { tier: tier, customer: customer, exp: expiresYYYYMM };
23
+ var json = JSON.stringify(payload);
24
+ var data = Buffer.from(json).toString('base64');
25
+ var signature = crypto.createHmac('sha256', secret).update(data).digest('hex');
26
+ return 'CECICD-' + data + '-' + signature;
27
+ }
28
+
29
+ function parseKey(keyString) {
30
+ var parts = keyString.split('-');
31
+ // Format: CECICD-{base64data}-{hex signature (64 chars)}
32
+ // The base64 data itself may not contain dashes, but the hex is always 64 chars
33
+ // Split strategy: first part is CECICD, last 64 chars hex part is signature, middle is data
34
+ if (parts.length < 3 || parts[0] !== 'CECICD') {
35
+ return { valid: false, error: 'Invalid key format. Expected: CECICD-{data}-{signature}' };
36
+ }
37
+
38
+ // The signature is always the last part (64-char hex string)
39
+ var signature = parts[parts.length - 1];
40
+ // The data is everything between CECICD- and the last -signature
41
+ var data = parts.slice(1, parts.length - 1).join('-');
42
+
43
+ if (!data || !signature) {
44
+ return { valid: false, error: 'Invalid key format. Expected: CECICD-{data}-{signature}' };
45
+ }
46
+
47
+ var decoded;
48
+ try {
49
+ decoded = Buffer.from(data, 'base64').toString('utf-8');
50
+ } catch (e) {
51
+ return { valid: false, error: 'Invalid key data: corrupted payload' };
52
+ }
53
+
54
+ var parsed;
55
+ try {
56
+ parsed = JSON.parse(decoded);
57
+ } catch (e) {
58
+ return { valid: false, error: 'Invalid key data: corrupted payload' };
59
+ }
60
+
61
+ if (!parsed.tier || !parsed.customer || !parsed.exp) {
62
+ return { valid: false, error: 'Invalid key data: missing required fields' };
63
+ }
64
+
65
+ return { valid: true, data: data, signature: signature, payload: parsed };
66
+ }
67
+
68
+ function verifyKey(keyString, secret) {
69
+ var parsed = parseKey(keyString);
70
+ if (!parsed.valid) {
71
+ return parsed;
72
+ }
73
+
74
+ var expected = crypto.createHmac('sha256', secret).update(parsed.data).digest('hex');
75
+
76
+ var sigBuf = Buffer.from(parsed.signature, 'hex');
77
+ var expBuf = Buffer.from(expected, 'hex');
78
+
79
+ if (sigBuf.length !== expBuf.length) {
80
+ return { valid: false, error: 'Invalid key signature' };
81
+ }
82
+
83
+ if (!crypto.timingSafeEqual(sigBuf, expBuf)) {
84
+ return { valid: false, error: 'Invalid key signature' };
85
+ }
86
+
87
+ // Check expiry
88
+ var now = new Date().toISOString().slice(0, 7);
89
+ if (now > parsed.payload.exp) {
90
+ return {
91
+ valid: false,
92
+ error: 'License expired (valid until ' + parsed.payload.exp + '). Renew your license:\n\n chrome-ext-ci-cd activate <new-key>',
93
+ expired: true,
94
+ payload: parsed.payload
95
+ };
96
+ }
97
+
98
+ return { valid: true, payload: parsed.payload };
99
+ }
100
+
101
+ // --- Trial Management ---
102
+
103
+ function readTrial() {
104
+ var trialPath = path.join(getConfigDir(), 'trial');
105
+ if (!fs.existsSync(trialPath)) {
106
+ return null;
107
+ }
108
+ return fs.readFileSync(trialPath, 'utf-8').trim();
109
+ }
110
+
111
+ function startTrial() {
112
+ var configDir = getConfigDir();
113
+ fs.mkdirSync(configDir, { recursive: true });
114
+ var today = new Date().toISOString().slice(0, 10);
115
+ var trialPath = path.join(configDir, 'trial');
116
+ try {
117
+ fs.writeFileSync(trialPath, today + '\n', { flag: 'wx' });
118
+ } catch (e) {
119
+ if (e.code === 'EEXIST') {
120
+ return fs.readFileSync(trialPath, 'utf-8').trim();
121
+ }
122
+ throw e;
123
+ }
124
+ return today;
125
+ }
126
+
127
+ function isTrialValid(startDate) {
128
+ var start = new Date(startDate);
129
+ var now = new Date();
130
+ var diffMs = now.getTime() - start.getTime();
131
+ var diffDays = diffMs / (1000 * 60 * 60 * 24);
132
+ return diffDays <= 7;
133
+ }
134
+
135
+ // --- License Check ---
136
+
137
+ function readLicense() {
138
+ var licensePath = path.join(getConfigDir(), 'license');
139
+ if (!fs.existsSync(licensePath)) {
140
+ return null;
141
+ }
142
+ return fs.readFileSync(licensePath, 'utf-8').trim();
143
+ }
144
+
145
+ function checkLicense() {
146
+ var licenseKey = readLicense();
147
+
148
+ if (licenseKey) {
149
+ var result = verifyKey(licenseKey, HMAC_SECRET);
150
+ if (result.valid) {
151
+ return {
152
+ valid: true,
153
+ tier: result.payload.tier,
154
+ customer: result.payload.customer,
155
+ exp: result.payload.exp
156
+ };
157
+ }
158
+ if (result.expired) {
159
+ return { valid: false, message: result.error };
160
+ }
161
+ // Invalid license — fall through to trial check
162
+ }
163
+
164
+ // No license or invalid license — check trial
165
+ var trialStart = readTrial();
166
+
167
+ if (!trialStart) {
168
+ var started = startTrial();
169
+ return { valid: true, trial: true, daysLeft: 7 };
170
+ }
171
+
172
+ if (isTrialValid(trialStart)) {
173
+ var daysLeft = Math.ceil(7 - (Date.now() - new Date(trialStart).getTime()) / 86400000);
174
+ if (daysLeft < 1) {
175
+ daysLeft = 1;
176
+ }
177
+ return { valid: true, trial: true, daysLeft: daysLeft };
178
+ }
179
+
180
+ return {
181
+ valid: false,
182
+ message: 'Trial expired. Activate a license to continue:\n\n chrome-ext-ci-cd activate <key>'
183
+ };
184
+ }
185
+
186
+ // --- Exports ---
187
+
188
+ module.exports = {
189
+ getConfigDir: getConfigDir,
190
+ generateKey: generateKey,
191
+ parseKey: parseKey,
192
+ verifyKey: verifyKey,
193
+ checkLicense: checkLicense,
194
+ startTrial: startTrial,
195
+ readTrial: readTrial,
196
+ isTrialValid: isTrialValid,
197
+ readLicense: readLicense,
198
+ HMAC_SECRET: HMAC_SECRET
199
+ };