recess-cli 1.9.2 → 2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Recess CLI
2
2
 
3
- `recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate, and goal-content commands; GUIDE accounts receive that same student surface for the students they hold an ACTIVE tutor assignment to — not their wider class roster. It uses the web-server OpenAPI document, authenticates through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with `--confirm` after human approval.
3
+ `recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate, and goal-content commands; GUIDE accounts receive that same student surface for the students they hold an ACTIVE tutor assignment to — not their wider class roster. It uses the web-server OpenAPI document, authenticates through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with `--confirm` plus the preview's operation key after human approval.
4
4
 
5
5
  ## Install (no checkout needed)
6
6
 
@@ -8,7 +8,7 @@ Published to npm as [`recess-cli`](https://www.npmjs.com/package/recess-cli). On
8
8
 
9
9
  ```bash
10
10
  npm install -g recess-cli
11
- recess setup
11
+ recess setup --reason "Install and update the Recess agent skill"
12
12
  ```
13
13
 
14
14
  `setup` installs the bundled skill in the Codex and Claude Code user directories, which Cursor also discovers for compatibility, and then opens Recess SSO in your browser (skip the browser step with `--skill-only`; it is also skipped when a live session already exists). Restart your agent afterwards so it discovers the skill. `npx -y recess-cli setup` works too, but leaves no `recess` on your PATH — which is the command the installed skill tells the agent to run — so `setup` warns when it detects it is running from an npx cache.
@@ -44,7 +44,7 @@ The browser SSO assertion is exchanged once and discarded. The CLI stores a sepa
44
44
 
45
45
  ```bash
46
46
  recess --json auth login
47
- recess --json doctor
47
+ recess --json doctor --reason "Verify CLI connectivity, identity, and scope"
48
48
  ```
49
49
 
50
50
  ## Testing against a local server
@@ -54,7 +54,7 @@ recess --json doctor
54
54
  ```bash
55
55
  export RECESS_CLI_API_ORIGIN=http://localhost:5068
56
56
  export RECESS_CLI_COOKIE='recess.auth-token=<signed-value>'
57
- recess --json doctor
57
+ recess --json doctor --reason "Verify the local Recess API connection"
58
58
  ```
59
59
 
60
60
  To mint `<signed-value>`: sign `{sub, role, cliScope}` with the server's `JWT_SECRET` (audience = `CLIENT_ORIGIN`), then sign THAT string with `cookie.signerFactory(COOKIE_SECRET)` from `@fastify/cookie`. Two traps, both silent:
@@ -97,32 +97,42 @@ Error or write preview:
97
97
  "error": {
98
98
  "code": "confirmation_required",
99
99
  "message": "...",
100
- "details": { "preview": {}, "requiredFlag": "--confirm" }
100
+ "details": { "preview": { "details": { "operationKey": "..." } } }
101
101
  }
102
102
  }
103
103
  ```
104
104
 
105
105
  Exit code `0` means success, `1` means an input/auth/API failure, and `2` means a write is awaiting explicit human confirmation.
106
106
 
107
+ `recess --json agent-context` returns the canonical command/flag/positional schema. `recess --json help payout recipients` returns scoped help. Unknown flags, duplicate non-repeatable flags, missing values, and extra positionals are errors instead of being silently ignored.
108
+
109
+ Every command-driven request to the Recess API except `auth` requires `--reason "..."`: a
110
+ non-empty, human-readable purpose of at most 1024 characters. The CLI sends it as
111
+ `x-recess-reason`; the server rejects missing reasons before route execution and stores one audit
112
+ row per request. The human-only `recess ui` console is explicitly exempt and identifies itself as
113
+ `x-recess-client: cli-ui` instead.
114
+
107
115
  ## Common flow
108
116
 
109
117
  ```bash
