@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.
- package/LICENSE +18 -0
- package/README.md +312 -0
- package/dist/auth.js +9 -0
- package/dist/cli.js +46 -0
- package/dist/client.js +125 -0
- package/dist/commands/_group.js +5 -0
- package/dist/commands/auth.js +125 -0
- package/dist/commands/challenges.js +51 -0
- package/dist/commands/custom-agents.js +120 -0
- package/dist/commands/dm.js +35 -0
- package/dist/commands/messages.js +360 -0
- package/dist/commands/room-agents.js +47 -0
- package/dist/commands/rooms.js +687 -0
- package/dist/commands/users.js +126 -0
- package/dist/commands/whoami.js +15 -0
- package/dist/generated/_helpers.js +8 -0
- package/dist/generated/commands/activity.js +46 -0
- package/dist/generated/commands/ai.js +40 -0
- package/dist/generated/commands/ambassador.js +34 -0
- package/dist/generated/commands/apple-iap.js +61 -0
- package/dist/generated/commands/attribution.js +130 -0
- package/dist/generated/commands/auth.js +205 -0
- package/dist/generated/commands/challenges.js +251 -0
- package/dist/generated/commands/clips.js +71 -0
- package/dist/generated/commands/contact-match.js +92 -0
- package/dist/generated/commands/content-crawler.js +27 -0
- package/dist/generated/commands/custom-agents.js +346 -0
- package/dist/generated/commands/desktop-releases.js +236 -0
- package/dist/generated/commands/discover.js +236 -0
- package/dist/generated/commands/dm.js +155 -0
- package/dist/generated/commands/drafts.js +41 -0
- package/dist/generated/commands/embed.js +34 -0
- package/dist/generated/commands/explore.js +19 -0
- package/dist/generated/commands/feed.js +67 -0
- package/dist/generated/commands/feedback.js +44 -0
- package/dist/generated/commands/gamification.js +33 -0
- package/dist/generated/commands/google-play-iap.js +73 -0
- package/dist/generated/commands/health.js +32 -0
- package/dist/generated/commands/internal.js +162 -0
- package/dist/generated/commands/interview.js +76 -0
- package/dist/generated/commands/invite-redirect.js +20 -0
- package/dist/generated/commands/jesuscord-admin.js +36 -0
- package/dist/generated/commands/join.js +27 -0
- package/dist/generated/commands/kits.js +108 -0
- package/dist/generated/commands/meetings.js +81 -0
- package/dist/generated/commands/message-folders.js +68 -0
- package/dist/generated/commands/messages.js +418 -0
- package/dist/generated/commands/one-emulator.js +32 -0
- package/dist/generated/commands/pdf-export.js +19 -0
- package/dist/generated/commands/pin-folders.js +72 -0
- package/dist/generated/commands/pinned-messages.js +81 -0
- package/dist/generated/commands/polls.js +152 -0
- package/dist/generated/commands/premium-feeds.js +32 -0
- package/dist/generated/commands/processors.js +22 -0
- package/dist/generated/commands/questions.js +54 -0
- package/dist/generated/commands/referrals.js +19 -0
- package/dist/generated/commands/reports.js +21 -0
- package/dist/generated/commands/room-a2-a.js +81 -0
- package/dist/generated/commands/room-agents.js +273 -0
- package/dist/generated/commands/room-invites.js +155 -0
- package/dist/generated/commands/room-owned-agent.js +36 -0
- package/dist/generated/commands/room-page.js +20 -0
- package/dist/generated/commands/rooms.js +1117 -0
- package/dist/generated/commands/saved-messages.js +84 -0
- package/dist/generated/commands/saved-stories.js +125 -0
- package/dist/generated/commands/search.js +44 -0
- package/dist/generated/commands/shadow-home-agent.js +36 -0
- package/dist/generated/commands/shadow.js +64 -0
- package/dist/generated/commands/share-cards.js +85 -0
- package/dist/generated/commands/share.js +20 -0
- package/dist/generated/commands/sponsors.js +20 -0
- package/dist/generated/commands/stories.js +324 -0
- package/dist/generated/commands/story-by-id.js +20 -0
- package/dist/generated/commands/submissions.js +112 -0
- package/dist/generated/commands/subscriptions.js +103 -0
- package/dist/generated/commands/tips.js +76 -0
- package/dist/generated/commands/users.js +373 -0
- package/dist/generated/commands/voice-token.js +21 -0
- package/dist/generated/commands/well-known.js +32 -0
- package/dist/generated/commands/wrapped.js +20 -0
- package/dist/generated/index.js +131 -0
- 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,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
|