@sanity/cli 8.10.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +69 -26
  2. package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
  3. package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
  4. package/dist/actions/assets/withUrlIngestProgress.js +45 -0
  5. package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
  6. package/dist/actions/auth/authServer.js +5 -8
  7. package/dist/actions/auth/authServer.js.map +1 -1
  8. package/dist/actions/auth/login/getProvider.js +10 -14
  9. package/dist/actions/auth/login/getProvider.js.map +1 -1
  10. package/dist/actions/auth/login/login.js +0 -1
  11. package/dist/actions/auth/login/login.js.map +1 -1
  12. package/dist/actions/deploy/deployApp.js +5 -2
  13. package/dist/actions/deploy/deployApp.js.map +1 -1
  14. package/dist/actions/deploy/deployChecks.js +3 -2
  15. package/dist/actions/deploy/deployChecks.js.map +1 -1
  16. package/dist/actions/deploy/findUserApplication.js +3 -2
  17. package/dist/actions/deploy/findUserApplication.js.map +1 -1
  18. package/dist/actions/deploy/resolveDeployTarget.js +6 -1
  19. package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
  20. package/dist/actions/documents/validateDocuments.worker.js +1 -4
  21. package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
  22. package/dist/actions/media/importMedia.js.map +1 -1
  23. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
  24. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
  25. package/dist/commands/assets/upload.js +78 -52
  26. package/dist/commands/assets/upload.js.map +1 -1
  27. package/dist/commands/deploy.js +33 -1
  28. package/dist/commands/deploy.js.map +1 -1
  29. package/dist/commands/media/import.js +149 -13
  30. package/dist/commands/media/import.js.map +1 -1
  31. package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
  32. package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
  33. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
  34. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  35. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
  36. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
  37. package/dist/exports/invokeSanityCli/help.js +8 -1
  38. package/dist/exports/invokeSanityCli/help.js.map +1 -1
  39. package/dist/exports/invokeSanityCli/index.d.ts +1 -2
  40. package/dist/exports/invokeSanityCli/index.js +37 -15
  41. package/dist/exports/invokeSanityCli/index.js.map +1 -1
  42. package/dist/generated/apiRoutes.js +2 -0
  43. package/dist/generated/apiRoutes.js.map +1 -1
  44. package/dist/services/assets.js +46 -1
  45. package/dist/services/assets.js.map +1 -1
  46. package/dist/services/mediaLibraries.js +39 -1
  47. package/dist/services/mediaLibraries.js.map +1 -1
  48. package/dist/util/assetSourceValidation.js +56 -0
  49. package/dist/util/assetSourceValidation.js.map +1 -0
  50. package/dist/util/assetUploadErrors.js +64 -0
  51. package/dist/util/assetUploadErrors.js.map +1 -0
  52. package/dist/util/isRemoteAssetSource.js +19 -0
  53. package/dist/util/isRemoteAssetSource.js.map +1 -0
  54. package/dist/util/parseAspectFlags.js +34 -0
  55. package/dist/util/parseAspectFlags.js.map +1 -0
  56. package/oclif.config.js +0 -1
  57. package/oclif.manifest.json +2134 -2055
  58. package/package.json +18 -18
  59. package/templates/app-quickstart/AGENTS.md +18 -0
  60. package/templates/app-sanity-ui/AGENTS.md +18 -0
  61. package/templates/shopify/.gitignore +26 -0
  62. package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
  63. package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
package/README.md CHANGED
@@ -244,17 +244,20 @@ EXAMPLES
244
244
 
245
245
  ## `sanity assets upload`
246
246
 
247
- Upload one local image or file to a Sanity dataset and print the asset document as JSON
247
+ Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON
248
248
 
249
249
  ```
250
250
  USAGE
251
- $ sanity assets upload --file <path> [-p <id>] [-d <name>] [--content-type <mime-type>] [--filename <filename>]
252
- [--type image|file]
251
+ $ sanity assets upload [-p <id>] [-d <name>] [--content-type <mime-type>] [--file <path>] [--filename <filename>]
252
+ [--from-url <url>] [--type image|file]
253
253
 
254
254
  FLAGS
255
- --content-type=<mime-type> MIME type of the asset, such as image/png or application/pdf
256
- --file=<path> (required) Path to the local file to upload
257
- --filename=<filename> Original filename stored on the asset document. Defaults to the local filename
255
+ --content-type=<mime-type> MIME type of the asset, such as image/png or application/pdf. Only applies to --file
256
+ --file=<path> Path to the local file to upload
257
+ --filename=<filename> Original filename stored on the asset document. Defaults to the local filename when using
258
+ --file
259
+ --from-url=<url> https URL for Sanity to fetch the asset from, instead of uploading a local file. Must be
260
+ reachable from the public internet without authentication
258
261
  --type=<option> [default: image] Asset type to create
259
262
  <options: image|file>
260
263
 
@@ -263,7 +266,7 @@ OVERRIDE FLAGS
263
266
  -p, --project-id=<id> Project ID to upload the asset to (overrides CLI configuration)
264
267
 
265
268
  DESCRIPTION
266
- Upload one local image or file to a Sanity dataset and print the asset document as JSON
269
+ Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON
267
270
 
268
271
  EXAMPLES
269
272
  Upload an image using the configured project
@@ -274,6 +277,10 @@ EXAMPLES
274
277
 
275
278
  $ sanity assets upload --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 \
276
279
  --dataset production
280
+
281
+ Have Sanity fetch an image from a public URL
282
+
283
+ $ sanity assets upload --from-url 'https://example.com/hero.png' --type image --dataset production
277
284
  ```
278
285
 
279
286
  ## `sanity backups disable [DATASET]`
@@ -2001,8 +2008,8 @@ Builds and deploys Sanity Studio or application to Sanity hosting
2001
2008
 
2002
2009
  ```
2003
2010
  USAGE
2004
- $ sanity deploy [SOURCEDIR] [--auto-updates] [--dry-run] [--external | --source-maps | --minify | --build]
2005
- [-j] [--schema-required] [--title <value>] [--url <value>] [--verbose] [-y]
2011
+ $ sanity deploy [SOURCEDIR] [--auto-updates] [--build] [--create | --external | --url <value>] [--dry-run]
2012
+ [-j] [--minify] [--schema-required] [--source-maps] [--title <value>] [--verbose] [-y]
2006
2013
 
2007
2014
  ARGUMENTS
2008
2015
  [SOURCEDIR] Source directory
@@ -2012,13 +2019,14 @@ FLAGS
2012
2019
  -y, --yes Unattended mode, answers "yes" to any "yes/no" prompt and otherwise uses defaults
2013
2020
  --[no-]auto-updates Automatically update the studio to the latest version
2014
2021
  --[no-]build Build the studio before deploying (use --no-build to deploy existing `dist/` output)
2022
+ --create Create a new App SDK app, even if the organization has other apps
2015
2023
  --dry-run Report what would be deployed without uploading or creating anything
2016
2024
  --external Register an externally hosted studio
2017
2025
  --[no-]minify Minify built JavaScript (use --no-minify to skip for faster builds)
2018
2026
  --schema-required Fail if schema deployment fails
2019
2027
  --source-maps Enable source maps for built bundles (increases size of bundle)
2020
- --title=<value> Title for a newly created application or studio. For apps it also skips the interactive title
2021
- prompt, enabling unattended creation
2028
+ --title=<value> Title for a newly created application or studio. For apps, use --create to select creation
2029
+ when the organization already has apps
2022
2030
  --url=<value> Studio URL for deployment. For external studios, the full URL. For hosted studios, the
2023
2031
  hostname (e.g. "my-studio" or "my-studio.sanity.studio")
2024
2032
  --verbose Enable verbose logging
@@ -2042,6 +2050,19 @@ EXAMPLES
2042
2050
  Register an externally hosted studio (studioHost contains full URL)
2043
2051
 
2044
2052
  $ sanity deploy --external
2053
+
2054
+ Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on
2055
+ later deploys
2056
+
2057
+ $ sanity deploy --create --title "My App" --yes --json
2058
+
2059
+ Preview a new App SDK deployment without creating or uploading anything
2060
+
2061
+ $ sanity deploy --create --title "My App" --dry-run --json
2062
+
2063
+ Deploy a studio without prompts using its hostname
2064
+
2065
+ $ sanity deploy --url my-studio --yes
2045
2066
  ```
2046
2067
 
2047
2068
  ## `sanity dev`
@@ -2511,8 +2532,8 @@ DESCRIPTION
2511
2532
 
2512
2533
  Open the emulator in your browser to interactively test your functions with the payload editor.
2513
2534
 
2514
- Optionally, set the host and port with the --host and --port flags. Port 8974 is reserved for the emulator's
2515
- live-reload WebSocket server. Function timeout can be configured with the --timeout flag.
2535
+ Optionally, set the host and port with the --host and --port flags. The live-reload WebSocket server shares this host
2536
+ and port. Function timeout can be configured with the --timeout flag.
2516
2537
 