110
- recess --json users search "Morgan Rivera"
111
- recess --json users tier get <kid-id>
112
- recess --json users tier preview <kid-id> --tier lite --slots 1
113
- recess --json students upload-map-scores --student <kid-id> --file /path/to/map-report.pdf
114
- recess --json enrollments list --user <kid-id>
115
- recess --json subscriptions list --family <family-id> --kid <kid-id>
116
- recess --json invoices list --subscription <subscription-id>
118
+ recess --json users search "Morgan Rivera" --reason "Find the exact student record"
119
+ recess --json users tier get <kid-id> --reason "Inspect the student's current tier"
120
+ recess --json users tier preview <kid-id> --tier lite --slots 1 --reason "Preview a tier change"
121
+ recess --json students upload-map-scores --student <kid-id> --file /path/to/map-report.pdf --reason "Import this student's MAP scores"
122
+ recess --json enrollments list --user <kid-id> --reason "Inspect the student's enrollments"
123
+ recess --json subscriptions list --family <family-id> --kid <kid-id> --reason "Inspect family subscriptions"
124
+ recess --json invoices list --subscription <subscription-id> --reason "Inspect subscription invoices"
117
125
  ```
118
126
 
119
127
  Preview a write by omitting `--confirm`:
120
128
 
121
129
  ```bash
122
- recess --json billing pause --subscription <subscription-id> --until 2026-09-01
130
+ recess --json billing pause --subscription <subscription-id> --until 2026-09-01 --reason "Pause billing through September 1"
123
131
  ```
124
132
 
125
- After a human approves that exact preview, rerun the unchanged command with `--confirm`. File-backed family edits, deterministic template apply, and goal-content writes also return `details.approvalToken`; echo it with `--approval-token TOKEN`. The token covers the local bytes plus the server preflight/CAS state, so a changed file or goal produces a new preview instead of consuming stale approval.
133
+ After a human approves that exact preview, rerun the unchanged command with `--confirm --operation-key <operationKey>`. File-backed family edits, deterministic template apply, and goal-content writes also return `details.approvalToken`; echo it with `--approval-token TOKEN`. The token covers the local bytes plus the server preflight/CAS state, so a changed file or goal produces a new preview instead of consuming stale approval. Confirmed requests are fenced server-side per actor, operation, route/body, and preview fingerprint; retry an interrupted command with the same operation key to replay a completed response rather than duplicate the write. `jobs list|get` reads the protected local recovery ledger.
134
+
135
+ Named `profile save|use|list` configurations keep environment selection explicit, with global `--profile` as a one-command override. `--deliver file:<path>` atomically writes the JSON envelope with mode 0600; webhook delivery is intentionally unavailable for authenticated Recess data. `feedback submit` stores deduplicated CLI friction locally and can forward it to `RECESS_CLI_FEEDBACK_ENDPOINT`.
126
136
 
127
137
  School tier writes require the `updatedAt` token from `users tier get`. Their read-only preflight
128
138
  shows `capabilitiesLockedNow`/`capabilitiesLockedAfter`, the resolved class allowance, current
@@ -137,34 +147,45 @@ Village store goods are main-Recess `StoreItem` rows, not Village-island databas
137
147
  and preview an availability change with:
138
148
 
139
149
  ```bash
140
- recess --json store-items list --item-type VILLAGE_ITEM
141
- recess --json store-items set-status <store-item-id> --status INACTIVE
150
+ recess --json store-items list --item-type VILLAGE_ITEM --reason "Inspect the Village item catalog"
151
+ recess --json store-items set-status <store-item-id> --status INACTIVE --reason "Deactivate this Village item"
142
152
  ```
143
153
 
144
154
  The status command resolves the exact `VILLAGE_ITEM` first and includes its name, catalog metadata,
145
155
  current status, purchase count, and lack of feed-publication side effects in the approval preview;
146
- it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm`. The
156
+ it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm` and
157
+ the preview's `--operation-key`. The
147
158
  separate `village models` commands edit the Village island's reusable models and placements.
148
159
 
149
160
  ## Authoring learning content
150
161
 
151
162
  ```bash
