@cwe-platform/plugin-cli 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
@@ -4,25 +4,32 @@ The CWE plugin developer loop. Thin by design: `build` runs your plugin's own ts
4
4
  `validate` talk to a dev runtime's harness endpoints.
5
5
 
6
6
  ```bash
7
+ npx @cwe-platform/plugin-cli login # staff login → saves devToken into cwe-plugin.json
7
8
  npx @cwe-platform/plugin-cli build # tsup + local validateManifest (doctor-lite)
8
9
  npx @cwe-platform/plugin-cli push # build once + sideload to the dev runtime
9
10
  npx @cwe-platform/plugin-cli dev # watch src/ → rebuild → hot-reload sideload
10
11
  npx @cwe-platform/plugin-cli validate # full plugin doctor on the runtime
11
12
  ```
12
13
 
13
- Config — `cwe-plugin.json` in your plugin repo:
14
+ Config — `cwe-plugin.json` in your plugin repo (`login` fills in `devToken` for you):
14
15
 
15
16
  ```json
16
17
  {
17
18
  "runtimeUrl": "http://localhost:3000",
18
19
  "pluginKey": "player-favorites",
19
- "devToken": "<staff bearer token>",
20
20
  "bundle": "dist/index.js"
21
21
  }
22
22
  ```
23
23
 
24
+ ## Auth
25
+
26
+ `devToken` is a staff access token holding the `plugin:manage` permission. `cwe-plugin login`
27
+ prompts for staff credentials, calls `POST /staff/auth/login` on the runtime and stores the
28
+ token. Tokens are short-lived (default 15 min); for long `dev` sessions export
29
+ `CWE_STAFF_EMAIL` / `CWE_STAFF_PASSWORD` and the watcher re-logs-in automatically on 401.
30
+
24
31
  The sideload endpoint (`PUT /dev/plugins/:key/bundle`) exists only on runtimes with the dev
25
- harness enabled (`docker compose -f docker-compose.plugindev.yml up` in the platform repo);
26
- production runtimes reject the harness at config time, so the endpoint 404s there.
32
+ harness enabled; production runtimes reject the harness at config time, so the endpoint 404s
33
+ there. Ask your CWE platform contact for a dev runtime.
27
34
 
28
35
  MIT © CasinoWebEngine
@@ -9,13 +9,63 @@
9
9
  * Config: cwe-plugin.json (runtimeUrl, pluginKey, devToken, bundle?).
10
10
  */
11
11
  import { watch } from "node:fs";
12
+ import { createInterface } from "node:readline";
12
13
  import { pathToFileURL } from "node:url";
13
14
  import { resolve } from "node:path";
14
15
  import { Command } from "commander";
15
- import { fetchTrace, loadConfig, runBuild, sideload, validateOnRuntime } from "../src/lib.mjs";
16
+ import {
17
+ fetchTrace,
18
+ loadConfig,
19
+ reloginFromEnv,
20
+ runBuild,
21
+ saveDevToken,
22
+ sideload,
23
+ staffLogin,
24
+ validateOnRuntime,
25
+ } from "../src/lib.mjs";
16
26
 
17
27
  const program = new Command();
