@mediamonks/canopy-cli 0.7.18 → 0.7.20

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 (29) hide show
  1. package/dist/{db-BAaqh_2O.mjs → db-TaSLpQZ5.mjs} +2 -2
  2. package/dist/{deploy-DID3xhMJ.mjs → deploy-DCggsLD7.mjs} +2 -2
  3. package/dist/dev-DjkK-50R.mjs +2 -0
  4. package/dist/{dev-CjaomJ2D.mjs → dev-DtEwiNYi.mjs} +4 -4
  5. package/dist/docs/index.json +7 -2
  6. package/dist/docs/notifications.md +14 -1
  7. package/dist/docs/sdk-email.md +114 -0
  8. package/dist/docs/sdk.md +4 -4
  9. package/dist/{docs-DP_p_mmy.mjs → docs-C2D7__Iz.mjs} +2 -2
  10. package/dist/{egress-DDM-_7Yo.mjs → egress-BhbQfjjV.mjs} +1 -1
  11. package/dist/{feedback-DX99gDFp.mjs → feedback-BvkCjS-g.mjs} +2 -2
  12. package/dist/index.mjs +25 -25
  13. package/dist/{local-dAQdVFEK.mjs → local-CKRZur_5.mjs} +2 -2
  14. package/dist/login-D-Ac_kFW.mjs +285 -0
  15. package/dist/{logs-D60VRbHZ.mjs → logs-Beqz--Df.mjs} +1 -1
  16. package/dist/{migrations-kKKDlR_H.mjs → migrations-Bg9FEGFh.mjs} +1 -1
  17. package/dist/{open-B5XHmEmv.mjs → open-D7f1wNn3.mjs} +1 -1
  18. package/dist/{package-x-_qF58q.mjs → package-B-aXVcQu.mjs} +1 -1
  19. package/dist/{projects-TE1PVwMy.mjs → projects-1r5387ui.mjs} +1 -1
  20. package/dist/{schedule-Blldchmj.mjs → schedule-DK_Cv6Xf.mjs} +1 -1
  21. package/dist/{skill-CSFhZmBk.mjs → skill-CqiSkac0.mjs} +2 -2
  22. package/dist/{staging-Cw-Rzbsj.mjs → staging-CdpV2YHB.mjs} +1 -1
  23. package/dist/{token-BqxRIRkT.mjs → token-C8Bmzwfl.mjs} +1 -1
  24. package/dist/{ui-DLZNEen6.mjs → ui--atbZ-TM.mjs} +4 -0
  25. package/dist/{upgrade-DcU4YMr_.mjs → upgrade-CpUCVTrN.mjs} +2 -2
  26. package/dist/{usage-2cGnpSVc.mjs → usage-CRpVLLi6.mjs} +1 -1
  27. package/package.json +4 -3
  28. package/dist/dev-A2doKMCS.mjs +0 -2
  29. package/dist/login-CU0pev0k.mjs +0 -401