152
- recess --json skills guardian get recess-goal-authoring --all-references
153
- recess --json content-library search "fractions through visual puzzles" --limit 8
154
- recess --json content-library status <gem-id-or-url>
155
- recess --json content-library set-stage <gem-id-or-url...> --stage archived
156
- recess --json goal-templates validate-spec --file ./template.json # iterate; writes nothing
157
- recess --json goal-templates create --file ./template.json # preview, exit 2
158
- recess --json goal-templates create --file ./template.json --confirm
159
- recess --json goal-templates patch-spec <id-or-slug> --expected-version 7 --patches-file ./patches.json
160
- recess --json goals files write --student <kid-id> --draft <draft-slug> --source-dir ./goal-content
161
- recess --json goals pdf upload --student <kid-id> --draft <draft-slug> --source-file ./textbook.pdf
162
- recess --json goal-templates capture-snapshot <id-or-slug> --source-draft <draft-slug> --student <kid-id> --dry-run
163
- recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json --dry-run
164
- recess --json goals create --student <kid-id> --title "..." --description-file ./goal.md
165
- recess --json goals create --student <kid-id> --draft <draft-slug> --title "..." \
166
- --description-file ./goal.md --enable-applet-follow-ups
167
- recess --json goals files list --student <kid-id> --goal <goal-id>
163
+ recess --json skills guardian get recess-goal-authoring --all-references --reason "Load goal authoring guidance"
164
+ recess --json content-library search "fractions through visual puzzles" --limit 8 --reason "Find visual fraction resources"
165
+ recess --json content-library status <gem-id-or-url> --reason "Inspect this gem's pipeline status"
166
+ recess --json content-library set-stage <gem-id-or-url...> --stage archived --reason "Archive these gems"
167
+ recess --json goal-templates validate-spec --file ./template.json --reason "Validate this template draft"
168
+ recess --json goal-templates create --file ./template.json --reason "Create this reusable goal template"
169
+ recess --json goal-templates create --file ./template.json --confirm \
170
+ --operation-key <preview-operation-key> --reason "Create this reusable goal template"
171
+ recess --json goal-templates patch-spec <id-or-slug> --expected-version 7 --patches-file ./patches.json --reason "Update this template specification"
172
+ recess --json goals files init --student <kid-id> --draft <draft-slug> \
173
+ --output-dir ./goal-content --reason "Start this goal draft"
174
+ git -C ./goal-content add -A
175
+ git -C ./goal-content commit -m "Author the learning path"
176
+ recess --json goals files push --source-dir ./goal-content --reason "Publish this goal draft"
177
+ recess --json goals pdf upload --student <kid-id> --draft <draft-slug> --source-file ./textbook.pdf --reason "Attach this textbook to the goal draft"
178
+ recess --json goal-templates capture-snapshot <id-or-slug> --source-dir ./goal-content --dry-run --reason "Preview a template snapshot"
179
+ recess --json goal-templates apply <id-or-slug> --answers-file ./answers.json --dry-run --reason "Preview applying this template"
180
+ recess --json goals create --student <kid-id> --title "..." --description-file ./goal.md --reason "Create this student goal"
181
+ recess --json goals create --source-dir ./goal-content --title "..." \
182
+ --description-file ./goal.md --enable-applet-follow-ups --reason "Create this student goal draft"
183
+ recess --json goals files list --student <kid-id> --goal <goal-id> --reason "Inspect the goal workspace files"
184
+ recess --json goals files checkout --student <kid-id> --goal <goal-id> \
185
+ --output-dir ./goal-content --reason "Check out this goal for editing"
186
+ git -C ./goal-content add -A
187
+ git -C ./goal-content commit -m "Revise module 3"
188
+ recess --json goals files push --source-dir ./goal-content --reason "Publish the module revision"
168
189
  ```
169
190
 
170
191
  ADMIN discovery batches use the same Content Library admission door as the dashboard. Omit
@@ -173,9 +194,10 @@ Review preselected; JSON/non-interactive runs safely default to Review. Use `--s
173
194
  start automatic decoration immediately:
174
195
 
