@fulldivevr/workcord-cli 1.0.0

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 (82) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +312 -0
  3. package/dist/auth.js +9 -0
  4. package/dist/cli.js +46 -0
  5. package/dist/client.js +125 -0
  6. package/dist/commands/_group.js +5 -0
  7. package/dist/commands/auth.js +125 -0
  8. package/dist/commands/challenges.js +51 -0
  9. package/dist/commands/custom-agents.js +120 -0
  10. package/dist/commands/dm.js +35 -0
  11. package/dist/commands/messages.js +360 -0
  12. package/dist/commands/room-agents.js +47 -0
  13. package/dist/commands/rooms.js +687 -0
  14. package/dist/commands/users.js +126 -0
  15. package/dist/commands/whoami.js +15 -0
  16. package/dist/generated/_helpers.js +8 -0
  17. package/dist/generated/commands/activity.js +46 -0
  18. package/dist/generated/commands/ai.js +40 -0
  19. package/dist/generated/commands/ambassador.js +34 -0
  20. package/dist/generated/commands/apple-iap.js +61 -0
  21. package/dist/generated/commands/attribution.js +130 -0
  22. package/dist/generated/commands/auth.js +205 -0
  23. package/dist/generated/commands/challenges.js +251 -0
  24. package/dist/generated/commands/clips.js +71 -0
  25. package/dist/generated/commands/contact-match.js +92 -0
  26. package/dist/generated/commands/content-crawler.js +27 -0
  27. package/dist/generated/commands/custom-agents.js +346 -0
  28. package/dist/generated/commands/desktop-releases.js +236 -0
  29. package/dist/generated/commands/discover.js +236 -0
  30. package/dist/generated/commands/dm.js +155 -0
  31. package/dist/generated/commands/drafts.js +41 -0
  32. package/dist/generated/commands/embed.js +34 -0
  33. package/dist/generated/commands/explore.js +19 -0
  34. package/dist/generated/commands/feed.js +67 -0
  35. package/dist/generated/commands/feedback.js +44 -0
  36. package/dist/generated/commands/gamification.js +33 -0
  37. package/dist/generated/commands/google-play-iap.js +73 -0
  38. package/dist/generated/commands/health.js +32 -0
  39. package/dist/generated/commands/internal.js +162 -0
  40. package/dist/generated/commands/interview.js +76 -0
  41. package/dist/generated/commands/invite-redirect.js +20 -0
  42. package/dist/generated/commands/jesuscord-admin.js +36 -0
  43. package/dist/generated/commands/join.js +27 -0
  44. package/dist/generated/commands/kits.js +108 -0
  45. package/dist/generated/commands/meetings.js +81 -0
  46. package/dist/generated/commands/message-folders.js +68 -0
  47. package/dist/generated/commands/messages.js +418 -0
  48. package/dist/generated/commands/one-emulator.js +32 -0
  49. package/dist/generated/commands/pdf-export.js +19 -0
  50. package/dist/generated/commands/pin-folders.js +72 -0
  51. package/dist/generated/commands/pinned-messages.js +81 -0
  52. package/dist/generated/commands/polls.js +152 -0
  53. package/dist/generated/commands/premium-feeds.js +32 -0
  54. package/dist/generated/commands/processors.js +22 -0
  55. package/dist/generated/commands/questions.js +54 -0
  56. package/dist/generated/commands/referrals.js +19 -0
  57. package/dist/generated/commands/reports.js +21 -0
  58. package/dist/generated/commands/room-a2-a.js +81 -0
  59. package/dist/generated/commands/room-agents.js +273 -0
  60. package/dist/generated/commands/room-invites.js +155 -0
  61. package/dist/generated/commands/room-owned-agent.js +36 -0
  62. package/dist/generated/commands/room-page.js +20 -0
  63. package/dist/generated/commands/rooms.js +1117 -0
  64. package/dist/generated/commands/saved-messages.js +84 -0
  65. package/dist/generated/commands/saved-stories.js +125 -0
  66. package/dist/generated/commands/search.js +44 -0
  67. package/dist/generated/commands/shadow-home-agent.js +36 -0
  68. package/dist/generated/commands/shadow.js +64 -0
  69. package/dist/generated/commands/share-cards.js +85 -0
  70. package/dist/generated/commands/share.js +20 -0
  71. package/dist/generated/commands/sponsors.js +20 -0
  72. package/dist/generated/commands/stories.js +324 -0
  73. package/dist/generated/commands/story-by-id.js +20 -0
  74. package/dist/generated/commands/submissions.js +112 -0
  75. package/dist/generated/commands/subscriptions.js +103 -0
  76. package/dist/generated/commands/tips.js +76 -0
  77. package/dist/generated/commands/users.js +373 -0
  78. package/dist/generated/commands/voice-token.js +21 -0
  79. package/dist/generated/commands/well-known.js +32 -0
  80. package/dist/generated/commands/wrapped.js +20 -0
  81. package/dist/generated/index.js +131 -0
  82. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2026 Fulldive. All rights reserved.