@@ -1,401 +0,0 @@
1
- import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-BFaWfSVu.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-DLZNEen6.mjs";
3
- import { defineCommand } from "citty";
4
- import * as p from "@clack/prompts";
5
- import { createHash, randomBytes } from "node:crypto";
6
- import { createServer } from "node:http";
7
- //#region src/login-page.ts
8
- /**
9
- * Branded HTML for the local OneLogin PKCE callback (127.0.0.1:8976).
10
- * Visual language matches apps/gateway/src/auth-page.ts.
11
- */
12
- function loginCallbackPage(ok) {
13
- const title = ok ? "You're signed in" : "Sign-in didn’t finish";
14
- return `<!doctype html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="utf-8">
18
- <meta name="viewport" content="width=device-width, initial-scale=1">
19
- <title>${title} · Canopy</title>
20
- <style>
21
- :root {
22
- color-scheme: light dark;
23
- --ink: #14201a;
24
- --muted: #4a5c52;
25
- --line: #d5ddd7;
26
- --panel: rgba(255, 255, 255, 0.82);
27
- --field: rgba(255, 255, 255, 0.7);
28
- --accent: #2f6b4f;
29
- --mark-ink: #f4faf6;
30
- --shadow: 0 18px 40px -28px rgba(20, 32, 26, 0.45);
31
- --bg: radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%),
32
- radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
33
- linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef);
34
- --mark-bg: ${ok ? "var(--accent)" : "#8a4b3a"};
35
- }
36
- @media (prefers-color-scheme: dark) {
37
- :root {
38
- --ink: #e9f0ec;
39
- --muted: #a2b3aa;
40
- --line: rgba(233, 240, 236, 0.14);
41
- --panel: rgba(24, 31, 27, 0.72);
42
- --field: rgba(233, 240, 236, 0.06);
43
- --accent: #7cc39d;
44
- --mark-ink: #0d1410;
45
- --shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.7);
46
- --bg: radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%),
47
- radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%),
48
- linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f);
49
- --mark-bg: ${ok ? "var(--accent)" : "#e0a292"};
50
- }
51
- }
52
- * { box-sizing: border-box; }
53
- body {
54
- margin: 0;
55
- min-height: 100vh;
56
- font-family: "Segoe UI", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
57
- color: var(--ink);
58
- background: var(--bg);
59
- display: grid;
60
- place-items: center;
61
- padding: 1.5rem;
62
- }
63
- main {
64
- width: min(100%, 26rem);
65
- background: var(--panel);
66
- border: 1px solid var(--line);
67
- border-radius: 1.25rem;
68
- padding: 2rem 1.75rem 1.75rem;
69
- backdrop-filter: blur(10px);
70
- box-shadow: var(--shadow);
71
- }
72
- .mark {
73
- width: 2.75rem;
74
- height: 2.75rem;
75
- border-radius: 0.85rem;
76
- display: grid;
77
- place-items: center;
78
- background: var(--mark-bg);
79
- color: var(--mark-ink);
80
- margin-bottom: 1.25rem;
81
- }
82
- .mark svg { width: 1.4rem; height: 1.4rem; display: block; }
83
- .brand {
84
- margin: 0 0 0.35rem;
85
- font-size: 0.8rem;
86
- font-weight: 600;
87
- letter-spacing: 0.04em;
88
- text-transform: uppercase;
89
- color: var(--accent);
90
- }
91
- h1 {
92
- margin: 0 0 0.75rem;
93
- font-size: 1.45rem;
94
- line-height: 1.25;
95
- letter-spacing: -0.02em;
96
- font-weight: 650;
97
- }
98
- p {
99
- margin: 0;
100
- color: var(--muted);
101
- font-size: 0.975rem;
102
- line-height: 1.55;
103
- }
104
- .hint {
105
- margin-top: 1.35rem;
106
- padding-top: 1rem;
107
- border-top: 1px solid var(--line);
108
- font-size: 0.8rem;
109
- color: var(--muted);
110
- }
111
- kbd {
112
- font: inherit;
113
- font-size: 0.85em;
114
- padding: 0.1em 0.35em;
115
- border: 1px solid var(--line);
116
- border-radius: 0.35rem;
117
- background: var(--field);
118
- }
119
- </style>
120
- </head>
121
- <body>
122
- <main>
123
- <div class="mark">${ok ? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
124
- <path d="M20 6 9 17l-5-5"/>
125
- </svg>` : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
126
- <circle cx="12" cy="12" r="9"/>
127
- <path d="M12 8v5"/>
128
- <path d="M12 16h.01"/>
129
- </svg>`}</div>
130
- <p class="brand">Canopy CLI</p>
131
- <h1>${title}</h1>
132
- <p>${ok ? "You can close this tab and return to the terminal — the CLI has your code." : "Return to the terminal and run canopy login again. Reloading this page won’t help."}</p>
133
- <p class="hint">${ok ? "This tab only finishes the browser step — keep using the terminal for the rest." : "If the terminal still says it’s waiting, cancel with <kbd>Ctrl</kbd>+<kbd>C</kbd> and try again."}</p>
134
- </main>
135
- </body>
136
- </html>`;
137
- }
138
- //#endregion
139
- //#region src/login.ts
140
- /**
141
- * canopy login | whoami | logout — browser PKCE flow against OneLogin (public
142
- * client), then trade the ID token for platform CLI tokens via POST
143
- * /control/v1/auth/cli-token on every baked site (docs/10-cli-sites.md D17).
144
- */
145
- const CALLBACK_PORT = 8976;
146
- const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
147
- const LOGIN_TIMEOUT_MS = 300 * 1e3;
148
- const base64url = (buf) => buf.toString("base64url");
149
- const whoamiCommand = defineCommand({
150
- meta: {
151
- name: "whoami",
152
- description: "Show the current Canopy login"
153
- },
154
- run: reportErrors("whoami", async () => {
155
- const config = siteConfig();
156
- if (!config.token) throw new Error("Not logged in — run `canopy login`.");
157
- console.log(config.email ?? "<unknown email>");
158
- if (config.cloud) console.log(`Cloud: ${config.cloud}`);
159
- console.log(`API: ${config.apiUrl}`);
160
- if (config.tokenExpiresAt !== void 0) {
161
- const expired = Date.parse(config.tokenExpiresAt) < Date.now();
162
- console.log(expired ? `Token EXPIRED ${config.tokenExpiresAt} — run \`canopy login\` again.` : `Token expires ${config.tokenExpiresAt}.`);
163
- }
164
- })
165
- });
166
- const logoutCommand = defineCommand({
167
- meta: {
168
- name: "logout",
169
- description: "Sign out: forget all local CLI tokens"
170
- },
171
- run: reportErrors("logout", async () => {
172
- const file = readConfigFile();
173
- if (!Object.values(file.profiles ?? {}).some((p) => p?.token)) {
174
- console.log("Not logged in.");
175
- return;
176
- }
177
- clearAllProfileTokens();
178
- console.log("Logged out — all site tokens removed from ~/.canopy/config.json.");
179
- })
180
- });
181
- /** Wait for OneLogin to redirect back to the local loopback server. */
182
- function waitForCallback(expectedState) {
183
- return new Promise((resolve, reject) => {
184
- const server = createServer((req, res) => {
185
- const url = new URL(req.url ?? "/", REDIRECT_URI);
186
- if (url.pathname !== "/callback") {
187
- res.writeHead(404).end();
188
- return;
189
- }
190
- const code = url.searchParams.get("code");
191
- const state = url.searchParams.get("state");
192
- const ok = Boolean(code) && state === expectedState;
193
- res.writeHead(ok ? 200 : 400, {
194
- "content-type": "text/html; charset=utf-8",
195
- "cache-control": "no-store"
196
- }).end(loginCallbackPage(ok));
197
- server.close();
198
- clearTimeout(timer);
199
- if (ok) resolve(code);
200
- else reject(new Error(url.searchParams.get("error_description") ?? "Login rejected"));
201
- });
202
- const timer = setTimeout(() => {
203
- server.close();
204
- reject(/* @__PURE__ */ new Error("Timed out waiting for the browser login (5 minutes)."));
205
- }, LOGIN_TIMEOUT_MS);
206
- server.on("error", (err) => reject(err.message.includes("EADDRINUSE") ? /* @__PURE__ */ new Error(`Port ${CALLBACK_PORT} is in use — close the other process and retry.`) : err));
207
- server.listen(CALLBACK_PORT, "127.0.0.1");
208
- });
209
- }
210
- async function pickActiveProfile(json, flag) {
211
- if (flag) {
212
- if (!isProfileId(flag)) throw new Error(`Unknown profile "${flag}". Choose one of: ${listBakedProfiles().map((p) => p.id).join(", ")}.`);
213
- return flag;
214
- }
215
- const env = process.env["CANOPY_PROFILE"]?.trim();
216
- if (env) {
217
- if (!isProfileId(env)) throw new Error(`Unknown CANOPY_PROFILE "${env}".`);
218
- return env;
219
- }
220
- const profiles = listBakedProfiles();
221
- if (!process.stdin.isTTY || json) throw new Error(`Pass --profile <id> (or CANOPY_PROFILE) when not on a TTY. Options: ${profiles.map((p) => p.id).join(", ")}.`);
222
- const choice = await p.select({
223
- message: "Which Canopy site should be your default?",
224
- options: profiles.map((prof) => ({
225
- value: prof.id,
226
- label: prof.label,
227
- hint: prof.cloud
228
- })),
229
- initialValue: "main"
230
- });
231
- if (p.isCancel(choice)) {
232
- p.cancel("Login cancelled.");
233
- process.exit(1);
234
- }
235
- p.note(`Using profile "${choice}". Agents/non-TTY: canopy login --profile ${choice}`, "Active profile");
236
- return choice;
237
- }
238
- async function mintForProfile(id, idToken) {
239
- const baked = getBakedProfile(id);
240
- try {
241
- const cliTokenRes = await fetch(`${baked.apiUrl}/control/v1/auth/cli-token`, {
242
- method: "POST",
243
- headers: { "content-type": "application/json" },
244
- body: JSON.stringify({ id_token: idToken })
245
- });
246
- if (!cliTokenRes.ok) return {
247
- ok: false,
248
- error: `${cliTokenRes.status} ${await cliTokenRes.text()}`
249
- };
250
- const { token, expires_at, user } = await cliTokenRes.json();
251
- upsertProfileAuth(id, {
252
- token,
253
- tokenExpiresAt: expires_at,
254
- email: user.email
255
- });
256
- return {
257
- ok: true,
258
- email: user.email,
259
- expiresAt: expires_at
260
- };
261
- } catch (err) {
262
- return {
263
- ok: false,
264
- error: err instanceof Error ? err.message : String(err)
265
- };
266
- }
267
- }
268
- async function login(json, profileFlag) {
269
- if (!json) p.intro("canopy login");
270
- const active = await pickActiveProfile(json, profileFlag);
271
- const file = readConfigFile();
272
- const { issuer, cliClientId } = {
273
- issuer: process.env["CANOPY_OIDC_ISSUER"] ?? file.issuer ?? "https://s4capital.onelogin.com/oidc/2",
274
- cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? file.cliClientId ?? "b69755d0-6678-013f-b5ce-09fe44a57aa0180501"
275
- };
276
- if (cliClientId.includes("REPLACE")) throw new Error("The CLI's OneLogin app is not configured yet (set CANOPY_OIDC_CLIENT_ID or packages/cli/src/config.ts).");
277
- writeConfigFile({
278
- ...file,
279
- issuer,
280
- cliClientId,
281
- activeProfile: active
282
- });
283
- const discoveryRes = await fetch(`${issuer}/.well-known/openid-configuration`);
284
- if (!discoveryRes.ok) throw new Error(`OIDC discovery failed: ${discoveryRes.status}`);
285
- const discovery = await discoveryRes.json();
286
- const state = base64url(randomBytes(32));
287
- const codeVerifier = base64url(randomBytes(32));
288
- const authorizeUrl = new URL(discovery.authorization_endpoint);
289
- authorizeUrl.search = new URLSearchParams({
290
- response_type: "code",
291
- client_id: cliClientId,
292
- redirect_uri: REDIRECT_URI,
293
- scope: "openid profile email",
294
- state,
295
- code_challenge: base64url(createHash("sha256").update(codeVerifier).digest()),
296
- code_challenge_method: "S256"
297
- }).toString();
298
- const authorizeHref = authorizeUrl.toString();
299
- openBrowser(authorizeHref);
300
- const spin = json ? void 0 : p.spinner();
301
- if (json) console.log(JSON.stringify({
302
- status: "waiting",
303
- url: authorizeHref
304
- }));
305
- else {
306
- p.log.step("Opening OneLogin in your browser");
307
- p.log.message("If nothing opens, visit:");
308
- console.log(authorizeHref);
309
- spin?.start("Waiting for you to sign in in the browser…");
310
- }
311
- let code;
312
- try {
313
- code = await waitForCallback(state);
314
- } catch (err) {
315
- spin?.error("Sign-in failed.");
316
- throw err;
317
- }
318
- spin?.stop("Signed in with OneLogin.");
319
- const tokenRes = await fetch(discovery.token_endpoint, {
320
- method: "POST",
321
- headers: { "content-type": "application/x-www-form-urlencoded" },
322
- body: new URLSearchParams({
323
- grant_type: "authorization_code",
324
- client_id: cliClientId,
325
- code,
326
- redirect_uri: REDIRECT_URI,
327
- code_verifier: codeVerifier
328
- })
329
- });
330
- if (!tokenRes.ok) throw new Error(`Token exchange failed: ${tokenRes.status} ${await tokenRes.text()}`);
331
- const { id_token: idToken } = await tokenRes.json();
332
- if (!idToken) throw new Error("OneLogin returned no id_token.");
333
- const mintSpin = json ? void 0 : p.spinner();
334
- mintSpin?.start("Minting CLI tokens for each site…");
335
- const results = [];
336
- for (const prof of listBakedProfiles()) {
337
- const result = await mintForProfile(prof.id, idToken);
338
- results.push({
339
- id: prof.id,
340
- ok: result.ok,
341
- detail: result.ok ? `expires ${result.expiresAt}` : result.error
342
- });
343
- }
344
- mintSpin?.stop("Token mint finished.");
345
- if (!results.find((r) => r.id === active)?.ok) {
346
- const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : "✗"}`).join(" ");
347
- throw new Error(`Could not mint a token for active profile "${active}". ${summary}. Re-run \`canopy login\`.`);
348
- }
349
- setActiveProfile(active);
350
- const email = readConfigFile().profiles?.[active]?.email;
351
- const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : `✗ (${r.detail})`}`).join("\n");
352
- if (json) console.log(JSON.stringify({
353
- status: "ok",
354
- email,
355
- activeProfile: active,
356
- profiles: Object.fromEntries(results.map((r) => [r.id, {
357
- ok: r.ok,
358
- detail: r.detail
359
- }]))
360
- }));
361
- else {
362
- p.note(summary, "Sites");
363
- p.outro(`Logged in as ${email ?? "unknown"} — default profile "${active}".`);
364
- for (const r of results.filter((x) => !x.ok)) p.log.warn(`${r.id} failed — re-run \`canopy login\` to retry all sites.`);
365
- }
366
- }
367
- const loginCommand = defineCommand({
368
- meta: {
369
- name: "login",
370
- description: "Sign in to Canopy via OneLogin"
371
- },
372
- args: {
373
- json: {
374
- type: "boolean",
375
- default: false,
376
- description: "Emit machine-readable status to stdout (for agents/CI)"
377
- },
378
- profile: {
379
- type: "string",
380
- description: "Active default profile (main | staging | gcp-pilot); required non-TTY"
381
- }
382
- },
383
- run: reportErrors("login", async ({ args }) => {
384
- const json = args.json;
385
- try {
386
- await login(json, args.profile);
387
- } catch (err) {
388
- if (json) {
389
- console.log(JSON.stringify({
390
- status: "error",
391
- message: err instanceof Error ? err.message : String(err)
392
- }));
393
- process.exitCode = 1;
394
- return;
395
- }
396
- throw err;
397
- }
398
- })
399
- });
400
- //#endregion
401
- export { loginCommand, logoutCommand, whoamiCommand };