2517
2538
  To invoke a function with the CLI, use 'functions test'.
2518
2539
 
@@ -3309,15 +3330,20 @@ Import a set of assets to the target media library.
3309
3330
 
3310
3331
  ```
3311
3332
  USAGE
3312
- $ sanity media import SOURCE [-p <id>] [--media-library-id <value>] [--replace-aspects]
3333
+ $ sanity media import SOURCE [-p <id>] [--aspect <key=value>...] [--filename <filename>] [--media-library-id
3334
+ <value>] [--replace-aspects]
3313
3335
 
3314
3336
  ARGUMENTS
3315
- SOURCE Image file or folder to import from
3337
+ SOURCE Directory, archive, or asset URL to import from. An https URL imports one asset that Sanity fetches itself
3316
3338
 
3317
3339
  FLAGS
3340
+ --aspect=<key=value>... Aspect value to set on the imported asset, as key=value. Repeatable. Only applies when the
3341
+ source is a URL - directory and archive imports read aspects from their data.ndjson
3342
+ --filename=<filename> Original filename to store on the asset. Only applies when the source is a URL. Defaults
3343
+ to a name derived from the URL
3318
3344
  --media-library-id=<value> The id of the target media library
3319
3345
  --replace-aspects Replace existing aspect data. All versions will be replaced (e.g. published and draft
3320
- aspect data)
3346
+ aspect data). Only applies to directory and archive imports
3321
3347
 
3322
3348
  OVERRIDE FLAGS
3323
3349
  -p, --project-id=<id> Project ID to import media to (overrides CLI configuration)
@@ -3337,6 +3363,14 @@ EXAMPLES
3337
3363
  Import all assets from the "products" directory and replace aspects
3338
3364
 
3339
3365
  $ sanity media import products --replace-aspects
3366
+
3367
+ Have Sanity fetch a single asset from a public URL
3368
+
3369
+ $ sanity media import https://example.com/hero.png
3370
+
3371
+ Fetch an asset from a URL and set aspect data on it
3372
+
3373
+ $ sanity media import https://example.com/hero.png --aspect department=Brand
3340
3374
  ```
3341
3375
 
3342
3376
  ## `sanity migrations create [TITLE]`