2
+
3
+ This software is proprietary and confidential.
4
+
5
+ It is published to the npm registry solely to allow authorized Fulldive
6
+ personnel and systems to install it. Publication does not grant any license
7
+ to use, copy, modify, merge, publish, distribute, sublicense, or sell this
8
+ software or derivative works of it.
9
+
10
+ No rights are granted except by a separate written agreement with Fulldive.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # @fulldivevr/workcord-cli
2
+
3
+ TypeScript CLI for the WorkCord API. **One invocation = one user action.**
4
+ Designed for automated agents that emulate real users — wake up, do one
5
+ thing, exit. Route-level commands are generated from the backend's own
6
+ OpenAPI spec; a small set of hand-written semantic commands sits on top.
7
+
8
+ Lives in the backend repo (`clients/workcord-cli`) so the spec it generates
9
+ from is always the spec this server serves.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -g @fulldivevr/workcord-cli
15
+ # or run without installing
16
+ npx @fulldivevr/workcord-cli --help
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ | Setting | How |
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 |
25
+ | Internal admin key | `WORKCORD_INTERNAL_API_KEY`, internal commands only |
26
+ | Acting user | `--as <userId>`, per invocation |
27
+
28
+ Base URL must be a bare origin (scheme + host, no path). The `/api/v1` prefix
29
+ is added by the CLI.
30
+
31
+ | Deployment | `WORKCORD_API_URL` |
32
+ | --- | --- |
33
+ | WorkCord prod | `https://work-rooms-api.wizeup.app` |
34
+ | VIA | `https://via-rooms-api.wizeup.app` |
35
+ | RoomCord prod | `https://rooms-api.wizeup.app` |
36
+ | RoomCord dev | `https://rooms-api-dev.wizeup.app` |
37
+ | JesusCord | `https://jesuscord-api.wizeup.app` |
38
+ | Local WorkCord | `http://localhost:3001` |
39
+ | Local RoomCord / JesusCord | `http://localhost:3000` |
40
+
41
+ ```bash
42
+ export WORKCORD_API_URL=https://work-rooms-api.wizeup.app
43
+ export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
44
+
45
+ workcord --as <userId> users get-me
46
+ ```
47
+
48
+ Output is JSON on stdout, errors on stderr, non-zero exit on failure.
49
+
50
+ ### How auth works
51
+
52
+ Three distinct modes.
53
+
54
+ **1. Normal user login (email code).** Deployed WorkCord/VIA stacks run with
55
+ `ENABLE_EMAIL_AUTH=true` and mail a 6-digit code:
56
+
57
+ ```bash
58
+ workcord auth login --email you@company.com
59
+ # → code arrives by email, CLI prompts for it, prints { token, accessToken, refreshToken, user, isNewUser }
60
+ ```
61
+
62
+ Non-interactive (no TTY) it prints a `sessionId` and you finish with:
63
+
64
+ ```bash
65
+ workcord auth verify-email --session-id <id> --code 123456
66
+ ```
67
+
68
+ Email login returns `accessToken`; phone/social login returns `token`. The CLI
69
+ emits both fields on either path so callers can read one consistently.
70
+
71
+ Email auth is domain-restricted. WorkCord prod and VIA both allow
72
+ `fulldive.com`, `via.com`, `via.com.tw`, `viatech.com.cn`, `*.tvbs.com`,
73
+ `tvbs.*.com`, `tvbs.com.*` (`EMAIL_AUTH_ALLOWED_DOMAINS` in
74
+ `.sc/stacks/wize-rooms-api/client.yaml`). Anything else gets
75
+ `DOMAIN_NOT_ALLOWED`.
76
+
77
+ `auth login --phone` is for local and RoomCord only. Deployed WorkCord does not
78
+ set `AUTH_PHONE_OTP_DEV_BYPASS`, so `POST /auth/send-otp` returns 503
79
+ `OTP_NOT_AVAILABLE` — the CLI turns that into an explicit "use `--email`" error.
80
+
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.
86
+
87
+ Same secret on both sides means the CLI can act as **any** user, with no
88
+ password and no server-side check. Treat it as privileged signing material:
89
+ 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.
91
+
92
+ **3. Internal admin routes.** `WORKCORD_INTERNAL_API_KEY` is sent as the
93
+ `x-internal-key` header; the server compares it to its own `INTERNAL_API_KEY`.
94
+ Required for `/internal/*` and the jesuscord admin routes. Treat it like the signing secret.
95
+
96
+ ## Usage
97
+
98
+ ```
99
+ workcord [--as <userId>] <namespace> <command> [flags]
100
+ ```
101
+
102
+ - `workcord --help` lists namespaces.
103
+ - `workcord <namespace> --help` lists that namespace's commands.
104
+ - `workcord <namespace> <command> --help` shows the command's flags.
105
+
106
+ Two layers coexist under the same namespaces.
107
+
108
+ **Auto-generated** (one per server route, namespaced by controller):
109
+
110
+ ```bash
111
+ workcord <namespace> <command> [--<path-param> <value>] [--body '<json>'] [--query '<json>']
112
+ ```
113
+
114
+ **Hand-written semantic** (small set, rich typed flags):
115
+
116
+ ```bash
117
+ workcord auth login --email you@company.com
118
+ workcord --as USER messages send-text --room R --text "hello"
119
+ ```
120
+
121
+ Semantic aliases cover common agent/admin actions with consistent flags:
122
+ paginated listing/searching public users, internal authenticated/anonymous
123
+ user filters, acting-user room lists, discoverable app-wide room lists, room
124
+ create/update/delete, join/leave/read, list members, remove members,
125
+ list/approve/deny join requests, rotate join codes, get the acting user's
126
+ membership, and merge recent messages visible to a user across rooms. The
127
+ generated route-level commands remain available for every endpoint.
128
+
129
+ `users list --account-type ...` and `users list --authenticated` use the
130
+ internal API and require `WORKCORD_INTERNAL_API_KEY`.
131
+ Without those flags, `users list` uses public search and only returns public
132
+ profile fields.
133
+
134
+ Server auto-extracts mention tokens from visible `@Name` text. Prefer
135
+ `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).
138
+
139
+ ### Error handling
140
+
141
+ Every error is **a single JSON line on stderr**, with a non-zero exit code:
142
+
143
+ ```json
144
+ {"error":"GET /api/v1/rooms/X -> 404","status":404,"body":{"error":{"code":"NOT_FOUND","message":"Room not found"}}}
145
+ ```
146
+
147
+ Agents can branch on `.status` (`401` → re-login, `404` → resource gone, `5xx`
148
+ → already retried, give up) and `.body.error.code` for app-specific codes.
149
+
150
+ ## Examples
151
+
152
+ ```bash
153
+ workcord --as USER rooms get-room --room-id R
154
+ workcord --as USER rooms create-room --body '{"name":"hello","isPublic":true}'
155
+ workcord --as USER messages list --room-id R --query '{"cursor":"2026-04-28T00:00:00Z","limit":50}'
156
+
157
+ workcord --as USER users list --q alex --limit 10
158
+ workcord --as ADMIN users list --account-type anonymous --limit 20
159
+ workcord --as ADMIN users set-plan --user USER_ID --tier pro --platform internal
160
+ workcord --as USER rooms discover --search "ai" --tags ai,tools --limit 20
161
+ workcord --as USER rooms create-with-message --name "ops" --public --tags ai,news --text "First post."
162
+ workcord --as USER rooms members --room R
163
+ workcord --as USER rooms upload-icon-file --room R --file ./icon.png
164
+ workcord --as USER custom-agents create-hosted --name "Helper" --description "Room helper" --prompt-file ./prompt.txt --tools web_search
165
+ workcord --as USER room-agents install-hosted --room R --agent system-wizeup
166
+ workcord --as USER messages react --room R --message M --emoji 👍
167
+ workcord --as USER dm send-text --to USER_B --text "hi"
168
+ ```
169
+
170
+ ### Common agent patterns
171
+
172
+ **Send a message with a mention + image:**
173
+
174
+ ```bash
175
+ URL=$(workcord --as $U rooms upload-image-file --room $R --file ./img.png | jq -r .url)
176
+ workcord --as $U messages send-text --room $R --text "look @user_abc" --attachments "$URL"
177
+ ```
178
+
179
+ **Poll for new messages since last check:**
180
+
181
+ ```bash
182
+ SINCE=2026-04-28T10:00:00Z
183
+ workcord --as $U messages list --room-id $R --query "{\"cursor\":\"$SINCE\",\"limit\":50}"
184
+ ```
185
+
186
+ **Receive latest messages visible to a user across rooms:**
187
+
188
+ ```bash
189
+ workcord --as $U messages latest --limit 50 --per-room-limit 10
190
+ workcord --as $U messages latest --since "$SINCE" --inbox-category aiRooms --limit 100
191
+ ```
192
+
193
+ This composite command lists the acting user's rooms, fetches recent messages
194
+ from each room, merges them by `sentAt` descending, and includes
195
+ `room`/`roomName` context on each message.
196
+
197
+ **Switch user identity per call:**
198
+
199
+ The CLI is stateless — `--as <userId>` per invocation. One process can act as
200
+ 100 users by changing the flag.
201
+
202
+ **Onboard a fresh agent identity:**
203
+
204
+ ```bash
205
+ # deployed WorkCord / VIA
206
+ USER=$(workcord auth verify-email --session-id $SID --code $CODE | jq -r .user.id)
207
+
208
+ # local / RoomCord dev, where AUTH_PHONE_OTP_DEV_BYPASS=true
209
+ USER=$(workcord auth login --phone "+14155550$RANDOM" | jq -r .user.id)
210
+ ```
211
+
212
+ Locally any phone with OTP `111111` works. On deployed WorkCord phone OTP is
213
+ disabled — start with `workcord auth login --email <address>`.
214
+
215
+ **React to a message + reply with mention:**
216
+
217
+ ```bash
218
+ workcord --as $U messages react --room $R --message $M --emoji 👍
219
+ workcord --as $U messages send-text --room $R --text "agreed @user_abc" --reply-to $M
220
+ ```
221
+
222
+ **DM a user once-and-done:**
223
+
224
+ ```bash
225
+ workcord --as $U dm send-text --to $OTHER --text "hey"
226
+ # → composite: opens (or finds) the DM room, sends the message, returns both
227
+ ```
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ cd clients/workcord-cli
233
+ npm install
234
+ npm run build
235
+ npx tsx src/cli.ts --as <userId> users get-me
236
+ ```
237
+
238
+ ### Refreshing the spec when the server changes
239
+
240
+ `npm run sync-spec` builds the backend from the repo root (`npm run
241
+ dump-spec`) and copies the resulting `cli/openapi.json` into this package.
242
+ No GitHub token and no network — the spec comes from the source tree you are
243
+ sitting in.
244
+
245
+ ```bash
246
+ npm run regen # sync-spec + generate
247
+ npm run typecheck # confirm clean
248
+ ```
249
+
250
+ Generated files live in `src/generated/` and are committed, so the CLI works
251
+ without running `regen` if you don't need the latest server changes.
252
+
253
+ ### Layout
254
+
255
+ ```
256
+ src/
257
+ ├── cli.ts commander entry, global --as flag
258
+ ├── auth.ts mints HS256 JWT from --as
259
+ ├── client.ts fetch wrapper (JSON + multipart)
260
+ ├── commands/ hand-written semantic commands
261
+ └── generated/ auto-generated from openapi.json
262
+ ├── index.ts
263
+ └── commands/<tag>.ts one file per controller namespace
264
+
265
+ scripts/
266
+ ├── sync-spec.ts backend build → openapi.json
267
+ └── generate-commands.ts spec → commander commands
268
+
269
+ tests/smoke.sh hermetic end-to-end test
270
+ ```
271
+
272
+ ### Adding a hand-written semantic command
273
+
274
+ When an auto-generated command has a use pattern that would benefit from rich
275
+ flags (e.g. typed fields instead of an opaque `--body`):
276
+
277
+ 1. Create `src/commands/<area>.ts`.
278
+ 2. Use `getOrCreateGroup(program, "<area>", "...")` to extend the
279
+ auto-generated namespace, then `.command("<verb>")` for the new subcommand.
280
+ 3. Register in `src/cli.ts`.
281
+ 4. Add an assertion in `tests/smoke.sh`.
282
+
283
+ ### Testing
284
+
285
+ ```bash
286
+ npm run smoke
287
+ ```
288
+
289
+ Hermetic end-to-end suite (creates two fresh users per run via OTP, exercises
290
+ multi-user flows, file upload, custom agents, error paths). Requires a running
291
+ server (`WORKCORD_API_URL`) and a matching `WORKCORD_JWT_SECRET`, so it is not
292
+ part of any automated gate.
293
+
294
+ ## Release
295
+
296
+ Publishing is a manual maintainer action — there is no auto-publish CI.
297
+
298
+ 1. `npm run regen` — refresh the spec and generated commands; commit the diff.
299
+ 2. `npm run typecheck`
300
+ 3. `npm run build`
301
+ 4. `npm pack --dry-run` — confirm the file list is exactly `dist/**/*.js`,
302
+ `README.md`, `LICENSE`, `package.json`. No sources, no `openapi.json`, no
303
+ `.npmrc`.
304
+ 5. Smoke: `node dist/cli.js --help` and `node dist/cli.js --version`.
305
+ 6. Bump `version` in `package.json`, then `npm publish`.
306
+
307
+ `prepack` re-runs `typecheck` + `build`, so steps 2–3 also gate the publish.
308
+
309
+ ## License
310
+
311
+ Proprietary — see [LICENSE](./LICENSE). Published to npm for installation by
312
+ authorized Fulldive personnel and systems only; no reuse rights are granted.
package/dist/auth.js ADDED
@@ -0,0 +1,9 @@
1
+ import jwt from "jsonwebtoken";
2
+ export function mintToken(userId) {
3
+ const secret = process.env.WORKCORD_JWT_SECRET;
4
+ if (!secret) {
5
+ throw new Error("WORKCORD_JWT_SECRET not set");
6
+ }
7
+ return jwt.sign({ userId, id: userId }, secret, { algorithm: "HS256", expiresIn: "60d" });
8
+ }
9
+ //# sourceMappingURL=auth.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
4
+ import { setBaseUrlOverride } from "./client.js";
5
+ import { registerWhoami } from "./commands/whoami.js";
6
+ import { registerAuth } from "./commands/auth.js";
7
+ import { registerDm } from "./commands/dm.js";
8
+ import { registerMessages } from "./commands/messages.js";
9
+ import { registerRooms } from "./commands/rooms.js";
10
+ import { registerUsers } from "./commands/users.js";
11
+ import { registerRoomAgents } from "./commands/room-agents.js";
12
+ import { registerCustomAgents } from "./commands/custom-agents.js";
13
+ import { registerChallenges } from "./commands/challenges.js";
14
+ import { registerAll as registerGenerated } from "./generated/index.js";
15
+ const program = new Command();
16
+ const require = createRequire(import.meta.url);
17
+ const { version } = require("../package.json");
18
+ program
19
+ .name("workcord")
20
+ .description("CLI for the WorkCord API. One invocation = one user action.")
21
+ .version(version)
22
+ .option("--as <userId>", "user to act as")
23
+ .option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL)");
24
+ program.hook("preAction", () => {
25
+ setBaseUrlOverride(program.opts().baseUrl);
26
+ });
27
+ registerWhoami(program);
28
+ registerAuth(program);
29
+ registerMessages(program);
30
+ registerRooms(program);
31
+ registerDm(program);
32
+ registerUsers(program);
33
+ registerRoomAgents(program);
34
+ registerCustomAgents(program);
35
+ registerChallenges(program); // adds regenerate-cards (not in the OpenAPI spec)
36
+ registerGenerated(program);
37
+ program.parseAsync().catch((err) => {
38
+ const out = { error: err.message ?? String(err) };
39
+ if (err.status !== undefined)
40
+ out.status = err.status;
41
+ if (err.body !== undefined)
42
+ out.body = err.body;
43
+ process.stderr.write(JSON.stringify(out) + "\n");
44
+ process.exit(1);
45
+ });
46
+ //# sourceMappingURL=cli.js.map
package/dist/client.js ADDED
@@ -0,0 +1,125 @@
1
+ import { mintToken } from "./auth.js";
2
+ let baseUrlOverride;
3
+ export function setBaseUrlOverride(url) {
4
+ baseUrlOverride = url;
5
+ }
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
+ }
11
+ const trimmed = raw.replace(/\/+$/, "");
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(trimmed);
15
+ }
16
+ catch {
17
+ throw new Error(`Invalid API base URL: ${raw}`);
18
+ }
19
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
20
+ throw new Error(`API base URL must be http or https: ${raw}`);
21
+ }
22
+ if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
23
+ throw new Error(`API base URL must be an origin with no path, query or fragment: ${raw}`);
24
+ }
25
+ return parsed.origin;
26
+ }
27
+ const RETRY_MAX = Number(process.env.WORKCORD_RETRY_MAX ?? 3);
28
+ const RETRY_BACKOFF_MS = Number(process.env.WORKCORD_RETRY_BACKOFF_MS ?? 200);
29
+ const RETRY_STATUSES = new Set([408, 429, 502, 503, 504]);
30
+ async function fetchWithRetry(build) {
31
+ let lastErr;
32
+ for (let attempt = 0; attempt <= RETRY_MAX; attempt++) {
33
+ try {
34
+ const { url, init } = build();
35
+ const res = await fetch(url, init);
36
+ if (!RETRY_STATUSES.has(res.status))
37
+ return res;
38
+ lastErr = res;
39
+ if (attempt >= RETRY_MAX)
40
+ return res;
41
+ }
42
+ catch (err) {
43
+ lastErr = err;
44
+ if (attempt >= RETRY_MAX)
45
+ throw err;
46
+ }
47
+ const delay = RETRY_BACKOFF_MS * Math.pow(3, attempt);
48
+ await new Promise((r) => setTimeout(r, delay));
49
+ }
50
+ if (lastErr instanceof Response)
51
+ return lastErr;
52
+ throw lastErr;
53
+ }
54
+ export async function call({ method = "GET", path, query, body, headers, asUserId }) {
55
+ const base = getBaseUrl();
56
+ const url = new URL(base + path);
57
+ if (query) {
58
+ for (const [k, v] of Object.entries(query)) {
59
+ if (v !== undefined && v !== null)
60
+ url.searchParams.set(k, String(v));
61
+ }
62
+ }
63
+ const token = mintToken(asUserId);
64
+ const res = await fetchWithRetry(() => ({
65
+ url,
66
+ init: {
67
+ method,
68
+ headers: {
69
+ "content-type": "application/json",
70
+ authorization: `Bearer ${token}`,
71
+ ...headers,
72
+ },
73
+ body: body !== undefined ? JSON.stringify(body) : undefined,
74
+ },
75
+ }));
76
+ const text = await res.text();
77
+ const data = text ? safeJson(text) : null;
78
+ if (!res.ok) {
79
+ const err = new Error(`${method} ${path} -> ${res.status}`);
80
+ err.status = res.status;
81
+ err.body = data;
82
+ throw err;
83
+ }
84
+ return data;
85
+ }
86
+ function safeJson(s) {
87
+ try {
88
+ return JSON.parse(s);
89
+ }
90
+ catch {
91
+ return s;
92
+ }
93
+ }
94
+ export async function callMultipart({ path, asUserId, fields, file }) {
95
+ const base = getBaseUrl();
96
+ const url = base + path;
97
+ const token = mintToken(asUserId);
98
+ const res = await fetchWithRetry(() => {
99
+ const fd = new FormData();
100
+ if (fields) {
101
+ for (const [k, v] of Object.entries(fields))
102
+ fd.append(k, v);
103
+ }
104
+ const blob = new Blob([new Uint8Array(file.data)], { type: file.contentType ?? "application/octet-stream" });
105
+ fd.append(file.fieldName, blob, file.fileName);
106
+ return {
107
+ url,
108
+ init: {
109
+ method: "POST",
110
+ headers: { authorization: `Bearer ${token}` },
111
+ body: fd,
112
+ },
113
+ };
114
+ });
115
+ const text = await res.text();
116
+ const data = text ? safeJson(text) : null;
117
+ if (!res.ok) {
118
+ const err = new Error(`POST ${path} -> ${res.status}`);
119
+ err.status = res.status;
120
+ err.body = data;
121
+ throw err;
122
+ }
123
+ return data;
124
+ }
125
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,5 @@
1
+ export function getOrCreateGroup(program, name, description) {
2
+ const existing = program.commands.find((c) => c.name() === name);
3
+ return existing ?? program.command(name).description(description);
4
+ }
5
+ //# sourceMappingURL=_group.js.map
@@ -0,0 +1,125 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { getBaseUrl } from "../client.js";
4
+ import { getOrCreateGroup } from "./_group.js";
5
+ async function postJson(path, body) {
6
+ const res = await fetch(getBaseUrl() + path, {
7
+ method: "POST",
8
+ headers: { "content-type": "application/json" },
9
+ body: JSON.stringify(body),
10
+ });
11
+ const text = await res.text();
12
+ let data = null;
13
+ try {
14
+ data = text ? JSON.parse(text) : null;
15
+ }
16
+ catch {
17
+ data = text;
18
+ }
19
+ if (!res.ok) {
20
+ const err = new Error(`POST ${path} -> ${res.status}`);
21
+ err.status = res.status;
22
+ err.body = data;
23
+ throw err;
24
+ }
25
+ return data;
26
+ }
27
+ function errorCode(err) {
28
+ return err.body?.error?.code;
29
+ }
30
+ // Phone/social login returns `token`, email login returns `accessToken`.
31
+ // Both are emitted so downstream consumers can read either.
32
+ function normalize(data) {
33
+ const r = { ...(data ?? {}) };
34
+ if (!r.token && r.accessToken)
35
+ r.token = r.accessToken;
36
+ if (!r.accessToken && r.token)
37
+ r.accessToken = r.token;
38
+ return r;
39
+ }
40
+ function emit(data) {
41
+ process.stdout.write(JSON.stringify(normalize(data)) + "\n");
42
+ }
43
+ async function promptCode() {
44
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
45
+ try {
46
+ return (await rl.question("Verification code (6 digits): ")).trim();
47
+ }
48
+ finally {
49
+ rl.close();
50
+ }
51
+ }
52
+ async function verifyEmailCode(sessionId, code) {
53
+ if (!/^\d{6}$/.test(code)) {
54
+ throw new Error("Verification code must be exactly 6 digits");
55
+ }
56
+ return postJson("/api/v1/auth/email/verify-code", { sessionId, code });
57
+ }
58
+ async function loginByEmail(email, code) {
59
+ const sessionId = randomUUID();
60
+ try {
61
+ await postJson(`/api/v1/auth/email/login/by-code/${encodeURIComponent(sessionId)}`, { email });
62
+ }
63
+ catch (err) {
64
+ const c = errorCode(err);
65
+ 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.");
68
+ }
69
+ throw err;
70
+ }
71
+ const entered = code ?? (process.stdin.isTTY ? await promptCode() : undefined);
72
+ if (!entered) {
73
+ process.stderr.write("Code sent. stdin is not a TTY, so finish with:\n" +
74
+ ` workcord auth verify-email --session-id ${sessionId} --code <code>\n`);
75
+ process.stdout.write(JSON.stringify({ sessionId, codeSent: true }) + "\n");
76
+ return;
77
+ }
78
+ emit(await verifyEmailCode(sessionId, entered));
79
+ }
80
+ async function loginByPhone(phone, code) {
81
+ try {
82
+ await postJson("/api/v1/auth/send-otp", { phone });
83
+ }
84
+ catch (err) {
85
+ if (errorCode(err) === "OTP_NOT_AVAILABLE") {
86
+ throw new Error("Phone OTP is not available on this deployment (server has no AUTH_PHONE_OTP_DEV_BYPASS). " +
87
+ "Use `auth login --email <address>` instead.");
88
+ }
89
+ throw err;
90
+ }
91
+ emit(await postJson("/api/v1/auth/verify-otp", { phone, code }));
92
+ }
93
+ export function registerAuth(program) {
94
+ const group = getOrCreateGroup(program, "auth", "Auth operations");
95
+ group
96
+ .command("login")
97
+ .description("End-to-end login. Returns { token, accessToken, refreshToken, user, isNewUser }. " +
98
+ "--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.")
100
+ .option("--email <address>", "email address (mutually exclusive with --phone)")
101
+ .option("--phone <e164>", 'phone number in E.164 format (e.g. "+14155550199")')
102
+ .option("--code <code>", "verification code; prompted interactively for --email when omitted")
103
+ .action(async (opts) => {
104
+ if (opts.email && opts.phone) {
105
+ throw new Error("--email and --phone are mutually exclusive");
106
+ }
107
+ if (!opts.email && !opts.phone) {
108
+ throw new Error("one of --email <address> or --phone <e164> is required");
109
+ }
110
+ if (opts.email) {
111
+ await loginByEmail(opts.email, opts.code);
112
+ return;
113
+ }
114
+ await loginByPhone(opts.phone, opts.code ?? "111111");
115
+ });
116
+ group
117
+ .command("verify-email")
118
+ .description("Verify an emailed code for a session started by `auth login --email`")
119
+ .requiredOption("--session-id <id>", "sessionId printed by `auth login --email`")
120
+ .requiredOption("--code <code>", "6-digit code from the email")
121
+ .action(async (opts) => {
122
+ emit(await verifyEmailCode(opts.sessionId, opts.code));
123
+ });
124
+ }
125
+ //# sourceMappingURL=auth.js.map