@fulldivevr/workcord-cli 1.0.0 → 1.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.
Files changed (81) hide show
  1. package/README.md +88 -30
  2. package/dist/auth.js +38 -4
  3. package/dist/cli.js +3 -1
  4. package/dist/client.js +21 -12
  5. package/dist/commands/auth.js +33 -14
  6. package/dist/commands/challenges.js +2 -7
  7. package/dist/commands/custom-agents.js +3 -8
  8. package/dist/commands/dm.js +2 -3
  9. package/dist/commands/health.js +29 -0
  10. package/dist/commands/messages.js +8 -21
  11. package/dist/commands/room-agents.js +2 -7
  12. package/dist/commands/rooms.js +24 -31
  13. package/dist/commands/users.js +4 -9
  14. package/dist/commands/whoami.js +24 -7
  15. package/dist/config.js +34 -0
  16. package/dist/generated/commands/activity.js +3 -6
  17. package/dist/generated/commands/ai.js +3 -6
  18. package/dist/generated/commands/ambassador.js +3 -6
  19. package/dist/generated/commands/apple-iap.js +4 -9
  20. package/dist/generated/commands/attribution.js +6 -15
  21. package/dist/generated/commands/auth.js +9 -24
  22. package/dist/generated/commands/challenges.js +13 -36
  23. package/dist/generated/commands/clips.js +3 -6
  24. package/dist/generated/commands/contact-match.js +7 -18
  25. package/dist/generated/commands/content-crawler.js +2 -3
  26. package/dist/generated/commands/custom-agents.js +22 -63
  27. package/dist/generated/commands/desktop-releases.js +5 -12
  28. package/dist/generated/commands/discover.js +9 -24
  29. package/dist/generated/commands/dm.js +10 -27
  30. package/dist/generated/commands/drafts.js +3 -6
  31. package/dist/generated/commands/embed.js +3 -6
  32. package/dist/generated/commands/explore.js +2 -3
  33. package/dist/generated/commands/feed.js +4 -9
  34. package/dist/generated/commands/feedback.js +2 -3
  35. package/dist/generated/commands/gamification.js +3 -6
  36. package/dist/generated/commands/google-play-iap.js +4 -9
  37. package/dist/generated/commands/internal.js +9 -24
  38. package/dist/generated/commands/interview.js +4 -9
  39. package/dist/generated/commands/invite-redirect.js +2 -3
  40. package/dist/generated/commands/jesuscord-admin.js +3 -6
  41. package/dist/generated/commands/join.js +2 -3
  42. package/dist/generated/commands/kits.js +7 -18
  43. package/dist/generated/commands/meetings.js +6 -15
  44. package/dist/generated/commands/message-folders.js +5 -12
  45. package/dist/generated/commands/messages.js +17 -48
  46. package/dist/generated/commands/one-emulator.js +2 -3
  47. package/dist/generated/commands/pdf-export.js +2 -3
  48. package/dist/generated/commands/pin-folders.js +5 -12
  49. package/dist/generated/commands/pinned-messages.js +4 -9
  50. package/dist/generated/commands/polls.js +6 -15
  51. package/dist/generated/commands/premium-feeds.js +2 -3
  52. package/dist/generated/commands/processors.js +2 -3
  53. package/dist/generated/commands/questions.js +3 -6
  54. package/dist/generated/commands/referrals.js +2 -3
  55. package/dist/generated/commands/reports.js +2 -3
  56. package/dist/generated/commands/room-a2-a.js +6 -15
  57. package/dist/generated/commands/room-agents.js +13 -36
  58. package/dist/generated/commands/room-invites.js +9 -24
  59. package/dist/generated/commands/room-owned-agent.js +3 -6
  60. package/dist/generated/commands/room-page.js +2 -3
  61. package/dist/generated/commands/rooms.js +45 -132
  62. package/dist/generated/commands/saved-messages.js +4 -9
  63. package/dist/generated/commands/saved-stories.js +7 -18
  64. package/dist/generated/commands/search.js +2 -3
  65. package/dist/generated/commands/shadow-home-agent.js +3 -6
  66. package/dist/generated/commands/shadow.js +5 -12
  67. package/dist/generated/commands/share-cards.js +4 -9
  68. package/dist/generated/commands/share.js +2 -3
  69. package/dist/generated/commands/sponsors.js +2 -3
  70. package/dist/generated/commands/stories.js +12 -33
  71. package/dist/generated/commands/story-by-id.js +2 -3
  72. package/dist/generated/commands/submissions.js +4 -9
  73. package/dist/generated/commands/subscriptions.js +8 -21
  74. package/dist/generated/commands/tips.js +3 -6
  75. package/dist/generated/commands/users.js +16 -45
  76. package/dist/generated/commands/voice-token.js +2 -3
  77. package/dist/generated/commands/well-known.js +3 -6
  78. package/dist/generated/commands/wrapped.js +2 -3
  79. package/dist/generated/index.js +0 -2
  80. package/package.json +3 -2
  81. package/dist/generated/commands/health.js +0 -32
