@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.
- package/README.md +69 -26
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
- package/dist/actions/assets/withUrlIngestProgress.js +45 -0
- package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
- package/dist/actions/auth/authServer.js +5 -8
- package/dist/actions/auth/authServer.js.map +1 -1
- package/dist/actions/auth/login/getProvider.js +10 -14
- package/dist/actions/auth/login/getProvider.js.map +1 -1
- package/dist/actions/auth/login/login.js +0 -1
- package/dist/actions/auth/login/login.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +5 -2
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +3 -2
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/findUserApplication.js +3 -2
- package/dist/actions/deploy/findUserApplication.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +6 -1
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/documents/validateDocuments.worker.js +1 -4
- package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
- package/dist/actions/media/importMedia.js.map +1 -1
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
- package/dist/commands/assets/upload.js +78 -52
- package/dist/commands/assets/upload.js.map +1 -1
- package/dist/commands/deploy.js +33 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/import.js +149 -13
- package/dist/commands/media/import.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +8 -1
- package/dist/exports/invokeSanityCli/help.js.map +1 -1
- package/dist/exports/invokeSanityCli/index.d.ts +1 -2
- package/dist/exports/invokeSanityCli/index.js +37 -15
- package/dist/exports/invokeSanityCli/index.js.map +1 -1
- package/dist/generated/apiRoutes.js +2 -0
- package/dist/generated/apiRoutes.js.map +1 -1
- package/dist/services/assets.js +46 -1
- package/dist/services/assets.js.map +1 -1
- package/dist/services/mediaLibraries.js +39 -1
- package/dist/services/mediaLibraries.js.map +1 -1
- package/dist/util/assetSourceValidation.js +56 -0
- package/dist/util/assetSourceValidation.js.map +1 -0
- package/dist/util/assetUploadErrors.js +64 -0
- package/dist/util/assetUploadErrors.js.map +1 -0
- package/dist/util/isRemoteAssetSource.js +19 -0
- package/dist/util/isRemoteAssetSource.js.map +1 -0
- package/dist/util/parseAspectFlags.js +34 -0
- package/dist/util/parseAspectFlags.js.map +1 -0
- package/oclif.config.js +0 -1
- package/oclif.manifest.json +2134 -2055
- package/package.json +18 -18
- package/templates/app-quickstart/AGENTS.md +18 -0
- package/templates/app-sanity-ui/AGENTS.md +18 -0
- package/templates/shopify/.gitignore +26 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
- 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
|
|
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
|
|
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>
|
|
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
|
|
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] [--
|
|
2005
|
-
[-j] [--schema-required] [--
|
|
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
|
|
2021
|
-
|
|
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.
|
|
2515
|
-
|
|
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>] [--
|
|
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
|
|
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>] [--
|
|
4647
|
+
<value>] [--assignment-user <value>] [--assignment-role <value>...] [--assignment-state unrouted|routed|held...]
|
|
4648
|
+
[--limit <value>] [--json]
|
|
4614
4649
|
|
|
4615
4650
|
FLAGS
|
|
4616
|
-
--
|
|
4617
|
-
--
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
--
|
|
4621
|
-
--
|
|
4622
|
-
|
|
4623
|
-
--
|
|
4624
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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({
|
|
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
|
-
|
|
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(
|
|
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
|
|
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"}
|
|
@@ -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
|
|
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', '\
|
|
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(`
|