18
- program.name("cwe-plugin").description("CasinoWebEngine plugin developer CLI").version("0.1.0");
28
+ program.name("cwe-plugin").description("CasinoWebEngine plugin developer CLI").version("0.1.1");
29
+
30
+ /** Interactive prompt; `hidden` masks the input (password entry). */
31
+ function prompt(question, { hidden = false } = {}) {
32
+ return new Promise((resolvePrompt) => {
33
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34
+ if (hidden) {
35
+ // Mask by rewriting the line with asterisks on every keypress echo.
36
+ const write = rl._writeToOutput.bind(rl);
37
+ rl._writeToOutput = (str) => {
38
+ if (str.includes(question)) write(str);
39
+ else write("*");
40
+ };
41
+ }
42
+ rl.question(question, (answer) => {
43
+ rl.close();
44
+ if (hidden) process.stdout.write("\n");
45
+ resolvePrompt(answer.trim());
46
+ });
47
+ });
48
+ }
49
+
50
+ program
51
+ .command("login")
52
+ .description("staff login against the dev runtime; saves devToken into cwe-plugin.json")
53
+ .option("--email <email>", "staff email (prompted if omitted)")
54
+ .option("--password <password>", "staff password (prompted if omitted — prefer the prompt: flags land in shell history)")
55
+ .action(async (opts) => {
56
+ const config = await loadConfig();
57
+ const email = opts.email ?? (await prompt("staff email: "));
58
+ const password = opts.password ?? (await prompt("staff password: ", { hidden: true }));
59
+ const token = await staffLogin(config.runtimeUrl, email, password);
60
+ const path = await saveDevToken(token);
61
+ console.log(` ✓ logged in — devToken saved to ${path}`);
62
+ console.log(
63
+ " ℹ staff tokens are short-lived (default 15 min). For long `dev` sessions, export",
64
+ );
65
+ console.log(
66
+ " CWE_STAFF_EMAIL / CWE_STAFF_PASSWORD and the watcher re-logs-in on 401 automatically.",
67
+ );
68
+ });
19
69
 
20
70
  async function validateLocal() {
21
71
  // Import the built bundle + the SDK's doctor-lite from the PLUGIN's own
@@ -58,13 +108,31 @@ program
58
108
  console.log(`\n${report.plugin}@${report.version}: doctor OK`);
59
109
  });
60
110
 
