@sanity/cli 7.12.0 → 7.13.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 (177) hide show
  1. package/README.md +88 -0
  2. package/dist/actions/api/constants.js +13 -0
  3. package/dist/actions/api/constants.js.map +1 -0
  4. package/dist/actions/api/distillApiRoutes.js +106 -0
  5. package/dist/actions/api/distillApiRoutes.js.map +1 -0
  6. package/dist/actions/api/errors.js +18 -0
  7. package/dist/actions/api/errors.js.map +1 -0
  8. package/dist/actions/api/parseFields.js +183 -0
  9. package/dist/actions/api/parseFields.js.map +1 -0
  10. package/dist/actions/api/resolveEndpoint.js +159 -0
  11. package/dist/actions/api/resolveEndpoint.js.map +1 -0
  12. package/dist/actions/api/types.js +10 -0
  13. package/dist/actions/api/types.js.map +1 -0
  14. package/dist/actions/build/buildApp.js +4 -2
  15. package/dist/actions/build/buildApp.js.map +1 -1
  16. package/dist/actions/build/buildStudio.js +105 -14
  17. package/dist/actions/build/buildStudio.js.map +1 -1
  18. package/dist/actions/build/eventListenerFactory.js +70 -0
  19. package/dist/actions/build/eventListenerFactory.js.map +1 -0
  20. package/dist/actions/dev/servers/startStudioDevServer.js +5 -1
  21. package/dist/actions/dev/servers/startStudioDevServer.js.map +1 -1
  22. package/dist/commands/api.js +294 -0
  23. package/dist/commands/api.js.map +1 -0
  24. package/dist/commands/backups/disable.js +4 -4
  25. package/dist/commands/backups/disable.js.map +1 -1
  26. package/dist/commands/backups/download.js +7 -7
  27. package/dist/commands/backups/download.js.map +1 -1
  28. package/dist/commands/backups/enable.js +4 -4
  29. package/dist/commands/backups/enable.js.map +1 -1
  30. package/dist/commands/backups/list.js +7 -7
  31. package/dist/commands/backups/list.js.map +1 -1
  32. package/dist/commands/codemod.js +5 -5
  33. package/dist/commands/codemod.js.map +1 -1
  34. package/dist/commands/cors/add.js +1 -1
  35. package/dist/commands/cors/add.js.map +1 -1
  36. package/dist/commands/cors/delete.js +4 -4
  37. package/dist/commands/cors/delete.js.map +1 -1
  38. package/dist/commands/cors/list.js +2 -2
  39. package/dist/commands/cors/list.js.map +1 -1
  40. package/dist/commands/datasets/alias/create.js +5 -5
  41. package/dist/commands/datasets/alias/create.js.map +1 -1
  42. package/dist/commands/datasets/alias/delete.js +2 -2
  43. package/dist/commands/datasets/alias/delete.js.map +1 -1
  44. package/dist/commands/datasets/alias/link.js +5 -5
  45. package/dist/commands/datasets/alias/link.js.map +1 -1
  46. package/dist/commands/datasets/alias/unlink.js +3 -3
  47. package/dist/commands/datasets/alias/unlink.js.map +1 -1
  48. package/dist/commands/datasets/copy.js +7 -7
  49. package/dist/commands/datasets/copy.js.map +1 -1
  50. package/dist/commands/datasets/create.js +3 -3
  51. package/dist/commands/datasets/create.js.map +1 -1
  52. package/dist/commands/datasets/delete.js +2 -2
  53. package/dist/commands/datasets/delete.js.map +1 -1
  54. package/dist/commands/datasets/embeddings/disable.js +3 -3
  55. package/dist/commands/datasets/embeddings/disable.js.map +1 -1
  56. package/dist/commands/datasets/embeddings/enable.js +5 -5
  57. package/dist/commands/datasets/embeddings/enable.js.map +1 -1
  58. package/dist/commands/datasets/embeddings/status.js +3 -3
  59. package/dist/commands/datasets/embeddings/status.js.map +1 -1
  60. package/dist/commands/datasets/export.js +6 -6
  61. package/dist/commands/datasets/export.js.map +1 -1
  62. package/dist/commands/datasets/import.js +3 -3
  63. package/dist/commands/datasets/import.js.map +1 -1
  64. package/dist/commands/datasets/list.js +2 -2
  65. package/dist/commands/datasets/list.js.map +1 -1
  66. package/dist/commands/datasets/visibility/get.js +2 -2
  67. package/dist/commands/datasets/visibility/get.js.map +1 -1
  68. package/dist/commands/datasets/visibility/set.js +3 -3
  69. package/dist/commands/datasets/visibility/set.js.map +1 -1
  70. package/dist/commands/deploy.js +2 -2
  71. package/dist/commands/deploy.js.map +1 -1
  72. package/dist/commands/dev.js +2 -2
  73. package/dist/commands/dev.js.map +1 -1
  74. package/dist/commands/docs/read.js +1 -1
  75. package/dist/commands/docs/read.js.map +1 -1
  76. package/dist/commands/docs/search.js +3 -3
  77. package/dist/commands/docs/search.js.map +1 -1
  78. package/dist/commands/doctor.js +3 -3
  79. package/dist/commands/doctor.js.map +1 -1
  80. package/dist/commands/documents/create.js +3 -3
  81. package/dist/commands/documents/create.js.map +1 -1
  82. package/dist/commands/documents/delete.js +3 -3
  83. package/dist/commands/documents/delete.js.map +1 -1
  84. package/dist/commands/documents/get.js +2 -2
  85. package/dist/commands/documents/get.js.map +1 -1
  86. package/dist/commands/documents/query.js +2 -2
  87. package/dist/commands/documents/query.js.map +1 -1
  88. package/dist/commands/documents/validate.js +3 -3
  89. package/dist/commands/documents/validate.js.map +1 -1
  90. package/dist/commands/graphql/deploy.js +13 -13
  91. package/dist/commands/graphql/deploy.js.map +1 -1
  92. package/dist/commands/graphql/list.js +2 -2
  93. package/dist/commands/graphql/list.js.map +1 -1
  94. package/dist/commands/graphql/undeploy.js +2 -2
  95. package/dist/commands/graphql/undeploy.js.map +1 -1
  96. package/dist/commands/hooks/attempt.js +2 -2
  97. package/dist/commands/hooks/attempt.js.map +1 -1
  98. package/dist/commands/hooks/create.js +3 -3
  99. package/dist/commands/hooks/create.js.map +1 -1
  100. package/dist/commands/hooks/delete.js +4 -4
  101. package/dist/commands/hooks/delete.js.map +1 -1
  102. package/dist/commands/hooks/list.js +2 -2
  103. package/dist/commands/hooks/list.js.map +1 -1
  104. package/dist/commands/hooks/logs.js +5 -5
  105. package/dist/commands/hooks/logs.js.map +1 -1
  106. package/dist/commands/login.js +2 -2
  107. package/dist/commands/login.js.map +1 -1
  108. package/dist/commands/logout.js +2 -2
  109. package/dist/commands/logout.js.map +1 -1
  110. package/dist/commands/manifest/extract.js +3 -3
  111. package/dist/commands/manifest/extract.js.map +1 -1
  112. package/dist/commands/mcp/configure.js +4 -4
  113. package/dist/commands/mcp/configure.js.map +1 -1
  114. package/dist/commands/media/create-aspect.js +3 -3
  115. package/dist/commands/media/create-aspect.js.map +1 -1
  116. package/dist/commands/media/delete-aspect.js +1 -1
  117. package/dist/commands/media/delete-aspect.js.map +1 -1
  118. package/dist/commands/media/deploy-aspect.js +4 -4
  119. package/dist/commands/media/deploy-aspect.js.map +1 -1
  120. package/dist/commands/media/export.js +7 -7
  121. package/dist/commands/media/export.js.map +1 -1
  122. package/dist/commands/media/import.js +6 -6
  123. package/dist/commands/media/import.js.map +1 -1
  124. package/dist/commands/migrations/create.js +4 -4
  125. package/dist/commands/migrations/create.js.map +1 -1
  126. package/dist/commands/migrations/list.js +2 -2
  127. package/dist/commands/migrations/list.js.map +1 -1
  128. package/dist/commands/migrations/run.js +14 -14
  129. package/dist/commands/migrations/run.js.map +1 -1
  130. package/dist/commands/openapi/get.js +3 -3
  131. package/dist/commands/openapi/get.js.map +1 -1
  132. package/dist/commands/openapi/list.js +2 -2
  133. package/dist/commands/openapi/list.js.map +1 -1
  134. package/dist/commands/organizations/create.js +5 -5
  135. package/dist/commands/organizations/create.js.map +1 -1
  136. package/dist/commands/organizations/delete.js +6 -6
  137. package/dist/commands/organizations/delete.js.map +1 -1
  138. package/dist/commands/organizations/get.js +3 -3
  139. package/dist/commands/organizations/get.js.map +1 -1
  140. package/dist/commands/organizations/list.js +2 -2
  141. package/dist/commands/organizations/list.js.map +1 -1
  142. package/dist/commands/organizations/update.js +6 -6
  143. package/dist/commands/organizations/update.js.map +1 -1
  144. package/dist/commands/preview.js +2 -2
  145. package/dist/commands/preview.js.map +1 -1
  146. package/dist/commands/projects/create.js +2 -2
  147. package/dist/commands/projects/create.js.map +1 -1
  148. package/dist/commands/projects/list.js +2 -2
  149. package/dist/commands/projects/list.js.map +1 -1
  150. package/dist/commands/schemas/delete.js +2 -2
  151. package/dist/commands/schemas/delete.js.map +1 -1
  152. package/dist/commands/schemas/deploy.js +2 -2
  153. package/dist/commands/schemas/deploy.js.map +1 -1
  154. package/dist/commands/schemas/list.js +1 -1
  155. package/dist/commands/schemas/list.js.map +1 -1
  156. package/dist/commands/schemas/validate.js +2 -1
  157. package/dist/commands/schemas/validate.js.map +1 -1
  158. package/dist/commands/skills/install.js +2 -2
  159. package/dist/commands/skills/install.js.map +1 -1
  160. package/dist/commands/telemetry/disable.js +2 -2
  161. package/dist/commands/telemetry/disable.js.map +1 -1
  162. package/dist/commands/telemetry/enable.js +2 -2
  163. package/dist/commands/telemetry/enable.js.map +1 -1
  164. package/dist/commands/tokens/create.js +2 -2
  165. package/dist/commands/tokens/create.js.map +1 -1
  166. package/dist/commands/tokens/delete.js +2 -2
  167. package/dist/commands/tokens/delete.js.map +1 -1
  168. package/dist/commands/tokens/list.js +2 -2
  169. package/dist/commands/tokens/list.js.map +1 -1
  170. package/dist/commands/users/invite.js +3 -3
  171. package/dist/commands/users/invite.js.map +1 -1
  172. package/dist/generated/apiRoutes.js +401 -0
  173. package/dist/generated/apiRoutes.js.map +1 -0
  174. package/dist/services/api.js +116 -0
  175. package/dist/services/api.js.map +1 -0
  176. package/oclif.manifest.json +433 -252
  177. package/package.json +5 -3