175
196
  ```bash
176
- recess --json content-library submit https://example.org/activity --stage review
177
- recess --json content-library submit --file ./gems.json
178
- recess --json content-library submit --file ./urls.txt --stage polish --confirm
197
+ recess --json content-library submit https://example.org/activity --stage review --reason "Submit this activity for review"
198
+ recess --json content-library submit --file ./gems.json --reason "Submit this resource batch for review"
199
+ recess --json content-library submit --file ./urls.txt --stage polish --confirm \
200
+ --operation-key <preview-operation-key> --reason "Submit this batch for polishing"
179
201
  ```
180
202
 
181
203
  JSON files are arrays of URL strings or `{ "url", "title"?, "summary"?, "lane"? }` objects;
@@ -186,21 +208,24 @@ reports its Review/Polishing/Live/Archived stage plus metadata, cover, and searc
186
208
  `content-library set-stage` accepts one or many IDs/URLs (or a newline/JSON-string-array `--file`),
187
209
  previews every resolved current stage, and requires `--confirm`. It uses the same lifecycle as
188
210
  Manage: direct-to-Live routes unfinished gems through Polishing, and moving out of Polishing
189
- cancels that exact run first.
211
+ cancels that exact run first. Add `--wait --timeout 900` when promoting to Live (or submitting to
212
+ polish) to poll the durable island status with bounded concurrency and exponential backoff.
190
213
 
191
214
  ## Family AI operations
192
215
 
193
216
  ```bash
194
- recess --json students list
195
- recess --json students today --student <kid-id>
196
- recess --json students schedule --student <kid-id> --days 30
197
- recess --json students xp-history --student <kid-id> --range month
198
- recess --json goals list --student <kid-id>
199
- recess --json todos create --student <kid-id> --title "Read chapter 4"
200
- recess --json todos generate-applet <todo-id> --student <kid-id> [--due-date YYYY-MM-DD]
201
- recess --json memories context --student <kid-id>
202
- recess --json memories log --student <kid-id> --date 2026-08-12
203
- recess --json rocky get --student <kid-id>
217
+ recess --json students list --reason "List the students I can support"
218
+ recess --json students today --student <kid-id> --reason "Review today's learning plan"
219
+ recess --json students schedule --student <kid-id> --days 30 --reason "Review the student's upcoming schedule"
220
+ recess --json students xp-history --student <kid-id> --range month --reason "Review recent XP history"
221
+ recess --json goals list --student <kid-id> --reason "Review the student's goals"
222
+ recess --json todos create --student <kid-id> --title "Read chapter 4" --reason "Add the assigned reading"
223
+ recess --json goals delete <goal-id> --student <kid-id> --reason "Remove this obsolete goal"
224
+ recess --json todos delete <todo-id> --reason "Remove this disposable todo"
225
+ recess --json todos generate-applet <todo-id> --student <kid-id> --reason "Generate this todo's applet"
226
+ recess --json memories context --student <kid-id> --reason "Review durable tutor context"
227
+ recess --json memories log --student <kid-id> --date 2026-08-12 --reason "Review the learning log for this date"
228
+ recess --json rocky get --student <kid-id> --reason "Inspect the student's Rocky configuration"
204
229
  ```
205
230
 
206
231
  All family writes still preview first. Goal/todo/Rocky edits also carry the current server version
@@ -216,8 +241,8 @@ not expose Postgres `UserMemory`.
216
241
 
217
242
  ```bash
218
243
  recess --json --version # {cliVersion, skillVersion}
219
- recess --json doctor # .skill reports whether a newer bundle exists
220
- recess --json setup --skill-only # bundled copy, then the served upgrade
244
+ recess --json doctor --reason "Check CLI health" # .skill reports whether a newer bundle exists
245
+ recess --json setup --skill-only --reason "Update the installed Recess skill"
221
246
  ```
222
247
 
223
248
  The CLI's small shared router skill is **both** bundled in this package and served from