package/README.md CHANGED
@@ -20,13 +20,15 @@ npx @fulldivevr/workcord-cli --help
20
20
 
21
21
  | Setting | How |
22
22
  | --- | --- |
23
- | Base URL | `WORKCORD_API_URL` env var or `--base-url <url>` — **required, no default** |
24
- | Signing secret | `WORKCORD_JWT_SECRET` env var **only** — never a flag |
23
+ | Base URL | WorkCord production by default; override with saved configuration, `WORKCORD_API_URL`, or `--base-url <url>` |
24
+ | User token | Saved by `auth login --save`, or supplied by `WORKCORD_TOKEN` |
25
+ | Signing secret | `WORKCORD_JWT_SECRET` env var **only** — never a flag; privileged, enables `--as` impersonation |
25
26
  | Internal admin key | `WORKCORD_INTERNAL_API_KEY`, internal commands only |
26
- | Acting user | `--as <userId>`, per invocation |
27
+ | Acting user | `--as <userId>`, per invocation — only with `WORKCORD_JWT_SECRET` |
27
28
 
28
29
  Base URL must be a bare origin (scheme + host, no path). The `/api/v1` prefix
29
- is added by the CLI.
30
+ is added by the CLI. Resolution order is `--base-url`, `WORKCORD_API_URL`,
31
+ saved configuration, then the WorkCord production default.
30
32
 
31
33
  | Deployment | `WORKCORD_API_URL` |
32
34
  | --- | --- |
@@ -39,38 +41,66 @@ is added by the CLI.
39
41
  | Local RoomCord / JesusCord | `http://localhost:3000` |
40
42
 
41
43
  ```bash
42
- export WORKCORD_API_URL=https://work-rooms-api.wizeup.app
43
- export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
44
+ workcord auth login --email you@company.com --save
44
45
 
45
- workcord --as <userId> users get-me
46
+ workcord rooms list
47
+ ```
48
+
49
+ Use another server for one command:
50
+
51
+ ```bash
52
+ workcord --base-url https://via-rooms-api.wizeup.app rooms list
53
+ ```
54
+
55
+ Save another server and login for future commands:
56
+
57
+ ```bash
58
+ workcord --base-url https://via-rooms-api.wizeup.app auth login --email you@via.com --save
59
+ ```
60
+
61
+ Reset the saved server to WorkCord by logging in and saving again:
62
+
63
+ ```bash
64
+ workcord --base-url https://work-rooms-api.wizeup.app auth login --email you@company.com --save
46
65
  ```
47
66
 
48
67
  Output is JSON on stdout, errors on stderr, non-zero exit on failure.
49
68
 
50
69
  ### How auth works
51
70
 
52
- Three distinct modes.
71
+ Every request carries a bearer token. There are two ways to get one, and one
72
+ extra header for internal admin routes.
53
73
 
54
- **1. Normal user login (email code).** Deployed WorkCord/VIA stacks run with
55
- `ENABLE_EMAIL_AUTH=true` and mail a 6-digit code:
74
+ **1. A real user token (normal path).** Log in once with `--save`, and every
75
+ command runs as that user with exactly that user's permissions:
56
76
 
57
77
  ```bash
58
- workcord auth login --email you@company.com
59
- # → code arrives by email, CLI prompts for it, prints { token, accessToken, refreshToken, user, isNewUser }
78
+ workcord auth login --email you@company.com --save
79
+ # → code arrives by email, CLI prompts for it, prints
80
+ # { token, accessToken, refreshToken, user, isNewUser } on stdout
81
+ # and confirms the login was saved locally
82
+
83
+ workcord rooms list
60
84
  ```
61
85
 
