script-runner-kit 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -58,12 +58,18 @@ Example `script-runner.config.json`:
58
58
  {
59
59
  "port": 8088,
60
60
  "auditDir": ".script-audit-logs",
61
- "authTokens": ["global-secret-a", "global-secret-b"],
61
+ "akSk": [
62
+ { "ak": "global-bot-v2", "sk": "global-secret-a" },
63
+ { "ak": "global-bot-v1", "sk": "global-secret-b" }
64
+ ],
62
65
  "scripts": {
63
66
  "check-update": {
64
67
  "scriptPath": "./scripts/check-update.sh",
65
68
  "rootDir": ".",
66
- "authTokens": ["check-secret-a", "check-secret-b"]
69
+ "akSk": [
70
+ { "ak": "check-bot-v2", "sk": "check-secret-a" },
71
+ { "ak": "check-bot-v1", "sk": "check-secret-b" }
72
+ ]
67
73
  }
68
74
  }
69
75
  }
@@ -75,7 +81,7 @@ Notes:
75
81
  - Each script supports either:
76
82
  - `scriptPath` (execute via `bash <scriptPath>`)
77
83
  - or `command` + optional `args`.
78
- - Auth uses JWT and supports multiple secrets per script via `authTokens`.
84
+ - Auth uses JWT and supports AK/SK pairs per script via `akSk`.
79
85
 
80
86
  ### Auto package.json scripts
81
87
 
@@ -94,11 +100,18 @@ Every API call requires a JWT token. Supported token sources:
94
100
  - `x-runner-token: <token>`
95
101
  - query parameter `?token=<token>` (convenient for EventSource demos)
96
102
 
103
+ Generate your JWT at `https://jwt.io` using:
104
+
105
+ - Header: `{"alg":"HS256","typ":"JWT"}`
106
+ - Payload example: `{"sub":"gitai","ak":"check-bot-v2","script":"check-update"}`
107
+ - Secret: use the SK mapped to that AK in your config
108
+
97
109
  Verification rules:
98
110
 
99
111
  - Uses `jsonwebtoken.verify()` with algorithms `HS256/HS384/HS512`
100
- - Supports multiple secrets per script (`authTokens` array), useful for key rotation
101
- - If a script has no local `authTokens`, top-level `authTokens` is used as fallback
112
+ - Supports multiple credentials per script (`akSk` array), useful for key rotation
113
+ - If a script has no local `akSk`, top-level `akSk` is used as fallback
114
+ - Legacy `authTokens` is still accepted for backward compatibility
102
115
 
103
116
  ## HTTP API
104
117
 
@@ -10,16 +10,22 @@ Supported sources (priority order):
10
10
  2. `x-runner-token: <token>`
11
11
  3. Query param `?token=<token>`
12
12
 
13
- ## Config example
13
+ ## Config example (AK/SK)
14
14
 
15
15
  ```json
16
16
  {
17
- "authTokens": ["global-secret-a", "global-secret-b"],
17
+ "akSk": [
18
+ { "ak": "global-bot-v2", "sk": "global-secret-a" },
19
+ { "ak": "global-bot-v1", "sk": "global-secret-b" }
20
+ ],
18
21
  "scripts": {
19
22
  "deploy": {
20
23
  "command": "npm",
21
24
  "args": ["run", "deploy"],
22
- "authTokens": ["deploy-secret-v2", "deploy-secret-v1"]
25
+ "akSk": [
26
+ { "ak": "deploy-bot-v2", "sk": "deploy-secret-v2" },
27
+ { "ak": "deploy-bot-v1", "sk": "deploy-secret-v1" }
28
+ ]
23
29
  },
24
30
  "build": {
25
31
  "command": "npm",
@@ -29,15 +35,58 @@ Supported sources (priority order):
29
35
  }
30
36
  ```
31
37
 
32
- - Script-level `authTokens` are preferred.
33
- - If missing, top-level `authTokens` is used.
38
+ - Script-level `akSk` are preferred.
39
+ - If missing, top-level `akSk` is used.
40
+
41
+ > Backward compatibility: legacy `authTokens` (string array) is still supported.
34
42
 
35
43
  ## Key rotation
36
44
 
37
- Use multiple secrets and keep new secret first:
45
+ Use multiple AK/SK pairs and keep new credential first:
38
46
 