@@ -243,11 +268,13 @@ previews the per-student outcome. `set-metadata` and `delete` require `--expecte
243
268
  it always runs the backend's guarded preview first and requires the preview's exact loss token in
244
269
  addition to `--confirm` when protected template data would be removed.
245
270
 
246
- Module-backed content is authored as a local workspace tree and batch-upserted to a named goal
247
- draft. For a personal one-off goal, `goals create --draft` validates and materializes that draft
248
- directly. For reusable content, attach it to a BLUEPRINT with `capture-snapshot`. Every mutation
249
- runs a server preview before the confirmation gate and is compare-and-set against the previewed
250
- revision. Direct live-goal `modules/` and `state/` writes remain blocked because those files have
251
- database projections.
271
+ Module-backed content starts with `goals files init --draft`, which creates a normal local Git
272
+ repository pinned to the student's Mesa revision. Edit, rename, delete, inspect, and commit with
273
+ ordinary Git, then use `goals files push`. The same checkout can become a personal goal through
274
+ `goals create --source-dir` or a reusable BLUEPRINT snapshot through
275
+ `goal-templates capture-snapshot --source-dir`; successful goal creation retargets its metadata to
276
+ the new live goal for continued pushes. Existing live goals use `checkout --goal`. Every push
277
+ previews the committed range as one Mesa change and refuses a stale remote tip. The direct
278
+ `goals files write` upsert remains available for small or automated writes.
252
279
 
253
280
  See `recess --help` for the complete command surface. The raw escape hatch is intentionally read-only: `recess --json request get /path`.
package/dist/api.js CHANGED
@@ -1,16 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import createClient from "openapi-fetch";
2
3
  import { apiError, CliError } from "./errors.js";
4
+ import { cliRequestHeaders, markCliRequest, RECESS_CLIENT_CLI, } from "./http.js";
5
+ const idempotencyContext = new AsyncLocalStorage();
6
+ export function withIdempotencyContext(context, execute) {
7
+ return idempotencyContext.run(context, execute);
8
+ }
9
+ function applyIdempotencyHeaders(headers) {
10
+ const context = idempotencyContext.getStore();
11
+ if (!context)
12
+ return;
13
+ headers.set("x-recess-operation-key", context.operationKey);
14
+ headers.set("x-recess-operation-fingerprint", context.fingerprint);
15
+ }
3
16
  export class RecessAdminApi {
4
17
  config;
18
+ reason;
19
+ clientTag;
5
20
  client;
6
- constructor(config) {
21
+ constructor(config, reason, clientTag = RECESS_CLIENT_CLI) {
7
22
  this.config = config;
23
+ this.reason = reason;
24
+ this.clientTag = clientTag;
8
25
  this.client = createClient({
9
26
  baseUrl: config.apiOrigin,
10
27
  headers: config.sessionCookie
11
28
  ? { cookie: config.sessionCookie }
12
29
  : undefined,
13
30
  });
31
+ const requestReason = this.reason;
32
+ const requestClientTag = this.clientTag;
33
+ this.client.use({
34
+ onRequest({ request }) {
35
+ markCliRequest(request.headers, requestReason, requestClientTag);
36
+ applyIdempotencyHeaders(request.headers);
37
+ return request;
38
+ },
39
+ });
14
40
  }
15
41
  requireAuth() {
16
42
  if (!this.config.sessionCookie) {
@@ -23,7 +49,7 @@ export class RecessAdminApi {
23
49
  throw new CliError("invalid_arguments", "Raw request paths must start with one '/'.");
24
50
  }
25
51
  const response = await fetch(new URL(path, this.config.apiOrigin), {
26
- headers: { cookie: this.config.sessionCookie },
52
+ headers: cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag),
27
53
  });
28
54
  const text = await response.text();
29
55
  let body = text;
@@ -39,14 +65,16 @@ export class RecessAdminApi {
39
65
  }
40
66
  async villageRequest(path, options = {}) {
41
67
  this.requireAuth();
68
+ const headers = cliRequestHeaders({
69
+ cookie: this.config.sessionCookie,
70
+ ...(options.body === undefined
71
+ ? {}
72
+ : { "content-type": "application/json" }),
73
+ }, this.reason, this.clientTag);
74
+ applyIdempotencyHeaders(headers);
42
75
  const response = await fetch(new URL(path, this.config.apiOrigin), {
43
76
  method: options.method ?? "GET",
44
- headers: {
45
- cookie: this.config.sessionCookie,
46
- ...(options.body === undefined
47
- ? {}
48
- : { "content-type": "application/json" }),
49
- },
77
+ headers,
50
78
  ...(options.body === undefined
51
79
  ? {}
52
80
  : { body: JSON.stringify(options.body) }),
@@ -72,9 +100,11 @@ export class RecessAdminApi {
72
100
  }
73
101
  const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
74
102
  form.set("file", new Blob([arrayBuffer], { type: "model/gltf-binary" }), fileName);
103
+ const headers = cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag);
104
+ applyIdempotencyHeaders(headers);
75
105
  const response = await fetch(new URL(`/admin/village/models/upload?worldId=${encodeURIComponent(worldId)}`, this.config.apiOrigin), {
76
106
  method: "POST",
77
- headers: { cookie: this.config.sessionCookie },
107
+ headers,
78
108
  body: form,
79
109
  });
80
110
  const text = await response.text();
@@ -93,9 +123,11 @@ export class RecessAdminApi {
93
123
  this.requireAuth();
94
124
  const formData = new FormData();
95
125
  formData.append("file", new Blob([pdf], { type: "application/pdf" }), fileName);
126
+ const headers = cliRequestHeaders({ cookie: this.config.sessionCookie }, this.reason, this.clientTag);
127
+ applyIdempotencyHeaders(headers);
96
128
  const response = await fetch(new URL(`/tutor/students/${encodeURIComponent(studentId)}/map-test-scores/upload`, this.config.apiOrigin), {
97
129
  method: "POST",
98
- headers: { cookie: this.config.sessionCookie },
130
+ headers,
99
131
  body: formData,
100
132
  });
101
133
  const text = await response.text();
package/dist/args.js CHANGED
@@ -10,6 +10,7 @@ const BOOLEAN_FLAGS = new Set([
10
10
  "allow-strand",
11
11
  "cancel-subscriptions",
12
12
  "archived",
13
+ "confirm-destructive-changes",
13
14
  "confirm",
14
15
  "dry-run",
15
16
  "disable-applet-follow-ups",
@@ -19,20 +20,28 @@ const BOOLEAN_FLAGS = new Set([
19
20
  "immediate",
20
21
  "include-deleted",
21
22
  "mirrored",
23
+ "no-invite",
22
24
  "no-collision",
23
25
  "json",
24
26
  "refresh",
25
27
  "restore",
26
28
  "revoke",
27
29
  "send-email",
30
+ "skill-only",
28
31
  "spec-only",
29
32
  "starter-only",
30
33
  "visual-only",
34
+ "version",
35
+ "wait",
31
36
  ]);
32
37
  export function parseArgs(args) {
33
38
  const positionals = [];
34
39
  const flags = new Map();
35
40
  const repeated = new Map();
41
+ const occurrences = new Map();
42
+ const markSeen = (name) => {
43
+ occurrences.set(name, (occurrences.get(name) ?? 0) + 1);
44
+ };
36
45
  const record = (name, value) => {
37
46
  if (!REPEATABLE_FLAGS.has(name))
38
47
  return;
@@ -52,11 +61,13 @@ export function parseArgs(args) {
52
61
  if (equalsAt > 2) {
53
62
  const name = value.slice(2, equalsAt);
54
63
  const flagValue = value.slice(equalsAt + 1);
64
+ markSeen(name);
55
65
  flags.set(name, flagValue);
56
66
  record(name, flagValue);
57
67
  continue;
58
68
  }
59
69
  const name = value.slice(2);
70
+ markSeen(name);
60
71
  if (BOOLEAN_FLAGS.has(name)) {
61
72
  flags.set(name, true);
62
73
  continue;
@@ -71,7 +82,7 @@ export function parseArgs(args) {
71
82
  flags.set(name, true);
72
83
  }
73
84
  }
74
- return { positionals, flags, repeated };
85
+ return { positionals, flags, repeated, occurrences };
75
86
  }
76
87
  /**
77
88
  * Every value given for a repeatable flag, in order.
package/dist/auth.js CHANGED
@@ -3,6 +3,7 @@ import { createServer } from "node:http";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { clearPendingDeviceAuth, updateStoredConfig } from "./config.js";
5
5
  import { apiError, CliError } from "./errors.js";
6
+ import { cliRequestHeaders } from "./http.js";
6
7
  function openBrowser(url) {
7
8
  // Windows goes through rundll32, not `cmd /c start`: cmd treats `&` (and the
8
9
  // `%`-encoded redirect_uri) as metacharacters and chops the URL at the first
@@ -76,7 +77,7 @@ export async function login(config, options) {
76
77
  const assertion = await callback;
77
78
  const response = await fetch(new URL("/auth/admin-cli/exchange/", config.apiOrigin), {
78
79
  method: "POST",
79
- headers: { authorization: `Bearer ${assertion}` },
80
+ headers: cliRequestHeaders({ authorization: `Bearer ${assertion}` }),
80
81
  });
81
82
  const body = (await response.json());
82
83
  if (!response.ok)
@@ -93,7 +94,7 @@ export async function login(config, options) {
93
94
  sessionCookie: pair,
94
95
  sessionExpiresAt: exchange.expiresAt,
95
96
  user: exchange.user,
96
- });
97
+ }, config.profileName);
97
98
  return exchange;
98
99
  }
99
100
  // Start the headless (device-authorization) sign-in. Non-blocking: it stores the secret
@@ -102,7 +103,7 @@ export async function login(config, options) {
102
103
  export async function requestDeviceAuth(config, options) {
103
104
  const response = await fetch(new URL("/auth/admin-cli/device/authorize/", config.apiOrigin), {
104
105
  method: "POST",
105
- headers: { "content-type": "application/json" },
106
+ headers: cliRequestHeaders({ "content-type": "application/json" }),
106
107
  body: JSON.stringify(options.label ? { label: options.label } : {}),
107
108
  });
108
109
  const body = (await response.json());
@@ -116,7 +117,7 @@ export async function requestDeviceAuth(config, options) {
116
117
  pendingUserCode: authorize.userCode,
117
118
  pendingApprovalUrl: authorize.approvalUrl,
118
119
  pendingExpiresAt: authorize.expiresAt,
119
- });
120
+ }, config.profileName);
120
121
  return {
121
122
  approvalUrl: authorize.approvalUrl,
122
123
  userCode: authorize.userCode,
@@ -142,12 +143,12 @@ export async function pollDeviceAuth(config, options) {
142
143
  for (;;) {
143
144
  const response = await fetch(new URL("/auth/admin-cli/device/token/", config.apiOrigin), {
144
145
  method: "POST",
145
- headers: { "content-type": "application/json" },
146
+ headers: cliRequestHeaders({ "content-type": "application/json" }),
146
147
  body: JSON.stringify({ deviceCode }),
147
148
  });
148
149
  const body = (await response.json());
149
150
  if (!response.ok) {
150
- await clearPendingDeviceAuth();
151
+ await clearPendingDeviceAuth(config.profileName);
151
152
  throw apiError(response.status, body);
152
153
  }
153
154
  const token = body;
@@ -167,16 +168,16 @@ export async function pollDeviceAuth(config, options) {
167
168
  sessionCookie: pair,
168
169
  sessionExpiresAt: exchange.expiresAt,
169
170
  user: exchange.user,
170
- });
171
- await clearPendingDeviceAuth();
171
+ }, config.profileName);
172
+ await clearPendingDeviceAuth(config.profileName);
172
173
  return exchange;
173
174
  }
174
175
  if (token.status === "denied") {
175
- await clearPendingDeviceAuth();
176
+ await clearPendingDeviceAuth(config.profileName);
176
177
  throw new CliError("auth_failed", "The sign-in request was denied.");
177
178
  }
178
179
  if (token.status === "expired" || token.status === "consumed") {
179
- await clearPendingDeviceAuth();
180
+ await clearPendingDeviceAuth(config.profileName);
180
181
  throw new CliError("auth_failed", token.status === "expired"
181
182
  ? "The sign-in request expired. Run `recess auth request` again."
182
183
  : "The sign-in request was already used. Run `recess auth request` again.");