@@ -4610,18 +4644,25 @@ List workflow instances in the configured dataset (in-flight by default).
4610
4644
  ```
4611
4645
  USAGE
4612
4646
  $ sanity workflows list [--tag <value>] [--include-completed] [--failed] [--definition <value>] [--document
4613
- <value>] [--limit <value>] [--json]
4647
+ <value>] [--assignment-user <value>] [--assignment-role <value>...] [--assignment-state unrouted|routed|held...]
4648
+ [--limit <value>] [--json]
4614
4649
 
4615
4650
  FLAGS
4616
- --definition=<value> Only instances of this workflow definition (its `name`; the instance's `definition` field).
4617
- --document=<value> Only instances that reference this document (resource-qualified GDR URI, e.g.
4618
- "dataset:proj:ds:article-1").
4619
- --failed Only instances with at least one failed activity.
4620
- --include-completed Include completed/aborted instances (default: in-flight only).
4621
- --json Emit structured JSON instead of rendered output.
4622
- --limit=<value> [default: 50] Maximum rows to return.
4623
- --tag=<value> Workflow environment tag (e.g. prod, test) an optional query filter that also narrows which
4624
- resources are searched; omit to span them all.
4651
+ --assignment-role=<value>... A literal project role held by --assignment-user. Repeat for multiple roles.
4652
+ --assignment-state=<option>... Assignment state to include: unrouted, routed (offered through a supplied role), or
4653
+ held. Repeat for multiple states.
4654
+ <options: unrouted|routed|held>
4655
+ --assignment-user=<value> Account-global user id used for viewer-scoped assignment filtering and counts.
4656
+ --definition=<value> Only instances of this workflow definition (its `name`; the instance's `definition`
4657
+ field).
4658
+ --document=<value> Only instances that reference this document (resource-qualified GDR URI, e.g.
4659
+ "dataset:proj:ds:article-1").
4660
+ --failed Only instances with at least one failed activity.
4661
+ --include-completed Include completed/aborted instances (default: in-flight only).
4662
+ --json Emit structured JSON instead of rendered output.
4663
+ --limit=<value> [default: 50] Maximum rows to return.
4664
+ --tag=<value> Workflow environment tag (e.g. prod, test) — an optional query filter that also
4665
+ narrows which resources are searched; omit to span them all.
4625
4666
 
4626
4667
  DESCRIPTION
4627
4668
  List workflow instances in the configured dataset (in-flight by default).
@@ -4635,6 +4676,8 @@ EXAMPLES
4635
4676
 
4636
4677
  $ sanity workflows list --document dataset:proj:ds:article-1
4637
4678
 
4679
+ $ sanity workflows list --assignment-user gAda --assignment-role legal --assignment-state routed
4680
+
4638
4681
  $ sanity workflows list --tag prod
4639
4682
 
4640
4683
  $ sanity workflows list --json
@@ -0,0 +1,21 @@
1
+ import { ingestAssetFromUrl } from '../../services/assets.js';
2
+ import { withUrlIngestProgress } from './withUrlIngestProgress.js';
3
+ /**
4
+ * Ingest a dataset asset from a URL, reporting the wait.
5
+ */ export async function ingestAssetFromUrlWithProgress(options) {
6
+ const { assetType, isInteractive, logToStderr, ...ingestOptions } = options;
7
+ const ingestMessage = `Fetching ${assetType} asset from URL`;
8
+ return withUrlIngestProgress({
9
+ ingest: (signal)=>ingestAssetFromUrl({
10
+ ...ingestOptions,
11
+ assetType,
12
+ signal
13
+ }),
14
+ isInteractive,
15
+ logToStderr,
16
+ successMessage: (asset)=>`Uploaded ${assetType} asset: ${asset._id}`,
17
+ waitMessage: `${ingestMessage}. Sanity downloads the asset, which may take several minutes.`
18
+ });
19
+ }
20
+
21
+ //# sourceMappingURL=ingestAssetFromUrlWithProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/assets/ingestAssetFromUrlWithProgress.ts"],"sourcesContent":["import {type AssetType, ingestAssetFromUrl} from '../../services/assets.js'\nimport {withUrlIngestProgress} from './withUrlIngestProgress.js'\n\ninterface IngestAssetFromUrlWithProgressOptions {\n assetType: AssetType\n dataset: string\n isInteractive: boolean\n logToStderr: (message: string) => void\n projectId: string\n url: string\n\n filename?: string\n}\n\n/**\n * Ingest a dataset asset from a URL, reporting the wait.\n */\nexport async function ingestAssetFromUrlWithProgress(\n options: IngestAssetFromUrlWithProgressOptions,\n) {\n const {assetType, isInteractive, logToStderr, ...ingestOptions} = options\n const ingestMessage = `Fetching ${assetType} asset from URL`\n\n return withUrlIngestProgress({\n ingest: (signal) => ingestAssetFromUrl({...ingestOptions, assetType, signal}),\n isInteractive,\n logToStderr,\n successMessage: (asset) => `Uploaded ${assetType} asset: ${asset._id}`,\n waitMessage: `${ingestMessage}. Sanity downloads the asset, which may take several minutes.`,\n })\n}\n"],"names":["ingestAssetFromUrl","withUrlIngestProgress","ingestAssetFromUrlWithProgress","options","assetType","isInteractive","logToStderr","ingestOptions","ingestMessage","ingest","signal","successMessage","asset","_id","waitMessage"],"mappings":"AAAA,SAAwBA,kBAAkB,QAAO,2BAA0B;AAC3E,SAAQC,qBAAqB,QAAO,6BAA4B;AAahE;;CAEC,GACD,OAAO,eAAeC,+BACpBC,OAA8C;IAE9C,MAAM,EAACC,SAAS,EAAEC,aAAa,EAAEC,WAAW,EAAE,GAAGC,eAAc,GAAGJ;IAClE,MAAMK,gBAAgB,CAAC,SAAS,EAAEJ,UAAU,eAAe,CAAC;IAE5D,OAAOH,sBAAsB;QAC3BQ,QAAQ,CAACC,SAAWV,mBAAmB;gBAAC,GAAGO,aAAa;gBAAEH;gBAAWM;YAAM;QAC3EL;QACAC;QACAK,gBAAgB,CAACC,QAAU,CAAC,SAAS,EAAER,UAAU,QAAQ,EAAEQ,MAAMC,GAAG,EAAE;QACtEC,aAAa,GAAGN,cAAc,6DAA6D,CAAC;IAC9F;AACF"}
@@ -0,0 +1,45 @@
1
+ import { getCliExecutionContext } from '@sanity/cli-core/executionContext';
2
+ import { spinner } from '@sanity/cli-core/ux';
3
+ /**
4
+ * Report progress for an asset ingest where Sanity fetches the source itself.
5
+ *
6
+ * The bytes never pass through this process, so there is no upload percentage
7
+ * to report — only a single wait that can run into minutes. The spinner exists
8
+ * to keep that wait legible rather than to track it.
9
+ *
10
+ * Shared by the dataset and media library ingest paths because the interrupt
11
+ * handling below is easy to get subtly wrong, not because the two have much
12
+ * else in common.
13
+ */ export async function withUrlIngestProgress({ ingest, isInteractive, logToStderr, successMessage, waitMessage }) {
14
+ const executionContext = getCliExecutionContext();
15
+ const isTTY = !executionContext && isInteractive;
16
+ const ingestProgress = spinner(waitMessage).start();
17
+ if (executionContext) logToStderr(waitMessage);
18
+ const ingestController = new AbortController();
19
+ const interruptIngest = ()=>ingestController.abort(new Error('SIGINT'));
20
+ const handlesInterrupt = !executionContext;
21
+ if (handlesInterrupt) process.once('SIGINT', interruptIngest);
22
+ // Ora uses raw mode for Ctrl+C, but its input listener does not always make stdin flow.
23
+ const resumesInterruptInput = handlesInterrupt && isTTY && ingestProgress.isSpinning && process.stdin.isTTY && process.stdin.isRaw && process.stdin.readableFlowing !== true;
24
+ if (resumesInterruptInput) process.stdin.resume();
25
+ try {
26
+ const result = await ingest(ingestController.signal);
27
+ const message = successMessage(result);
28
+ if (executionContext) {
29
+ ingestProgress.stop();
30
+ logToStderr(message);
31
+ } else {
32
+ ingestProgress.succeed(message);
33
+ }
34
+ return result;
35
+ } catch (error) {
36
+ ingestProgress.stop();
37
+ ingestController.signal.throwIfAborted();
38
+ throw error;
39
+ } finally{
40
+ if (handlesInterrupt) process.off('SIGINT', interruptIngest);
41
+ if (resumesInterruptInput) process.stdin.pause();
42
+ }
43
+ }
44
+
45
+ //# sourceMappingURL=withUrlIngestProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/assets/withUrlIngestProgress.ts"],"sourcesContent":["import {getCliExecutionContext} from '@sanity/cli-core/executionContext'\nimport {spinner} from '@sanity/cli-core/ux'\n\ninterface WithUrlIngestProgressOptions<T> {\n /**\n * Performs the ingest. Receives a signal that aborts with an `Error('SIGINT')`\n * when the user interrupts, so callers can pass it straight to the request.\n */\n ingest: (signal: AbortSignal) => Promise<T>\n isInteractive: boolean\n logToStderr: (message: string) => void\n successMessage: (result: T) => string\n waitMessage: string\n}\n\n/**\n * Report progress for an asset ingest where Sanity fetches the source itself.\n *\n * The bytes never pass through this process, so there is no upload percentage\n * to report — only a single wait that can run into minutes. The spinner exists\n * to keep that wait legible rather than to track it.\n *\n * Shared by the dataset and media library ingest paths because the interrupt\n * handling below is easy to get subtly wrong, not because the two have much\n * else in common.\n */\nexport async function withUrlIngestProgress<T>({\n ingest,\n isInteractive,\n logToStderr,\n successMessage,\n waitMessage,\n}: WithUrlIngestProgressOptions<T>): Promise<T> {\n const executionContext = getCliExecutionContext()\n const isTTY = !executionContext && isInteractive\n const ingestProgress = spinner(waitMessage).start()\n if (executionContext) logToStderr(waitMessage)\n\n const ingestController = new AbortController()\n const interruptIngest = () => ingestController.abort(new Error('SIGINT'))\n const handlesInterrupt = !executionContext\n if (handlesInterrupt) process.once('SIGINT', interruptIngest)\n\n // Ora uses raw mode for Ctrl+C, but its input listener does not always make stdin flow.\n const resumesInterruptInput =\n handlesInterrupt &&\n isTTY &&\n ingestProgress.isSpinning &&\n process.stdin.isTTY &&\n process.stdin.isRaw &&\n process.stdin.readableFlowing !== true\n if (resumesInterruptInput) process.stdin.resume()\n\n try {\n const result = await ingest(ingestController.signal)\n const message = successMessage(result)\n\n if (executionContext) {\n ingestProgress.stop()\n logToStderr(message)\n } else {\n ingestProgress.succeed(message)\n }\n\n return result\n } catch (error) {\n ingestProgress.stop()\n ingestController.signal.throwIfAborted()\n throw error\n } finally {\n if (handlesInterrupt) process.off('SIGINT', interruptIngest)\n if (resumesInterruptInput) process.stdin.pause()\n }\n}\n"],"names":["getCliExecutionContext","spinner","withUrlIngestProgress","ingest","isInteractive","logToStderr","successMessage","waitMessage","executionContext","isTTY","ingestProgress","start","ingestController","AbortController","interruptIngest","abort","Error","handlesInterrupt","process","once","resumesInterruptInput","isSpinning","stdin","isRaw","readableFlowing","resume","result","signal","message","stop","succeed","error","throwIfAborted","off","pause"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,oCAAmC;AACxE,SAAQC,OAAO,QAAO,sBAAqB;AAc3C;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,sBAAyB,EAC7CC,MAAM,EACNC,aAAa,EACbC,WAAW,EACXC,cAAc,EACdC,WAAW,EACqB;IAChC,MAAMC,mBAAmBR;IACzB,MAAMS,QAAQ,CAACD,oBAAoBJ;IACnC,MAAMM,iBAAiBT,QAAQM,aAAaI,KAAK;IACjD,IAAIH,kBAAkBH,YAAYE;IAElC,MAAMK,mBAAmB,IAAIC;IAC7B,MAAMC,kBAAkB,IAAMF,iBAAiBG,KAAK,CAAC,IAAIC,MAAM;IAC/D,MAAMC,mBAAmB,CAACT;IAC1B,IAAIS,kBAAkBC,QAAQC,IAAI,CAAC,UAAUL;IAE7C,wFAAwF;IACxF,MAAMM,wBACJH,oBACAR,SACAC,eAAeW,UAAU,IACzBH,QAAQI,KAAK,CAACb,KAAK,IACnBS,QAAQI,KAAK,CAACC,KAAK,IACnBL,QAAQI,KAAK,CAACE,eAAe,KAAK;IACpC,IAAIJ,uBAAuBF,QAAQI,KAAK,CAACG,MAAM;IAE/C,IAAI;QACF,MAAMC,SAAS,MAAMvB,OAAOS,iBAAiBe,MAAM;QACnD,MAAMC,UAAUtB,eAAeoB;QAE/B,IAAIlB,kBAAkB;YACpBE,eAAemB,IAAI;YACnBxB,YAAYuB;QACd,OAAO;YACLlB,eAAeoB,OAAO,CAACF;QACzB;QAEA,OAAOF;IACT,EAAE,OAAOK,OAAO;QACdrB,eAAemB,IAAI;QACnBjB,iBAAiBe,MAAM,CAACK,cAAc;QACtC,MAAMD;IACR,SAAU;QACR,IAAId,kBAAkBC,QAAQe,GAAG,CAAC,UAAUnB;QAC5C,IAAIM,uBAAuBF,QAAQI,KAAK,CAACY,KAAK;IAChD;AACF"}
@@ -3,22 +3,19 @@ import os from 'node:os';
3
3
  import { getSanityUrl, subdebug } from '@sanity/cli-core';
4
4
  import { getTokenDetails } from '../../services/auth.js';
5
5
  const debug = subdebug('auth');
6
- const defaultCallbackPort = 4321;
6
+ const defaultCallbackPort = 0;
7
7
  const callbackEndpoint = '/callback';
8
8
  /**
9
9
  * Get the port to first attempt binding the auth callback server to.
10
10
  *
11
11
  * The auth backend accepts any `http://localhost:<port>` origin for token
12
- * callbacks, but we prefer a predictable port: it is friendlier for users who
13
- * need to allow or forward the port (e.g. logging in from a remote machine over
14
- * an SSH tunnel). If the preferred port is taken, we fall back to an
15
- * OS-assigned ephemeral port.
12
+ * callbacks, so by default we let the OS assign an available ephemeral port.
16
13
  *
17
14
  * The `SANITY_CLI_CALLBACK_PORT` environment variable overrides the preferred
18
15
  * port (`0` for an OS-assigned port). The override exists primarily for tests,
19
16
  * where OS-assigned ports prevent collisions between tests running in parallel.
20
17
  *
21
- * @returns Port number to attempt first
18
+ * @returns Port number to attempt first (`0` for an OS-assigned port)
22
19
  * @internal