39
47
  ```json
40
- "authTokens": ["new-secret", "old-secret"]
48
+ "akSk": [
49
+ { "ak": "deploy-bot-v3", "sk": "new-secret" },
50
+ { "ak": "deploy-bot-v2", "sk": "old-secret" }
51
+ ]
41
52
  ```
42
53
 
43
- The runner tries each secret until verification succeeds.
54
+ The runner tries matching AK first (from JWT claim `ak`), then falls back to all SKs.
55
+
56
+ ## Generate token (jwt.io)
57
+
58
+ Use `https://jwt.io` and sign with HS256.
59
+
60
+ - Header:
61
+
62
+ ```json
63
+ { "alg": "HS256", "typ": "JWT" }
64
+ ```
65
+
66
+ - Payload example:
67
+
68
+ ```json
69
+ { "sub": "gitai", "ak": "deploy-bot-v2", "script": "deploy" }
70
+ ```
71
+
72
+ ### Payload fields (recommended)
73
+
74
+ - `sub` (string): caller identity, for audit display (for example `gitai`)
75
+ - `ak` (string): AK name from your `akSk` config (for example `deploy-bot-v2`)
76
+ - `script` (string): target script name (for example `deploy`)
77
+ - `exp` (number, optional): UNIX timestamp expiration (or set via jwt.io expiration UI)
78
+
79
+ Example payload with expiration:
80
+
81
+ ```json
82
+ {
83
+ "sub": "gitai",
84
+ "ak": "deploy-bot-v2",
85
+ "script": "deploy",
86
+ "exp": 1893456000
87
+ }
88
+ ```
89
+
90
+ - Secret: use the SK corresponding to `deploy-bot-v2` in your `akSk` config.
91
+
92
+ Then send the generated JWT via `Authorization: Bearer <token>` (or `x-runner-token` / `?token=`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "script-runner-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Run project scripts via webhook-friendly HTTP endpoints",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "port": 8088,
3
3
  "auditDir": ".script-audit-logs",
4
- "authTokens": [
5
- "replace-with-strong-jwt-secret-1",
6
- "replace-with-strong-jwt-secret-2"
4
+ "akSk": [
5
+ {
6
+ "ak": "global-bot-v2",
7
+ "sk": "replace-with-strong-jwt-secret-1"
8
+ },
9
+ {
10
+ "ak": "global-bot-v1",
11
+ "sk": "replace-with-strong-jwt-secret-2"
12
+ }
7
13
  ],
8
14
  "scripts": {
9
15
  "check-update": {
package/src/config.js CHANGED
@@ -97,7 +97,7 @@ function loadJsConfig(configPath) {
97
97
  return loaded;
98
98
  }
99
99
 
100
- function normalizeAuthTokens(value, fieldPath) {
100
+ function normalizeLegacyAuthTokens(value, fieldPath) {
101
101
  if (!Array.isArray(value)) {
102
102
  throw new Error(`${fieldPath} must be an array of strings`);
103
103
  }
@@ -105,7 +105,41 @@ function normalizeAuthTokens(value, fieldPath) {
105
105
  if (tokens.length === 0) {
106
106
  throw new Error(`${fieldPath} must contain at least one non-empty token`);
107
107
  }
108
- return tokens;
108
+ return tokens.map((sk, index) => ({ ak: `legacy-${index + 1}`, sk }));
109
+ }
110
+
111
+ function normalizeAkSk(value, fieldPath) {
112
+ if (!Array.isArray(value)) {
113
+ throw new Error(`${fieldPath} must be an array of { ak, sk } objects`);
114
+ }
115
+
116
+ const entries = value.map((item, index) => {
117
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
118
+ throw new Error(`${fieldPath}[${index}] must be an object with ak and sk`);
119
+ }
120
+ const ak = String(item.ak || "").trim();
121
+ const sk = String(item.sk || "").trim();
122
+ if (!ak || !sk) {
123
+ throw new Error(`${fieldPath}[${index}] must provide non-empty ak and sk`);
124
+ }
125
+ return { ak, sk };
126
+ });
127
+
128
+ if (entries.length === 0) {
129
+ throw new Error(`${fieldPath} must contain at least one credential`);
130
+ }
131
+
132
+ return entries;
133
+ }
134
+
135
+ function readAkSkField(source, akSkFieldPath, legacyFieldPath) {
136
+ if (source.akSk !== undefined) {
137
+ return normalizeAkSk(source.akSk, akSkFieldPath);
138
+ }
139
+ if (source.authTokens !== undefined) {
140
+ return normalizeLegacyAuthTokens(source.authTokens, legacyFieldPath);
141
+ }
142
+ return undefined;
109
143
  }
110
144
 
111
145
  function normalizeScriptFromConfig(configDir, scriptName, scriptConfig) {
@@ -119,16 +153,17 @@ function normalizeScriptFromConfig(configDir, scriptName, scriptConfig) {
119
153
  ? scriptConfig.rootDir
120
154
  : "."
121
155
  );
122
- const authTokens =
123
- scriptConfig.authTokens === undefined
124
- ? undefined
125
- : normalizeAuthTokens(scriptConfig.authTokens, `scripts.${scriptName}.authTokens`);
156
+ const akSk = readAkSkField(
157
+ scriptConfig,
158
+ `scripts.${scriptName}.akSk`,
159
+ `scripts.${scriptName}.authTokens`
160
+ );
126
161
 
127
162
  if (typeof scriptConfig.scriptPath === "string" && scriptConfig.scriptPath.trim()) {
128
163
  return {
129
164
  rootDir: resolvedRootDir,
130
165
  scriptPath: path.resolve(configDir, scriptConfig.scriptPath),
131
- authTokens,
166
+ akSk,
132
167
  };
133
168
  }
134
169
 
@@ -146,7 +181,7 @@ function normalizeScriptFromConfig(configDir, scriptName, scriptConfig) {
146
181
  rootDir: resolvedRootDir,
147
182
  command: scriptConfig.command,
148
183
  args,
149
- authTokens,
184
+ akSk,
150
185
  };
151
186
  }
152
187
 
@@ -200,10 +235,7 @@ function resolveScriptConfig(configPath, rawConfig, cwd) {
200
235
  }
201
236
 
202
237
  const configDir = path.dirname(configPath);
203
- const globalAuthTokens =
204
- rawConfig.authTokens === undefined
205
- ? undefined
206
- : normalizeAuthTokens(rawConfig.authTokens, "authTokens");
238
+ const globalAkSk = readAkSkField(rawConfig, "akSk", "authTokens");
207
239
  const scripts = rawConfig.scripts || {};
208
240
  if (typeof scripts !== "object" || Array.isArray(scripts)) {
209
241
  throw new Error("Config field 'scripts' must be an object when provided");
@@ -232,15 +264,15 @@ function resolveScriptConfig(configPath, rawConfig, cwd) {
232
264
  }
233
265
 
234
266
  for (const [scriptName, scriptConfig] of Object.entries(resolvedScripts)) {
235
- if (!Array.isArray(scriptConfig.authTokens) || scriptConfig.authTokens.length === 0) {
236
- if (globalAuthTokens && globalAuthTokens.length > 0) {
267
+ if (!Array.isArray(scriptConfig.akSk) || scriptConfig.akSk.length === 0) {
268
+ if (globalAkSk && globalAkSk.length > 0) {
237
269
  resolvedScripts[scriptName] = {
238
270
  ...scriptConfig,
239
- authTokens: globalAuthTokens,
271
+ akSk: globalAkSk,
240
272
  };
241
273
  } else {
242
274
  throw new Error(
243
- `Auth required for scripts.${scriptName}. Add scripts.${scriptName}.authTokens or top-level authTokens`
275
+ `Auth required for scripts.${scriptName}. Add scripts.${scriptName}.akSk (or legacy authTokens) or top-level akSk`
244
276
  );
245
277
  }
246
278
  }
package/src/server.js CHANGED
@@ -51,13 +51,24 @@ function readTokenFromRequest(req, url) {
51
51
  return "";
52
52
  }
53
53
 
54
- function verifyJwtWithSecrets(token, secrets) {
55
- for (const secret of secrets) {
54
+ function verifyJwtWithAkSk(token, akSkList) {
55
+ const decoded = jwt.decode(token);
56
+ const hintedAk =
57
+ decoded && typeof decoded === "object" && typeof decoded.ak === "string"
58
+ ? decoded.ak.trim()
59
+ : "";
60
+
61
+ const preferred = hintedAk
62
+ ? akSkList.filter((entry) => entry.ak === hintedAk)
63
+ : [];
64
+ const candidates = preferred.length > 0 ? preferred : akSkList;
65
+
66
+ for (const credential of candidates) {
56
67
  try {
57
- const payload = jwt.verify(token, secret, {
68
+ const payload = jwt.verify(token, credential.sk, {
58
69
  algorithms: ["HS256", "HS384", "HS512"],
59
70
  });
60
- return { ok: true, payload };
71
+ return { ok: true, payload, ak: credential.ak };
61
72
  } catch (err) {
62
73
  continue;
63
74
  }
@@ -145,7 +156,16 @@ function renderHome(scripts) {
145
156
  <li>Trigger: <code>GET /api/&lt;script-name&gt;</code> (for example <code>/api/check</code>)</li>
146
157
  <li>Auto-loads scripts from local <code>package.json</code> (config entries take precedence on name conflicts)</li>
147
158
  <li>Auth: send JWT via <code>Authorization: Bearer &lt;token&gt;</code>, <code>x-runner-token</code>, or <code>?token=</code></li>
159
+ <li>AK/SK mode: generate JWT at <a href="https://jwt.io" target="_blank" rel="noopener noreferrer">jwt.io</a> using your script AK's SK</li>
148
160
  </ul>
161
+ <h3>jwt.io payload guide</h3>
162
+ <p>Use HS256 and sign with the SK for your AK. Suggested payload:</p>
163
+ <pre>{
164
+ "sub": "gitai",
165
+ "ak": "&lt;your-ak&gt;",
166
+ "script": "&lt;script-name&gt;",
167
+ "exp": 1893456000
168
+ }</pre>
149
169
  <label>script:</label>
150
170
  <select id="name">${options}</select>
151
171
  <label>jwt token:</label>
@@ -225,7 +245,7 @@ function createServer({ scripts, auditDir }) {
225
245
  return;
226
246
  }
227
247
 
228
- const authResult = verifyJwtWithSecrets(token, scriptConfig.authTokens || []);
248
+ const authResult = verifyJwtWithAkSk(token, scriptConfig.akSk || []);
229
249
  if (!authResult.ok) {
230
250
  send(res, 403, "Forbidden: invalid token");
231
251
  return;
@@ -323,6 +343,40 @@ function createServer({ scripts, auditDir }) {
323
343
  function startServer({ scripts, auditDir, port }) {
324
344
  const server = createServer({ scripts, auditDir });
325
345
  server.listen(port, () => {
346
+ const lines = [];
347
+ lines.push("=== Script Runner Effective Config ===");
348
+ lines.push(`Port: ${port}`);
349
+ lines.push(`Audit dir: ${auditDir}`);
350
+ lines.push(`Scripts: ${Object.keys(scripts).length}`);
351
+ lines.push("");
352
+ lines.push("Configured scripts:");
353
+
354
+ for (const [scriptName, script] of Object.entries(scripts)) {
355
+ const runTarget = script.scriptPath
356
+ ? `bash ${script.scriptPath}`
357
+ : `${script.command || ""} ${Array.isArray(script.args) ? script.args.join(" ") : ""}`.trim();
358
+ lines.push(`- ${scriptName}`);
359
+ lines.push(` rootDir : ${script.rootDir}`);
360
+ lines.push(` run : ${runTarget}`);
361
+
362
+ if (Array.isArray(script.akSk) && script.akSk.length > 0) {
363
+ lines.push(" secrets :");
364
+ for (const item of script.akSk) {
365
+ lines.push(` - ${item.ak} => ${item.sk}`);
366
+ }
367
+ } else {
368
+ lines.push(" secrets : (none)");
369
+ }
370
+ }
371
+
372
+ lines.push("");
373
+ lines.push("JWT generation (recommended): https://jwt.io");
374
+ lines.push("Header : {\"alg\":\"HS256\",\"typ\":\"JWT\"}");
375
+ lines.push("Payload: {\"sub\":\"<user>\",\"ak\":\"<ak>\",\"script\":\"<script-name>\"}");
376
+ lines.push("Secret : use the SK mapped to that AK in your config");
377
+ lines.push("======================================");
378
+
379
+ process.stdout.write(`${lines.join("\n")}\n`);
326
380
  process.stdout.write(`Server listening on http://0.0.0.0:${port}\n`);
327
381
  });
328
382
  return server;