@liiift-studio/deploy-vercel-from-sanity 1.4.0 → 1.5.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 +105 -48
- package/dist/index.d.mts +35 -15
- package/dist/index.d.ts +35 -15
- package/dist/index.js +92 -7
- package/dist/index.mjs +92 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -291,12 +291,72 @@ HEAD, which is the same commit with the same author, so it is blocked
|
|
|
291
291
|
identically. The only way out is a new commit by an authorised author.
|
|
292
292
|
|
|
293
293
|
A Studio running in a browser holds no git credential, so it cannot make that
|
|
294
|
-
commit itself.
|
|
295
|
-
that does.
|
|
294
|
+
commit itself. `unblock` gives it two ways to borrow one.
|
|
296
295
|
|
|
297
|
-
###
|
|
296
|
+
### Choose a mode
|
|
298
297
|
|
|
299
|
-
|
|
298
|
+
| | `endpoint` — **preferred** | `token` — workflow dispatch |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| Needs a server | yes, one API route | no |
|
|
301
|
+
| GitHub credential in the bundle | **none** | one, `Actions: write` |
|
|
302
|
+
| Tokens to maintain | **1** | 2 |
|
|
303
|
+
| Workflow file on the default branch | not needed | required, or dispatch 404s |
|
|
304
|
+
|
|
305
|
+
Use `endpoint` if you have anywhere to put an API route. Its whole advantage is
|
|
306
|
+
that nothing sensitive reaches the browser, so there is only one credential and
|
|
307
|
+
it is a server secret like any other.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## `endpoint` mode
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
vercelDeploy({
|
|
315
|
+
unblock: {
|
|
316
|
+
endpoint: 'https://example.com/api/deploy-unblock',
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
That is the entire Studio-side configuration — note the absence of a token.
|
|
322
|
+
|
|
323
|
+
The Studio posts `{ ref, requestedBy }` to that URL with the signed-in user's
|
|
324
|
+
**Sanity session token** in an `Authorization: Bearer` header. Your route verifies
|
|
325
|
+
that token against Sanity, then makes the commit with its own server-held GitHub
|
|
326
|
+
token.
|
|
327
|
+
|
|
328
|
+
Requirements for the route:
|
|
329
|
+
|
|
330
|
+
- **Verify the session token.** Do not accept a shared secret instead — the bundle
|
|
331
|
+
is public, so a shared secret only moves the bar from "know the URL" to "open
|
|
332
|
+
devtools". Verify against
|
|
333
|
+
`https://<projectId>.api.sanity.io/v2021-06-07/users/me`. Note Sanity answers
|
|
334
|
+
`200` with a null `id` for an unauthenticated request rather than `401`.
|
|
335
|
+
- **Allow the Studio's origin.** A deployed Studio is on `*.sanity.studio`, not
|
|
336
|
+
your site's domain, so every call is cross-origin and the preflight fails
|
|
337
|
+
without an allow-list. Echo one allow-listed origin; do not send `*` to an
|
|
338
|
+
endpoint that takes an `Authorization` header.
|
|
339
|
+
- **Restrict which branches it will bump.** The recovery flow only ever needs the
|
|
340
|
+
branches you deploy.
|
|
341
|
+
- **Answer `{ error }` on failure.** The plugin shows that string to the editor
|
|
342
|
+
verbatim, in preference to its own generic message.
|
|
343
|
+
- **Serve it over https.** The plugin refuses a plaintext endpoint, because the
|
|
344
|
+
session token travels with the request. `localhost` is exempt for development.
|
|
345
|
+
|
|
346
|
+
The token forwarded is `client.config().token`. Sanity only exposes it under
|
|
347
|
+
token-based auth, so it can be absent under cookie-based login; the plugin
|
|
348
|
+
detects that and says so, rather than sending an anonymous request and letting
|
|
349
|
+
your route report it as a permissions failure.
|
|
350
|
+
|
|
351
|
+
A worked Next.js Pages Router implementation is in
|
|
352
|
+
[`docs/deploy-unblock-route.js`](docs/deploy-unblock-route.js).
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## `token` mode — workflow dispatch
|
|
357
|
+
|
|
358
|
+
For setups with no server. The Studio dispatches a GitHub Actions workflow, and
|
|
359
|
+
the workflow makes the commit.
|
|
300
360
|
|
|
301
361
|
```
|
|
302
362
|
Studio bundle ── unblock.token Actions: write, one repo
|
|
@@ -312,8 +372,7 @@ Handing the Studio a `Contents: write` token would be far simpler and is the
|
|
|
312
372
|
obvious first design. Do not: the Studio bundle is served publicly, so that token
|
|
313
373
|
would let anyone who can load the Studio push arbitrary commits to the production
|
|
314
374
|
repository — and the next build would run them. The dispatch token is scoped so
|
|
315
|
-
that the worst a leak permits is *running the bump workflow
|
|
316
|
-
version bump and a deploy.
|
|
375
|
+
that the worst a leak permits is *running the bump workflow*.
|
|
317
376
|
|
|
318
377
|
### What a leaked dispatch token can actually do
|
|
319
378
|
|
|
@@ -331,61 +390,60 @@ The shipped workflow therefore opens with a branch allowlist, and a `concurrency
|
|
|
331
390
|
group that serialises bumps per branch. Narrow the allowlist to the branches you
|
|
332
391
|
actually deploy. Rotate the token if it leaks.
|
|
333
392
|
|
|
334
|
-
###
|
|
393
|
+
### Setup
|
|
335
394
|
|
|
336
|
-
Copy [`docs/version-bump.yml`](docs/version-bump.yml) to
|
|
337
|
-
`.github/workflows/version-bump.yml
|
|
338
|
-
|
|
339
|
-
allowlist to the branches you deploy.
|
|
395
|
+
1. Copy [`docs/version-bump.yml`](docs/version-bump.yml) to
|
|
396
|
+
`.github/workflows/version-bump.yml`, set the git identity in it to the account
|
|
397
|
+
whose commits Vercel accepts, and narrow the branch allowlist.
|
|
340
398
|
|
|
341
|
-
> **The workflow file must exist on the repository's default branch**, and on
|
|
342
|
-
> every branch you deploy from. GitHub resolves a dispatch against the default
|
|
343
|
-
> branch's copy, then runs the copy on the requested ref. A file present only on
|
|
344
|
-
> `staging` returns a 404 — which the plugin reports in full, since GitHub uses
|
|
345
|
-
> the same 404 for "no such workflow" and "your token cannot see this repo".
|
|
399
|
+
> **The workflow file must exist on the repository's default branch**, and on
|
|
400
|
+
> every branch you deploy from. GitHub resolves a dispatch against the default
|
|
401
|
+
> branch's copy, then runs the copy on the requested ref. A file present only on
|
|
402
|
+
> `staging` returns a 404 — which the plugin reports in full, since GitHub uses
|
|
403
|
+
> the same 404 for "no such workflow" and "your token cannot see this repo".
|
|
346
404
|
|
|
347
|
-
|
|
405
|
+
2. Create a fine-grained token with **`Contents: write`** on that one repository
|
|
406
|
+
and save it as the repository Actions secret **`DEPLOY_COMMIT_TOKEN`**.
|
|
348
407
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
408
|
+
It must not be the default `GITHUB_TOKEN`: commits made with it are authored by
|
|
409
|
+
`github-actions[bot]`, which is not a team member either, so Vercel would block
|
|
410
|
+
the bump for the same reason it blocked the original commit.
|
|
352
411
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
the bump for the same reason it blocked the original commit.
|
|
412
|
+
3. Create a second fine-grained token scoped to **`Actions: write`** on the same
|
|
413
|
+
repository — and nothing else — and expose it to the Studio build:
|
|
356
414
|
|
|
357
|
-
|
|
415
|
+
```sh
|
|
416
|
+
SANITY_STUDIO_DEPLOY_UNBLOCK_GH_TOKEN=github_pat_…
|
|
417
|
+
```
|
|
358
418
|
|
|
359
|
-
|
|
360
|
-
repository — and nothing else — and expose it to the Studio build:
|
|
419
|
+
4. Configure the plugin:
|
|
361
420
|
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
|
|
421
|
+
```ts
|
|
422
|
+
vercelDeploy({
|
|
423
|
+
unblock: {
|
|
424
|
+
token: process.env.SANITY_STUDIO_DEPLOY_UNBLOCK_GH_TOKEN,
|
|
425
|
+
owner: 'your-org',
|
|
426
|
+
repo: 'your-site-repo',
|
|
427
|
+
workflow: 'version-bump.yml', // optional, this is the default
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
```
|
|
365
431
|
|
|
366
|
-
|
|
432
|
+
---
|
|
367
433
|
|
|
368
|
-
|
|
369
|
-
vercelDeploy({
|
|
370
|
-
unblock: {
|
|
371
|
-
token: process.env.SANITY_STUDIO_DEPLOY_UNBLOCK_GH_TOKEN,
|
|
372
|
-
owner: 'your-org',
|
|
373
|
-
repo: 'your-site-repo',
|
|
374
|
-
workflow: 'version-bump.yml', // optional, this is the default
|
|
375
|
-
},
|
|
376
|
-
})
|
|
377
|
-
```
|
|
434
|
+
## `unblock` reference
|
|
378
435
|
|
|
379
436
|
| Field | Required | Description |
|
|
380
437
|
|---|---|---|
|
|
381
|
-
| `
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
438
|
+
| `endpoint` | one of | URL of your site route. Wins when both modes are configured. Must be https. |
|
|
439
|
+
| `token` | one of | Fine-grained token, `Actions: write` on `repo` only. Ships in the Studio bundle — treat it as public. |
|
|
440
|
+
| `owner` | dispatch only | Repository owner, e.g. `your-org` |
|
|
441
|
+
| `repo` | dispatch only | Repository name |
|
|
384
442
|
| `workflow` | no | Workflow filename. Defaults to `version-bump.yml`. |
|
|
385
443
|
| `defaultRef` | no | Branch to bump when the blocked deployment names none. Normally unnecessary — the branch is read from the deployment being recovered. |
|
|
386
444
|
|
|
387
|
-
|
|
388
|
-
half-finished
|
|
445
|
+
A config with neither `endpoint` nor all of `token`/`owner`/`repo` is discarded,
|
|
446
|
+
so a half-finished setup renders no button rather than one that only errors.
|
|
389
447
|
|
|
390
448
|
### What the editor sees
|
|
391
449
|
|
|
@@ -394,9 +452,8 @@ not a general "deploy harder" control, and it is deliberately not offered when
|
|
|
394
452
|
the plugin has nothing to target.
|
|
395
453
|
|
|
396
454
|
Pressing it reports that the bump was **requested**. That is the honest claim:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
later. Polling picks it up on its own.
|
|
455
|
+
the commit is made, but Vercel still has to notice the push and build it, so the
|
|
456
|
+
new deployment turns up shortly after. Polling picks it up on its own.
|
|
400
457
|
|
|
401
458
|
### The real fix
|
|
402
459
|
|
package/dist/index.d.mts
CHANGED
|
@@ -84,37 +84,57 @@ type VercelDeployMode = 'direct' | 'proxy';
|
|
|
84
84
|
* commit's git author is not a member of the Vercel team.
|
|
85
85
|
*
|
|
86
86
|
* The Studio cannot fix this itself — the remedy is a commit by an authorised
|
|
87
|
-
* author, and a browser holds no git credential.
|
|
88
|
-
*
|
|
89
|
-
* in Actions secrets.
|
|
87
|
+
* author, and a browser holds no git credential. There are two ways to borrow
|
|
88
|
+
* one, and they are not equivalent:
|
|
90
89
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
90
|
+
* - **{@link endpoint} (preferred).** The Studio posts the signed-in user's
|
|
91
|
+
* Sanity session token to a route on your own site; the route verifies it and
|
|
92
|
+
* commits with its own server-held GitHub token. Nothing secret reaches the
|
|
93
|
+
* browser, there is one token to maintain, and there is no workflow file whose
|
|
94
|
+
* presence on the default branch you have to remember.
|
|
95
|
+
*
|
|
96
|
+
* - **{@link token}.** The Studio dispatches a GitHub Actions workflow directly.
|
|
97
|
+
* Needs no server, but puts a GitHub token in the bundle, which is public.
|
|
98
|
+
* Scope it to **Actions: write on the one repo** so the worst a leak permits is
|
|
99
|
+
* running that workflow, and never give it `Contents: write` — a public token
|
|
100
|
+
* that can push code is a public token that can run code on your next build.
|
|
101
|
+
*
|
|
102
|
+
* Set `endpoint` if you have anywhere to put a route. It wins when both are set.
|
|
95
103
|
*/
|
|
96
104
|
interface UnblockConfig {
|
|
105
|
+
/**
|
|
106
|
+
* URL of a site API route that performs the bump server-side. **Preferred.**
|
|
107
|
+
*
|
|
108
|
+
* In this mode the Studio carries no GitHub credential at all. It posts the
|
|
109
|
+
* signed-in user's Sanity session token, the route verifies it against Sanity,
|
|
110
|
+
* and the route's own server-held GitHub token makes the commit. One token,
|
|
111
|
+
* never public, and no workflow file to keep on the default branch.
|
|
112
|
+
*
|
|
113
|
+
* Takes precedence over {@link token} when both are set.
|
|
114
|
+
*/
|
|
115
|
+
endpoint?: string;
|
|
97
116
|
/**
|
|
98
117
|
* Fine-grained GitHub token, scoped to `Actions: write` on {@link repo} alone.
|
|
118
|
+
* Only used when {@link endpoint} is not set.
|
|
99
119
|
*
|
|
100
120
|
* Compiled into the Studio bundle, so anyone who can load the Studio can read
|
|
101
121
|
* it and dispatch the workflow. Never give it `Contents: write` — that would
|
|
102
|
-
* let a reader push arbitrary commits to the production repo.
|
|
103
|
-
*
|
|
104
|
-
* Leave unset to hide the button entirely.
|
|
122
|
+
* let a reader push arbitrary commits to the production repo. If you have a
|
|
123
|
+
* server to put a route on, prefer {@link endpoint} and avoid this entirely.
|
|
105
124
|
*/
|
|
106
125
|
token?: string;
|
|
107
|
-
/** Repository owner, e.g. `
|
|
108
|
-
owner
|
|
109
|
-
/** Repository name
|
|
110
|
-
repo
|
|
126
|
+
/** Repository owner, e.g. `your-org`. Required for workflow-dispatch mode only. */
|
|
127
|
+
owner?: string;
|
|
128
|
+
/** Repository name. Required for workflow-dispatch mode only. */
|
|
129
|
+
repo?: string;
|
|
111
130
|
/**
|
|
112
131
|
* Workflow filename to dispatch. Defaults to `version-bump.yml`.
|
|
132
|
+
* Workflow-dispatch mode only.
|
|
113
133
|
*
|
|
114
134
|
* GitHub resolves a dispatch against the workflow file **on the repository's
|
|
115
135
|
* default branch**, then runs the copy on the requested ref — so the file must
|
|
116
136
|
* exist on the default branch as well as on every branch you deploy from, or
|
|
117
|
-
* the dispatch returns 404.
|
|
137
|
+
* the dispatch returns 404. `endpoint` mode has no such constraint.
|
|
118
138
|
*/
|
|
119
139
|
workflow?: string;
|
|
120
140
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -84,37 +84,57 @@ type VercelDeployMode = 'direct' | 'proxy';
|
|
|
84
84
|
* commit's git author is not a member of the Vercel team.
|
|
85
85
|
*
|
|
86
86
|
* The Studio cannot fix this itself — the remedy is a commit by an authorised
|
|
87
|
-
* author, and a browser holds no git credential.
|
|
88
|
-
*
|
|
89
|
-
* in Actions secrets.
|
|
87
|
+
* author, and a browser holds no git credential. There are two ways to borrow
|
|
88
|
+
* one, and they are not equivalent:
|
|
90
89
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
90
|
+
* - **{@link endpoint} (preferred).** The Studio posts the signed-in user's
|
|
91
|
+
* Sanity session token to a route on your own site; the route verifies it and
|
|
92
|
+
* commits with its own server-held GitHub token. Nothing secret reaches the
|
|
93
|
+
* browser, there is one token to maintain, and there is no workflow file whose
|
|
94
|
+
* presence on the default branch you have to remember.
|
|
95
|
+
*
|
|
96
|
+
* - **{@link token}.** The Studio dispatches a GitHub Actions workflow directly.
|
|
97
|
+
* Needs no server, but puts a GitHub token in the bundle, which is public.
|
|
98
|
+
* Scope it to **Actions: write on the one repo** so the worst a leak permits is
|
|
99
|
+
* running that workflow, and never give it `Contents: write` — a public token
|
|
100
|
+
* that can push code is a public token that can run code on your next build.
|
|
101
|
+
*
|
|
102
|
+
* Set `endpoint` if you have anywhere to put a route. It wins when both are set.
|
|
95
103
|
*/
|
|
96
104
|
interface UnblockConfig {
|
|
105
|
+
/**
|
|
106
|
+
* URL of a site API route that performs the bump server-side. **Preferred.**
|
|
107
|
+
*
|
|
108
|
+
* In this mode the Studio carries no GitHub credential at all. It posts the
|
|
109
|
+
* signed-in user's Sanity session token, the route verifies it against Sanity,
|
|
110
|
+
* and the route's own server-held GitHub token makes the commit. One token,
|
|
111
|
+
* never public, and no workflow file to keep on the default branch.
|
|
112
|
+
*
|
|
113
|
+
* Takes precedence over {@link token} when both are set.
|
|
114
|
+
*/
|
|
115
|
+
endpoint?: string;
|
|
97
116
|
/**
|
|
98
117
|
* Fine-grained GitHub token, scoped to `Actions: write` on {@link repo} alone.
|
|
118
|
+
* Only used when {@link endpoint} is not set.
|
|
99
119
|
*
|
|
100
120
|
* Compiled into the Studio bundle, so anyone who can load the Studio can read
|
|
101
121
|
* it and dispatch the workflow. Never give it `Contents: write` — that would
|
|
102
|
-
* let a reader push arbitrary commits to the production repo.
|
|
103
|
-
*
|
|
104
|
-
* Leave unset to hide the button entirely.
|
|
122
|
+
* let a reader push arbitrary commits to the production repo. If you have a
|
|
123
|
+
* server to put a route on, prefer {@link endpoint} and avoid this entirely.
|
|
105
124
|
*/
|
|
106
125
|
token?: string;
|
|
107
|
-
/** Repository owner, e.g. `
|
|
108
|
-
owner
|
|
109
|
-
/** Repository name
|
|
110
|
-
repo
|
|
126
|
+
/** Repository owner, e.g. `your-org`. Required for workflow-dispatch mode only. */
|
|
127
|
+
owner?: string;
|
|
128
|
+
/** Repository name. Required for workflow-dispatch mode only. */
|
|
129
|
+
repo?: string;
|
|
111
130
|
/**
|
|
112
131
|
* Workflow filename to dispatch. Defaults to `version-bump.yml`.
|
|
132
|
+
* Workflow-dispatch mode only.
|
|
113
133
|
*
|
|
114
134
|
* GitHub resolves a dispatch against the workflow file **on the repository's
|
|
115
135
|
* default branch**, then runs the copy on the requested ref — so the file must
|
|
116
136
|
* exist on the default branch as well as on every branch you deploy from, or
|
|
117
|
-
* the dispatch returns 404.
|
|
137
|
+
* the dispatch returns 404. `endpoint` mode has no such constraint.
|
|
118
138
|
*/
|
|
119
139
|
workflow?: string;
|
|
120
140
|
/**
|
package/dist/index.js
CHANGED
|
@@ -102,6 +102,11 @@ var import_react2 = require("react");
|
|
|
102
102
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
103
103
|
var DEFAULTS = { mode: "direct" };
|
|
104
104
|
var ConfigContext = (0, import_react2.createContext)(DEFAULTS);
|
|
105
|
+
function isUsableUnblock(unblock) {
|
|
106
|
+
if (!unblock) return false;
|
|
107
|
+
if (unblock.endpoint) return true;
|
|
108
|
+
return Boolean(unblock.token && unblock.owner && unblock.repo);
|
|
109
|
+
}
|
|
105
110
|
function resolveConfig(options) {
|
|
106
111
|
const config = options ?? {};
|
|
107
112
|
return {
|
|
@@ -109,9 +114,11 @@ function resolveConfig(options) {
|
|
|
109
114
|
// Trailing slashes would double up when request paths are appended.
|
|
110
115
|
proxyUrl: config.proxyUrl?.replace(/\/+$/, ""),
|
|
111
116
|
statusKey: config.statusKey,
|
|
112
|
-
// Dropped unless it can actually be used
|
|
113
|
-
//
|
|
114
|
-
|
|
117
|
+
// Dropped unless it can actually be used, so an incomplete configuration renders
|
|
118
|
+
// no button rather than one whose only outcome is an error. Either mode will do:
|
|
119
|
+
// an `endpoint` needs nothing else, while workflow dispatch needs all three of
|
|
120
|
+
// token, owner and repo to build an authenticated request.
|
|
121
|
+
unblock: isUsableUnblock(config.unblock) ? config.unblock : void 0
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
124
|
function ConfigProvider({ value, children }) {
|
|
@@ -760,6 +767,78 @@ function dispatchErrorMessage(status, workflow, ref) {
|
|
|
760
767
|
}
|
|
761
768
|
}
|
|
762
769
|
|
|
770
|
+
// src/lib/unblock.ts
|
|
771
|
+
function assertSafeEndpoint(endpoint) {
|
|
772
|
+
let parsed;
|
|
773
|
+
try {
|
|
774
|
+
parsed = new URL(endpoint);
|
|
775
|
+
} catch {
|
|
776
|
+
throw new Error(`The configured recovery endpoint is not a valid URL: "${endpoint}".`);
|
|
777
|
+
}
|
|
778
|
+
const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
779
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
780
|
+
throw new Error("The recovery endpoint must be https \u2014 the Studio session token is sent with the request.");
|
|
781
|
+
}
|
|
782
|
+
return parsed;
|
|
783
|
+
}
|
|
784
|
+
async function postToEndpoint(opts) {
|
|
785
|
+
const { config, ref, requestedBy, studioToken } = opts;
|
|
786
|
+
const endpoint = assertSafeEndpoint(config.endpoint);
|
|
787
|
+
if (!studioToken) {
|
|
788
|
+
throw new Error(
|
|
789
|
+
"Your Studio session token is not available, so the site cannot verify who you are. This happens under cookie-based login \u2014 sign out and back in, or ask a developer to deploy manually."
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
let res;
|
|
793
|
+
try {
|
|
794
|
+
res = await fetch(endpoint.toString(), {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: {
|
|
797
|
+
"Content-Type": "application/json",
|
|
798
|
+
Authorization: `Bearer ${studioToken}`
|
|
799
|
+
},
|
|
800
|
+
body: JSON.stringify({ ref, requestedBy })
|
|
801
|
+
});
|
|
802
|
+
} catch {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`Could not reach ${endpoint.host}. Check the site is up and that the route allows requests from this Studio's origin.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (res.ok) return;
|
|
808
|
+
let detail = "";
|
|
809
|
+
try {
|
|
810
|
+
const body = await res.json();
|
|
811
|
+
if (typeof body?.error === "string") detail = body.error;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
throw new Error(detail || endpointErrorMessage(res.status, endpoint.host));
|
|
815
|
+
}
|
|
816
|
+
function endpointErrorMessage(status, host) {
|
|
817
|
+
switch (status) {
|
|
818
|
+
case 401:
|
|
819
|
+
return "The site did not accept your Studio session. Try signing out of the Studio and back in.";
|
|
820
|
+
case 403:
|
|
821
|
+
return "Your Studio account is not permitted to trigger a deploy recovery.";
|
|
822
|
+
case 404:
|
|
823
|
+
return `No recovery route at ${host}. It may not be deployed yet \u2014 check the endpoint URL.`;
|
|
824
|
+
case 429:
|
|
825
|
+
return "A bump was requested very recently. Wait a moment before trying again.";
|
|
826
|
+
default:
|
|
827
|
+
return status >= 500 ? "The site failed while making the bump commit. Check its function logs." : `The site refused the request (${status}).`;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function requestUnblock(opts) {
|
|
831
|
+
if (opts.config.endpoint) return postToEndpoint(opts);
|
|
832
|
+
if (!opts.config.owner || !opts.config.repo) {
|
|
833
|
+
throw new Error("Deploy recovery is misconfigured \u2014 set either `endpoint`, or `owner` and `repo`.");
|
|
834
|
+
}
|
|
835
|
+
return dispatchVersionBump({
|
|
836
|
+
config: opts.config,
|
|
837
|
+
ref: opts.ref,
|
|
838
|
+
requestedBy: opts.requestedBy
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
763
842
|
// src/components/DeployItem.tsx
|
|
764
843
|
var import_sanity = require("sanity");
|
|
765
844
|
|
|
@@ -1158,10 +1237,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1158
1237
|
setBumping(true);
|
|
1159
1238
|
setBumpResult(null);
|
|
1160
1239
|
try {
|
|
1161
|
-
await
|
|
1240
|
+
await requestUnblock({
|
|
1162
1241
|
config: unblockConfig,
|
|
1163
1242
|
ref,
|
|
1164
|
-
requestedBy: currentUser?.name || currentUser?.email || void 0
|
|
1243
|
+
requestedBy: currentUser?.name || currentUser?.email || void 0,
|
|
1244
|
+
// Forwarded so a site endpoint can verify the caller is a signed-in project
|
|
1245
|
+
// user. Sanity only exposes this under token-based auth, so it can be
|
|
1246
|
+
// absent; `requestUnblock` reports that as its own case rather than
|
|
1247
|
+
// letting the server see an anonymous request and call it a permissions
|
|
1248
|
+
// failure. Unused by workflow-dispatch mode.
|
|
1249
|
+
studioToken: client.config().token
|
|
1165
1250
|
});
|
|
1166
1251
|
setBumpResult({
|
|
1167
1252
|
ok: true,
|
|
@@ -1179,7 +1264,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1179
1264
|
} finally {
|
|
1180
1265
|
setBumping(false);
|
|
1181
1266
|
}
|
|
1182
|
-
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast]);
|
|
1267
|
+
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast, client]);
|
|
1183
1268
|
const branch = latest?.meta?.githubCommitRef;
|
|
1184
1269
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
1185
1270
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
@@ -1785,7 +1870,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
1785
1870
|
}
|
|
1786
1871
|
|
|
1787
1872
|
// src/version.ts
|
|
1788
|
-
var VERSION = "1.
|
|
1873
|
+
var VERSION = "1.5.0";
|
|
1789
1874
|
|
|
1790
1875
|
// src/components/DeployTool.tsx
|
|
1791
1876
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
package/dist/index.mjs
CHANGED
|
@@ -67,6 +67,11 @@ import { createContext, useContext } from "react";
|
|
|
67
67
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
68
68
|
var DEFAULTS = { mode: "direct" };
|
|
69
69
|
var ConfigContext = createContext(DEFAULTS);
|
|
70
|
+
function isUsableUnblock(unblock) {
|
|
71
|
+
if (!unblock) return false;
|
|
72
|
+
if (unblock.endpoint) return true;
|
|
73
|
+
return Boolean(unblock.token && unblock.owner && unblock.repo);
|
|
74
|
+
}
|
|
70
75
|
function resolveConfig(options) {
|
|
71
76
|
const config = options ?? {};
|
|
72
77
|
return {
|
|
@@ -74,9 +79,11 @@ function resolveConfig(options) {
|
|
|
74
79
|
// Trailing slashes would double up when request paths are appended.
|
|
75
80
|
proxyUrl: config.proxyUrl?.replace(/\/+$/, ""),
|
|
76
81
|
statusKey: config.statusKey,
|
|
77
|
-
// Dropped unless it can actually be used
|
|
78
|
-
//
|
|
79
|
-
|
|
82
|
+
// Dropped unless it can actually be used, so an incomplete configuration renders
|
|
83
|
+
// no button rather than one whose only outcome is an error. Either mode will do:
|
|
84
|
+
// an `endpoint` needs nothing else, while workflow dispatch needs all three of
|
|
85
|
+
// token, owner and repo to build an authenticated request.
|
|
86
|
+
unblock: isUsableUnblock(config.unblock) ? config.unblock : void 0
|
|
80
87
|
};
|
|
81
88
|
}
|
|
82
89
|
function ConfigProvider({ value, children }) {
|
|
@@ -725,6 +732,78 @@ function dispatchErrorMessage(status, workflow, ref) {
|
|
|
725
732
|
}
|
|
726
733
|
}
|
|
727
734
|
|
|
735
|
+
// src/lib/unblock.ts
|
|
736
|
+
function assertSafeEndpoint(endpoint) {
|
|
737
|
+
let parsed;
|
|
738
|
+
try {
|
|
739
|
+
parsed = new URL(endpoint);
|
|
740
|
+
} catch {
|
|
741
|
+
throw new Error(`The configured recovery endpoint is not a valid URL: "${endpoint}".`);
|
|
742
|
+
}
|
|
743
|
+
const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
744
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
745
|
+
throw new Error("The recovery endpoint must be https \u2014 the Studio session token is sent with the request.");
|
|
746
|
+
}
|
|
747
|
+
return parsed;
|
|
748
|
+
}
|
|
749
|
+
async function postToEndpoint(opts) {
|
|
750
|
+
const { config, ref, requestedBy, studioToken } = opts;
|
|
751
|
+
const endpoint = assertSafeEndpoint(config.endpoint);
|
|
752
|
+
if (!studioToken) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
"Your Studio session token is not available, so the site cannot verify who you are. This happens under cookie-based login \u2014 sign out and back in, or ask a developer to deploy manually."
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
let res;
|
|
758
|
+
try {
|
|
759
|
+
res = await fetch(endpoint.toString(), {
|
|
760
|
+
method: "POST",
|
|
761
|
+
headers: {
|
|
762
|
+
"Content-Type": "application/json",
|
|
763
|
+
Authorization: `Bearer ${studioToken}`
|
|
764
|
+
},
|
|
765
|
+
body: JSON.stringify({ ref, requestedBy })
|
|
766
|
+
});
|
|
767
|
+
} catch {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`Could not reach ${endpoint.host}. Check the site is up and that the route allows requests from this Studio's origin.`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
if (res.ok) return;
|
|
773
|
+
let detail = "";
|
|
774
|
+
try {
|
|
775
|
+
const body = await res.json();
|
|
776
|
+
if (typeof body?.error === "string") detail = body.error;
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
throw new Error(detail || endpointErrorMessage(res.status, endpoint.host));
|
|
780
|
+
}
|
|
781
|
+
function endpointErrorMessage(status, host) {
|
|
782
|
+
switch (status) {
|
|
783
|
+
case 401:
|
|
784
|
+
return "The site did not accept your Studio session. Try signing out of the Studio and back in.";
|
|
785
|
+
case 403:
|
|
786
|
+
return "Your Studio account is not permitted to trigger a deploy recovery.";
|
|
787
|
+
case 404:
|
|
788
|
+
return `No recovery route at ${host}. It may not be deployed yet \u2014 check the endpoint URL.`;
|
|
789
|
+
case 429:
|
|
790
|
+
return "A bump was requested very recently. Wait a moment before trying again.";
|
|
791
|
+
default:
|
|
792
|
+
return status >= 500 ? "The site failed while making the bump commit. Check its function logs." : `The site refused the request (${status}).`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async function requestUnblock(opts) {
|
|
796
|
+
if (opts.config.endpoint) return postToEndpoint(opts);
|
|
797
|
+
if (!opts.config.owner || !opts.config.repo) {
|
|
798
|
+
throw new Error("Deploy recovery is misconfigured \u2014 set either `endpoint`, or `owner` and `repo`.");
|
|
799
|
+
}
|
|
800
|
+
return dispatchVersionBump({
|
|
801
|
+
config: opts.config,
|
|
802
|
+
ref: opts.ref,
|
|
803
|
+
requestedBy: opts.requestedBy
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
728
807
|
// src/components/DeployItem.tsx
|
|
729
808
|
import { useClient, useCurrentUser } from "sanity";
|
|
730
809
|
|
|
@@ -1123,10 +1202,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1123
1202
|
setBumping(true);
|
|
1124
1203
|
setBumpResult(null);
|
|
1125
1204
|
try {
|
|
1126
|
-
await
|
|
1205
|
+
await requestUnblock({
|
|
1127
1206
|
config: unblockConfig,
|
|
1128
1207
|
ref,
|
|
1129
|
-
requestedBy: currentUser?.name || currentUser?.email || void 0
|
|
1208
|
+
requestedBy: currentUser?.name || currentUser?.email || void 0,
|
|
1209
|
+
// Forwarded so a site endpoint can verify the caller is a signed-in project
|
|
1210
|
+
// user. Sanity only exposes this under token-based auth, so it can be
|
|
1211
|
+
// absent; `requestUnblock` reports that as its own case rather than
|
|
1212
|
+
// letting the server see an anonymous request and call it a permissions
|
|
1213
|
+
// failure. Unused by workflow-dispatch mode.
|
|
1214
|
+
studioToken: client.config().token
|
|
1130
1215
|
});
|
|
1131
1216
|
setBumpResult({
|
|
1132
1217
|
ok: true,
|
|
@@ -1144,7 +1229,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
1144
1229
|
} finally {
|
|
1145
1230
|
setBumping(false);
|
|
1146
1231
|
}
|
|
1147
|
-
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast]);
|
|
1232
|
+
}, [pluginConfig.unblock, latest?.meta?.githubCommitRef, currentUser, target.name, toast, client]);
|
|
1148
1233
|
const branch = latest?.meta?.githubCommitRef;
|
|
1149
1234
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
1150
1235
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
@@ -1750,7 +1835,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
1750
1835
|
}
|
|
1751
1836
|
|
|
1752
1837
|
// src/version.ts
|
|
1753
|
-
var VERSION = "1.
|
|
1838
|
+
var VERSION = "1.5.0";
|
|
1754
1839
|
|
|
1755
1840
|
// src/components/DeployTool.tsx
|
|
1756
1841
|
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports Studio v3.30 through v6.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|