23
20
  */ function getPreferredCallbackPort() {
24
21
  const override = process.env.SANITY_CLI_CALLBACK_PORT;
@@ -49,7 +46,7 @@ const platformNames = {
49
46
  * do a request to the `/auth/fetch` endpoint with to get the actual auth token,
50
47
  * invalidating the SID in the process.
51
48
  *
52
- * If we fail to bind to the preferred port, we retry with an OS-assigned port.
49
+ * If an explicitly configured port is busy, we retry with an OS-assigned port.
53
50
  *
54
51
  * @param providerUrl - The URL of the login provider
55
52
  * @returns Resolves with HTTP server instance, a login URL to send user to, and a `token` promise
@@ -125,7 +122,7 @@ const platformNames = {
125
122
  });
126
123
  server.on('error', function onCallbackServerError(err) {
127
124
  // The auth backend accepts any localhost port in the callback origin, so if
128
- // the preferred port is busy we fall back to an OS-assigned ephemeral port.
125
+ // an explicitly configured port is busy we fall back to an OS-assigned ephemeral port.
129
126
  // Port 0 cannot itself be "in use" - an EADDRINUSE there means something is
130
127
  // genuinely wrong, so only fall back once.
131
128
  if ('code' in err && err.code === 'EADDRINUSE' && callbackPort !== 0) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/auth/authServer.ts"],"sourcesContent":["import {createServer, type Server} from 'node:http'\nimport os from 'node:os'\n\nimport {getSanityUrl, subdebug} from '@sanity/cli-core'\n\nimport {getTokenDetails} from '../../services/auth.js'\nimport {type TokenDetails} from './types.js'\n\nconst debug = subdebug('auth')\nconst defaultCallbackPort = 4321\nconst callbackEndpoint = '/callback'\n\n/**\n * Get the port to first attempt binding the auth callback server to.\n *\n * The auth backend accepts any `http://localhost:<port>` origin for token\n * callbacks, but we prefer a predictable port: it is friendlier for users who\n * need to allow or forward the port (e.g. logging in from a remote machine over\n * an SSH tunnel). If the preferred port is taken, we fall back to an\n * OS-assigned ephemeral port.\n *\n * The `SANITY_CLI_CALLBACK_PORT` environment variable overrides the preferred\n * port (`0` for an OS-assigned port). The override exists primarily for tests,\n * where OS-assigned ports prevent collisions between tests running in parallel.\n *\n * @returns Port number to attempt first\n * @internal\n */\nfunction getPreferredCallbackPort(): number {\n const override = process.env.SANITY_CLI_CALLBACK_PORT\n if (!override) {\n return defaultCallbackPort\n }\n\n debug('Using callback port from SANITY_CLI_CALLBACK_PORT: %s', override)\n\n const port = Number.parseInt(override.trim(), 10)\n if (Number.isNaN(port) || port < 0 || port > 65_535) {\n throw new Error(`Invalid SANITY_CLI_CALLBACK_PORT value: \"${override}\"`)\n }\n\n return port\n}\n\nconst platformNames: Record<string, string | undefined> = {\n aix: 'AIX',\n android: 'Android',\n darwin: 'MacOS',\n freebsd: 'FreeBSD',\n linux: 'Linux',\n openbsd: 'OpenBSD',\n sunos: 'SunOS',\n win32: 'Windows',\n}\n\n/**\n * Start a local HTTP server and wait for a request to the auth callback endpoint.\n * This happens by the user being sent to a login page with a callback URL that points to\n * this local server. This request includes a short-lived \"SID\" (session ID) that we then\n * do a request to the `/auth/fetch` endpoint with to get the actual auth token,\n * invalidating the SID in the process.\n *\n * If we fail to bind to the preferred port, we retry with an OS-assigned port.\n *\n * @param providerUrl - The URL of the login provider\n * @returns Resolves with HTTP server instance, a login URL to send user to, and a `token` promise\n * @internal\n */\nexport function startServerForTokenCallback(\n providerUrl: string,\n): Promise<{loginUrl: URL; server: Server; token: Promise<TokenDetails>}> {\n const sanityUrl = getSanityUrl()\n\n // note: replace with `Promise.withResolvers()` when minimum Node.js is 22+\n let resolveToken: (resolvedToken: PromiseLike<TokenDetails> | TokenDetails) => void\n let rejectToken: (reason: Error) => void\n const tokenPromise = new Promise<TokenDetails>((resolve, reject) => {\n resolveToken = resolve\n rejectToken = reject\n })\n\n return new Promise((resolve, reject) => {\n let callbackPort = getPreferredCallbackPort()\n\n const server = createServer(async function onCallbackServerRequest(req, res) {\n function failLoginRequest(code = '') {\n res.writeHead(303, 'See Other', {\n Connection: 'close',\n Location: `${sanityUrl}/login/error${code ? `?error=${code}` : ''}`,\n })\n res.end()\n server.close()\n }\n\n const url = new URL(req.url || '/', `http://localhost:${callbackPort}`)\n if (url.pathname !== callbackEndpoint) {\n res.writeHead(404, 'Not Found', {Connection: 'close', 'Content-Type': 'text/plain'})\n res.write('404 Not Found')\n res.end()\n return\n }\n\n const absoluteTokenUrl = url.searchParams.get('url')\n if (!absoluteTokenUrl) {\n failLoginRequest()\n rejectToken(new Error('Missing callback URL'))\n return\n }\n\n const tokenUrl = new URL(absoluteTokenUrl)\n if (!tokenUrl.searchParams.has('sid')) {\n failLoginRequest('NO_SESSION_ID')\n rejectToken(new Error('Missing session ID in callback'))\n return\n }\n\n let token: TokenDetails\n try {\n token = await getTokenDetails(tokenUrl.search)\n } catch (err) {\n failLoginRequest('UNRESOLVED_SESSION')\n rejectToken(err instanceof Error ? err : new Error(`Unknown error: ${err}`))\n return\n }\n\n res.writeHead(303, 'See Other', {\n Connection: 'close',\n Location: `${sanityUrl}/login/success`,\n })\n res.end()\n server.close()\n resolveToken(token)\n })\n\n server.on('listening', function onCallbackListen() {\n // Once the server is successfully listening on a port, we can return the promise.\n // We'll then await the _token promise_, while the server is running in the background.\n const callbackUrl = getCallbackUrl(server)\n const loginUrl = getLoginUrl(providerUrl, callbackUrl)\n resolve({loginUrl, server, token: tokenPromise})\n })\n\n server.on('error', function onCallbackServerError(err) {\n // The auth backend accepts any localhost port in the callback origin, so if\n // the preferred port is busy we fall back to an OS-assigned ephemeral port.\n // Port 0 cannot itself be \"in use\" - an EADDRINUSE there means something is\n // genuinely wrong, so only fall back once.\n if ('code' in err && err.code === 'EADDRINUSE' && callbackPort !== 0) {\n debug('Port %d busy, falling back to OS-assigned port', callbackPort)\n callbackPort = 0\n server.listen(callbackPort)\n } else {\n reject(err)\n }\n })\n\n debug('Starting callback server on port %d', callbackPort)\n server.listen(callbackPort)\n })\n}\n\n/**\n * Get the login URL to send the user to for the given auth provider.\n *\n * The generated URL will include a label for the session that includes the\n * hostname and platform of the current computer, to help identify the session.\n *\n * @param providerUrl - The URL of the login provider\n * @param callbackUrl - The callback URL for the local auth token server\n * @returns The login URL\n * @internal\n */\nfunction getLoginUrl(providerUrl: string, callbackUrl: URL): URL {\n // Build a login URL that redirects back back to OAuth flow on success\n const loginUrl = new URL(providerUrl)\n\n // Prefer `MacOS` over `darwin` etc\n const platformName = os.platform()\n const platform = platformName in platformNames ? platformNames[platformName] : platformName\n\n // Prefer `espens-macbook` over `espens-macbook.local`\n const hostname = os.hostname().replaceAll(/\\.(local|lan)$/g, '')\n\n loginUrl.searchParams.set('type', 'token')\n loginUrl.searchParams.set('label', `${hostname} / ${platform}`)\n loginUrl.searchParams.set('origin', callbackUrl.href)\n\n return loginUrl\n}\n\nfunction getCallbackUrl(server: Server): URL {\n const serverUrl = server.address()\n if (!serverUrl || typeof serverUrl === 'string') {\n // Note: `serverUrl` is string only when binding to unix sockets,\n // thus we can safely assume Something Is Wrong™ if it's a string\n throw new Error('Failed to start auth callback server')\n }\n\n return new URL(callbackEndpoint, `http://localhost:${serverUrl.port}`)\n}\n"],"names":["createServer","os","getSanityUrl","subdebug","getTokenDetails","debug","defaultCallbackPort","callbackEndpoint","getPreferredCallbackPort","override","process","env","SANITY_CLI_CALLBACK_PORT","port","Number","parseInt","trim","isNaN","Error","platformNames","aix","android","darwin","freebsd","linux","openbsd","sunos","win32","startServerForTokenCallback","providerUrl","sanityUrl","resolveToken","rejectToken","tokenPromise","Promise","resolve","reject","callbackPort","server","onCallbackServerRequest","req","res","failLoginRequest","code","writeHead","Connection","Location","end","close","url","URL","pathname","write","absoluteTokenUrl","searchParams","get","tokenUrl","has","token","search","err","on","onCallbackListen","callbackUrl","getCallbackUrl","loginUrl","getLoginUrl","onCallbackServerError","listen","platformName","platform","hostname","replaceAll","set","href","serverUrl","address"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/auth/authServer.ts"],"sourcesContent":["import {createServer, type Server} from 'node:http'\nimport os from 'node:os'\n\nimport {getSanityUrl, subdebug} from '@sanity/cli-core'\n\nimport {getTokenDetails} from '../../services/auth.js'\nimport {type TokenDetails} from './types.js'\n\nconst debug = subdebug('auth')\nconst defaultCallbackPort = 0\nconst callbackEndpoint = '/callback'\n\n/**\n * Get the port to first attempt binding the auth callback server to.\n *\n * The auth backend accepts any `http://localhost:<port>` origin for token\n * callbacks, so by default we let the OS assign an available ephemeral port.\n *\n * The `SANITY_CLI_CALLBACK_PORT` environment variable overrides the preferred\n * port (`0` for an OS-assigned port). The override exists primarily for tests,\n * where OS-assigned ports prevent collisions between tests running in parallel.\n *\n * @returns Port number to attempt first (`0` for an OS-assigned port)\n * @internal\n */\nfunction getPreferredCallbackPort(): number {\n const override = process.env.SANITY_CLI_CALLBACK_PORT\n if (!override) {\n return defaultCallbackPort\n }\n\n debug('Using callback port from SANITY_CLI_CALLBACK_PORT: %s', override)\n\n const port = Number.parseInt(override.trim(), 10)\n if (Number.isNaN(port) || port < 0 || port > 65_535) {\n throw new Error(`Invalid SANITY_CLI_CALLBACK_PORT value: \"${override}\"`)\n }\n\n return port\n}\n\nconst platformNames: Record<string, string | undefined> = {\n aix: 'AIX',\n android: 'Android',\n darwin: 'MacOS',\n freebsd: 'FreeBSD',\n linux: 'Linux',\n openbsd: 'OpenBSD',\n sunos: 'SunOS',\n win32: 'Windows',\n}\n\n/**\n * Start a local HTTP server and wait for a request to the auth callback endpoint.\n * This happens by the user being sent to a login page with a callback URL that points to\n * this local server. This request includes a short-lived \"SID\" (session ID) that we then\n * do a request to the `/auth/fetch` endpoint with to get the actual auth token,\n * invalidating the SID in the process.\n *\n * If an explicitly configured port is busy, we retry with an OS-assigned port.\n *\n * @param providerUrl - The URL of the login provider\n * @returns Resolves with HTTP server instance, a login URL to send user to, and a `token` promise\n * @internal\n */\nexport function startServerForTokenCallback(\n providerUrl: string,\n): Promise<{loginUrl: URL; server: Server; token: Promise<TokenDetails>}> {\n const sanityUrl = getSanityUrl()\n\n // note: replace with `Promise.withResolvers()` when minimum Node.js is 22+\n let resolveToken: (resolvedToken: PromiseLike<TokenDetails> | TokenDetails) => void\n let rejectToken: (reason: Error) => void\n const tokenPromise = new Promise<TokenDetails>((resolve, reject) => {\n resolveToken = resolve\n rejectToken = reject\n })\n\n return new Promise((resolve, reject) => {\n let callbackPort = getPreferredCallbackPort()\n\n const server = createServer(async function onCallbackServerRequest(req, res) {\n function failLoginRequest(code = '') {\n res.writeHead(303, 'See Other', {\n Connection: 'close',\n Location: `${sanityUrl}/login/error${code ? `?error=${code}` : ''}`,\n })\n res.end()\n server.close()\n }\n\n const url = new URL(req.url || '/', `http://localhost:${callbackPort}`)\n if (url.pathname !== callbackEndpoint) {\n res.writeHead(404, 'Not Found', {Connection: 'close', 'Content-Type': 'text/plain'})\n res.write('404 Not Found')\n res.end()\n return\n }\n\n const absoluteTokenUrl = url.searchParams.get('url')\n if (!absoluteTokenUrl) {\n failLoginRequest()\n rejectToken(new Error('Missing callback URL'))\n return\n }\n\n const tokenUrl = new URL(absoluteTokenUrl)\n if (!tokenUrl.searchParams.has('sid')) {\n failLoginRequest('NO_SESSION_ID')\n rejectToken(new Error('Missing session ID in callback'))\n return\n }\n\n let token: TokenDetails\n try {\n token = await getTokenDetails(tokenUrl.search)\n } catch (err) {\n failLoginRequest('UNRESOLVED_SESSION')\n rejectToken(err instanceof Error ? err : new Error(`Unknown error: ${err}`))\n return\n }\n\n res.writeHead(303, 'See Other', {\n Connection: 'close',\n Location: `${sanityUrl}/login/success`,\n })\n res.end()\n server.close()\n resolveToken(token)\n })\n\n server.on('listening', function onCallbackListen() {\n // Once the server is successfully listening on a port, we can return the promise.\n // We'll then await the _token promise_, while the server is running in the background.\n const callbackUrl = getCallbackUrl(server)\n const loginUrl = getLoginUrl(providerUrl, callbackUrl)\n resolve({loginUrl, server, token: tokenPromise})\n })\n\n server.on('error', function onCallbackServerError(err) {\n // The auth backend accepts any localhost port in the callback origin, so if\n // an explicitly configured port is busy we fall back to an OS-assigned ephemeral port.\n // Port 0 cannot itself be \"in use\" - an EADDRINUSE there means something is\n // genuinely wrong, so only fall back once.\n if ('code' in err && err.code === 'EADDRINUSE' && callbackPort !== 0) {\n debug('Port %d busy, falling back to OS-assigned port', callbackPort)\n callbackPort = 0\n server.listen(callbackPort)\n } else {\n reject(err)\n }\n })\n\n debug('Starting callback server on port %d', callbackPort)\n server.listen(callbackPort)\n })\n}\n\n/**\n * Get the login URL to send the user to for the given auth provider.\n *\n * The generated URL will include a label for the session that includes the\n * hostname and platform of the current computer, to help identify the session.\n *\n * @param providerUrl - The URL of the login provider\n * @param callbackUrl - The callback URL for the local auth token server\n * @returns The login URL\n * @internal\n */\nfunction getLoginUrl(providerUrl: string, callbackUrl: URL): URL {\n // Build a login URL that redirects back back to OAuth flow on success\n const loginUrl = new URL(providerUrl)\n\n // Prefer `MacOS` over `darwin` etc\n const platformName = os.platform()\n const platform = platformName in platformNames ? platformNames[platformName] : platformName\n\n // Prefer `espens-macbook` over `espens-macbook.local`\n const hostname = os.hostname().replaceAll(/\\.(local|lan)$/g, '')\n\n loginUrl.searchParams.set('type', 'token')\n loginUrl.searchParams.set('label', `${hostname} / ${platform}`)\n loginUrl.searchParams.set('origin', callbackUrl.href)\n\n return loginUrl\n}\n\nfunction getCallbackUrl(server: Server): URL {\n const serverUrl = server.address()\n if (!serverUrl || typeof serverUrl === 'string') {\n // Note: `serverUrl` is string only when binding to unix sockets,\n // thus we can safely assume Something Is Wrong™ if it's a string\n throw new Error('Failed to start auth callback server')\n }\n\n return new URL(callbackEndpoint, `http://localhost:${serverUrl.port}`)\n}\n"],"names":["createServer","os","getSanityUrl","subdebug","getTokenDetails","debug","defaultCallbackPort","callbackEndpoint","getPreferredCallbackPort","override","process","env","SANITY_CLI_CALLBACK_PORT","port","Number","parseInt","trim","isNaN","Error","platformNames","aix","android","darwin","freebsd","linux","openbsd","sunos","win32","startServerForTokenCallback","providerUrl","sanityUrl","resolveToken","rejectToken","tokenPromise","Promise","resolve","reject","callbackPort","server","onCallbackServerRequest","req","res","failLoginRequest","code","writeHead","Connection","Location","end","close","url","URL","pathname","write","absoluteTokenUrl","searchParams","get","tokenUrl","has","token","search","err","on","onCallbackListen","callbackUrl","getCallbackUrl","loginUrl","getLoginUrl","onCallbackServerError","listen","platformName","platform","hostname","replaceAll","set","href","serverUrl","address"],"mappings":"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"}
@@ -10,7 +10,7 @@ const debug = subdebug('login:getProvider');
10
10
  * @param options - Options for the provider resolve operation
11
11
  * @returns Promise that resolves to the selected login provider
12
12
  * @internal
13
- */ export async function getProvider({ experimental, orgSlug, specifiedProvider, ssoProvider }) {
13
+ */ export async function getProvider({ orgSlug, specifiedProvider, ssoProvider }) {
14
14
  if (specifiedProvider === 'vercel') {
15
15
  return {
16
16
  name: 'vercel',
@@ -25,19 +25,8 @@ const debug = subdebug('login:getProvider');
25
25
  }
26
26
  spin = spinner('Fetching providers...').start();
27
27
  // Fetch and prompt for login provider to use
28
- let { providers } = await getProviders();
29
- if (experimental) {
30
- providers = [
31
- ...providers,
32
- {
33
- name: 'sso',
34
- title: 'SSO',
35
- url: '_not_used_'
36
- }
37
- ];
38
- }
28
+ const { providers } = await getProviders();
39
29
  spin.stop();
40
- // Real providers excludes the synthetic SSO entry used by --experimental
41
30
  const realProviderNames = providers.filter((p)=>p.name !== 'sso').map((p)=>p.name);
42
31
  if (specifiedProvider) {
43
32
  const provider = providers.find((prov)=>prov.name === specifiedProvider);
@@ -57,7 +46,14 @@ const debug = subdebug('login:getProvider');
57
46
  throw new Error(`Multiple login providers available: ${realProviderNames.join(', ')}. ` + 'Use `--provider <name>` to select one in unattended mode.');
58
47
  }
59
48
  }
60
- const provider = await promptForProviders(providers);
49
+ const provider = await promptForProviders([
50
+ ...providers,
51
+ {
52
+ name: 'sso',
53
+ title: 'SSO',
54
+ url: '_not_used_'
55
+ }
56
+ ]);
61
57
  if (provider.name === 'sso') {
62
58
  const orgSlug = await input({
63
59
  message: 'Organization slug:'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/actions/auth/login/getProvider.ts"],"sourcesContent":["import {isInteractive, subdebug} from '@sanity/cli-core'\nimport {input, spinner, type SpinnerInstance} from '@sanity/cli-core/ux'\n\nimport {promptForProviders} from '../../../prompts/promptForProviders.js'\nimport {getProviders, getVercelProviderUrl} from '../../../services/auth.js'\nimport {type LoginProvider} from '../types.js'\nimport {getSSOProvider} from './getSSOProvider.js'\n\nconst debug = subdebug('login:getProvider')\n\n/**\n * Prompt the user to select a login provider, or use the specified provider if given.\n *\n * @param options - Options for the provider resolve operation\n * @returns Promise that resolves to the selected login provider\n * @internal\n */\nexport async function getProvider({\n experimental,\n orgSlug,\n specifiedProvider,\n ssoProvider,\n}: {\n experimental: boolean | undefined\n orgSlug: string | undefined\n specifiedProvider: string | undefined\n ssoProvider: string | undefined\n}): Promise<LoginProvider | undefined> {\n if (specifiedProvider === 'vercel') {\n return {\n name: 'vercel',\n title: 'Vercel',\n url: await getVercelProviderUrl(),\n }\n }\n\n let spin: SpinnerInstance | undefined\n\n try {\n if (orgSlug) {\n return getSSOProvider(orgSlug, ssoProvider)\n }\n\n spin = spinner('Fetching providers...').start()\n // Fetch and prompt for login provider to use\n let {providers} = await getProviders()\n if (experimental) {\n providers = [...providers, {name: 'sso', title: 'SSO', url: '_not_used_'}]\n }\n spin.stop()\n\n // Real providers excludes the synthetic SSO entry used by --experimental\n const realProviderNames = providers.filter((p) => p.name !== 'sso').map((p) => p.name)\n\n if (specifiedProvider) {\n const provider = providers.find((prov) => prov.name === specifiedProvider)\n if (!provider) {\n throw new Error(\n `Cannot find login provider with name \"${specifiedProvider}\". ` +\n (realProviderNames.length > 0\n ? `Available providers: ${realProviderNames.join(', ')}`\n : 'No providers are available'),\n )\n }\n return provider\n }\n\n if (providers.length === 0) {\n return undefined\n }\n\n if (!isInteractive()) {\n if (realProviderNames.length === 1) {\n return providers.find((provider) => provider.name === realProviderNames[0])\n }\n\n if (realProviderNames.length > 1) {\n throw new Error(\n `Multiple login providers available: ${realProviderNames.join(', ')}. ` +\n 'Use `--provider <name>` to select one in unattended mode.',\n )\n }\n }\n\n const provider = await promptForProviders(providers)\n if (provider.name === 'sso') {\n const orgSlug = await input({message: 'Organization slug:'})\n return getSSOProvider(orgSlug)\n }\n\n return provider\n } catch (err) {\n spin?.stop()\n debug('Error retrieving providers', err)\n throw err\n }\n}\n"],"names":["isInteractive","subdebug","input","spinner","promptForProviders","getProviders","getVercelProviderUrl","getSSOProvider","debug","getProvider","experimental","orgSlug","specifiedProvider","ssoProvider","name","title","url","spin","start","providers","stop","realProviderNames","filter","p","map","provider","find","prov","Error","length","join","undefined","message","err"],"mappings":"AAAA,SAAQA,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACxD,SAAQC,KAAK,EAAEC,OAAO,QAA6B,sBAAqB;AAExE,SAAQC,kBAAkB,QAAO,yCAAwC;AACzE,SAAQC,YAAY,EAAEC,oBAAoB,QAAO,4BAA2B;AAE5E,SAAQC,cAAc,QAAO,sBAAqB;AAElD,MAAMC,QAAQP,SAAS;AAEvB;;;;;;CAMC,GACD,OAAO,eAAeQ,YAAY,EAChCC,YAAY,EACZC,OAAO,EACPC,iBAAiB,EACjBC,WAAW,EAMZ;IACC,IAAID,sBAAsB,UAAU;QAClC,OAAO;YACLE,MAAM;YACNC,OAAO;YACPC,KAAK,MAAMV;QACb;IACF;IAEA,IAAIW;IAEJ,IAAI;QACF,IAAIN,SAAS;YACX,OAAOJ,eAAeI,SAASE;QACjC;QAEAI,OAAOd,QAAQ,yBAAyBe,KAAK;QAC7C,6CAA6C;QAC7C,IAAI,EAACC,SAAS,EAAC,GAAG,MAAMd;QACxB,IAAIK,cAAc;YAChBS,YAAY;mBAAIA;gBAAW;oBAACL,MAAM;oBAAOC,OAAO;oBAAOC,KAAK;gBAAY;aAAE;QAC5E;QACAC,KAAKG,IAAI;QAET,yEAAyE;QACzE,MAAMC,oBAAoBF,UAAUG,MAAM,CAAC,CAACC,IAAMA,EAAET,IAAI,KAAK,OAAOU,GAAG,CAAC,CAACD,IAAMA,EAAET,IAAI;QAErF,IAAIF,mBAAmB;YACrB,MAAMa,WAAWN,UAAUO,IAAI,CAAC,CAACC,OAASA,KAAKb,IAAI,KAAKF;YACxD,IAAI,CAACa,UAAU;gBACb,MAAM,IAAIG,MACR,CAAC,sCAAsC,EAAEhB,kBAAkB,GAAG,CAAC,GAC5DS,CAAAA,kBAAkBQ,MAAM,GAAG,IACxB,CAAC,qBAAqB,EAAER,kBAAkBS,IAAI,CAAC,OAAO,GACtD,4BAA2B;YAErC;YACA,OAAOL;QACT;QAEA,IAAIN,UAAUU,MAAM,KAAK,GAAG;YAC1B,OAAOE;QACT;QAEA,IAAI,CAAC/B,iBAAiB;YACpB,IAAIqB,kBAAkBQ,MAAM,KAAK,GAAG;gBAClC,OAAOV,UAAUO,IAAI,CAAC,CAACD,WAAaA,SAASX,IAAI,KAAKO,iBAAiB,CAAC,EAAE;YAC5E;YAEA,IAAIA,kBAAkBQ,MAAM,GAAG,GAAG;gBAChC,MAAM,IAAID,MACR,CAAC,oCAAoC,EAAEP,kBAAkBS,IAAI,CAAC,MAAM,EAAE,CAAC,GACrE;YAEN;QACF;QAEA,MAAML,WAAW,MAAMrB,mBAAmBe;QAC1C,IAAIM,SAASX,IAAI,KAAK,OAAO;YAC3B,MAAMH,UAAU,MAAMT,MAAM;gBAAC8B,SAAS;YAAoB;YAC1D,OAAOzB,eAAeI;QACxB;QAEA,OAAOc;IACT,EAAE,OAAOQ,KAAK;QACZhB,MAAMG;QACNZ,MAAM,8BAA8ByB;QACpC,MAAMA;IACR;AACF"}
1
+ {"version":3,"sources":["../../../../src/actions/auth/login/getProvider.ts"],"sourcesContent":["import {isInteractive, subdebug} from '@sanity/cli-core'\nimport {input, spinner, type SpinnerInstance} from '@sanity/cli-core/ux'\n\nimport {promptForProviders} from '../../../prompts/promptForProviders.js'\nimport {getProviders, getVercelProviderUrl} from '../../../services/auth.js'\nimport {type LoginProvider} from '../types.js'\nimport {getSSOProvider} from './getSSOProvider.js'\n\nconst debug = subdebug('login:getProvider')\n\n/**\n * Prompt the user to select a login provider, or use the specified provider if given.\n *\n * @param options - Options for the provider resolve operation\n * @returns Promise that resolves to the selected login provider\n * @internal\n */\nexport async function getProvider({\n orgSlug,\n specifiedProvider,\n ssoProvider,\n}: {\n orgSlug: string | undefined\n specifiedProvider: string | undefined\n ssoProvider: string | undefined\n}): Promise<LoginProvider | undefined> {\n if (specifiedProvider === 'vercel') {\n return {\n name: 'vercel',\n title: 'Vercel',\n url: await getVercelProviderUrl(),\n }\n }\n\n let spin: SpinnerInstance | undefined\n\n try {\n if (orgSlug) {\n return getSSOProvider(orgSlug, ssoProvider)\n }\n\n spin = spinner('Fetching providers...').start()\n // Fetch and prompt for login provider to use\n const {providers} = await getProviders()\n spin.stop()\n\n const realProviderNames = providers.filter((p) => p.name !== 'sso').map((p) => p.name)\n\n if (specifiedProvider) {\n const provider = providers.find((prov) => prov.name === specifiedProvider)\n if (!provider) {\n throw new Error(\n `Cannot find login provider with name \"${specifiedProvider}\". ` +\n (realProviderNames.length > 0\n ? `Available providers: ${realProviderNames.join(', ')}`\n : 'No providers are available'),\n )\n }\n return provider\n }\n\n if (providers.length === 0) {\n return undefined\n }\n\n if (!isInteractive()) {\n if (realProviderNames.length === 1) {\n return providers.find((provider) => provider.name === realProviderNames[0])\n }\n\n if (realProviderNames.length > 1) {\n throw new Error(\n `Multiple login providers available: ${realProviderNames.join(', ')}. ` +\n 'Use `--provider <name>` to select one in unattended mode.',\n )\n }\n }\n\n const provider = await promptForProviders([\n ...providers,\n {name: 'sso', title: 'SSO', url: '_not_used_'},\n ])\n if (provider.name === 'sso') {\n const orgSlug = await input({message: 'Organization slug:'})\n return getSSOProvider(orgSlug)\n }\n\n return provider\n } catch (err) {\n spin?.stop()\n debug('Error retrieving providers', err)\n throw err\n }\n}\n"],"names":["isInteractive","subdebug","input","spinner","promptForProviders","getProviders","getVercelProviderUrl","getSSOProvider","debug","getProvider","orgSlug","specifiedProvider","ssoProvider","name","title","url","spin","start","providers","stop","realProviderNames","filter","p","map","provider","find","prov","Error","length","join","undefined","message","err"],"mappings":"AAAA,SAAQA,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACxD,SAAQC,KAAK,EAAEC,OAAO,QAA6B,sBAAqB;AAExE,SAAQC,kBAAkB,QAAO,yCAAwC;AACzE,SAAQC,YAAY,EAAEC,oBAAoB,QAAO,4BAA2B;AAE5E,SAAQC,cAAc,QAAO,sBAAqB;AAElD,MAAMC,QAAQP,SAAS;AAEvB;;;;;;CAMC,GACD,OAAO,eAAeQ,YAAY,EAChCC,OAAO,EACPC,iBAAiB,EACjBC,WAAW,EAKZ;IACC,IAAID,sBAAsB,UAAU;QAClC,OAAO;YACLE,MAAM;YACNC,OAAO;YACPC,KAAK,MAAMT;QACb;IACF;IAEA,IAAIU;IAEJ,IAAI;QACF,IAAIN,SAAS;YACX,OAAOH,eAAeG,SAASE;QACjC;QAEAI,OAAOb,QAAQ,yBAAyBc,KAAK;QAC7C,6CAA6C;QAC7C,MAAM,EAACC,SAAS,EAAC,GAAG,MAAMb;QAC1BW,KAAKG,IAAI;QAET,MAAMC,oBAAoBF,UAAUG,MAAM,CAAC,CAACC,IAAMA,EAAET,IAAI,KAAK,OAAOU,GAAG,CAAC,CAACD,IAAMA,EAAET,IAAI;QAErF,IAAIF,mBAAmB;YACrB,MAAMa,WAAWN,UAAUO,IAAI,CAAC,CAACC,OAASA,KAAKb,IAAI,KAAKF;YACxD,IAAI,CAACa,UAAU;gBACb,MAAM,IAAIG,MACR,CAAC,sCAAsC,EAAEhB,kBAAkB,GAAG,CAAC,GAC5DS,CAAAA,kBAAkBQ,MAAM,GAAG,IACxB,CAAC,qBAAqB,EAAER,kBAAkBS,IAAI,CAAC,OAAO,GACtD,4BAA2B;YAErC;YACA,OAAOL;QACT;QAEA,IAAIN,UAAUU,MAAM,KAAK,GAAG;YAC1B,OAAOE;QACT;QAEA,IAAI,CAAC9B,iBAAiB;YACpB,IAAIoB,kBAAkBQ,MAAM,KAAK,GAAG;gBAClC,OAAOV,UAAUO,IAAI,CAAC,CAACD,WAAaA,SAASX,IAAI,KAAKO,iBAAiB,CAAC,EAAE;YAC5E;YAEA,IAAIA,kBAAkBQ,MAAM,GAAG,GAAG;gBAChC,MAAM,IAAID,MACR,CAAC,oCAAoC,EAAEP,kBAAkBS,IAAI,CAAC,MAAM,EAAE,CAAC,GACrE;YAEN;QACF;QAEA,MAAML,WAAW,MAAMpB,mBAAmB;eACrCc;YACH;gBAACL,MAAM;gBAAOC,OAAO;gBAAOC,KAAK;YAAY;SAC9C;QACD,IAAIS,SAASX,IAAI,KAAK,OAAO;YAC3B,MAAMH,UAAU,MAAMR,MAAM;gBAAC6B,SAAS;YAAoB;YAC1D,OAAOxB,eAAeG;QACxB;QAEA,OAAOc;IACT,EAAE,OAAOQ,KAAK;QACZhB,MAAMG;QACNX,MAAM,8BAA8BwB;QACpC,MAAMA;IACR;AACF"}
@@ -36,7 +36,6 @@ const debug = subdebug('login');
36
36
  return;
37
37
  }
38
38
  const provider = await getProvider({
39
- experimental: options.experimental,
40
39
  orgSlug: options.sso,
41
40
  specifiedProvider: options.provider,
42
41
  ssoProvider: options.ssoProvider
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/actions/auth/login/login.ts"],"sourcesContent":["import {\n type CLITelemetryStore,\n getCliToken,\n getUserConfig,\n type Output,\n setCliUserConfig,\n subdebug,\n} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {isHttpError} from '@sanity/client'\nimport open from 'open'\n\nimport {logout} from '../../../services/auth.js'\nimport {LoginTrace} from '../../../telemetry/login.telemetry.js'\nimport {canLaunchBrowser} from '../../../util/canLaunchBrowser.js'\nimport {startServerForTokenCallback} from '../authServer.js'\nimport {getProvider} from './getProvider.js'\nimport {isSanityApiToken, validateToken} from './validateToken.js'\n\nconst debug = subdebug('login')\n\ninterface LoginOptions {\n output: Output\n\n telemetry: CLITelemetryStore\n\n experimental?: boolean\n open?: boolean\n provider?: string\n sso?: string\n ssoProvider?: string\n token?: string\n}\n\n/**\n * Trigger the authentication flow for the CLI.\n *\n * NOTE: Without a token option, this uses terminal prompts and will not work for\n * non-interactive/programmatic uses.\n *\n * @param options - Options for the login operation\n * @returns Promise that resolves when the login operation is complete\n * @throws Will throw if login fails or is cancelled\n * @internal\n */\nexport async function login(options: LoginOptions) {\n const {output, telemetry} = options\n const previousToken = await getCliToken()\n\n const trace = telemetry.trace(LoginTrace)\n trace.start()\n\n if (typeof options.token === 'string') {\n try {\n const authToken = await validateToken(options.token)\n await storeAuthToken(authToken, previousToken, output)\n trace.complete()\n } catch (err: unknown) {\n trace.error(err as Error)\n throw err\n }\n return\n }\n\n const provider = await getProvider({\n experimental: options.experimental,\n orgSlug: options.sso,\n specifiedProvider: options.provider,\n ssoProvider: options.ssoProvider,\n })\n\n trace.log({provider: provider?.name, step: 'selectProvider'})\n\n if (provider === undefined) {\n throw new Error('No authentication providers found')\n }\n\n const {loginUrl, server, token: tokenPromise} = await startServerForTokenCallback(provider.url)\n\n trace.log({step: 'waitForToken'})\n\n // Open a browser on the login page (or tell the user to)\n const shouldLaunchBrowser = canLaunchBrowser() && options.open !== false\n const actionText = shouldLaunchBrowser ? 'Opening browser at' : 'Please open a browser at'\n\n output.log(`\\n${actionText} ${loginUrl.href}\\n`)\n\n const spin = spinner({\n text: 'Waiting for browser login to complete... Press Ctrl + C to cancel',\n }).start()\n\n if (shouldLaunchBrowser) {\n open(loginUrl.href)\n }\n\n // Wait for a success/error on the HTTP callback server\n let authToken: string\n try {\n authToken = (await tokenPromise).token\n spin.stop()\n } catch (err: unknown) {\n spin.stop()\n trace.error(err as Error)\n debug('Error retrieving token: %O', err)\n throw err\n } finally {\n await new Promise<void>((resolve) => {\n server.close(() => resolve())\n })\n }\n\n await storeAuthToken(authToken, previousToken, output)\n\n trace.complete()\n}\n\nasync function storeAuthToken(\n authToken: string,\n previousToken: string | undefined,\n output: Output,\n) {\n setCliUserConfig('authToken', authToken)\n getUserConfig().delete('telemetryConsent')\n\n // If we had a session previously, attempt to clear it\n if (previousToken && previousToken !== authToken) {\n await invalidateAuthToken(previousToken, output)\n }\n}\n\nasync function invalidateAuthToken(token: string, output: Output) {\n try {\n if (await isSanityApiToken(token)) return\n } catch (err) {\n if (isHttpError(err) && err.statusCode === 401) return\n }\n\n try {\n await logout(token)\n } catch (err) {\n if (!isHttpError(err) || err.statusCode !== 401) {\n output.warn('Failed to invalidate previous session')\n }\n }\n}\n"],"names":["getCliToken","getUserConfig","setCliUserConfig","subdebug","spinner","isHttpError","open","logout","LoginTrace","canLaunchBrowser","startServerForTokenCallback","getProvider","isSanityApiToken","validateToken","debug","login","options","output","telemetry","previousToken","trace","start","token","authToken","storeAuthToken","complete","err","error","provider","experimental","orgSlug","sso","specifiedProvider","ssoProvider","log","name","step","undefined","Error","loginUrl","server","tokenPromise","url","shouldLaunchBrowser","actionText","href","spin","text","stop","Promise","resolve","close","delete","invalidateAuthToken","statusCode","warn"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/actions/auth/login/login.ts"],"sourcesContent":["import {\n type CLITelemetryStore,\n getCliToken,\n getUserConfig,\n type Output,\n setCliUserConfig,\n subdebug,\n} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {isHttpError} from '@sanity/client'\nimport open from 'open'\n\nimport {logout} from '../../../services/auth.js'\nimport {LoginTrace} from '../../../telemetry/login.telemetry.js'\nimport {canLaunchBrowser} from '../../../util/canLaunchBrowser.js'\nimport {startServerForTokenCallback} from '../authServer.js'\nimport {getProvider} from './getProvider.js'\nimport {isSanityApiToken, validateToken} from './validateToken.js'\n\nconst debug = subdebug('login')\n\ninterface LoginOptions {\n output: Output\n\n telemetry: CLITelemetryStore\n\n experimental?: boolean\n open?: boolean\n provider?: string\n sso?: string\n ssoProvider?: string\n token?: string\n}\n\n/**\n * Trigger the authentication flow for the CLI.\n *\n * NOTE: Without a token option, this uses terminal prompts and will not work for\n * non-interactive/programmatic uses.\n *\n * @param options - Options for the login operation\n * @returns Promise that resolves when the login operation is complete\n * @throws Will throw if login fails or is cancelled\n * @internal\n */\nexport async function login(options: LoginOptions) {\n const {output, telemetry} = options\n const previousToken = await getCliToken()\n\n const trace = telemetry.trace(LoginTrace)\n trace.start()\n\n if (typeof options.token === 'string') {\n try {\n const authToken = await validateToken(options.token)\n await storeAuthToken(authToken, previousToken, output)\n trace.complete()\n } catch (err: unknown) {\n trace.error(err as Error)\n throw err\n }\n return\n }\n\n const provider = await getProvider({\n orgSlug: options.sso,\n specifiedProvider: options.provider,\n ssoProvider: options.ssoProvider,\n })\n\n trace.log({provider: provider?.name, step: 'selectProvider'})\n\n if (provider === undefined) {\n throw new Error('No authentication providers found')\n }\n\n const {loginUrl, server, token: tokenPromise} = await startServerForTokenCallback(provider.url)\n\n trace.log({step: 'waitForToken'})\n\n // Open a browser on the login page (or tell the user to)\n const shouldLaunchBrowser = canLaunchBrowser() && options.open !== false\n const actionText = shouldLaunchBrowser ? 'Opening browser at' : 'Please open a browser at'\n\n output.log(`\\n${actionText} ${loginUrl.href}\\n`)\n\n const spin = spinner({\n text: 'Waiting for browser login to complete... Press Ctrl + C to cancel',\n }).start()\n\n if (shouldLaunchBrowser) {\n open(loginUrl.href)\n }\n\n // Wait for a success/error on the HTTP callback server\n let authToken: string\n try {\n authToken = (await tokenPromise).token\n spin.stop()\n } catch (err: unknown) {\n spin.stop()\n trace.error(err as Error)\n debug('Error retrieving token: %O', err)\n throw err\n } finally {\n await new Promise<void>((resolve) => {\n server.close(() => resolve())\n })\n }\n\n await storeAuthToken(authToken, previousToken, output)\n\n trace.complete()\n}\n\nasync function storeAuthToken(\n authToken: string,\n previousToken: string | undefined,\n output: Output,\n) {\n setCliUserConfig('authToken', authToken)\n getUserConfig().delete('telemetryConsent')\n\n // If we had a session previously, attempt to clear it\n if (previousToken && previousToken !== authToken) {\n await invalidateAuthToken(previousToken, output)\n }\n}\n\nasync function invalidateAuthToken(token: string, output: Output) {\n try {\n if (await isSanityApiToken(token)) return\n } catch (err) {\n if (isHttpError(err) && err.statusCode === 401) return\n }\n\n try {\n await logout(token)\n } catch (err) {\n if (!isHttpError(err) || err.statusCode !== 401) {\n output.warn('Failed to invalidate previous session')\n }\n }\n}\n"],"names":["getCliToken","getUserConfig","setCliUserConfig","subdebug","spinner","isHttpError","open","logout","LoginTrace","canLaunchBrowser","startServerForTokenCallback","getProvider","isSanityApiToken","validateToken","debug","login","options","output","telemetry","previousToken","trace","start","token","authToken","storeAuthToken","complete","err","error","provider","orgSlug","sso","specifiedProvider","ssoProvider","log","name","step","undefined","Error","loginUrl","server","tokenPromise","url","shouldLaunchBrowser","actionText","href","spin","text","stop","Promise","resolve","close","delete","invalidateAuthToken","statusCode","warn"],"mappings":"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"}
@@ -320,6 +320,7 @@ export function deployApp(options) {
320
320
  applicationId: application.id,
321
321
  cliConfig,
322
322
  created: appCreated,
323
+ createRequested: flags.create,
323
324
  organizationId: application.organizationId,
324
325
  output,
325
326
  title: application.title
@@ -346,6 +347,7 @@ export function deployApp(options) {
346
347
  if (dryRun) {
347
348
  await checkAppTarget(reporter, {
348
349
  appId: getAppId(cliConfig),
350
+ create: flags.create,
349
351
  organizationId,
350
352
  title
351
353
  });
@@ -356,6 +358,7 @@ export function deployApp(options) {
356
358
  }
357
359
  let application = await findUserApplication({
358
360
  cliConfig,
361
+ create: flags.create,
359
362
  organizationId,
360
363
  output,
361
364
  title,
@@ -440,14 +443,14 @@ async function shipAppDeployment({ application, isAutoUpdating, manifest, source
440
443
  }
441
444
  spin.succeed();
442
445
  }
443
- export function logAppDeployed({ applicationId, cliConfig, created, organizationId, output, title, url = getCoreAppUrl(organizationId, applicationId) }) {
446
+ export function logAppDeployed({ applicationId, cliConfig, created, createRequested = false, organizationId, output, title, url = getCoreAppUrl(organizationId, applicationId) }) {
444
447
  const named = title ? ` — "${title}"` : '';
445
448
  output.log(`\nSuccess! Application deployed to ${styleText('cyan', url)}${named}`);
446
449
  output.log(created ? 'Created a new application.' : 'Updated the existing application.');
447
450
  if (getAppId(cliConfig)) return;
448
451
  output.log(`\n════ ${styleText('bold', 'Next step:')} ════`);
449
452
  if (created) {
450
- output.log(styleText('yellow', '\nDeploying again without `deployment.appId` creates another new application.'));
453
+ output.log(styleText('yellow', createRequested ? '\nSave `deployment.appId` and omit --create on later deploys to update this application.' : '\nSave `deployment.appId` to update this application on later deploys.'));
451
454
  }
452
455
  output.log(styleText('bold', '\nAdd the deployment.appId to your sanity.cli.js or sanity.cli.ts file:'));
453
456
  output.log(`