@purveyors/cli 0.25.0 → 0.30.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 +48 -45
- package/dist/commands/auth.d.ts +6 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +119 -71
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +11 -47
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +18 -45
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/market.d.ts.map +1 -1
- package/dist/commands/market.js +4 -2
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/roast.d.ts +5 -2
- package/dist/commands/roast.d.ts.map +1 -1
- package/dist/commands/roast.js +29 -28
- package/dist/commands/roast.js.map +1 -1
- package/dist/commands/sales.d.ts +17 -1
- package/dist/commands/sales.d.ts.map +1 -1
- package/dist/commands/sales.js +33 -26
- package/dist/commands/sales.js.map +1 -1
- package/dist/commands/tasting.d.ts.map +1 -1
- package/dist/commands/tasting.js +20 -9
- package/dist/commands/tasting.js.map +1 -1
- package/dist/lib/ai.d.ts +2 -2
- package/dist/lib/ai.d.ts.map +1 -1
- package/dist/lib/ai.js.map +1 -1
- package/dist/lib/auth-client.d.ts +33 -0
- package/dist/lib/auth-client.d.ts.map +1 -0
- package/dist/lib/auth-client.js +63 -0
- package/dist/lib/auth-client.js.map +1 -0
- package/dist/lib/auth-guard.d.ts +3 -3
- package/dist/lib/auth-guard.d.ts.map +1 -1
- package/dist/lib/auth-guard.js +16 -8
- package/dist/lib/auth-guard.js.map +1 -1
- package/dist/lib/catalog.d.ts +12 -14
- package/dist/lib/catalog.d.ts.map +1 -1
- package/dist/lib/catalog.js +177 -520
- package/dist/lib/catalog.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +23 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/interactive/forms.d.ts +4 -5
- package/dist/lib/interactive/forms.d.ts.map +1 -1
- package/dist/lib/interactive/forms.js +18 -46
- package/dist/lib/interactive/forms.js.map +1 -1
- package/dist/lib/interactive/watch.d.ts +5 -2
- package/dist/lib/interactive/watch.d.ts.map +1 -1
- package/dist/lib/interactive/watch.js +35 -15
- package/dist/lib/interactive/watch.js.map +1 -1
- package/dist/lib/inventory.d.ts +8 -16
- package/dist/lib/inventory.d.ts.map +1 -1
- package/dist/lib/inventory.js +44 -227
- package/dist/lib/inventory.js.map +1 -1
- package/dist/lib/manifest.d.ts.map +1 -1
- package/dist/lib/manifest.js +20 -27
- package/dist/lib/manifest.js.map +1 -1
- package/dist/lib/market.d.ts +1 -1
- package/dist/lib/market.d.ts.map +1 -1
- package/dist/lib/parchment-base.d.ts +3 -0
- package/dist/lib/parchment-base.d.ts.map +1 -0
- package/dist/lib/parchment-base.js +9 -0
- package/dist/lib/parchment-base.js.map +1 -0
- package/dist/lib/parchment.d.ts +5 -5
- package/dist/lib/parchment.d.ts.map +1 -1
- package/dist/lib/parchment.js +9 -11
- package/dist/lib/parchment.js.map +1 -1
- package/dist/lib/roast.d.ts +16 -24
- package/dist/lib/roast.d.ts.map +1 -1
- package/dist/lib/roast.js +33 -224
- package/dist/lib/roast.js.map +1 -1
- package/dist/lib/sales.d.ts +18 -23
- package/dist/lib/sales.d.ts.map +1 -1
- package/dist/lib/sales.js +100 -156
- package/dist/lib/sales.js.map +1 -1
- package/dist/lib/tasting.d.ts +4 -5
- package/dist/lib/tasting.d.ts.map +1 -1
- package/dist/lib/tasting.js +14 -94
- package/dist/lib/tasting.js.map +1 -1
- package/dist/program.js +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -5
- package/dist/lib/supabase.d.ts +0 -22
- package/dist/lib/supabase.d.ts.map +0 -1
- package/dist/lib/supabase.js +0 -103
- package/dist/lib/supabase.js.map +0 -1
- package/dist/types/database.types.d.ts +0 -13
- package/dist/types/database.types.d.ts.map +0 -1
- package/dist/types/database.types.js +0 -8
- package/dist/types/database.types.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Use `purvey --help` for quick command discovery, `purvey context` for the dense
|
|
|
11
11
|
- Official binary: `purvey`
|
|
12
12
|
- Package: `@purveyors/cli`
|
|
13
13
|
- Runtime: Node.js 20+
|
|
14
|
-
- No pre-existing
|
|
14
|
+
- No pre-existing credentials required: `auth`, `config`, `context`, `manifest`
|
|
15
15
|
- Viewer role required: `catalog` (excluding structured process filters on `catalog search`)
|
|
16
16
|
- Member role required: `price-index`, `procurement`, `inventory`, `roast`, `sales`, `tasting`
|
|
17
17
|
- Mixed public and entitled access: `market` public teaser slices are unauthenticated; filtered slices require Parchment Intelligence access
|
|
@@ -71,7 +71,7 @@ purvey auth login
|
|
|
71
71
|
|
|
72
72
|
# If automatic browser callback handling fails or you paste a bad URL, paste the full callback URL back into the terminal. The CLI keeps waiting until a valid callback is received.
|
|
73
73
|
|
|
74
|
-
# 2. Confirm the
|
|
74
|
+
# 2. Confirm the stored API key and role
|
|
75
75
|
purvey auth status
|
|
76
76
|
|
|
77
77
|
# 3. Search the catalog (viewer role for basic filters, member role for structured process filters)
|
|
@@ -133,17 +133,21 @@ Export discipline:
|
|
|
133
133
|
|
|
134
134
|
## Authentication and access model
|
|
135
135
|
|
|
136
|
-
No pre-existing
|
|
136
|
+
No pre-existing credentials are required for `auth`, `config`, `context`, or `manifest`.
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Remote data commands require a valid owner-bound API key with the required scope:
|
|
139
139
|
|
|
140
140
|
- `catalog` requires the `viewer` role by default
|
|
141
|
-
- `catalog search` structured process filters require the `member` role
|
|
141
|
+
- `catalog search` structured process filters require the `member` role
|
|
142
142
|
- `market` has public teaser slices for `signals --summary`, unfiltered retail `stats`, and process/retail/month `metadata`; all filtered or non-public slices require Parchment Intelligence access enforced server-side
|
|
143
|
-
- `price-index`, `procurement`, `inventory`, `roast`, `sales`, and `tasting` require the `member` role
|
|
143
|
+
- `price-index`, `procurement`, `inventory`, `roast`, `sales`, and `tasting` require the `member` role
|
|
144
144
|
|
|
145
145
|
`purvey` uses Google OAuth through purveyors.io.
|
|
146
146
|
|
|
147
|
+
Set `PARCHMENT_API_KEY` (or `PURVEYORS_API_KEY`) to authenticate SDK-backed catalog,
|
|
148
|
+
inventory, roast, sales, tasting-read, market, price-index, and procurement operations
|
|
149
|
+
without using the API key created by `purvey auth login`. Environment credentials take precedence.
|
|
150
|
+
|
|
147
151
|
Interactive login:
|
|
148
152
|
|
|
149
153
|
```bash
|
|
@@ -179,16 +183,16 @@ Credentials are stored at `~/.config/purvey/credentials.json`.
|
|
|
179
183
|
|
|
180
184
|
### Auth roles
|
|
181
185
|
|
|
182
|
-
| Role | Access
|
|
183
|
-
| -------- |
|
|
184
|
-
| `viewer` | `catalog search`, `catalog get`, `catalog stats`, excluding structured process filters
|
|
185
|
-
| `member` | All viewer commands, `catalog similar`, structured process filters on `catalog search`, plus `price-index`, `procurement`, `inventory`, `roast`, `sales`, `tasting`
|
|
186
|
+
| Role | Access |
|
|
187
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `viewer` | `catalog search`, `catalog get`, `catalog stats`, excluding structured process filters |
|
|
189
|
+
| `member` | All viewer commands, `catalog similar`, structured process filters on `catalog search`, plus `price-index`, `procurement`, `inventory`, `roast`, `sales`, and `tasting` through the scoped key created by `purvey auth login` |
|
|
186
190
|
|
|
187
|
-
Market Index teaser slices are public. Filtered `market signals`, origin/process/wholesale `market stats`, and non-public `market metadata` slices require Parchment Intelligence access; API-key
|
|
191
|
+
Market Index teaser slices are public. Filtered `market signals`, origin/process/wholesale `market stats`, and non-public `market metadata` slices require Parchment Intelligence access; API-key denial is enforced by the canonical API. The stored login key carries `catalog:read`, which is also the canonical read scope for Market Index, Price Index, and procurement.
|
|
188
192
|
|
|
189
|
-
`auth`, `config`, `context`, and `manifest` remain available without
|
|
193
|
+
`auth`, `config`, `context`, and `manifest` remain available without pre-existing credentials.
|
|
190
194
|
|
|
191
|
-
Commands that require a higher role exit with code `3` on auth failure. That includes
|
|
195
|
+
Commands that require a higher role exit with code `3` on auth failure. That includes missing, revoked, or invalid credentials and an insufficient role.
|
|
192
196
|
|
|
193
197
|
## Output contract and scripting
|
|
194
198
|
|
|
@@ -246,15 +250,15 @@ The JSON error envelope includes:
|
|
|
246
250
|
|
|
247
251
|
All `purvey` commands exit with a numeric code your scripts can check with `$?`.
|
|
248
252
|
|
|
249
|
-
| Code | Meaning
|
|
250
|
-
| ---- |
|
|
251
|
-
| `0` | Success
|
|
252
|
-
| `1` | Unexpected or unclassified error
|
|
253
|
-
| `2` | Invalid argument or bad input
|
|
254
|
-
| `3` | Auth error:
|
|
255
|
-
| `4` | Not found
|
|
256
|
-
| `5` | Dependency conflict
|
|
257
|
-
| `6` | Local config error
|
|
253
|
+
| Code | Meaning |
|
|
254
|
+
| ---- | ------------------------------------------------------------------ |
|
|
255
|
+
| `0` | Success |
|
|
256
|
+
| `1` | Unexpected or unclassified error |
|
|
257
|
+
| `2` | Invalid argument or bad input |
|
|
258
|
+
| `3` | Auth error: missing, revoked, or invalid key; or insufficient role |
|
|
259
|
+
| `4` | Not found |
|
|
260
|
+
| `5` | Dependency conflict |
|
|
261
|
+
| `6` | Local config error |
|
|
258
262
|
|
|
259
263
|
Scripting pattern:
|
|
260
264
|
|
|
@@ -342,7 +346,6 @@ Notes:
|
|
|
342
346
|
|
|
343
347
|
- Fields: `supplier`, `country`, `processing_base_method`, `fermentation_type`, `drying_method`, `grade`, `wholesale`
|
|
344
348
|
- `--all`; use all visible catalog rows instead of the default stocked-only scope.
|
|
345
|
-
- `--sample-size <n>`; catalog rows to sample before counting, default `5000`, max `5000`
|
|
346
349
|
- `--limit <n>`; default `60`, max `100`
|
|
347
350
|
|
|
348
351
|
`catalog rank` options:
|
|
@@ -403,14 +406,14 @@ purvey catalog get 1182 --include-proof --json
|
|
|
403
406
|
Notes:
|
|
404
407
|
|
|
405
408
|
- Catalog commands require an authenticated `viewer` role by default.
|
|
406
|
-
- Structured process filters on `catalog search` require an authenticated `member` role
|
|
409
|
+
- Structured process filters on `catalog search` require an authenticated `member` role.
|
|
407
410
|
- Structured process filters use the canonical `/v1/catalog` query contract names while preserving the legacy `--process` label filter.
|
|
408
411
|
- `--include-proof` is an opt-in API-backed catalog read. It consumes the canonical proof summary returned by `/v1/catalog?include=proof`; the CLI does not compute proof fields locally or duplicate web/API proof logic.
|
|
409
412
|
- `--include-proof` rejects CLI-only filters that `/v1/catalog` cannot yet preserve exactly, such as `--flavor`, `--supplier`, `--drying-method`, and `--sort newest`.
|
|
410
|
-
- If you want proof output against
|
|
413
|
+
- If you want proof output against a specific API-key deployment, set `PARCHMENT_API_KEY` or `PURVEYORS_API_KEY`. Otherwise the CLI uses the key created by `purvey auth login`.
|
|
411
414
|
- `catalog get` and `catalog similar` both take `coffee_catalog.catalog_id`.
|
|
412
415
|
- `catalog rank` and `catalog rank-premium` read `coffee_catalog.purveyor_score` as the canonical quality signal; the CLI does not recompute the upstream Purveyor Score model.
|
|
413
|
-
- `catalog facets` and `catalog rank` are generic agent/client intelligence surfaces.
|
|
416
|
+
- `catalog facets` and `catalog rank` are generic agent/client intelligence surfaces. Facet counts come from the canonical API across the selected stocked/all-visible scope; ranking responses include sample metadata so callers do not mistake sampled rarity for whole-catalog guarantees.
|
|
414
417
|
- Catalog intelligence responses include `meta.sample_limited`, `meta.sample_order`, `meta.truncated`, and rows-examined style metadata where relevant so agents can distinguish ranked samples from full supplier aggregates. Supplier aggregate responses also include `meta.rows_examined`.
|
|
415
418
|
- Supplier aggregate commands summarize catalog row counts, stocked counts, Purveyor Score coverage, average score, average confidence, price range, origin/process coverage, and representative top coffees with score qualifiers.
|
|
416
419
|
- `catalog similar` uses the beta canonical `/v1/catalog/{id}/similar` API contract, not the legacy direct RPC path.
|
|
@@ -497,7 +500,7 @@ Notes:
|
|
|
497
500
|
|
|
498
501
|
- `market` commands are backed by canonical Parchment API endpoints through `@purveyors/sdk`; the CLI does not compute Market Index intelligence locally.
|
|
499
502
|
- Public teaser slices: `signals --summary`, `stats` with no origin/process and `market=retail`, and `metadata` at dimension=process/no-origin/market=retail/grain=month.
|
|
500
|
-
- Any filtered or non-public market slice requires Parchment Intelligence access, enforced server-side for
|
|
503
|
+
- Any filtered or non-public market slice requires Parchment Intelligence access, enforced server-side for API-key calls.
|
|
501
504
|
- `--json` returns the API response verbatim.
|
|
502
505
|
|
|
503
506
|
### procurement
|
|
@@ -564,7 +567,6 @@ Notes:
|
|
|
564
567
|
|
|
565
568
|
`inventory delete <id>` options:
|
|
566
569
|
|
|
567
|
-
- `--force`; cascade dependent roasts and sales
|
|
568
570
|
- `--yes`; skip confirmation prompt
|
|
569
571
|
|
|
570
572
|
Examples:
|
|
@@ -579,9 +581,9 @@ purvey inventory delete 7 --yes
|
|
|
579
581
|
|
|
580
582
|
Notes:
|
|
581
583
|
|
|
582
|
-
- Inventory commands require an
|
|
584
|
+
- Inventory commands require an owner-bound member API key with the corresponding inventory scope.
|
|
583
585
|
- Inventory `id` is `green_coffee_inv.id`, not `catalog_id`.
|
|
584
|
-
- `inventory delete`
|
|
586
|
+
- `inventory delete` refuses to cascade. Delete dependent roasts or sales explicitly first.
|
|
585
587
|
|
|
586
588
|
### roast
|
|
587
589
|
|
|
@@ -677,7 +679,7 @@ Notes:
|
|
|
677
679
|
|
|
678
680
|
`sales list` filters:
|
|
679
681
|
|
|
680
|
-
- `--
|
|
682
|
+
- `--coffee-id <id>`; green coffee inventory ID
|
|
681
683
|
- `--date-start <YYYY-MM-DD>`
|
|
682
684
|
- `--date-end <YYYY-MM-DD>`
|
|
683
685
|
- `--buyer <name>`
|
|
@@ -686,7 +688,7 @@ Notes:
|
|
|
686
688
|
|
|
687
689
|
`sales record` flags:
|
|
688
690
|
|
|
689
|
-
- `--roast-id <id>`;
|
|
691
|
+
- `--roast-id <id>`; resolve the sale inventory and batch from a roast profile
|
|
690
692
|
- `--coffee-id <id>`; resolved selector mode, requires `--batch-name`
|
|
691
693
|
- `--batch-name <name>`; resolved selector mode, requires `--coffee-id`
|
|
692
694
|
- `--oz <amount>`; required in flag mode
|
|
@@ -719,8 +721,8 @@ purvey sales delete 5 --yes
|
|
|
719
721
|
Notes:
|
|
720
722
|
|
|
721
723
|
- Sales commands require an authenticated `member` role.
|
|
722
|
-
- Use exactly one selector mode for `sales record`:
|
|
723
|
-
-
|
|
724
|
+
- Use exactly one selector mode for `sales record`: `--roast-id`, or `--coffee-id` plus `--batch-name`.
|
|
725
|
+
- Sales retain inventory and batch, not roast ID. Duplicate batch names on one inventory item are rejected even when selected through `--roast-id`.
|
|
724
726
|
- `--price` is total sale price, not per-ounce price.
|
|
725
727
|
|
|
726
728
|
### tasting
|
|
@@ -759,9 +761,9 @@ purvey tasting rate --form
|
|
|
759
761
|
|
|
760
762
|
Notes:
|
|
761
763
|
|
|
762
|
-
- Tasting
|
|
764
|
+
- Tasting reads accept an owner-bound `tasting:read` API key.
|
|
763
765
|
- `tasting get` combines supplier notes with your own notes when available.
|
|
764
|
-
- `tasting rate` writes
|
|
766
|
+
- `tasting rate` writes through the canonical Parchment tasting endpoint.
|
|
765
767
|
|
|
766
768
|
### config
|
|
767
769
|
|
|
@@ -874,16 +876,15 @@ Use the right ID for the right command.
|
|
|
874
876
|
|
|
875
877
|
- `catalog_id`: `coffee_catalog` rows; used by `catalog get`, `catalog similar`, `inventory add --catalog-id`, `tasting get`, `roast list --catalog-id`
|
|
876
878
|
- `inventory id`: `green_coffee_inv` rows; used by `inventory get/update/delete`, `roast --coffee-id`, `tasting rate`, `roast list --coffee-id`
|
|
877
|
-
- `roast_id`: `roast_data` rows; used by `roast get/delete`, `sales --roast-id`, `roast list --roast-id`
|
|
878
|
-
- `sales record` also supports resolving a roast from `inventory id` plus `--batch-name`;
|
|
879
|
+
- `roast_id`: `roast_data` rows; used by `roast get/delete`, `sales record --roast-id`, `roast list --roast-id`
|
|
880
|
+
- `sales record` also supports resolving a roast from `inventory id` plus `--batch-name`; because sales retain inventory + batch rather than roast ID, duplicate batch names on one inventory item are rejected
|
|
879
881
|
- `sale id`: `coffee_sales` rows; used by `sales update/delete`
|
|
880
882
|
|
|
881
883
|
## Environment variables
|
|
882
884
|
|
|
883
|
-
- `PURVEYORS_SUPABASE_URL`: override the Supabase
|
|
884
|
-
- `PURVEYORS_SUPABASE_ANON_KEY`: override the Supabase anon key
|
|
885
|
+
- `PURVEYORS_SUPABASE_URL`: override the Supabase Auth issuer used only for OAuth bootstrap
|
|
885
886
|
- `PURVEYORS_BASE_URL`: override the Purveyors web base URL
|
|
886
|
-
- `PURVEYORS_API_KEY`: API-key
|
|
887
|
+
- `PURVEYORS_API_KEY`: explicit API-key override for canonical Parchment commands
|
|
887
888
|
- `PARCHMENT_API_KEY`: preferred API-key variable for SDK-backed Parchment commands; also accepted for API-backed proof and paid-tier similarity paths
|
|
888
889
|
- `PARCHMENT_API_BASE_URL`: override the SDK-backed Parchment API base URL for `market`, `price-index`, and `procurement` commands
|
|
889
890
|
- `PURVEY_DEBUG`: enable verbose error output
|
|
@@ -918,7 +919,7 @@ Agent integration rules of thumb:
|
|
|
918
919
|
|
|
919
920
|
- Discover first with `purvey manifest`, then call the narrowest command or package subpath that fits the job.
|
|
920
921
|
- Use `purvey context` when a human-readable operator summary is useful before tool selection.
|
|
921
|
-
-
|
|
922
|
+
- Use `purvey auth login` for normal user workflows. OAuth is used only to bootstrap a machine-scoped Parchment API key; the CLI does not retain Supabase session or refresh tokens. Environment `PURVEYORS_API_KEY` or `PARCHMENT_API_KEY` values remain available for explicit automation overrides.
|
|
922
923
|
- Treat `--include-proof` as API output, not a local scoring feature. If a filter cannot round-trip through `/v1/catalog?include=proof`, the CLI rejects that invocation instead of returning misleading proof data.
|
|
923
924
|
- Treat `catalog similar` as the canonical `/v1/catalog/{id}/similar` contract. Preserve the distinction between `canonical_candidates` and `similar_recommendations`; do not flatten or re-sort grouped results unless you have a specific downstream reason.
|
|
924
925
|
- Treat `market`, `price-index`, and `procurement` as SDK-backed canonical API reads. Do not add procurement create/write behavior to this command group until the Phase 2 write contract ships.
|
|
@@ -935,7 +936,7 @@ purvey auth login --headless
|
|
|
935
936
|
|
|
936
937
|
**Catalog commands fail after logging in**
|
|
937
938
|
|
|
938
|
-
Run `purvey auth status` to confirm
|
|
939
|
+
Run `purvey auth status` to confirm the stored CLI key is active and has a `viewer` or `member` role. If it is stale or revoked, run `purvey auth logout` and log in again.
|
|
939
940
|
|
|
940
941
|
**Wrong ID type passed to a command**
|
|
941
942
|
|
|
@@ -954,7 +955,9 @@ purvey inventory list --limit 20 --offset 40
|
|
|
954
955
|
**`inventory delete` fails with dependency conflict**
|
|
955
956
|
|
|
956
957
|
```bash
|
|
957
|
-
purvey
|
|
958
|
+
purvey roast delete <roast-id> --yes
|
|
959
|
+
purvey sales delete <sale-id> --yes
|
|
960
|
+
purvey inventory delete 7 --yes
|
|
958
961
|
```
|
|
959
962
|
|
|
960
963
|
**Enable verbose error output**
|
|
@@ -985,7 +988,7 @@ Key files:
|
|
|
985
988
|
- `src/index.ts`: executable entrypoint
|
|
986
989
|
- `src/program.ts`: top-level CLI registration and global options
|
|
987
990
|
- `src/commands/`: command definitions and help text
|
|
988
|
-
- `src/lib/`: business logic and
|
|
991
|
+
- `src/lib/`: business logic and Parchment SDK integration
|
|
989
992
|
- `src/commands/context.ts`: dense human-readable agent reference
|
|
990
993
|
- `src/commands/manifest.ts`: machine-readable CLI manifest command
|
|
991
994
|
- `src/lib/manifest.ts`: shared manifest contract, package export list, command metadata, ID guidance, and context renderer
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import type { StoredCredentials } from '../types/index.js';
|
|
3
|
+
import { type ParchmentClient } from '@purveyors/sdk';
|
|
2
4
|
interface CallbackResult {
|
|
3
5
|
accessToken: string;
|
|
4
6
|
refreshToken: string;
|
|
@@ -9,12 +11,14 @@ interface ManualCallbackReader {
|
|
|
9
11
|
close: () => void;
|
|
10
12
|
}
|
|
11
13
|
type ManualCallbackQuestion = (query: string, callback: (answer: string) => void) => void;
|
|
14
|
+
export declare function createOAuthUrl(redirectTo: string): string;
|
|
15
|
+
export declare function exchangeOAuthSessionForApiKey(accessToken: string, clientOverride?: ParchmentClient): Promise<StoredCredentials>;
|
|
12
16
|
/**
|
|
13
17
|
* Extract Supabase OAuth tokens from a full callback URL, URL fragment, or
|
|
14
18
|
* query string. Exported for auth-flow contract tests.
|
|
15
19
|
*/
|
|
16
|
-
export declare function parseOAuthCallbackUrl(callbackUrl: string): CallbackResult;
|
|
17
|
-
export declare function createManualCallbackReaderForQuestion(question: ManualCallbackQuestion, closeQuestion: () => void, canRead: boolean): ManualCallbackReader | null;
|
|
20
|
+
export declare function parseOAuthCallbackUrl(callbackUrl: string, expectedState?: string): CallbackResult;
|
|
21
|
+
export declare function createManualCallbackReaderForQuestion(question: ManualCallbackQuestion, closeQuestion: () => void, canRead: boolean, expectedState?: string): ManualCallbackReader | null;
|
|
18
22
|
/**
|
|
19
23
|
* Build and return the `auth` command subtree.
|
|
20
24
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI1E,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAwB7E,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AASD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,KAAK,sBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;AAE1F,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAKzD;AAiBD,wBAAsB,6BAA6B,CACjD,WAAW,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,eAAe,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAwC5B;AAsHD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,cAAc,CA2CjG;AAED,wBAAgB,qCAAqC,CACnD,QAAQ,EAAE,sBAAsB,EAChC,aAAa,EAAE,MAAM,IAAI,EACzB,OAAO,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,oBAAoB,GAAG,IAAI,CAwC7B;AAsLD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAmF1C"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,14 +1,90 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { createServer } from 'http';
|
|
3
|
-
import {
|
|
3
|
+
import { validateSession } from '../lib/auth-client.js';
|
|
4
4
|
import { writeCredentials, deleteCredentials } from '../lib/config.js';
|
|
5
5
|
import { outputData, shouldUseInteractiveOutput, success, info, warn } from '../lib/output.js';
|
|
6
6
|
import { withErrorHandling, AuthError, exitCodeForError } from '../lib/errors.js';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
9
|
import { createInterface } from 'readline';
|
|
10
|
+
import { createParchmentClient } from '@purveyors/sdk';
|
|
11
|
+
import { getParchmentBaseUrl } from '../lib/parchment-base.js';
|
|
12
|
+
import { hostname } from 'os';
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
10
14
|
const DEFAULT_CALLBACK_HOST = 'localhost';
|
|
11
15
|
const CALLBACK_PATH = '/auth/callback';
|
|
16
|
+
const SUPABASE_AUTH_URL = process.env.PURVEYORS_SUPABASE_URL || 'https://bjblfzfdtfvuitqdbodn.supabase.co';
|
|
17
|
+
const CLI_API_KEY_SCOPES = [
|
|
18
|
+
// The canonical API uses catalog:read for catalog, Market Index,
|
|
19
|
+
// Price Index, and procurement reads; those data planes do not define
|
|
20
|
+
// separate key scopes.
|
|
21
|
+
'catalog:read',
|
|
22
|
+
'inventory:read',
|
|
23
|
+
'inventory:write',
|
|
24
|
+
'roast:read',
|
|
25
|
+
'roast:write',
|
|
26
|
+
'sales:read',
|
|
27
|
+
'sales:write',
|
|
28
|
+
'tasting:read',
|
|
29
|
+
'tasting:write',
|
|
30
|
+
];
|
|
31
|
+
export function createOAuthUrl(redirectTo) {
|
|
32
|
+
const url = new URL('/auth/v1/authorize', SUPABASE_AUTH_URL);
|
|
33
|
+
url.searchParams.set('provider', 'google');
|
|
34
|
+
url.searchParams.set('redirect_to', redirectTo);
|
|
35
|
+
return url.toString();
|
|
36
|
+
}
|
|
37
|
+
function decodeSessionIdentity(accessToken) {
|
|
38
|
+
try {
|
|
39
|
+
const payload = JSON.parse(Buffer.from(accessToken.split('.')[1] ?? '', 'base64url').toString('utf8'));
|
|
40
|
+
return {
|
|
41
|
+
id: typeof payload.sub === 'string' ? payload.sub : 'unknown',
|
|
42
|
+
email: typeof payload.email === 'string' ? payload.email : 'unknown',
|
|
43
|
+
role: typeof payload.role === 'string' ? payload.role : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { id: 'unknown', email: 'unknown' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function exchangeOAuthSessionForApiKey(accessToken, clientOverride) {
|
|
51
|
+
const client = clientOverride ?? createParchmentClient({ baseUrl: getParchmentBaseUrl(), token: accessToken });
|
|
52
|
+
const me = await client.me();
|
|
53
|
+
if (!me.response.ok || me.error || !me.data?.authenticated || !me.data.userId) {
|
|
54
|
+
throw new AuthError('OAuth session could not be verified by Parchment.');
|
|
55
|
+
}
|
|
56
|
+
const keyName = `purvey-cli-${hostname()}`;
|
|
57
|
+
const existing = await client.apiKeys.list();
|
|
58
|
+
if (!existing.response.ok || existing.error) {
|
|
59
|
+
throw new AuthError('Failed to inspect existing CLI API keys.', existing.error);
|
|
60
|
+
}
|
|
61
|
+
const supersededKeys = (existing.data?.data ?? []).filter((key) => key.name === keyName && key.isActive);
|
|
62
|
+
const result = await client.apiKeys.create({
|
|
63
|
+
name: keyName,
|
|
64
|
+
scopes: [...CLI_API_KEY_SCOPES],
|
|
65
|
+
});
|
|
66
|
+
if (!result.response.ok || result.error || !result.data?.apiKey) {
|
|
67
|
+
throw new AuthError('Failed to create a scoped Parchment API key.', result.error);
|
|
68
|
+
}
|
|
69
|
+
for (const key of supersededKeys) {
|
|
70
|
+
const revoked = await client.apiKeys.revoke(key.id);
|
|
71
|
+
if (!revoked.response.ok || revoked.error) {
|
|
72
|
+
await client.apiKeys.revoke(result.data.key.id).catch(() => undefined);
|
|
73
|
+
throw new AuthError('Failed to replace the existing CLI API key.', revoked.error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const identity = decodeSessionIdentity(accessToken);
|
|
77
|
+
return {
|
|
78
|
+
apiKey: result.data.apiKey,
|
|
79
|
+
keyId: result.data.key.id,
|
|
80
|
+
createdAt: result.data.key.createdAt ?? new Date().toISOString(),
|
|
81
|
+
user: {
|
|
82
|
+
...identity,
|
|
83
|
+
id: me.data.userId,
|
|
84
|
+
role: me.data.primaryAppRole ?? identity.role,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
12
88
|
/**
|
|
13
89
|
* Start a one-shot local HTTP server to receive the Supabase OAuth callback.
|
|
14
90
|
*
|
|
@@ -18,6 +94,7 @@ const CALLBACK_PATH = '/auth/callback';
|
|
|
18
94
|
*/
|
|
19
95
|
function startCallbackServer() {
|
|
20
96
|
return new Promise((resolve, reject) => {
|
|
97
|
+
const state = randomUUID();
|
|
21
98
|
let tokenResolve;
|
|
22
99
|
let tokenReject;
|
|
23
100
|
const tokenPromise = new Promise((res, rej) => {
|
|
@@ -26,7 +103,7 @@ function startCallbackServer() {
|
|
|
26
103
|
});
|
|
27
104
|
const server = createServer((req, res) => {
|
|
28
105
|
const url = new URL(req.url ?? '/', `http://${DEFAULT_CALLBACK_HOST}`);
|
|
29
|
-
if (url.pathname === CALLBACK_PATH) {
|
|
106
|
+
if (url.pathname === CALLBACK_PATH && url.searchParams.get('state') === state) {
|
|
30
107
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
31
108
|
res.end(`<!DOCTYPE html>
|
|
32
109
|
<html>
|
|
@@ -43,6 +120,7 @@ function startCallbackServer() {
|
|
|
43
120
|
access_token: params.get('access_token'),
|
|
44
121
|
refresh_token: params.get('refresh_token'),
|
|
45
122
|
expires_in: params.get('expires_in'),
|
|
123
|
+
state: new URLSearchParams(window.location.search).get('state'),
|
|
46
124
|
})
|
|
47
125
|
}).then(() => {
|
|
48
126
|
document.body.innerHTML =
|
|
@@ -61,8 +139,8 @@ function startCallbackServer() {
|
|
|
61
139
|
res.end('ok');
|
|
62
140
|
server.close();
|
|
63
141
|
try {
|
|
64
|
-
const { access_token, refresh_token, expires_in } = JSON.parse(body);
|
|
65
|
-
if (!access_token || !refresh_token) {
|
|
142
|
+
const { access_token, refresh_token, expires_in, state: callbackState, } = JSON.parse(body);
|
|
143
|
+
if (!access_token || !refresh_token || callbackState !== state) {
|
|
66
144
|
tokenReject(new AuthError('OAuth callback did not include tokens. Try again.'));
|
|
67
145
|
return;
|
|
68
146
|
}
|
|
@@ -89,6 +167,7 @@ function startCallbackServer() {
|
|
|
89
167
|
}
|
|
90
168
|
resolve({
|
|
91
169
|
port: addr.port,
|
|
170
|
+
state,
|
|
92
171
|
tokenPromise,
|
|
93
172
|
close: () => {
|
|
94
173
|
try {
|
|
@@ -107,7 +186,7 @@ function startCallbackServer() {
|
|
|
107
186
|
* Extract Supabase OAuth tokens from a full callback URL, URL fragment, or
|
|
108
187
|
* query string. Exported for auth-flow contract tests.
|
|
109
188
|
*/
|
|
110
|
-
export function parseOAuthCallbackUrl(callbackUrl) {
|
|
189
|
+
export function parseOAuthCallbackUrl(callbackUrl, expectedState) {
|
|
111
190
|
const trimmed = callbackUrl.trim();
|
|
112
191
|
if (!trimmed) {
|
|
113
192
|
throw new AuthError('No URL provided.');
|
|
@@ -129,6 +208,12 @@ export function parseOAuthCallbackUrl(callbackUrl) {
|
|
|
129
208
|
const accessToken = params.get('access_token');
|
|
130
209
|
const refreshToken = params.get('refresh_token');
|
|
131
210
|
const expiresIn = params.get('expires_in');
|
|
211
|
+
if (expectedState) {
|
|
212
|
+
const parsedUrl = new URL(trimmed, 'http://localhost');
|
|
213
|
+
if (parsedUrl.searchParams.get('state') !== expectedState) {
|
|
214
|
+
throw new AuthError('OAuth callback state did not match this login attempt.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
132
217
|
if (!accessToken || !refreshToken) {
|
|
133
218
|
throw new AuthError('Could not extract tokens from the URL.\n' +
|
|
134
219
|
' Make sure you copied the full callback URL including the #access_token=... part.');
|
|
@@ -139,7 +224,7 @@ export function parseOAuthCallbackUrl(callbackUrl) {
|
|
|
139
224
|
expiresIn: parseInt(expiresIn ?? '3600', 10),
|
|
140
225
|
};
|
|
141
226
|
}
|
|
142
|
-
export function createManualCallbackReaderForQuestion(question, closeQuestion, canRead) {
|
|
227
|
+
export function createManualCallbackReaderForQuestion(question, closeQuestion, canRead, expectedState) {
|
|
143
228
|
if (!canRead)
|
|
144
229
|
return null;
|
|
145
230
|
let closed = false;
|
|
@@ -152,7 +237,7 @@ export function createManualCallbackReaderForQuestion(question, closeQuestion, c
|
|
|
152
237
|
if (closed)
|
|
153
238
|
return;
|
|
154
239
|
try {
|
|
155
|
-
const callbackResult = parseOAuthCallbackUrl(answer);
|
|
240
|
+
const callbackResult = parseOAuthCallbackUrl(answer, expectedState);
|
|
156
241
|
closed = true;
|
|
157
242
|
closeQuestion();
|
|
158
243
|
resolve(callbackResult);
|
|
@@ -177,14 +262,14 @@ export function createManualCallbackReaderForQuestion(question, closeQuestion, c
|
|
|
177
262
|
},
|
|
178
263
|
};
|
|
179
264
|
}
|
|
180
|
-
function createManualCallbackReader() {
|
|
265
|
+
function createManualCallbackReader(expectedState) {
|
|
181
266
|
if (!process.stdin.isTTY)
|
|
182
267
|
return null;
|
|
183
268
|
const rl = createInterface({
|
|
184
269
|
input: process.stdin,
|
|
185
270
|
output: process.stdout,
|
|
186
271
|
});
|
|
187
|
-
return createManualCallbackReaderForQuestion(rl.question.bind(rl), () => rl.close(), process.stdin.isTTY);
|
|
272
|
+
return createManualCallbackReaderForQuestion(rl.question.bind(rl), () => rl.close(), process.stdin.isTTY, expectedState);
|
|
188
273
|
}
|
|
189
274
|
/**
|
|
190
275
|
* `purvey auth login`
|
|
@@ -192,19 +277,12 @@ function createManualCallbackReader() {
|
|
|
192
277
|
*/
|
|
193
278
|
const loginAction = withErrorHandling(async () => {
|
|
194
279
|
const callbackServer = await startCallbackServer();
|
|
195
|
-
const { port, tokenPromise } = callbackServer;
|
|
196
|
-
const redirectTo = `http://${DEFAULT_CALLBACK_HOST}:${port}${CALLBACK_PATH}`;
|
|
280
|
+
const { port, state, tokenPromise } = callbackServer;
|
|
281
|
+
const redirectTo = `http://${DEFAULT_CALLBACK_HOST}:${port}${CALLBACK_PATH}?state=${encodeURIComponent(state)}`;
|
|
197
282
|
try {
|
|
198
|
-
const
|
|
199
|
-
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
200
|
-
provider: 'google',
|
|
201
|
-
options: { redirectTo },
|
|
202
|
-
});
|
|
203
|
-
if (error || !data.url) {
|
|
204
|
-
throw new AuthError('Failed to generate OAuth URL.', error);
|
|
205
|
-
}
|
|
283
|
+
const oauthUrl = createOAuthUrl(redirectTo);
|
|
206
284
|
console.log(chalk.bold('\n Opening browser for authentication...'));
|
|
207
|
-
console.log(chalk.dim(` If the browser does not open, visit:\n ${
|
|
285
|
+
console.log(chalk.dim(` If the browser does not open, visit:\n ${oauthUrl}\n`));
|
|
208
286
|
console.log(chalk.dim(' Waiting for the browser callback. If the browser lands on a localhost URL but\n' +
|
|
209
287
|
' the CLI does not finish, copy that full URL and paste it below.\n'));
|
|
210
288
|
// Open browser cross-platform (graceful fallback for headless)
|
|
@@ -215,7 +293,7 @@ const loginAction = withErrorHandling(async () => {
|
|
|
215
293
|
: process.platform === 'win32'
|
|
216
294
|
? 'start'
|
|
217
295
|
: 'xdg-open';
|
|
218
|
-
const child = spawn(openCmd, [
|
|
296
|
+
const child = spawn(openCmd, [oauthUrl], { detached: true, stdio: 'ignore' });
|
|
219
297
|
child.on('error', () => {
|
|
220
298
|
// Browser open failed (headless environment) — user can visit URL manually
|
|
221
299
|
});
|
|
@@ -224,7 +302,7 @@ const loginAction = withErrorHandling(async () => {
|
|
|
224
302
|
catch {
|
|
225
303
|
// Gracefully ignore — URL is already printed above
|
|
226
304
|
}
|
|
227
|
-
const manualReader = createManualCallbackReader();
|
|
305
|
+
const manualReader = createManualCallbackReader(state);
|
|
228
306
|
const spinner = ora({ text: 'Waiting for authentication...', stream: process.stderr }).start();
|
|
229
307
|
let callbackResult;
|
|
230
308
|
try {
|
|
@@ -236,22 +314,9 @@ const loginAction = withErrorHandling(async () => {
|
|
|
236
314
|
finally {
|
|
237
315
|
manualReader?.close();
|
|
238
316
|
}
|
|
239
|
-
const { accessToken
|
|
317
|
+
const { accessToken } = callbackResult;
|
|
240
318
|
spinner.succeed('Authentication received');
|
|
241
|
-
|
|
242
|
-
const client = createAnonClient();
|
|
243
|
-
await client.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });
|
|
244
|
-
const { data: { user }, } = await client.auth.getUser();
|
|
245
|
-
const creds = {
|
|
246
|
-
accessToken,
|
|
247
|
-
refreshToken,
|
|
248
|
-
expiresAt: Date.now() + expiresIn * 1000,
|
|
249
|
-
user: {
|
|
250
|
-
id: user?.id ?? 'unknown',
|
|
251
|
-
email: user?.email ?? 'unknown',
|
|
252
|
-
role: user?.role,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
319
|
+
const creds = await exchangeOAuthSessionForApiKey(accessToken);
|
|
255
320
|
await writeCredentials(creds);
|
|
256
321
|
success(`Logged in as ${chalk.bold(creds.user.email)}`);
|
|
257
322
|
}
|
|
@@ -289,12 +354,14 @@ const statusAction = withErrorHandling(async (_, cmd) => {
|
|
|
289
354
|
authenticated: true,
|
|
290
355
|
email: session.email,
|
|
291
356
|
role: session.role ?? 'authenticated',
|
|
292
|
-
|
|
357
|
+
keyId: session.keyId,
|
|
358
|
+
keyCreated: session.createdAt,
|
|
293
359
|
};
|
|
294
360
|
if (isInteractive) {
|
|
295
361
|
success(`Logged in as ${chalk.bold(session.email)}`);
|
|
296
362
|
info(`Role: ${result.role}`);
|
|
297
|
-
info(`
|
|
363
|
+
info(`API key: ${result.keyId}`);
|
|
364
|
+
info(`Created: ${result.keyCreated}`);
|
|
298
365
|
return;
|
|
299
366
|
}
|
|
300
367
|
outputData(result, opts);
|
|
@@ -305,18 +372,13 @@ const statusAction = withErrorHandling(async (_, cmd) => {
|
|
|
305
372
|
*/
|
|
306
373
|
const headlessLoginAction = withErrorHandling(async () => {
|
|
307
374
|
// Generate OAuth URL with purveyors.io callback page as redirect
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
options: { redirectTo, skipBrowserRedirect: true },
|
|
313
|
-
});
|
|
314
|
-
if (error || !data.url) {
|
|
315
|
-
throw new AuthError('Failed to generate OAuth URL.');
|
|
316
|
-
}
|
|
375
|
+
const state = randomUUID();
|
|
376
|
+
const redirectTo = new URL('https://purveyors.io/auth/cli-callback');
|
|
377
|
+
redirectTo.searchParams.set('state', state);
|
|
378
|
+
const oauthUrl = createOAuthUrl(redirectTo.toString());
|
|
317
379
|
console.log(chalk.bold('\n Headless Login\n'));
|
|
318
380
|
console.log(' 1. Open this URL in a browser and sign in:\n');
|
|
319
|
-
console.log(` ${chalk.cyan(
|
|
381
|
+
console.log(` ${chalk.cyan(oauthUrl)}\n`);
|
|
320
382
|
console.log(" 2. After login, you'll see a page with a callback URL.");
|
|
321
383
|
console.log(' 3. Copy the full callback URL and paste it below.\n');
|
|
322
384
|
// Read the callback URL from stdin
|
|
@@ -331,24 +393,9 @@ const headlessLoginAction = withErrorHandling(async () => {
|
|
|
331
393
|
if (!callbackUrl) {
|
|
332
394
|
throw new AuthError('No URL provided.');
|
|
333
395
|
}
|
|
334
|
-
const { accessToken
|
|
396
|
+
const { accessToken } = parseOAuthCallbackUrl(callbackUrl, state);
|
|
335
397
|
const spinner = ora({ text: 'Validating session...', stream: process.stderr }).start();
|
|
336
|
-
const
|
|
337
|
-
const { data: { user }, error: userError, } = await client.auth.getUser(accessToken);
|
|
338
|
-
if (userError || !user) {
|
|
339
|
-
spinner.fail('Token validation failed');
|
|
340
|
-
throw new AuthError('Invalid or expired token. Try the login flow again.');
|
|
341
|
-
}
|
|
342
|
-
const creds = {
|
|
343
|
-
accessToken,
|
|
344
|
-
refreshToken,
|
|
345
|
-
expiresAt: Date.now() + expiresIn * 1000,
|
|
346
|
-
user: {
|
|
347
|
-
id: user.id,
|
|
348
|
-
email: user.email ?? 'unknown',
|
|
349
|
-
role: user.role,
|
|
350
|
-
},
|
|
351
|
-
};
|
|
398
|
+
const creds = await exchangeOAuthSessionForApiKey(accessToken);
|
|
352
399
|
await writeCredentials(creds);
|
|
353
400
|
spinner.succeed(`Logged in as ${chalk.bold(creds.user.email)}`);
|
|
354
401
|
});
|
|
@@ -358,7 +405,7 @@ const headlessLoginAction = withErrorHandling(async () => {
|
|
|
358
405
|
*/
|
|
359
406
|
const logoutAction = withErrorHandling(async () => {
|
|
360
407
|
await deleteCredentials();
|
|
361
|
-
|
|
408
|
+
warn('Local credentials cleared. The server API key remains active; revoke it from the Purveyors account key dashboard if this machine is lost or compromised.');
|
|
362
409
|
});
|
|
363
410
|
/**
|
|
364
411
|
* Build and return the `auth` command subtree.
|
|
@@ -390,8 +437,8 @@ Examples:
|
|
|
390
437
|
|
|
391
438
|
Notes:
|
|
392
439
|
Credentials are stored at ~/.config/purvey/credentials.json (mode 0600).
|
|
393
|
-
|
|
394
|
-
|
|
440
|
+
OAuth is used once to mint a scoped Parchment API key; session tokens are not retained.
|
|
441
|
+
Re-login replaces the prior CLI key for this machine.
|
|
395
442
|
`);
|
|
396
443
|
auth
|
|
397
444
|
.command('status')
|
|
@@ -407,10 +454,11 @@ Examples:
|
|
|
407
454
|
purvey auth status --json 2>/dev/null | jq .
|
|
408
455
|
|
|
409
456
|
Output fields:
|
|
410
|
-
authenticated boolean — true if
|
|
457
|
+
authenticated boolean — true if the stored API key is valid
|
|
411
458
|
email your Google account email
|
|
412
459
|
role your purveyors.io role (viewer or member)
|
|
413
|
-
|
|
460
|
+
keyId identifier of the stored Parchment API key
|
|
461
|
+
keyCreated ISO timestamp when the key was created
|
|
414
462
|
|
|
415
463
|
Notes:
|
|
416
464
|
When stdout is a TTY (interactive terminal), output is human-readable unless
|