62
- Non-interactive (no TTY) it prints a `sessionId` and you finish with:
86
+ `WORKCORD_TOKEN` is sent as-is in `Authorization: Bearer`; the CLI never signs
87
+ anything in this mode and `--as` is not needed. This is the least-privilege
88
+ setup and the right one for agents.
89
+
90
+ Non-interactive (no TTY) `auth login --email` prints a `sessionId` and you
91
+ finish with:
63
92
 
64
93
  ```bash
65
94
  workcord auth verify-email --session-id <id> --code 123456
66
95
  ```
67
96
 
68
97
  Email login returns `accessToken`; phone/social login returns `token`. The CLI
69
- emits both fields on either path so callers can read one consistently.
98
+ emits both fields on either path so callers can read one consistently — either
99
+ value works as `WORKCORD_TOKEN`.
70
100
 
71
101
  Email auth is domain-restricted. WorkCord prod and VIA both allow
72
102
  `fulldive.com`, `via.com`, `via.com.tw`, `viatech.com.cn`, `*.tvbs.com`,
73
- `tvbs.*.com`, `tvbs.com.*` (`EMAIL_AUTH_ALLOWED_DOMAINS` in
103
+ `tvbs.*.com`, `tvbs.com.*`, and `*.tvbs.com.tw` (`EMAIL_AUTH_ALLOWED_DOMAINS` in
74
104
  `.sc/stacks/wize-rooms-api/client.yaml`). Anything else gets
75
105
  `DOMAIN_NOT_ALLOWED`.
76
106
 
@@ -78,16 +108,37 @@ Email auth is domain-restricted. WorkCord prod and VIA both allow
78
108
  set `AUTH_PHONE_OTP_DEV_BYPASS`, so `POST /auth/send-otp` returns 503
79
109
  `OTP_NOT_AVAILABLE` — the CLI turns that into an explicit "use `--email`" error.
80
110
 
81
- **2. Impersonation / agent auth.** With `WORKCORD_JWT_SECRET` set, the global
82
- `--as <userId>` flag mints an HS256 JWT locally (payload `{userId, id}`, 60-day
83
- expiry) instead of logging in. `WORKCORD_JWT_SECRET` must equal the **server's
84
- `JWT_SECRET`** env var — the names deliberately differ, and this is the most
85
- common setup mistake.
111
+ Tokens expire. On `401`, log in again with `--save`, or replace `WORKCORD_TOKEN`.
112
+
113
+ **2. Local minting / impersonation (privileged, trusted CI only).** With
114
+ `WORKCORD_JWT_SECRET` set, the global `--as <userId>` flag mints an HS256 JWT
115
+ locally (payload `{userId, id}`, 60-day expiry) instead of logging in.
116
+ `WORKCORD_JWT_SECRET` must equal the **server's `JWT_SECRET`** env var — the
117
+ names deliberately differ, and this is the most common setup mistake.
118
+
119
+ ```bash
120
+ export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
121
+ workcord --as <userId> users get-me
122
+ ```
86
123
 
87
124
  Same secret on both sides means the CLI can act as **any** user, with no
88
125
  password and no server-side check. Treat it as privileged signing material:
89
126
  keep it out of source control, package metadata, logs, screenshots and public
90
- examples, and run this CLI only from trusted machines and CI jobs.
127
+ examples, and run this CLI only from trusted machines and CI jobs. Prefer
128
+ `WORKCORD_TOKEN` unless you actually need to switch identities.
129
+
130
+ Resolution order, in one place (`resolveAuth` in `src/auth.ts`):
131
+
132
+ | Situation | Result |
133
+ | --- | --- |
134
+ | `--as <userId>` passed, `WORKCORD_JWT_SECRET` set | mint and impersonate that user |
135
+ | `--as <userId>` passed, no secret | error — set the secret or drop `--as` |
136
+ | no `--as`, `WORKCORD_TOKEN` set | send that token verbatim |
137
+ | no `--as`, only `WORKCORD_JWT_SECRET` set | error — `--as <userId>` required |
138
+ | neither set | error naming both options |
139
+
140
+ `--as` wins over `WORKCORD_TOKEN` when both are available: passing it is an
141
+ explicit statement of impersonation intent.
91
142
 
92
143
  **3. Internal admin routes.** `WORKCORD_INTERNAL_API_KEY` is sent as the
93
144
  `x-internal-key` header; the server compares it to its own `INTERNAL_API_KEY`.
@@ -99,6 +150,9 @@ Required for `/internal/*` and the jesuscord admin routes. Treat it like the sig
99
150
  workcord [--as <userId>] <namespace> <command> [flags]
100
151
  ```
101
152
 
153
+ `--as` is only needed when minting with `WORKCORD_JWT_SECRET`; with
154
+ `WORKCORD_TOKEN` set, omit it everywhere.
155
+
102
156
  - `workcord --help` lists namespaces.
103
157
  - `workcord <namespace> --help` lists that namespace's commands.
104
158
  - `workcord <namespace> <command> --help` shows the command's flags.
@@ -133,8 +187,8 @@ profile fields.
133
187
 
134
188
  Server auto-extracts mention tokens from visible `@Name` text. Prefer
135
189
  `messages send-text --mention-user USER_ID` so the CLI resolves the room
136
- member's display name and avoids exposing raw user ids. `auth login` is the
137
- only semantic command that doesn't take `--as` (it returns one).
190
+ member's display name and avoids exposing raw user ids. `auth login` never
191
+ takes `--as` — it is how you obtain a `WORKCORD_TOKEN` in the first place.
138
192
 
139
193
  ### Error handling
140
194
 
@@ -196,8 +250,9 @@ from each room, merges them by `sentAt` descending, and includes
196
250
 
197
251
  **Switch user identity per call:**
198
252
 
199
- The CLI is stateless — `--as <userId>` per invocation. One process can act as
200
- 100 users by changing the flag.
253
+ Saved login is for one interactive user. Agents can use `WORKCORD_TOKEN` to
254
+ choose the token owner per invocation; with `WORKCORD_JWT_SECRET`, one process
255
+ can act as 100 users by changing `--as <userId>` per invocation.
201
256
 
202
257
  **Onboard a fresh agent identity:**
203
258
 
@@ -255,7 +310,7 @@ without running `regen` if you don't need the latest server changes.
255
310
  ```
256
311
  src/
257
312
  ├── cli.ts commander entry, global --as flag
258
- ├── auth.ts mints HS256 JWT from --as
313
+ ├── auth.ts auth resolution: WORKCORD_TOKEN vs minted --as JWT
259
314
  ├── client.ts fetch wrapper (JSON + multipart)
260
315
  ├── commands/ hand-written semantic commands
261
316
  └── generated/ auto-generated from openapi.json
@@ -266,7 +321,9 @@ scripts/
266
321
  ├── sync-spec.ts backend build → openapi.json
267
322
  └── generate-commands.ts spec → commander commands
268
323
 
269
- tests/smoke.sh hermetic end-to-end test
324
+ tests/
325
+ ├── auth.test.ts node --test suite for auth resolution
326
+ └── smoke.sh end-to-end test, needs a live server
270
327
  ```
271
328
 
272
329
  ### Adding a hand-written semantic command
@@ -283,7 +340,8 @@ flags (e.g. typed fields instead of an opaque `--body`):
283
340
  ### Testing
284
341
 
285
342
  ```bash
286
- npm run smoke
343
+ npm test # hermetic, no network — auth resolution rules
344
+ npm run smoke # end-to-end, needs a live server
287
345
  ```
288
346
 
289
347
  Hermetic end-to-end suite (creates two fresh users per run via OTP, exercises
@@ -297,14 +355,14 @@ Publishing is a manual maintainer action — there is no auto-publish CI.
297
355
 
298
356
  1. `npm run regen` — refresh the spec and generated commands; commit the diff.
299
357
  2. `npm run typecheck`
300
- 3. `npm run build`
358
+ 3. `npm test` then `npm run build`
301
359
  4. `npm pack --dry-run` — confirm the file list is exactly `dist/**/*.js`,
302
360
  `README.md`, `LICENSE`, `package.json`. No sources, no `openapi.json`, no
303
361
  `.npmrc`.
304
362
  5. Smoke: `node dist/cli.js --help` and `node dist/cli.js --version`.
305
363
  6. Bump `version` in `package.json`, then `npm publish`.
306
364
 
307
- `prepack` re-runs `typecheck` + `build`, so steps 2–3 also gate the publish.
365
+ `prepack` re-runs `typecheck` + `test` + `build`, so steps 2–3 also gate the publish.
308
366
 
309
367
  ## License
310
368
 
package/dist/auth.js CHANGED
@@ -1,9 +1,43 @@
1
1
  import jwt from "jsonwebtoken";
2
- export function mintToken(userId) {
3
- const secret = process.env.WORKCORD_JWT_SECRET;
4
- if (!secret) {
2
+ export function actingUserId(program) {
3
+ return program.opts().as;
4
+ }
5
+ export function mintToken(userId, secret) {
6
+ const key = secret ?? process.env.WORKCORD_JWT_SECRET;
7
+ if (!key) {
5
8
  throw new Error("WORKCORD_JWT_SECRET not set");
6
9
  }
7
- return jwt.sign({ userId, id: userId }, secret, { algorithm: "HS256", expiresIn: "60d" });
10
+ return jwt.sign({ userId, id: userId }, key, { algorithm: "HS256", expiresIn: "60d" });
11
+ }
12
+ export function resolveAuth(asUserId, env = process.env, config = {}) {
13
+ const secret = env.WORKCORD_JWT_SECRET?.trim() || undefined;
14
+ const token = env.WORKCORD_TOKEN?.trim() || config.token?.trim() || undefined;
15
+ if (asUserId) {
16
+ if (!secret) {
17
+ throw new Error("--as <userId> mints a token and needs WORKCORD_JWT_SECRET. Set it, or drop --as and set WORKCORD_TOKEN to a token from `workcord auth login`.");
18
+ }
19
+ const minted = mintToken(asUserId, secret);
20
+ return { mode: "minted", bearer: `Bearer ${minted}`, token: minted, userId: asUserId };
21
+ }
22
+ if (token) {
23
+ return { mode: "token", bearer: `Bearer ${token}`, token };
24
+ }
25
+ if (secret) {
26
+ throw new Error("--as <userId> is required when authenticating with WORKCORD_JWT_SECRET");
27
+ }
28
+ throw new Error("No credentials. Run `workcord auth login --email <address> --save`, or set WORKCORD_TOKEN. For privileged impersonation, set WORKCORD_JWT_SECRET and pass --as <userId>.");
29
+ }
30
+ export function decodeJwtPayload(token) {
31
+ const parts = token.split(".");
32
+ if (parts.length !== 3)
33
+ return null;
34
+ try {
35
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
36
+ const parsed = JSON.parse(json);
37
+ return parsed && typeof parsed === "object" ? parsed : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
8
42
  }
9
43
  //# sourceMappingURL=auth.js.map
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { registerUsers } from "./commands/users.js";
11
11
  import { registerRoomAgents } from "./commands/room-agents.js";
12
12
  import { registerCustomAgents } from "./commands/custom-agents.js";
13
13
  import { registerChallenges } from "./commands/challenges.js";
14
+ import { registerHealth } from "./commands/health.js";
14
15
  import { registerAll as registerGenerated } from "./generated/index.js";
15
16
  const program = new Command();
16
17
  const require = createRequire(import.meta.url);
@@ -20,7 +21,7 @@ program
20
21
  .description("CLI for the WorkCord API. One invocation = one user action.")
21
22
  .version(version)
22
23
  .option("--as <userId>", "user to act as")
23
- .option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL)");
24
+ .option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL and saved configuration)");
24
25
  program.hook("preAction", () => {
25
26
  setBaseUrlOverride(program.opts().baseUrl);
26
27
  });
@@ -33,6 +34,7 @@ registerUsers(program);
33
34
  registerRoomAgents(program);
34
35
  registerCustomAgents(program);
35
36
  registerChallenges(program); // adds regenerate-cards (not in the OpenAPI spec)
37
+ registerHealth(program);
36
38
  registerGenerated(program);
37
39
  program.parseAsync().catch((err) => {
38
40
  const out = { error: err.message ?? String(err) };
package/dist/client.js CHANGED
@@ -1,13 +1,14 @@
1
- import { mintToken } from "./auth.js";
1
+ import { resolveAuth } from "./auth.js";
2
+ import { DEFAULT_BASE_URL, readConfig } from "./config.js";
2
3
  let baseUrlOverride;
3
4
  export function setBaseUrlOverride(url) {
4
5
  baseUrlOverride = url;
5
6
  }
6
- export function getBaseUrl() {
7
- const raw = baseUrlOverride || process.env.WORKCORD_API_URL;
8
- if (!raw) {
9
- throw new Error("No API base URL configured. Set WORKCORD_API_URL or pass --base-url <url> (e.g. https://work-rooms-api.wizeup.app)");
10
- }
7
+ export async function getBaseUrl() {
8
+ const config = await readConfig();
9
+ return normalizeBaseUrl(baseUrlOverride || process.env.WORKCORD_API_URL || config.baseUrl || DEFAULT_BASE_URL);
10
+ }
11
+ function normalizeBaseUrl(raw) {
11
12
  const trimmed = raw.replace(/\/+$/, "");
12
13
  let parsed;
13
14
  try {
@@ -52,7 +53,15 @@ async function fetchWithRetry(build) {
52
53
  throw lastErr;
53
54
  }
54
55
  export async function call({ method = "GET", path, query, body, headers, asUserId }) {
55
- const base = getBaseUrl();
56
+ const config = await readConfig();
57
+ const { bearer } = resolveAuth(asUserId, process.env, config);
58
+ return request({ method, path, query, body, headers, bearer });
59
+ }
60
+ export async function callPublic({ method = "GET", path, query, body, headers }) {
61
+ return request({ method, path, query, body, headers });
62
+ }
63
+ async function request({ method = "GET", path, query, body, headers, bearer }) {
64
+ const base = await getBaseUrl();
56
65
  const url = new URL(base + path);
57
66
  if (query) {
58
67
  for (const [k, v] of Object.entries(query)) {
@@ -60,14 +69,13 @@ export async function call({ method = "GET", path, query, body, headers, asUserI
60
69
  url.searchParams.set(k, String(v));
61
70
  }
62
71
  }
63
- const token = mintToken(asUserId);
64
72
  const res = await fetchWithRetry(() => ({
65
73
  url,
66
74
  init: {
67
75
  method,
68
76
  headers: {
69
77
  "content-type": "application/json",
70
- authorization: `Bearer ${token}`,
78
+ ...(bearer ? { authorization: bearer } : {}),
71
79
  ...headers,
72
80
  },
73
81
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -92,9 +100,10 @@ function safeJson(s) {
92
100
  }
93
101
  }
94
102
  export async function callMultipart({ path, asUserId, fields, file }) {
95
- const base = getBaseUrl();
103
+ const config = await readConfig();
104
+ const { bearer } = resolveAuth(asUserId, process.env, config);
105
+ const base = await getBaseUrl();
96
106
  const url = base + path;
97
- const token = mintToken(asUserId);
98
107
  const res = await fetchWithRetry(() => {
99
108
  const fd = new FormData();
100
109
  if (fields) {
@@ -107,7 +116,7 @@ export async function callMultipart({ path, asUserId, fields, file }) {
107
116
  url,
108
117
  init: {
109
118
  method: "POST",
110
- headers: { authorization: `Bearer ${token}` },
119
+ headers: { authorization: bearer },
111
120
  body: fd,
112
121
  },
113
122
  };
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { getBaseUrl } from "../client.js";
4
+ import { getConfigPath, saveConfig } from "../config.js";
4
5
  import { getOrCreateGroup } from "./_group.js";
5
6
  async function postJson(path, body) {
6
- const res = await fetch(getBaseUrl() + path, {
7
+ const res = await fetch((await getBaseUrl()) + path, {
7
8
  method: "POST",
8
9
  headers: { "content-type": "application/json" },
9
10
  body: JSON.stringify(body),
@@ -37,8 +38,22 @@ function normalize(data) {
37
38
  r.accessToken = r.token;
38
39
  return r;
39
40
  }
40
- function emit(data) {
41
- process.stdout.write(JSON.stringify(normalize(data)) + "\n");
41
+ async function emit(data, save = false) {
42
+ const result = normalize(data);
43
+ process.stdout.write(JSON.stringify(result) + "\n");
44
+ if (save && result.token) {
45
+ await saveConfig({
46
+ baseUrl: await getBaseUrl(),
47
+ token: result.token,
48
+ });
49
+ if (process.stdout.isTTY) {
50
+ process.stderr.write(`Logged in and saved to ${getConfigPath()}. Next: workcord rooms list\n`);
51
+ }
52
+ return;
53
+ }
54
+ if (result.token) {
55
+ process.stderr.write("Logged in. Re-run with --save to remember this login on this computer.\n");
56
+ }
42
57
  }
43
58
  async function promptCode() {
44
59
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -55,7 +70,7 @@ async function verifyEmailCode(sessionId, code) {
55
70
  }
56
71
  return postJson("/api/v1/auth/email/verify-code", { sessionId, code });
57
72
  }
58
- async function loginByEmail(email, code) {
73
+ async function loginByEmail(email, code, save) {
59
74
  const sessionId = randomUUID();
60
75
  try {
61
76
  await postJson(`/api/v1/auth/email/login/by-code/${encodeURIComponent(sessionId)}`, { email });
@@ -63,8 +78,9 @@ async function loginByEmail(email, code) {
63
78
  catch (err) {
64
79
  const c = errorCode(err);
65
80
  if (c === "DOMAIN_NOT_ALLOWED" || c === "EMAIL_NOT_ALLOWED") {
66
- throw new Error(`Email ${email} is not allowed to sign in on this deployment. ` +
67
- "Check the stack's EMAIL_AUTH_ALLOWED_DOMAINS / email_auth_allowlist.");
81
+ const allowed = err.body?.error?.allowedDomains;
82
+ const domains = Array.isArray(allowed) ? allowed.filter((value) => typeof value === "string") : [];
83
+ throw new Error(`Email ${email} cannot sign in on this deployment.${domains.length ? ` Allowed domains: ${domains.join(", ")}.` : ""} Try an allowed work address or ask your WorkCord administrator to enable this domain.`);
68
84
  }
69
85
  throw err;
70
86
  }
@@ -75,9 +91,9 @@ async function loginByEmail(email, code) {
75
91
  process.stdout.write(JSON.stringify({ sessionId, codeSent: true }) + "\n");
76
92
  return;
77
93
  }
78
- emit(await verifyEmailCode(sessionId, entered));
94
+ await emit(await verifyEmailCode(sessionId, entered), save);
79
95
  }
80
- async function loginByPhone(phone, code) {
96
+ async function loginByPhone(phone, code, save) {
81
97
  try {
82
98
  await postJson("/api/v1/auth/send-otp", { phone });
83
99
  }
@@ -88,18 +104,20 @@ async function loginByPhone(phone, code) {
88
104
  }
89
105
  throw err;
90
106
  }
91
- emit(await postJson("/api/v1/auth/verify-otp", { phone, code }));
107
+ await emit(await postJson("/api/v1/auth/verify-otp", { phone, code }), save);
92
108
  }
93
109
  export function registerAuth(program) {
94
110
  const group = getOrCreateGroup(program, "auth", "Auth operations");
95
111
  group
96
112
  .command("login")
97
- .description("End-to-end login. Returns { token, accessToken, refreshToken, user, isNewUser }. " +
113
+ .description("End-to-end login. Use --save to remember this login on this computer. Returns { token, accessToken, refreshToken, user, isNewUser }. " +
98
114
  "--email uses the emailed 6-digit code (deployed WorkCord/VIA); --phone uses OTP " +
99
- "(local/RoomCord only). Does NOT use --as / WORKCORD_JWT_SECRET — the server mints the token.")
115
+ "(local/RoomCord only). Does NOT use --as / WORKCORD_JWT_SECRET — the server mints the token. " +
116
+ "Without --save, set the returned token as WORKCORD_TOKEN to authenticate later commands.")
100
117
  .option("--email <address>", "email address (mutually exclusive with --phone)")
101
118
  .option("--phone <e164>", 'phone number in E.164 format (e.g. "+14155550199")')
102
119
  .option("--code <code>", "verification code; prompted interactively for --email when omitted")
120
+ .option("--save", "remember this login on this computer")
103
121
  .action(async (opts) => {
104
122
  if (opts.email && opts.phone) {
105
123
  throw new Error("--email and --phone are mutually exclusive");
@@ -108,18 +126,19 @@ export function registerAuth(program) {
108
126
  throw new Error("one of --email <address> or --phone <e164> is required");
109
127
  }
110
128
  if (opts.email) {
111
- await loginByEmail(opts.email, opts.code);
129
+ await loginByEmail(opts.email, opts.code, !!opts.save);
112
130
  return;
113
131
  }
114
- await loginByPhone(opts.phone, opts.code ?? "111111");
132
+ await loginByPhone(opts.phone, opts.code ?? "111111", !!opts.save);
115
133
  });
116
134
  group
117
135
  .command("verify-email")
118
136
  .description("Verify an emailed code for a session started by `auth login --email`")
119
137
  .requiredOption("--session-id <id>", "sessionId printed by `auth login --email`")
120
138
  .requiredOption("--code <code>", "6-digit code from the email")
139
+ .option("--save", "remember this login on this computer")
121
140
  .action(async (opts) => {
122
- emit(await verifyEmailCode(opts.sessionId, opts.code));
141
+ await emit(await verifyEmailCode(opts.sessionId, opts.code), !!opts.save);
123
142
  });
124
143
  }
125
144
  //# sourceMappingURL=auth.js.map
@@ -1,11 +1,6 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
- function currentUser(program) {
4
- const { as } = program.opts();
5
- if (!as)
6
- throw new Error("--as <userId> is required");
7
- return as;
8
- }
9
4
  /**
10
5
  * Custom challenge commands that are NOT covered by the OpenAPI-generated
11
6
  * commands (src/generated/commands/challenges.ts). Everything else — create,
@@ -34,7 +29,7 @@ export function registerChallenges(program) {
34
29
  const data = (await call({
35
30
  method: "POST",
36
31
  path: `/api/v1/challenges/${encodeURIComponent(opts.id)}/regenerate-cards?offset=${offset}&limit=10`,
37
- asUserId: currentUser(program),
32
+ asUserId: actingUserId(program),
38
33
  body: {},
39
34
  }));
40
35
  totals.total = data.total;
@@ -1,12 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { actingUserId } from "../auth.js";
2
3
  import { call } from "../client.js";
3
4
  import { getOrCreateGroup } from "./_group.js";
4
- function currentUser(program) {
5
- const { as } = program.opts();
6
- if (!as)
7
- throw new Error("--as <userId> is required");
8
- return as;
9
- }
10
5
  function bool(v, name) {
11
6
  if (v === undefined)
12
7
  return undefined;
@@ -54,7 +49,7 @@ export function registerCustomAgents(program) {
54
49
  const data = await call({
55
50
  method: "POST",
56
51
  path: "/api/v1/agents",
57
- asUserId: currentUser(program),
52
+ asUserId: actingUserId(program),
58
53
  body: {
59
54
  name: opts.name,
60
55
  description: opts.description,
@@ -111,7 +106,7 @@ export function registerCustomAgents(program) {
111
106
  const data = await call({
112
107
  method: "PATCH",
113
108
  path: `/api/v1/agents/${encodeURIComponent(opts.agent)}`,
114
- asUserId: currentUser(program),
109
+ asUserId: actingUserId(program),
115
110
  body,
116
111
  });
117
112
  process.stdout.write(JSON.stringify(data) + "\n");
@@ -1,3 +1,4 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
4
  export function registerDm(program) {
@@ -10,9 +11,7 @@ export function registerDm(program) {
10
11
  .requiredOption("--text <text>", "message body — mention users with @userId inline")
11
12
  .option("--reply-to <messageId>", "parent message id (for threaded replies)")
12
13
  .action(async (opts) => {
13
- const { as } = program.opts();
14
- if (!as)
15
- throw new Error("--as <userId> is required");
14
+ const as = actingUserId(program);
16
15
  const dm = (await call({
17
16
  method: "POST",
18
17
  path: "/api/v1/dm/start",
@@ -0,0 +1,29 @@
1
+ import { call, callPublic, getBaseUrl } from "../client.js";
2
+ export function registerHealth(program) {
3
+ program
4
+ .command("health")
5
+ .allowExcessArguments(false)
6
+ .description("Check the selected server and saved login")
7
+ .action(async () => {
8
+ const baseUrl = await getBaseUrl();
9
+ const health = await callPublic({ path: "/api/health" });
10
+ let loggedIn = false;
11
+ let userId;
12
+ try {
13
+ const user = await call({ path: "/api/v1/users/me", asUserId: program.opts().as });
14
+ loggedIn = true;
15
+ userId = user.id;
16
+ }
17
+ catch {
18
+ loggedIn = false;
19
+ }
20
+ process.stdout.write(JSON.stringify({ baseUrl, loggedIn, userId, health }) + "\n");
21
+ });
22
+ program
23
+ .command("hello")
24
+ .description("Check the selected server")
25
+ .action(async () => {
26
+ process.stdout.write(JSON.stringify(await callPublic({ path: "/api/hello" })) + "\n");
27
+ });
28
+ }
29
+ //# sourceMappingURL=health.js.map