package/README.md CHANGED
@@ -13,6 +13,7 @@ Code for sanity cli
13
13
 
14
14
  <!-- commands -->
15
15
 
16
+ - [`sanity api ENDPOINT`](#sanity-api-endpoint)
16
17
  - [`sanity backups disable [DATASET]`](#sanity-backups-disable-dataset)
17
18
  - [`sanity backups download [DATASET]`](#sanity-backups-download-dataset)
18
19
  - [`sanity backups enable [DATASET]`](#sanity-backups-enable-dataset)
@@ -122,6 +123,93 @@ Code for sanity cli
122
123
  - [`sanity users list`](#sanity-users-list)
123
124
  - [`sanity versions`](#sanity-versions)
124
125
 
126
+ ## `sanity api ENDPOINT`
127
+
128
+ Make an authenticated HTTP request to a Sanity API
129
+
130
+ ```
131
+ USAGE
132
+ $ sanity api ENDPOINT [-p <id>] [-d <name>] [--api-version <version>] [--global | --project-hosted] [-H
133
+ <key:value>...] [-i] [--input <file> | -F <key=value>... | -f <key=value>...] [-X <method>] [--pretty] [-t <token> |
134
+ --anonymous]
135
+
136
+ ARGUMENTS
137
+ ENDPOINT API path (eg "projects" or "data/query/{dataset}"), optionally with placeholders, or a full
138
+ https://*.api.sanity.io URL
139
+
140
+ FLAGS
141
+ -F, --field=<key=value>... Add a typed parameter (key=value): true/false/null and numbers are converted, @file
142
+ reads the value from a file, @- from stdin
143
+ -H, --header=<key:value>... Add an HTTP request header (key: value)
144
+ -X, --method=<method> HTTP method to use (default GET, or POST when fields or --input are provided)
145
+ -f, --raw-field=<key=value>... Add a string parameter (key=value)
146
+ -i, --include Include the HTTP response status and headers in the output
147
+ -t, --token=<token> API token to authenticate with, instead of the logged-in user token
148
+ --anonymous Send the request without an authorization token
149
+ --api-version=<version> API version to use (eg v2025-02-19). Defaults to a version embedded in the endpoint
150
+ path, or the version from the matching OpenAPI spec
151
+ --global Force the request to the global API host (api.sanity.io)
152
+ --input=<file> Read the raw request body from a file (use "-" for stdin). Sent without a default
153
+ Content-Type - provide one with -H when the API requires it
154
+ --pretty Colorize JSON output
155
+ --project-hosted Force the request to the project API host (<projectId>.api.sanity.io)
156
+
157
+ OVERRIDE FLAGS
158
+ -d, --dataset=<name> Dataset for {dataset} placeholders (overrides CLI configuration)
159
+ -p, --project-id=<id> Project ID for {projectId} placeholders and project-hosted APIs (overrides CLI configuration)
160
+
161
+ DESCRIPTION
162
+ Make an authenticated HTTP request to a Sanity API
163
+
164
+ The endpoint argument is an API path as documented in the published OpenAPI
165
+ specifications - list them with "sanity openapi list" and inspect one with
166
+ "sanity openapi get <slug>". Paths can be copied verbatim from the specs:
167
+ {projectId} and {dataset} placeholders are filled in from flags or the CLI
168
+ configuration, and the API host (api.sanity.io or <projectId>.api.sanity.io)
169
+ is chosen based on the specs' routing information.
170
+
171
+ The default request method is GET, or POST when fields or --input are
172
+ provided. For GET/HEAD requests, fields are sent as query parameters;
173
+ otherwise they are combined into a JSON request body sent with
174
+ "Content-Type: application/json". Raw --input bodies are sent without a
175
+ default Content-Type - provide one with -H when the API requires it. The
176
+ response body is written to stdout.
177
+
178
+ Requests are authenticated with the token from "sanity login". To use a
179
+ specific token instead - for example in CI or when the CLI is not logged in
180
+ - pass --token or set the SANITY_AUTH_TOKEN environment variable. Pass
181
+ --anonymous to send no token at all.
182
+
183
+ EXAMPLES
184
+ Get the current user
185
+
186
+ $ sanity api users/me
187
+
188
+ Get the current project (placeholder filled from CLI config)
189
+
190
+ $ sanity api projects/{projectId}
191
+
192
+ Run a GROQ query against the project host
193
+
194
+ $ sanity api 'data/query/{dataset}' -f query='*[_type == "movie"][0..2]'
195
+
196
+ Send a JSON body built from typed fields
197
+
198
+ $ sanity api projects/{projectId} -X PATCH -F displayName="My project"
199
+
200
+ Send a raw request body from stdin
201
+
202
+ echo '{"mutations": []}' | sanity api 'data/mutate/{dataset}' --input - -H 'Content-Type: application/json'
203
+
204
+ Include the response status and headers, pinning the API version
205
+
206
+ $ sanity api jobs/123 --include --api-version v2025-02-19
207
+
208
+ Authenticate with a specific token instead of the logged-in session
209
+
210
+ SANITY_AUTH_TOKEN=<token> sanity api users/me
211
+ ```
212
+
125
213
  ## `sanity backups disable [DATASET]`
126
214
 
127
215
  Disable backup for a dataset
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Fallback API version used when neither the endpoint path, the `--api-version`
3
+ * flag, nor the matched OpenAPI spec provides one.
4
+ */ export const API_DEFAULT_VERSION = 'v2025-02-19';
5
+ /**
6
+ * Base URL for the published OpenAPI specification index.
7
+ * Also used by the `sanity openapi` commands.
8
+ */ export const OPENAPI_SPEC_INDEX_URL = 'https://www.sanity.io/docs/api/openapi';
9
+ /**
10
+ * Matches an API version path segment: `v1`, `vX` or `v<YYYY-MM-DD>`.
11
+ */ export const API_VERSION_SEGMENT_RE = /^v(?:\d{4}-\d{2}-\d{2}|X|\d+)$/;
12
+
13
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/api/constants.ts"],"sourcesContent":["/**\n * Fallback API version used when neither the endpoint path, the `--api-version`\n * flag, nor the matched OpenAPI spec provides one.\n */\nexport const API_DEFAULT_VERSION = 'v2025-02-19'\n\n/**\n * Base URL for the published OpenAPI specification index.\n * Also used by the `sanity openapi` commands.\n */\nexport const OPENAPI_SPEC_INDEX_URL = 'https://www.sanity.io/docs/api/openapi'\n\n/**\n * Matches an API version path segment: `v1`, `vX` or `v<YYYY-MM-DD>`.\n */\nexport const API_VERSION_SEGMENT_RE = /^v(?:\\d{4}-\\d{2}-\\d{2}|X|\\d+)$/\n"],"names":["API_DEFAULT_VERSION","OPENAPI_SPEC_INDEX_URL","API_VERSION_SEGMENT_RE"],"mappings":"AAAA;;;CAGC,GACD,OAAO,MAAMA,sBAAsB,cAAa;AAEhD;;;CAGC,GACD,OAAO,MAAMC,yBAAyB,yCAAwC;AAE9E;;CAEC,GACD,OAAO,MAAMC,yBAAyB,iCAAgC"}
@@ -0,0 +1,106 @@
1
+ import { API_VERSION_SEGMENT_RE } from './constants.js';
2
+ /**
3
+ * Distill fetched OpenAPI specifications into the routing manifest used by
4
+ * `sanity api` to decide which host serves a request path and which API
5
+ * version to default to.
6
+ *
7
+ * The output is deterministic (sorted) so the generated manifest can be
8
+ * compared against a fresh distillation to detect drift.
9
+ */ export function distillApiRoutes(specs) {
10
+ const entries = [];
11
+ for (const { document, slug, title } of specs){
12
+ const servers = document.servers ?? [];
13
+ const paths = Object.keys(document.paths ?? {});
14
+ if (servers.length === 0 || paths.length === 0) continue;
15
+ // A spec may be served on both hosts (eg the Applications API). Emit one
16
+ // entry per host, each carrying the full set of path patterns.
17
+ const byHost = new Map();
18
+ for (const server of servers){
19
+ const base = parseServerUrl(server.url, server.variables);
20
+ if (!base) continue;
21
+ if (!byHost.has(base.host)) byHost.set(base.host, base);
22
+ }
23
+ for (const base of byHost.values()){
24
+ const patterns = new Set();
25
+ let defaultApiVersion = base.defaultApiVersion;
26
+ for (const path of paths){
27
+ const { segments, version } = normalizePathSegments(path);
28
+ defaultApiVersion = defaultApiVersion ?? version;
29
+ const pattern = [
30
+ ...base.segments,
31
+ ...segments
32
+ ].join('/');
33
+ if (pattern !== '') patterns.add(pattern);
34
+ }
35
+ if (patterns.size === 0) continue;
36
+ entries.push({
37
+ ...defaultApiVersion === undefined ? {} : {
38
+ defaultApiVersion
39
+ },
40
+ host: base.host,
41
+ pathPatterns: [
42
+ ...patterns
43
+ ].toSorted(),
44
+ slug,
45
+ title
46
+ });
47
+ }
48
+ }
49
+ return entries.toSorted((a, b)=>a.slug.localeCompare(b.slug) || a.host.localeCompare(b.host));
50
+ }
51
+ /**
52
+ * Parse an OpenAPI server URL template (eg
53
+ * `https://{projectId}.api.sanity.io/{apiVersion}/agent/action`) into a host
54
+ * classification, base path segments and a default API version.
55
+ *
56
+ * Returns `undefined` for URLs that don't look like a Sanity API host.
57
+ */ function parseServerUrl(url, variables) {
58
+ const withoutScheme = url.replace(/^https?:\/\//, '');
59
+ const [hostname = '', ...pathSegments] = withoutScheme.split('/');
60
+ if (!hostname.includes('api.sanity.io') && !hostname.includes('api.sanity.work')) {
61
+ return undefined;
62
+ }
63
+ const host = hostname.includes('{projectId}') ? 'project' : 'global';
64
+ let defaultApiVersion = normalizeVersion(variables?.apiVersion?.default);
65
+ const segments = [];
66
+ for (const segment of pathSegments){
67
+ if (segment === '') continue;
68
+ if (segment === '{apiVersion}') continue;
69
+ if (API_VERSION_SEGMENT_RE.test(segment)) {
70
+ defaultApiVersion = defaultApiVersion ?? segment;
71
+ continue;
72
+ }
73
+ segments.push(segment);
74
+ }
75
+ return {
76
+ defaultApiVersion,
77
+ host,
78
+ segments
79
+ };
80
+ }
81
+ /**
82
+ * Normalize an OpenAPI path template into version-less path segments, plus the
83
+ * API version if one was embedded in the path (eg `/v2024-07-01/access/...`).
84
+ *
85
+ * Some specs carry a stray query marker in the path key (eg `/foo/{id}?`) -
86
+ * anything from `?` on is not part of the path and is dropped.
87
+ */ function normalizePathSegments(path) {
88
+ const [pathPart = ''] = path.split('?');
89
+ const segments = pathPart.split('/').filter((segment)=>segment !== '');
90
+ let version;
91
+ if (segments[0] === '{apiVersion}') {
92
+ segments.shift();
93
+ } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {
94
+ version = segments.shift();
95
+ }
96
+ return {
97
+ segments,
98
+ version
99
+ };
100
+ }
101
+ function normalizeVersion(version) {
102
+ if (!version) return undefined;
103
+ return API_VERSION_SEGMENT_RE.test(version) ? version : undefined;
104
+ }
105
+
106
+ //# sourceMappingURL=distillApiRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/api/distillApiRoutes.ts"],"sourcesContent":["import {API_VERSION_SEGMENT_RE} from './constants.js'\nimport {type ApiHost, type ApiRouteEntry, type OpenApiDocument} from './types.js'\n\n/**\n * A fetched OpenAPI specification, ready for distillation.\n */\nexport interface SpecSource {\n document: OpenApiDocument\n slug: string\n title: string\n}\n\ninterface ServerBase {\n host: ApiHost\n segments: string[]\n\n defaultApiVersion?: string\n}\n\n/**\n * Distill fetched OpenAPI specifications into the routing manifest used by\n * `sanity api` to decide which host serves a request path and which API\n * version to default to.\n *\n * The output is deterministic (sorted) so the generated manifest can be\n * compared against a fresh distillation to detect drift.\n */\nexport function distillApiRoutes(specs: SpecSource[]): ApiRouteEntry[] {\n const entries: ApiRouteEntry[] = []\n\n for (const {document, slug, title} of specs) {\n const servers = document.servers ?? []\n const paths = Object.keys(document.paths ?? {})\n if (servers.length === 0 || paths.length === 0) continue\n\n // A spec may be served on both hosts (eg the Applications API). Emit one\n // entry per host, each carrying the full set of path patterns.\n const byHost = new Map<ApiHost, ServerBase>()\n for (const server of servers) {\n const base = parseServerUrl(server.url, server.variables)\n if (!base) continue\n if (!byHost.has(base.host)) byHost.set(base.host, base)\n }\n\n for (const base of byHost.values()) {\n const patterns = new Set<string>()\n let defaultApiVersion = base.defaultApiVersion\n\n for (const path of paths) {\n const {segments, version} = normalizePathSegments(path)\n defaultApiVersion = defaultApiVersion ?? version\n const pattern = [...base.segments, ...segments].join('/')\n if (pattern !== '') patterns.add(pattern)\n }\n\n if (patterns.size === 0) continue\n\n entries.push({\n ...(defaultApiVersion === undefined ? {} : {defaultApiVersion}),\n host: base.host,\n pathPatterns: [...patterns].toSorted(),\n slug,\n title,\n })\n }\n }\n\n return entries.toSorted((a, b) => a.slug.localeCompare(b.slug) || a.host.localeCompare(b.host))\n}\n\n/**\n * Parse an OpenAPI server URL template (eg\n * `https://{projectId}.api.sanity.io/{apiVersion}/agent/action`) into a host\n * classification, base path segments and a default API version.\n *\n * Returns `undefined` for URLs that don't look like a Sanity API host.\n */\nfunction parseServerUrl(\n url: string,\n variables?: Record<string, {default?: string}>,\n): ServerBase | undefined {\n const withoutScheme = url.replace(/^https?:\\/\\//, '')\n const [hostname = '', ...pathSegments] = withoutScheme.split('/')\n\n if (!hostname.includes('api.sanity.io') && !hostname.includes('api.sanity.work')) {\n return undefined\n }\n\n const host: ApiHost = hostname.includes('{projectId}') ? 'project' : 'global'\n\n let defaultApiVersion = normalizeVersion(variables?.apiVersion?.default)\n const segments: string[] = []\n for (const segment of pathSegments) {\n if (segment === '') continue\n if (segment === '{apiVersion}') continue\n if (API_VERSION_SEGMENT_RE.test(segment)) {\n defaultApiVersion = defaultApiVersion ?? segment\n continue\n }\n segments.push(segment)\n }\n\n return {defaultApiVersion, host, segments}\n}\n\n/**\n * Normalize an OpenAPI path template into version-less path segments, plus the\n * API version if one was embedded in the path (eg `/v2024-07-01/access/...`).\n *\n * Some specs carry a stray query marker in the path key (eg `/foo/{id}?`) -\n * anything from `?` on is not part of the path and is dropped.\n */\nfunction normalizePathSegments(path: string): {segments: string[]; version?: string} {\n const [pathPart = ''] = path.split('?')\n const segments = pathPart.split('/').filter((segment) => segment !== '')\n let version: string | undefined\n\n if (segments[0] === '{apiVersion}') {\n segments.shift()\n } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {\n version = segments.shift()\n }\n\n return {segments, version}\n}\n\nfunction normalizeVersion(version: string | undefined): string | undefined {\n if (!version) return undefined\n return API_VERSION_SEGMENT_RE.test(version) ? version : undefined\n}\n"],"names":["API_VERSION_SEGMENT_RE","distillApiRoutes","specs","entries","document","slug","title","servers","paths","Object","keys","length","byHost","Map","server","base","parseServerUrl","url","variables","has","host","set","values","patterns","Set","defaultApiVersion","path","segments","version","normalizePathSegments","pattern","join","add","size","push","undefined","pathPatterns","toSorted","a","b","localeCompare","withoutScheme","replace","hostname","pathSegments","split","includes","normalizeVersion","apiVersion","default","segment","test","pathPart","filter","shift"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The user provided invalid input to `sanity api` (bad field syntax, missing
3
+ * placeholder values, unsupported host, ...). Mapped to exit code 2 by the
4
+ * command, per the CLI exit code convention.
5
+ */ export class ApiUsageError extends Error {
6
+ }
7
+ /**
8
+ * The request targets the project API host (or the path contains a
9
+ * `{projectId}` placeholder) but no project ID has been resolved yet. The
10
+ * command catches this to trigger project resolution (flag, CLI config or
11
+ * interactive prompt) and retries.
12
+ */ export class ProjectIdRequiredError extends Error {
13
+ constructor(){
14
+ super('A project ID is required for this request');
15
+ }
16
+ }
17
+
18
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/api/errors.ts"],"sourcesContent":["/**\n * The user provided invalid input to `sanity api` (bad field syntax, missing\n * placeholder values, unsupported host, ...). Mapped to exit code 2 by the\n * command, per the CLI exit code convention.\n */\nexport class ApiUsageError extends Error {}\n\n/**\n * The request targets the project API host (or the path contains a\n * `{projectId}` placeholder) but no project ID has been resolved yet. The\n * command catches this to trigger project resolution (flag, CLI config or\n * interactive prompt) and retries.\n */\nexport class ProjectIdRequiredError extends Error {\n constructor() {\n super('A project ID is required for this request')\n }\n}\n"],"names":["ApiUsageError","Error","ProjectIdRequiredError"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,MAAMA,sBAAsBC;AAAO;AAE1C;;;;;CAKC,GACD,OAAO,MAAMC,+BAA+BD;IAC1C,aAAc;QACZ,KAAK,CAAC;IACR;AACF"}
@@ -0,0 +1,183 @@
1
+ import { ApiUsageError } from './errors.js';
2
+ const KEY_RE = /^([^=[\]]+)((?:\[[^[\]]*\])*)$/;
3
+ /**
4
+ * Convert parsed fields to query parameters, for request methods without a
5
+ * body (GET/HEAD). Only scalar values (and arrays of scalars) are supported.
6
+ */ export function fieldsToQuery(fields) {
7
+ // Null-prototype for the same reason as in parseFields.
8
+ const query = Object.create(null);
9
+ for (const [key, value] of Object.entries(fields)){
10
+ if (Array.isArray(value)) {
11
+ if (value.some((item)=>typeof item === 'object' && item !== null)) {
12
+ throw new ApiUsageError(`Cannot use nested field "${key}" as a query parameter`);
13
+ }
14
+ query[key] = value.map(String);
15
+ } else if (typeof value === 'object' && value !== null) {
16
+ throw new ApiUsageError(`Cannot use nested field "${key}" as a query parameter`);
17
+ } else {
18
+ query[key] = String(value);
19
+ }
20
+ }
21
+ return query;
22
+ }
23
+ /**
24
+ * Parse `gh api`-style field flags into a JSON-compatible object.
25
+ *
26
+ * Supports nested keys with bracket syntax: `a[b]=1` produces `{a: {b: 1}}`,
27
+ * `a[]=1` appends to an array and a bare `a[]` declares an empty one.
28
+ * Raw fields (`-f`) keep values as strings; typed fields (`-F`) convert
29
+ * `true`/`false`/`null` and numbers, and expand `@<file>` / `@-` to file or
30
+ * stdin contents.
31
+ */ export function parseFields(options) {
32
+ const { fields = [], rawFields = [], readFile, stdin } = options;
33
+ // Null-prototype containers keep user-supplied keys like `__proto__` or
34
+ // `toString` ordinary own properties: nothing to pollute, nothing inherited.
35
+ const result = Object.create(null);
36
+ for (const raw of rawFields){
37
+ const { key, value } = splitField(raw, '--raw-field');
38
+ setField(result, key, value);
39
+ }
40
+ for (const field of fields){
41
+ const { key, value } = splitField(field, '--field');
42
+ setField(result, key, value === undefined ? undefined : coerceValue(value, {
43
+ flag: field,
44
+ readFile,
45
+ stdin
46
+ }));
47
+ }
48
+ return result;
49
+ }
50
+ function splitField(input, flagName) {
51
+ const separatorIndex = input.indexOf('=');
52
+ // A bare `key[]` without a value declares an empty array (gh api parity)
53
+ if (separatorIndex === -1 && input.endsWith('[]')) {
54
+ return {
55
+ key: input
56
+ };
57
+ }
58
+ if (separatorIndex < 1) {
59
+ throw new ApiUsageError(`Invalid ${flagName} "${input}": expected key=value format`);
60
+ }
61
+ return {
62
+ key: input.slice(0, separatorIndex),
63
+ value: input.slice(separatorIndex + 1)
64
+ };
65
+ }
66
+ function coerceValue(value, context) {
67
+ if (value === 'true') return true;
68
+ if (value === 'false') return false;
69
+ if (value === 'null') return null;
70
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
71
+ if (value.startsWith('@')) {
72
+ const source = value.slice(1);
73
+ if (source === '-') {
74
+ if (context.stdin === undefined) {
75
+ throw new ApiUsageError(`Unable to read stdin for field "${context.flag}"`);
76
+ }
77
+ return context.stdin;
78
+ }
79
+ if (!context.readFile) {
80
+ throw new ApiUsageError(`Unable to read file for field "${context.flag}"`);
81
+ }
82
+ try {
83
+ return context.readFile(source);
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ throw new ApiUsageError(`Failed to read "${source}" for field "${context.flag}": ${message}`);
87
+ }
88
+ }
89
+ return value;
90
+ }
91
+ /**
92
+ * Set a (possibly nested) field on the target object, `gh api`-style:
93
+ * `a=1` sets a top-level key, `a[b]=1` a nested object key, `a[]=1` appends
94
+ * to an array and a bare `a[]` declares an empty one (`value` is `undefined`).
95
+ *
96
+ * Array elements accumulate the way they do in gh: consecutive fields keep
97
+ * filling the current (last) element - `a[][k]=1 a[][j]=2` builds one object
98
+ * with both keys - until a key repeats, which starts the next element.
99
+ */ function setField(target, key, value) {
100
+ const match = KEY_RE.exec(key);
101
+ if (!match) {
102
+ throw new ApiUsageError(`Invalid field key "${key}"`);
103
+ }
104
+ const [, head, brackets] = match;
105
+ const path = [
106
+ head
107
+ ];
108
+ if (brackets) {
109
+ for (const part of brackets.matchAll(/\[([^[\]]*)\]/g)){
110
+ path.push(part[1]);
111
+ }
112
+ }
113
+ let container = target;
114
+ let subkey = '';
115
+ let inArray = false;
116
+ for (const segment of path){
117
+ if (segment === '') {
118
+ inArray = true;
119
+ continue;
120
+ }
121
+ if (subkey !== '') {
122
+ container = inArray ? descendIntoArray(container, subkey, segment, key) : descendIntoObject(container, subkey, key);
123
+ inArray = false;
124
+ }
125
+ subkey = segment;
126
+ }
127
+ if (inArray) {
128
+ const existing = Object.hasOwn(container, subkey) ? container[subkey] : undefined;
129
+ if (existing !== undefined && !Array.isArray(existing)) {
130
+ throw new ApiUsageError(`Field "${key}" conflicts with an earlier field`);
131
+ }
132
+ const values = Array.isArray(existing) ? existing : [];
133
+ if (existing === undefined) {
134
+ container[subkey] = values;
135
+ }
136
+ if (value !== undefined) {
137
+ values.push(value);
138
+ }
139
+ return;
140
+ }
141
+ if (Object.hasOwn(container, subkey)) {
142
+ throw new ApiUsageError(`Field "${key}" conflicts with an earlier field`);
143
+ }
144
+ // A `value` of undefined only occurs for keys ending in `[]`, which take
145
+ // the array branch above.
146
+ container[subkey] = value;
147
+ }
148
+ /** Descend into (creating if needed) the object at `container[segment]`. */ function descendIntoObject(container, segment, key) {
149
+ const existing = Object.hasOwn(container, segment) ? container[segment] : undefined;
150
+ if (existing === undefined) {
151
+ const next = Object.create(null);
152
+ container[segment] = next;
153
+ return next;
154
+ }
155
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
156
+ throw new ApiUsageError(`Field "${key}" conflicts with an earlier field`);
157
+ }
158
+ return existing;
159
+ }
160
+ /**
161
+ * Descend into the array at `container[segment]`, returning the object
162
+ * element the field at hand should land in: the current (last) element while
163
+ * `nextKey` is new to it or accumulating a nested array, or a freshly
164
+ * appended element once `nextKey` repeats (gh api semantics).
165
+ */ function descendIntoArray(container, segment, nextKey, key) {
166
+ const existing = Object.hasOwn(container, segment) ? container[segment] : undefined;
167
+ if (existing !== undefined && !Array.isArray(existing)) {
168
+ throw new ApiUsageError(`Field "${key}" conflicts with an earlier field`);
169
+ }
170
+ const values = Array.isArray(existing) ? existing : [];
171
+ if (existing === undefined) {
172
+ container[segment] = values;
173
+ }
174
+ const last = values.at(-1);
175
+ if (typeof last === 'object' && last !== null && !Array.isArray(last) && (!Object.hasOwn(last, nextKey) || Array.isArray(last[nextKey]))) {
176
+ return last;
177
+ }
178
+ const next = Object.create(null);
179
+ values.push(next);
180
+ return next;
181
+ }
182
+
183
+ //# sourceMappingURL=parseFields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/api/parseFields.ts"],"sourcesContent":["import {ApiUsageError} from './errors.js'\n\n/**\n * A JSON-compatible value produced by field parsing.\n */\nexport type FieldValue =\n | boolean\n | FieldValue[]\n | number\n | string\n | {[key: string]: FieldValue}\n | null\n\n/**\n * Options for {@link parseFields}.\n */\nexport interface ParseFieldsOptions {\n /**\n * Typed fields (`-F/--field`): `true`, `false`, `null` and numbers are\n * converted; `@<file>` reads the value from a file, `@-` from stdin.\n */\n fields?: string[]\n\n /** Raw string fields (`-f/--raw-field`): the value is used verbatim. */\n rawFields?: string[]\n\n /** Reads a file referenced via `@<file>`. */\n readFile?: (path: string) => string\n\n /** Content of stdin, for `@-` values. */\n stdin?: string\n}\n\ntype FieldContainer = Record<string, FieldValue>\n\nconst KEY_RE = /^([^=[\\]]+)((?:\\[[^[\\]]*\\])*)$/\n\n/**\n * Convert parsed fields to query parameters, for request methods without a\n * body (GET/HEAD). Only scalar values (and arrays of scalars) are supported.\n */\nexport function fieldsToQuery(\n fields: Record<string, FieldValue>,\n): Record<string, string | string[]> {\n // Null-prototype for the same reason as in parseFields.\n const query: Record<string, string | string[]> = Object.create(null)\n\n for (const [key, value] of Object.entries(fields)) {\n if (Array.isArray(value)) {\n if (value.some((item) => typeof item === 'object' && item !== null)) {\n throw new ApiUsageError(`Cannot use nested field \"${key}\" as a query parameter`)\n }\n query[key] = value.map(String)\n } else if (typeof value === 'object' && value !== null) {\n throw new ApiUsageError(`Cannot use nested field \"${key}\" as a query parameter`)\n } else {\n query[key] = String(value)\n }\n }\n\n return query\n}\n\n/**\n * Parse `gh api`-style field flags into a JSON-compatible object.\n *\n * Supports nested keys with bracket syntax: `a[b]=1` produces `{a: {b: 1}}`,\n * `a[]=1` appends to an array and a bare `a[]` declares an empty one.\n * Raw fields (`-f`) keep values as strings; typed fields (`-F`) convert\n * `true`/`false`/`null` and numbers, and expand `@<file>` / `@-` to file or\n * stdin contents.\n */\nexport function parseFields(options: ParseFieldsOptions): Record<string, FieldValue> {\n const {fields = [], rawFields = [], readFile, stdin} = options\n // Null-prototype containers keep user-supplied keys like `__proto__` or\n // `toString` ordinary own properties: nothing to pollute, nothing inherited.\n const result: FieldContainer = Object.create(null)\n\n for (const raw of rawFields) {\n const {key, value} = splitField(raw, '--raw-field')\n setField(result, key, value)\n }\n\n for (const field of fields) {\n const {key, value} = splitField(field, '--field')\n setField(\n result,\n key,\n value === undefined ? undefined : coerceValue(value, {flag: field, readFile, stdin}),\n )\n }\n\n return result\n}\n\nfunction splitField(input: string, flagName: string): {key: string; value?: string} {\n const separatorIndex = input.indexOf('=')\n // A bare `key[]` without a value declares an empty array (gh api parity)\n if (separatorIndex === -1 && input.endsWith('[]')) {\n return {key: input}\n }\n if (separatorIndex < 1) {\n throw new ApiUsageError(`Invalid ${flagName} \"${input}\": expected key=value format`)\n }\n return {key: input.slice(0, separatorIndex), value: input.slice(separatorIndex + 1)}\n}\n\nfunction coerceValue(\n value: string,\n context: {flag: string; readFile?: (path: string) => string; stdin?: string},\n): FieldValue {\n if (value === 'true') return true\n if (value === 'false') return false\n if (value === 'null') return null\n if (/^-?\\d+(\\.\\d+)?$/.test(value)) return Number(value)\n\n if (value.startsWith('@')) {\n const source = value.slice(1)\n if (source === '-') {\n if (context.stdin === undefined) {\n throw new ApiUsageError(`Unable to read stdin for field \"${context.flag}\"`)\n }\n return context.stdin\n }\n if (!context.readFile) {\n throw new ApiUsageError(`Unable to read file for field \"${context.flag}\"`)\n }\n try {\n return context.readFile(source)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new ApiUsageError(`Failed to read \"${source}\" for field \"${context.flag}\": ${message}`)\n }\n }\n\n return value\n}\n\n/**\n * Set a (possibly nested) field on the target object, `gh api`-style:\n * `a=1` sets a top-level key, `a[b]=1` a nested object key, `a[]=1` appends\n * to an array and a bare `a[]` declares an empty one (`value` is `undefined`).\n *\n * Array elements accumulate the way they do in gh: consecutive fields keep\n * filling the current (last) element - `a[][k]=1 a[][j]=2` builds one object\n * with both keys - until a key repeats, which starts the next element.\n */\nfunction setField(target: FieldContainer, key: string, value: FieldValue | undefined): void {\n const match = KEY_RE.exec(key)\n if (!match) {\n throw new ApiUsageError(`Invalid field key \"${key}\"`)\n }\n\n const [, head, brackets] = match\n const path: string[] = [head]\n if (brackets) {\n for (const part of brackets.matchAll(/\\[([^[\\]]*)\\]/g)) {\n path.push(part[1])\n }\n }\n\n let container = target\n let subkey = ''\n let inArray = false\n for (const segment of path) {\n if (segment === '') {\n inArray = true\n continue\n }\n if (subkey !== '') {\n container = inArray\n ? descendIntoArray(container, subkey, segment, key)\n : descendIntoObject(container, subkey, key)\n inArray = false\n }\n subkey = segment\n }\n\n if (inArray) {\n const existing = Object.hasOwn(container, subkey) ? container[subkey] : undefined\n if (existing !== undefined && !Array.isArray(existing)) {\n throw new ApiUsageError(`Field \"${key}\" conflicts with an earlier field`)\n }\n const values = Array.isArray(existing) ? existing : []\n if (existing === undefined) {\n container[subkey] = values\n }\n if (value !== undefined) {\n values.push(value)\n }\n return\n }\n\n if (Object.hasOwn(container, subkey)) {\n throw new ApiUsageError(`Field \"${key}\" conflicts with an earlier field`)\n }\n // A `value` of undefined only occurs for keys ending in `[]`, which take\n // the array branch above.\n container[subkey] = value as FieldValue\n}\n\n/** Descend into (creating if needed) the object at `container[segment]`. */\nfunction descendIntoObject(\n container: FieldContainer,\n segment: string,\n key: string,\n): FieldContainer {\n const existing = Object.hasOwn(container, segment) ? container[segment] : undefined\n if (existing === undefined) {\n const next: FieldContainer = Object.create(null)\n container[segment] = next\n return next\n }\n if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {\n throw new ApiUsageError(`Field \"${key}\" conflicts with an earlier field`)\n }\n return existing\n}\n\n/**\n * Descend into the array at `container[segment]`, returning the object\n * element the field at hand should land in: the current (last) element while\n * `nextKey` is new to it or accumulating a nested array, or a freshly\n * appended element once `nextKey` repeats (gh api semantics).\n */\nfunction descendIntoArray(\n container: FieldContainer,\n segment: string,\n nextKey: string,\n key: string,\n): FieldContainer {\n const existing = Object.hasOwn(container, segment) ? container[segment] : undefined\n if (existing !== undefined && !Array.isArray(existing)) {\n throw new ApiUsageError(`Field \"${key}\" conflicts with an earlier field`)\n }\n const values = Array.isArray(existing) ? existing : []\n if (existing === undefined) {\n container[segment] = values\n }\n\n const last = values.at(-1)\n if (\n typeof last === 'object' &&\n last !== null &&\n !Array.isArray(last) &&\n (!Object.hasOwn(last, nextKey) || Array.isArray(last[nextKey]))\n ) {\n return last\n }\n\n const next: FieldContainer = Object.create(null)\n values.push(next)\n return next\n}\n"],"names":["ApiUsageError","KEY_RE","fieldsToQuery","fields","query","Object","create","key","value","entries","Array","isArray","some","item","map","String","parseFields","options","rawFields","readFile","stdin","result","raw","splitField","setField","field","undefined","coerceValue","flag","input","flagName","separatorIndex","indexOf","endsWith","slice","context","test","Number","startsWith","source","error","message","Error","target","match","exec","head","brackets","path","part","matchAll","push","container","subkey","inArray","segment","descendIntoArray","descendIntoObject","existing","hasOwn","values","next","nextKey","last","at"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import { parse } from 'node:querystring';
2
+ import { API_DEFAULT_VERSION, API_VERSION_SEGMENT_RE } from './constants.js';
3
+ import { ApiUsageError, ProjectIdRequiredError } from './errors.js';
4
+ const PLACEHOLDER_RE = /^\{([^{}]+)\}$/;
5
+ const ALLOWED_URL_HOST_RE = /(?:^|\.)api\.sanity\.(?:io|work)$/;
6
+ /**
7
+ * Resolve the endpoint argument of `sanity api` into a concrete request
8
+ * target: substitute placeholders, split off the query string, determine the
9
+ * host family (via the generated OpenAPI routing manifest) and the API
10
+ * version.
11
+ *
12
+ * Throws {@link ProjectIdRequiredError} when the request needs a project ID
13
+ * that hasn't been provided - the command catches this to resolve one (flags,
14
+ * CLI config or interactive prompt) and retries.
15
+ */ export function resolveEndpoint(options) {
16
+ const { apiVersion, dataset, endpoint, forceHost, projectId, routes } = options;
17
+ if (/^https?:\/\//.test(endpoint)) {
18
+ return resolveUrlEndpoint(endpoint);
19
+ }
20
+ const [pathPart = '', ...queryParts] = endpoint.split('?');
21
+ const query = parseQueryString(queryParts.join('?'));
22
+ const segments = pathPart.split('/').filter((segment)=>segment !== '');
23
+ // An API version embedded in the path (eg `v2021-06-07/projects`) is peeled
24
+ // off and re-applied as the version segment of the final URL.
25
+ let embeddedVersion;
26
+ if (segments[0] === '{apiVersion}') {
27
+ segments.shift();
28
+ } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {
29
+ embeddedVersion = segments.shift();
30
+ }
31
+ // Checked after version peeling so a bare version (eg `v2025-02-19`) is
32
+ // also rejected as an empty path.
33
+ if (segments.length === 0) {
34
+ throw new ApiUsageError('Endpoint path is empty');
35
+ }
36
+ const unresolved = [];
37
+ const substituted = segments.map((segment, index)=>{
38
+ const placeholder = PLACEHOLDER_RE.exec(segment)?.[1];
39
+ if (!placeholder) return segment;
40
+ if (placeholder === 'projectId') {
41
+ if (!projectId) throw new ProjectIdRequiredError();
42
+ return projectId;
43
+ }
44
+ // The projects spec names the dataset placeholder `{name}` (eg
45
+ // `projects/{projectId}/datasets/{name}`) - only that context is a
46
+ // dataset; `{name}` elsewhere (eg video renditions) stays unresolved.
47
+ const isDatasetName = placeholder === 'name' && segments[index - 1] === 'datasets';
48
+ if (placeholder === 'dataset' || placeholder === 'datasetName' || isDatasetName) {
49
+ if (!dataset) {
50
+ throw new ApiUsageError(`Unable to resolve {${placeholder}} - provide a dataset with --dataset or configure one in sanity.cli.ts`);
51
+ }
52
+ return dataset;
53
+ }
54
+ unresolved.push(segment);
55
+ return segment;
56
+ });
57
+ if (unresolved.length > 0) {
58
+ throw new ApiUsageError(`Unable to resolve placeholder(s) ${unresolved.join(', ')} - replace them with actual values`);
59
+ }
60
+ // Forcing a host restricts matching to specs served on that host, so the
61
+ // matched spec's default API version still applies when one exists there.
62
+ const candidates = forceHost ? routes.filter((entry)=>entry.host === forceHost) : routes;
63
+ const match = matchRoutes(substituted, candidates);
64
+ const host = forceHost ?? match?.host ?? 'global';
65
+ if (host === 'project' && !projectId) {
66
+ throw new ProjectIdRequiredError();
67
+ }
68
+ return {
69
+ apiVersion: apiVersion ?? embeddedVersion ?? match?.defaultApiVersion ?? API_DEFAULT_VERSION,
70
+ host,
71
+ kind: 'path',
72
+ matchedSlug: match?.slug,
73
+ path: substituted.join('/'),
74
+ ...host === 'project' ? {
75
+ projectId
76
+ } : {},
77
+ query
78
+ };
79
+ }
80
+ function resolveUrlEndpoint(endpoint) {
81
+ let url;
82
+ try {
83
+ url = new URL(endpoint);
84
+ } catch {
85
+ throw new ApiUsageError(`Invalid URL "${endpoint}"`);
86
+ }
87
+ // Never put the token on the wire unencrypted
88
+ if (url.protocol !== 'https:') {
89
+ throw new ApiUsageError(`Refusing to send requests over "${url.protocol}//" - only https:// URLs are supported`);
90
+ }
91
+ if (!ALLOWED_URL_HOST_RE.test(url.hostname)) {
92
+ throw new ApiUsageError(`Refusing to send authenticated requests to "${url.hostname}" - only *.api.sanity.io hosts are supported`);
93
+ }
94
+ const query = parseQueryString(url.search.replace(/^\?/, ''));
95
+ url.search = '';
96
+ return {
97
+ kind: 'url',
98
+ query,
99
+ url: url.toString()
100
+ };
101
+ }
102
+ /**
103
+ * Find the route entry whose path pattern best matches the request segments.
104
+ *
105
+ * A pattern only matches when the request path consumes it completely, and
106
+ * segments are compared pairwise: literal matches score higher than
107
+ * placeholder matches, and a literal mismatch disqualifies the pattern. On
108
+ * equal scores across hosts the global host wins (APIs served on both hosts
109
+ * don't need a project ID there); on equal scores within the same host the
110
+ * first entry wins, keeping the result independent of later entries.
111
+ */ function matchRoutes(segments, routes) {
112
+ let best;
113
+ for (const entry of routes){
114
+ for (const pattern of entry.pathPatterns){
115
+ const score = scorePattern(segments, pattern.split('/'));
116
+ if (score === 0) continue;
117
+ const isBetter = !best || score > best.score || score === best.score && best.host === 'project' && entry.host === 'global';
118
+ if (isBetter) {
119
+ best = {
120
+ defaultApiVersion: entry.defaultApiVersion,
121
+ host: entry.host,
122
+ score,
123
+ slug: entry.slug
124
+ };
125
+ }
126
+ }
127
+ }
128
+ return best;
129
+ }
130
+ function scorePattern(segments, patternSegments) {
131
+ // A pattern only matches when the request path consumes it completely - a
132
+ // request that is a mere prefix of a longer pattern (eg `data` vs
133
+ // `data/query/{dataset}`) must not inherit that route's host or version.
134
+ if (patternSegments.length > segments.length) return 0;
135
+ let score = 0;
136
+ let literalMatches = 0;
137
+ for (const [index, patternSegment] of patternSegments.entries()){
138
+ if (PLACEHOLDER_RE.test(patternSegment)) {
139
+ score += 1;
140
+ } else if (patternSegment === segments[index]) {
141
+ score += 2;
142
+ literalMatches += 1;
143
+ } else {
144
+ return 0;
145
+ }
146
+ }
147
+ // Require at least one literal segment match so a placeholder-only pattern
148
+ // like `{resourceType}/{resourceId}` can't capture arbitrary paths.
149
+ return literalMatches === 0 ? 0 : score;
150
+ }
151
+ function parseQueryString(queryString) {
152
+ // node:querystring collects repeated keys into arrays and returns a
153
+ // null-prototype object, so user-supplied keys like `__proto__` stay
154
+ // ordinary own properties. Parsed values are never `undefined`, hence the
155
+ // narrowing cast.
156
+ return parse(queryString);
157
+ }
158
+
159
+ //# sourceMappingURL=resolveEndpoint.js.map