111
+ /** Sideload with one 401 → env-credential re-login retry. */
112
+ async function sideloadWithRelogin(config) {
113
+ try {
114
+ return await sideload(config);
115
+ } catch (error) {
116
+ if (error?.unauthorized && (await reloginFromEnv(config))) {
117
+ console.log(" ↻ devToken expired — re-logged-in from CWE_STAFF_EMAIL/PASSWORD");
118
+ return sideload(config);
119
+ }
120
+ if (error?.unauthorized) {
121
+ throw new Error(
122
+ "devToken missing/expired — run `cwe-plugin login` (or export CWE_STAFF_EMAIL/CWE_STAFF_PASSWORD for auto re-login)",
123
+ );
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+
61
129
  program
62
130
  .command("push")
63
131
  .description("build once and sideload to the dev runtime")
64
132
  .action(async () => {
65
133
  const config = await loadConfig();
66
134
  await runBuild();
67
- const result = await sideload(config);
135
+ const result = await sideloadWithRelogin(config);
68
136
  console.log(` ✓ sideloaded ${result.plugin}@${result.version} (dev channel, hot-reloaded)`);
69
137
  for (const warning of result.warnings ?? []) console.warn(` ⚠ ${warning}`);
70
138
  });
@@ -86,7 +154,9 @@ program
86
154
  const startedAt = Date.now();
87
155
  try {
88
156
  await runBuild();
89
- const result = await sideload(config);
157
+ // 401s re-login from env credentials and retry once, so the 15-min
158
+ // token TTL never interrupts a watch session.
159
+ const result = await sideloadWithRelogin(config);
90
160
  console.log(
91
161
  ` ✓ ${result.plugin}@${result.version} reloaded in ${Date.now() - startedAt}ms`,
92
162
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cwe-platform/plugin-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CasinoWebEngine plugin developer CLI — build, validate and hot-reload plugins against a dev runtime.",
5
5
  "license": "MIT",
6
6
  "private": false,
package/src/lib.mjs CHANGED
@@ -4,10 +4,12 @@
4
4
  * Plain ESM JS — the CLI ships source, no build step of its own.
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { readFile } from "node:fs/promises";
7
+ import { readFile, writeFile } from "node:fs/promises";
8
8
  import { spawn } from "node:child_process";
9
9
  import { join, resolve } from "node:path";
10
10
 
11
+ export const STAFF_ACCESS_COOKIE = "cwe_staff_access_token";
12
+
11
13
  /** cwe-plugin.json: { runtimeUrl, devToken, pluginKey, bundle? } */
12
14
  export async function loadConfig(cwd = process.cwd()) {
13
15
  const path = join(cwd, "cwe-plugin.json");
@@ -60,11 +62,76 @@ async function api(config, method, path, body) {
60
62
  }
61
63
  if (!response.ok) {
62
64
  const message = json?.error?.message ?? json?.message ?? text.slice(0, 300);
63
- throw new Error(`${method} ${path} → HTTP ${response.status}: ${message}`);
65
+ const error = new Error(`${method} ${path} → HTTP ${response.status}: ${message}`);
66
+ // Marker for callers: 401 = missing/expired devToken → try a re-login.
67
+ error.unauthorized = response.status === 401;
68
+ throw error;
64
69
  }
65
70
  return json;
66
71
  }
67
72
 
73
+ /**
74
+ * Staff login → staff access token. The API deliberately never returns the
75
+ * JWT in the response body — it arrives as the HttpOnly staff cookie — so
76
+ * this parses it out of Set-Cookie. Tokens are tenant-bound and short-lived
77
+ * (AUTH_ACCESS_TOKEN_TTL_SECONDS, default 15 min).
78
+ */
79
+ export async function staffLogin(runtimeUrl, email, password) {
80
+ const response = await fetch(`${runtimeUrl.replace(/\/$/, "")}/staff/auth/login`, {
81
+ method: "POST",
82
+ headers: { "content-type": "application/json" },
83
+ body: JSON.stringify({ email, password }),
84
+ });
85
+ if (!response.ok) {
86
+ let message = `HTTP ${response.status}`;
87
+ try {
88
+ const json = await response.json();
89
+ message = json?.error?.message ?? json?.message ?? message;
90
+ } catch {
91
+ // keep the status-only message
92
+ }
93
+ throw new Error(`staff login failed: ${message}`);
94
+ }
95
+ const cookies = response.headers.getSetCookie?.() ?? [];
96
+ const tokenCookie = cookies.find((c) => c.startsWith(`${STAFF_ACCESS_COOKIE}=`));
97
+ if (!tokenCookie) {
98
+ throw new Error("login succeeded but the staff access token cookie was not returned");
99
+ }
100
+ const token = tokenCookie.split(";")[0].slice(STAFF_ACCESS_COOKIE.length + 1);
101
+ if (!token) throw new Error("staff access token cookie was empty");
102
+ return token;
103
+ }
104
+
105
+ /** Persist the devToken into cwe-plugin.json (preserving other fields). */
106
+ export async function saveDevToken(token, cwd = process.cwd()) {
107
+ const path = join(cwd, "cwe-plugin.json");
108
+ let config = {};
109
+ try {
110
+ config = JSON.parse(await readFile(path, "utf8"));
111
+ } catch {
112
+ throw new Error(`Missing cwe-plugin.json in ${cwd} — create it first (see README)`);
113
+ }
114
+ config.devToken = token;
115
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
116
+ return path;
117
+ }
118
+
119
+ /**
120
+ * Re-login with credentials from CWE_STAFF_EMAIL / CWE_STAFF_PASSWORD env
121
+ * vars (for unattended `dev` sessions — the 15-min token TTL otherwise
122
+ * interrupts the loop). Returns the fresh token, or null when the env vars
123
+ * are not both set.
124
+ */
125
+ export async function reloginFromEnv(config, env = process.env) {
126
+ const email = env.CWE_STAFF_EMAIL;
127
+ const password = env.CWE_STAFF_PASSWORD;
128
+ if (!email || !password) return null;
129
+ const token = await staffLogin(config.runtimeUrl, email, password);
130
+ await saveDevToken(token);
131
+ config.devToken = token;
132
+ return token;
133
+ }
134
+
68
135
  /** Upload the built bundle to the dev runtime (checksum-verified sideload). */
69
136
  export async function sideload(config, cwd = process.cwd()) {
70
137
  const bundlePath = resolve(cwd, config.bundle);