three-blocks-login 0.0.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.
Files changed (2) hide show
  1. package/bin/login.js +272 -0
  2. package/package.json +16 -0
package/bin/login.js ADDED
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ // Minimal, dependency-free CLI
3
+ // Node 18+ required (built-in fetch)
4
+
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ // Simple ANSI color helpers (no deps)
10
+ const ESC = (n) => `\u001b[${n}m`;
11
+ const reset = ESC(0);
12
+ const bold = (s) => ESC(1) + s + reset;
13
+ const dim = (s) => ESC(2) + s + reset;
14
+ const red = (s) => ESC(31) + s + reset;
15
+ const green = (s) => ESC(32) + s + reset;
16
+ const yellow = (s) => ESC(33) + s + reset;
17
+ const cyan = (s) => ESC(36) + s + reset;
18
+ const plainBanner = "[three-blocks-login]";
19
+ const banner = bold(cyan(plainBanner));
20
+
21
+ const args = parseArgs(process.argv.slice(2));
22
+
23
+ const SCOPE = (args.scope || "@three-blocks").replace(/^\s+|\s+$/g, "");
24
+
25
+ const MODE = (args.mode || "env").toLowerCase(); // env | project | user
26
+ const QUIET = !!args.quiet;
27
+ const VERBOSE = !!args.verbose;
28
+
29
+ // Load .env from current working directory (no deps)
30
+ loadEnvFromDotfile(process.cwd());
31
+
32
+ const BROKER_URL =
33
+ args.endpoint ||
34
+ process.env.THREE_BLOCKS_BROKER_URL ||
35
+ "http://localhost:3000/api/npm/token"; // your Astro broker endpoint
36
+
37
+ const LICENSE =
38
+ args.license || process.env.THREE_BLOCKS_SECRET_KEY || process.env.THREE_BLOCKS_LICENSE_KEY;
39
+
40
+ // Sanitize + validate license early to avoid header ByteString errors
41
+ const LICENSE_CLEAN = sanitizeLicense(LICENSE);
42
+ const invalidIdx = findFirstNonByteChar(LICENSE_CLEAN);
43
+ if (invalidIdx !== -1) {
44
+ if (VERBOSE) log.warn(`[debug] license contains non-ASCII/byte char at index ${invalidIdx}`);
45
+ fail("License appears malformed. Please copy your tb_… key exactly without extra characters.");
46
+ }
47
+ if (!looksLikeLicense(LICENSE_CLEAN)) {
48
+ if (VERBOSE) log.warn(`[debug] license failed format check: ${truncate(LICENSE_CLEAN, 16)}`);
49
+ fail("License appears malformed. Please copy your tb_… key exactly.");
50
+ }
51
+
52
+
53
+ // Pretty logger (respects --quiet except for errors)
54
+ const log = {
55
+ info: (msg) => { if (!QUIET) console.error(`${cyan("i")} ${msg}`); },
56
+ ok: (msg) => { if (!QUIET) console.error(`${green("✔")} ${msg}`); },
57
+ warn: (msg) => { if (!QUIET) console.error(`${yellow("⚠")} ${msg}`); },
58
+ error:(msg) => { console.error(`${red("✖")} ${msg}`); }
59
+ };
60
+
61
+ if (!LICENSE) {
62
+ fail(
63
+ "Missing license key. Provide --license <key> or set THREE_BLOCKS_SECRET_KEY/THREE_BLOCKS_LICENSE_KEY (env or .env)."
64
+ );
65
+ }
66
+
67
+ (async () => {
68
+ try {
69
+ const tokenData = await fetchToken(BROKER_URL, LICENSE_CLEAN);
70
+ const { registry, token, expiresAt } = tokenData;
71
+
72
+ if (!registry || !token) fail("Broker response missing registry/token.");
73
+
74
+ const u = new URL(ensureTrailingSlash(registry));
75
+ const hostPath = `${u.host}${u.pathname}`; // e.g. my-domain-.../npm/my-repo/
76
+
77
+ const npmrcContent = [
78
+ `${normalizeScope(SCOPE)}:registry=${u.href}`,
79
+ `//${hostPath}:_authToken=${token}`,
80
+ `//${hostPath}:always-auth=true`
81
+ ].join(os.EOL) + os.EOL;
82
+
83
+ if (MODE === "env") {
84
+ // Write to a temp .npmrc and export env so the *current shell* can reuse it
85
+ const tmpFile = path.join(
86
+ os.tmpdir(),
87
+ `three-blocks-${Date.now()}-${Math.random().toString(36).slice(2)}.npmrc`
88
+ );
89
+ fs.writeFileSync(tmpFile, npmrcContent, { mode: 0o600 });
90
+
91
+ // Print shell exports; caller should `eval "$(npx -y three-blocks-login --mode env)"`
92
+ const lines = [
93
+ `# ${plainBanner} configure npm for current shell`,
94
+ `# scope: ${SCOPE} | registry: ${u.href} | expires: ${expiresAt ?? "unknown"}`,
95
+ `export NPM_CONFIG_USERCONFIG="${tmpFile}"`,
96
+ `export npm_config_userconfig="${tmpFile}"`,
97
+ `echo "${plainBanner} ${SCOPE} -> ${u.href} (expires ${expiresAt ?? "unknown"})"`
98
+ ];
99
+ console.log(lines.join("\n"));
100
+ return;
101
+ }
102
+
103
+ if (MODE === "project") {
104
+ // Write to local ./.npmrc (recommend users gitignore this)
105
+ const out = path.resolve(process.cwd(), ".npmrc");
106
+ fs.writeFileSync(out, npmrcContent, { mode: 0o600 });
107
+ log.ok(`${banner} wrote ${bold(out)} ${dim(`(${SCOPE} → ${u.href}, expires ${expiresAt ?? "unknown"})`)}`);
108
+ return;
109
+ }
110
+
111
+ if (MODE === "user") {
112
+ // Update user's npm config (requires npm on PATH)
113
+ await run("npm", ["config", "set", `${normalizeScope(SCOPE)}:registry`, u.href]);
114
+ await run("npm", [
115
+ "config",
116
+ "set",
117
+ `//${hostPath}:_authToken`,
118
+ token
119
+ ]);
120
+ await run("npm", [
121
+ "config",
122
+ "set",
123
+ `//${hostPath}:always-auth`,
124
+ "true"
125
+ ]);
126
+ log.ok(`${banner} updated user npm config ${dim(`(${SCOPE} → ${u.href}, expires ${expiresAt ?? "unknown"})`)}`);
127
+ return;
128
+ }
129
+
130
+ fail(`Unknown --mode "${MODE}". Use env | project | user.`);
131
+ } catch (e) {
132
+ const msg = e?.message || String(e);
133
+ if (VERBOSE) {
134
+ log.error(`[debug] ${msg}`);
135
+ return process.exit(1);
136
+ }
137
+ if (/ByteString/i.test(msg) || /character at index/i.test(msg)) {
138
+ return fail("License appears malformed. Please copy your tb_… key exactly and retry.");
139
+ }
140
+ return fail("Request failed. Please try again. Use --verbose for details.");
141
+ }
142
+ })();
143
+
144
+ function normalizeScope(scope) {
145
+ return scope.startsWith("@") ? scope : `@${scope}`;
146
+ }
147
+
148
+ function ensureTrailingSlash(url) {
149
+ return url.endsWith("/") ? url : url + "/";
150
+ }
151
+
152
+ function loadEnvFromDotfile(dir) {
153
+ try {
154
+ const file = path.join(dir, ".env");
155
+ if (!fs.existsSync(file)) return;
156
+ const txt = fs.readFileSync(file, "utf8");
157
+ for (const raw of txt.split(/\r?\n/)) {
158
+ const line = raw.trim();
159
+ if (!line || line.startsWith("#")) continue;
160
+ const eq = line.indexOf("=");
161
+ if (eq === -1) continue;
162
+ const key = line.slice(0, eq).trim();
163
+ let val = line.slice(eq + 1).trim();
164
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
165
+ val = val.slice(1, -1);
166
+ }
167
+ if (process.env[key] === undefined) process.env[key] = val;
168
+ }
169
+ } catch {}
170
+ }
171
+
172
+ async function fetchToken(endpoint, license) {
173
+ const res = await fetch(endpoint, {
174
+ method: "GET",
175
+ headers: {
176
+ "authorization": `Bearer ${license}`,
177
+ "accept": "application/json"
178
+ }
179
+ });
180
+ if (!res.ok) {
181
+ let body = "";
182
+ try { body = await res.text(); } catch {}
183
+ if (VERBOSE) {
184
+ log.warn(`[debug] broker response: status=${res.status} body=${truncate(body, 300)}`);
185
+ }
186
+ let msg = "Request failed. Please try again.";
187
+ if (res.status === 401 || res.status === 403) {
188
+ msg = "Authentication failed. Verify your license or access rights.";
189
+ } else if (res.status === 404) {
190
+ msg = "Service not found or endpoint misconfigured.";
191
+ } else if (res.status === 429) {
192
+ msg = "Too many requests. Please retry later.";
193
+ } else if (res.status >= 500) {
194
+ msg = "Upstream service error. Please retry later.";
195
+ }
196
+ throw new Error(msg);
197
+ }
198
+ const json = await res.json();
199
+ // expected: { registry, token, expiresAt? }
200
+ return json;
201
+ }
202
+
203
+ function truncate(s, n) {
204
+ if (!s) return s;
205
+ return s.length > n ? s.slice(0, n) + "…" : s;
206
+ }
207
+
208
+ function sanitizeLicense(x) {
209
+ if (!x) return "";
210
+ let s = String(x);
211
+ // Strip surrounding quotes
212
+ s = s.trim().replace(/^['"]+|['"]+$/g, "");
213
+ // Remove common pasted/hidden chars: bullet, zero-width, NBSP
214
+ s = s
215
+ .replace(/[\u2022\u2023\u25E6\u2043\u2219]/g, "") // bullets
216
+ .replace(/[\u200B-\u200D\uFEFF]/g, "") // zero-width
217
+ .replace(/\u00A0/g, " ") // NBSP -> space
218
+ .replace(/\s+/g, ""); // remove whitespace
219
+ // Normalize fancy quotes to plain
220
+ s = s.replace(/[\u2018\u2019\u201C\u201D]/g, "");
221
+ return s;
222
+ }
223
+
224
+ function findFirstNonByteChar(s) {
225
+ if (!s) return -1;
226
+ for (let i = 0; i < s.length; i++) {
227
+ const code = s.charCodeAt(i);
228
+ if (code > 255) return i;
229
+ }
230
+ return -1;
231
+ }
232
+
233
+ function looksLikeLicense(s) {
234
+ if (!s) return false;
235
+ // tb_ followed by base64url-ish payload
236
+ return /^tb_[A-Za-z0-9_-]{10,}$/.test(s);
237
+ }
238
+
239
+ async function run(cmd, args) {
240
+ const { spawn } = await import("node:child_process");
241
+ return new Promise((resolve, reject) => {
242
+ const p = spawn(cmd, args, { stdio: "inherit", shell: true });
243
+ p.on("close", (code) => {
244
+ if (code === 0) resolve(undefined);
245
+ else reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`));
246
+ });
247
+ });
248
+ }
249
+
250
+ function parseArgs(argv) {
251
+ const out = {};
252
+ for (let i = 0; i < argv.length; i++) {
253
+ const a = argv[i];
254
+ if (!a.startsWith("-")) continue;
255
+ const key = a.replace(/^-+/, "");
256
+ const next = argv[i + 1];
257
+ if (["--quiet", "-q"].includes(a)) out.quiet = true;
258
+ else if (["--verbose", "-v"].includes(a)) out.verbose = true;
259
+ else if (next && !next.startsWith("-")) {
260
+ out[key] = next;
261
+ i++;
262
+ } else {
263
+ out[key] = true;
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+
269
+ function fail(msg) {
270
+ console.error(`${red("✖")} ${banner} ${msg}`);
271
+ process.exit(1);
272
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "three-blocks-login",
3
+ "version": "0.0.1",
4
+ "description": "Fetch a short-lived token from the three-blocks broker and configure npm for the current context.",
5
+ "type": "module",
6
+ "bin": {
7
+ "three-blocks-login": "bin/login.js"
8
+ },
9
+ "license": "MIT",
10
+ "files": ["bin/"],
11
+ "keywords": ["npm", "login", "three-blocks", "token", "ci"],
12
+ "dependencies": {},
13
+ "publishConfig": {
14
+ "access": "public"
15
+ }
16
+ }