@sanity/preview-url-secret
Advanced tools
Comparing version 1.5.5 to 1.6.0
@@ -15,2 +15,9 @@ # Changelog | ||
## [1.6.0](https://github.com/sanity-io/visual-editing/compare/preview-url-secret-v1.5.5...preview-url-secret-v1.6.0) (2024-02-02) | ||
### Features | ||
* add `studioOrigin` to `validatePreviewUrl` ([#818](https://github.com/sanity-io/visual-editing/issues/818)) ([a1ba977](https://github.com/sanity-io/visual-editing/commit/a1ba977a5fb38ff383d2d14f24dfa31e2a613c32)) | ||
## [1.5.5](https://github.com/sanity-io/visual-editing/compare/preview-url-secret-v1.5.4...preview-url-secret-v1.5.5) (2024-01-29) | ||
@@ -17,0 +24,0 @@ |
@@ -1,1 +0,1 @@ | ||
import{uuid as t}from"@sanity/uuid";import{a as e,S as a,s as r,c as n,t as o,d as c}from"./_chunks/constants-xlbGwjKx.js";async function s(s,i,p,u,l=t()){const d=s.withConfig({apiVersion:e});try{const t=new Date(Date.now()+1e3*a),e="".concat(r,".").concat(l),c=function(){if("undefined"!=typeof crypto){const t=new Uint8Array(16);crypto.getRandomValues(t);let e="";for(let a=0;a<t.length;a++)e+=t[a].toString(16).padStart(2,"0");return e=btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/[=]+$/,""),e}return Math.random().toString(36).slice(2)}(),s=d.patch(e).set({secret:c,source:i,studioUrl:p,userId:u});return await d.transaction().createOrReplace({_id:e,_type:n}).patch(s).commit({tag:o}),{secret:c,expiresAt:t}}finally{await d.delete({query:c})}}export{s as createPreviewSecret};//# sourceMappingURL=create-secret.js.map | ||
import{uuid as t}from"@sanity/uuid";import{a as e,S as a,s as r,c as n,t as o,d as c}from"./_chunks/constants-mg9CwKOs.js";async function s(s,i,p,u,l=t()){const d=s.withConfig({apiVersion:e});try{const t=new Date(Date.now()+1e3*a),e="".concat(r,".").concat(l),c=function(){if("undefined"!=typeof crypto){const t=new Uint8Array(16);crypto.getRandomValues(t);let e="";for(let a=0;a<t.length;a++)e+=t[a].toString(16).padStart(2,"0");return e=btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/[=]+$/,""),e}return Math.random().toString(36).slice(2)}(),s=d.patch(e).set({secret:c,source:i,studioUrl:p,userId:u});return await d.transaction().createOrReplace({_id:e,_type:n}).patch(s).commit({tag:o}),{secret:c,expiresAt:t}}finally{await d.delete({query:c})}}export{s as createPreviewSecret};//# sourceMappingURL=create-secret.js.map |
@@ -1,1 +0,1 @@ | ||
import{u as e,b as t}from"./_chunks/constants-xlbGwjKx.js";function n(n){const{draftMode:a,origin:r=("undefined"==typeof location?"https://localhost":location.origin)}=n;let{preview:o="/"}=n;const i=new URL(o,r),c=a.enable?new URL(a.enable,r):void 0;return async a=>{try{if(a.previewSearchParam){const e=new URL(a.previewSearchParam,i);e.origin===i.origin&&(o="".concat(e.pathname).concat(e.search))}else if(a.referrer){const e=new URL(a.referrer);e.origin===i.origin&&(o="".concat(e.pathname).concat(e.search))}}catch{}"undefined"!=typeof location&&location.origin===i.origin&&a.studioBasePath&&(o.startsWith("".concat(a.studioBasePath,"/"))||o===a.studioBasePath)&&(o=n.preview||"/");const r=new URL(o,i);if(c){const n=new URL(c),{searchParams:o}=n;return o.set(e,a.previewUrlSecret),r.pathname!==n.pathname&&o.set(t,"".concat(r.pathname).concat(r.search)),n.toString()}return r.toString()}}export{n as definePreviewUrl};//# sourceMappingURL=define-preview-url.js.map | ||
import{u as e,b as t}from"./_chunks/constants-mg9CwKOs.js";function n(n){const{draftMode:a,origin:r=("undefined"==typeof location?"https://localhost":location.origin)}=n;let{preview:o="/"}=n;const i=new URL(o,r),c=a.enable?new URL(a.enable,r):void 0;return async a=>{try{if(a.previewSearchParam){const e=new URL(a.previewSearchParam,i);e.origin===i.origin&&(o="".concat(e.pathname).concat(e.search))}else if(a.referrer){const e=new URL(a.referrer);e.origin===i.origin&&(o="".concat(e.pathname).concat(e.search))}}catch{}"undefined"!=typeof location&&location.origin===i.origin&&a.studioBasePath&&(o.startsWith("".concat(a.studioBasePath,"/"))||o===a.studioBasePath)&&(o=n.preview||"/");const r=new URL(o,i);if(c){const n=new URL(c),{searchParams:o}=n;return o.set(e,a.previewUrlSecret),r.pathname!==n.pathname&&o.set(t,"".concat(r.pathname).concat(r.search)),n.toString()}return r.toString()}}export{n as definePreviewUrl};//# sourceMappingURL=define-preview-url.js.map |
@@ -1,1 +0,1 @@ | ||
import{b as r}from"./_chunks/constants-xlbGwjKx.js";function s(s){return s.searchParams.has(r)?new URL(s.searchParams.get(r),s.origin):s}export{s as getRedirectTo};//# sourceMappingURL=get-redirect-to.js.map | ||
import{b as r}from"./_chunks/constants-mg9CwKOs.js";function s(s){return s.searchParams.has(r)?new URL(s.searchParams.get(r),s.origin):s}export{s as getRedirectTo};//# sourceMappingURL=get-redirect-to.js.map |
@@ -10,2 +10,6 @@ /** | ||
redirectTo?: string | ||
/** | ||
* If the URL is valid, and the studior URL is known and valid, then its origin will be here | ||
*/ | ||
studioOrigin?: string | ||
} | ||
@@ -50,3 +54,3 @@ | ||
/** | ||
* @alpha | ||
* @public | ||
*/ | ||
@@ -56,2 +60,5 @@ export declare function validatePreviewUrl( | ||
previewUrl: string, | ||
/** | ||
* @deprecated - this option is automatically determined based on the environment | ||
*/ | ||
disableCacheNoStore?: boolean, | ||
@@ -58,0 +65,0 @@ ): Promise<PreviewUrlValidateUrlResult> |
@@ -1,1 +0,1 @@ | ||
import{a as e,u as t,b as r,f as n,t as i,i as o}from"./_chunks/constants-xlbGwjKx.js";async function s(s,c,a){const u=function(t){if(!t)throw new TypeError("`client` is required");if(!t.config().token)throw new TypeError("`client` must have a `token` specified");return t.withConfig({apiVersion:e,useCdn:!1,perspective:"published",resultSourceMap:!1,stega:!1})}(s);let l;try{l=function(e){const n=new URL(e,"http://localhost"),i=n.searchParams.get(t);if(!i)throw new Error("Missing secret");let o;const s=n.searchParams.get(r);if(s){const{pathname:e,search:t}=new URL(s,"http://localhost");o="".concat(e).concat(t)}return{secret:i,redirectTo:o}}(c)}catch(e){return o&&console.error("Failed to parse preview URL",e,{previewUrl:c,client:u}),{isValid:!1}}const d=await async function(e,t,r){if("undefined"!=typeof EdgeRuntime&&await new Promise((e=>setTimeout(e,300))),!t||!t.trim())return!1;const o=await e.fetch(n,{secret:t},{tag:i,...r?void 0:{cache:"no-store"}});return!!((null==o?void 0:o._id)&&(null==o?void 0:o._updatedAt)&&(null==o?void 0:o.secret))&&t===o.secret}(u,l.secret,a);return{isValid:d,redirectTo:d?l.redirectTo:void 0}}export{r as urlSearchParamPreviewPathname,t as urlSearchParamPreviewSecret,s as validatePreviewUrl};//# sourceMappingURL=index.js.map | ||
import{a as e,u as t,b as r,f as i,t as o,i as s}from"./_chunks/constants-mg9CwKOs.js";async function n(n,a,c="Cloudflare-Workers"===(e=>null==(e=globalThis.navigator)?void 0:e.userAgent)()){const l=function(t){if(!t)throw new TypeError("`client` is required");if(!t.config().token)throw new TypeError("`client` must have a `token` specified");return t.withConfig({apiVersion:e,useCdn:!1,perspective:"published",resultSourceMap:!1,stega:!1})}(n);let u;try{u=function(e){const i=new URL(e,"http://localhost"),o=i.searchParams.get(t);if(!o)throw new Error("Missing secret");let s;const n=i.searchParams.get(r);if(n){const{pathname:e,search:t}=new URL(n,"http://localhost");s="".concat(e).concat(t)}return{secret:o,redirectTo:s}}(a)}catch(e){return s&&console.error("Failed to parse preview URL",e,{previewUrl:a,client:l}),{isValid:!1}}const{isValid:d,studioUrl:h}=await async function(e,t,r){if("undefined"!=typeof EdgeRuntime&&await new Promise((e=>setTimeout(e,300))),!t||!t.trim())return{isValid:!1,studioUrl:null};const s=await e.fetch(i,{secret:t},{tag:o,...r?void 0:{cache:"no-store"}});return(null==s?void 0:s._id)&&(null==s?void 0:s._updatedAt)&&(null==s?void 0:s.secret)?{isValid:t===s.secret,studioUrl:s.studioUrl}:{isValid:!1,studioUrl:null}}(l,u.secret,c),p=d?u.redirectTo:void 0;let f;if(d)try{f=new URL(h).origin}catch(e){s&&console.error("Failed to parse studioUrl",e,{previewUrl:a,studioUrl:h})}return{isValid:d,redirectTo:p,studioOrigin:f}}export{r as urlSearchParamPreviewPathname,t as urlSearchParamPreviewSecret,n as validatePreviewUrl};//# sourceMappingURL=index.js.map |
@@ -1,1 +0,1 @@ | ||
import{defineType as e,definePlugin as t}from"sanity";import{c as i,S as n}from"./_chunks/constants-xlbGwjKx.js";import{LockIcon as s,CloseCircleIcon as o,CheckmarkCircleIcon as r}from"@sanity/icons";const a=e({type:"document",icon:s,name:i,title:"@sanity/preview-url-secret",readOnly:!0,fields:[{type:"string",name:"secret",title:"Secret"},{type:"string",name:"source",title:"Source Tool"},{type:"string",name:"studioUrl",title:"Studio URL"},{type:"string",name:"userId",title:"Sanity User ID"}],preview:{select:{source:"source",studioUrl:"studioUrl",updatedAt:"_updatedAt"},prepare(e){const t=e.studioUrl?new URL(e.studioUrl,location.origin):void 0,i=new Date(e.updatedAt).getTime(),s=new Date(i+1e3*n),a=s<new Date,c=a?o:r;return{title:t?"".concat(t.host).concat(t.pathname):e.source,subtitle:a?"Expired":"Expires in ".concat(Math.round((s.getTime()-Date.now())/6e4)," minutes"),media:c}}}}),c=t((()=>({name:"sanity-plugin-debug-secrets",schema:{types:[a]},document:{actions:(e,t)=>t.schemaType!==i?e:e.filter((({action:e})=>"delete"===e)),inspectors:(e,t)=>t.documentType!==i?e:[],unstable_fieldActions:(e,t)=>t.schemaType.name!==i?e:[]}})));export{c as debugSecrets};//# sourceMappingURL=sanity-plugin-debug-secrets.js.map | ||
import{defineType as e,definePlugin as t}from"sanity";import{c as i,S as n}from"./_chunks/constants-mg9CwKOs.js";import{LockIcon as s,CloseCircleIcon as o,CheckmarkCircleIcon as r}from"@sanity/icons";const a=e({type:"document",icon:s,name:i,title:"@sanity/preview-url-secret",readOnly:!0,fields:[{type:"string",name:"secret",title:"Secret"},{type:"string",name:"source",title:"Source Tool"},{type:"string",name:"studioUrl",title:"Studio URL"},{type:"string",name:"userId",title:"Sanity User ID"}],preview:{select:{source:"source",studioUrl:"studioUrl",updatedAt:"_updatedAt"},prepare(e){const t=e.studioUrl?new URL(e.studioUrl,location.origin):void 0,i=new Date(e.updatedAt).getTime(),s=new Date(i+1e3*n),a=s<new Date,c=a?o:r;return{title:t?"".concat(t.host).concat(t.pathname):e.source,subtitle:a?"Expired":"Expires in ".concat(Math.round((s.getTime()-Date.now())/6e4)," minutes"),media:c}}}}),c=t((()=>({name:"sanity-plugin-debug-secrets",schema:{types:[a]},document:{actions:(e,t)=>t.schemaType!==i?e:e.filter((({action:e})=>"delete"===e)),inspectors:(e,t)=>t.documentType!==i?e:[],unstable_fieldActions:(e,t)=>t.schemaType.name!==i?e:[]}})));export{c as debugSecrets};//# sourceMappingURL=sanity-plugin-debug-secrets.js.map |
@@ -1,1 +0,1 @@ | ||
import{b as s,u as a}from"./_chunks/constants-xlbGwjKx.js";function e(e){const r=new URL(e);return r.searchParams.delete(s),r.searchParams.delete(a),r}function r(s){return s.searchParams.has(a)}function n(e,r,n){const t=new URL(e);return t.searchParams.set(a,r),t.searchParams.set(s,n),t}export{r as hasSecretSearchParams,n as setSecretSearchParams,e as withoutSecretSearchParams};//# sourceMappingURL=without-secret-search-params.js.map | ||
import{b as s,u as a}from"./_chunks/constants-mg9CwKOs.js";function e(e){const r=new URL(e);return r.searchParams.delete(s),r.searchParams.delete(a),r}function r(s){return s.searchParams.has(a)}function n(e,r,n){const t=new URL(e);return t.searchParams.set(a,r),t.searchParams.set(s,n),t}export{r as hasSecretSearchParams,n as setSecretSearchParams,e as withoutSecretSearchParams};//# sourceMappingURL=without-secret-search-params.js.map |
{ | ||
"name": "@sanity/preview-url-secret", | ||
"version": "1.5.5", | ||
"version": "1.6.0", | ||
"homepage": "https://github.com/sanity-io/visual-editing/tree/main/packages/preview-url-secret#readme", | ||
@@ -172,3 +172,3 @@ "bugs": { | ||
"@sanity/icons": "^2.10.0", | ||
"@sanity/pkg-utils": "^4.1.1", | ||
"@sanity/pkg-utils": "^4.1.2", | ||
"@typescript-eslint/eslint-plugin": "^6.20.0", | ||
@@ -180,3 +180,3 @@ "@typescript-eslint/parser": "^6.20.0", | ||
"eslint-plugin-simple-import-sort": "^10.0.0", | ||
"sanity": "^3.26.1", | ||
"sanity": "^3.27.1", | ||
"typescript": "^5.3.3", | ||
@@ -183,0 +183,0 @@ "vitest": "^1.2.2" |
@@ -140,2 +140,19 @@ # @sanity/preview-url-secret | ||
## Checking the Studio origin | ||
You can inspect the URL origin of the Studio that initiated the preview on the `studioOrigin` property of `validatePreviewUrl`: | ||
```ts | ||
const { | ||
isValid, | ||
redirectTo = '/', | ||
studioOrigin, | ||
} = await validatePreviewUrl(clientWithToken, req.url) | ||
if (studioOrigin === 'http://localhost:3333') { | ||
console.log('This preview was initiated from the local development Studio') | ||
} | ||
``` | ||
You don't have to check `isValid` before using it, as it'll be `undefined` if the preview URL secret failed validation. It's also `undefined` if the way the secret were created didn't provide an origin. | ||
## Debugging generated secrets | ||
@@ -142,0 +159,0 @@ |
@@ -38,2 +38,3 @@ import type { | ||
secret, | ||
studioUrl, | ||
}` as const | ||
@@ -40,0 +41,0 @@ |
@@ -54,2 +54,6 @@ /** @internal */ | ||
redirectTo?: string | ||
/** | ||
* If the URL is valid, and the studior URL is known and valid, then its origin will be here | ||
*/ | ||
studioOrigin?: string | ||
} | ||
@@ -112,2 +116,3 @@ | ||
secret: string | null | ||
studioUrl: string | null | ||
} | null | ||
@@ -114,0 +119,0 @@ |
@@ -12,3 +12,3 @@ import { isDev } from './constants' | ||
/** | ||
* @alpha | ||
* @public | ||
*/ | ||
@@ -18,3 +18,8 @@ export async function validatePreviewUrl( | ||
previewUrl: string, | ||
disableCacheNoStore?: boolean, | ||
/** | ||
* @deprecated - this option is automatically determined based on the environment | ||
*/ | ||
// Default value based on https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent | ||
disableCacheNoStore: boolean = globalThis.navigator?.userAgent === | ||
'Cloudflare-Workers', | ||
): Promise<PreviewUrlValidateUrlResult> { | ||
@@ -36,3 +41,3 @@ const client = createClientWithConfig(_client) | ||
const isValid = await validateSecret( | ||
const { isValid, studioUrl } = await validateSecret( | ||
client, | ||
@@ -43,6 +48,20 @@ parsedPreviewUrl.secret, | ||
const redirectTo = isValid ? parsedPreviewUrl.redirectTo : undefined | ||
let studioOrigin: string | undefined | ||
if (isValid) { | ||
try { | ||
studioOrigin = new URL(studioUrl!).origin | ||
} catch (error) { | ||
if (isDev) { | ||
// eslint-disable-next-line no-console | ||
console.error('Failed to parse studioUrl', error, { | ||
previewUrl, | ||
studioUrl, | ||
}) | ||
} | ||
} | ||
} | ||
return { isValid, redirectTo } | ||
return { isValid, redirectTo, studioOrigin } | ||
} | ||
export type { PreviewUrlValidateUrlResult, SanityClientLike } |
@@ -12,4 +12,4 @@ import { fetchSecretQuery, tag } from './constants' | ||
secret: string, | ||
disableCacheNoStore?: boolean, | ||
): Promise<boolean> { | ||
disableCacheNoStore: boolean, | ||
): Promise<{ isValid: boolean; studioUrl: string | null }> { | ||
// If we're in the Edge Runtime it's usually too quick and we need to delay fetching the secret a little bit | ||
@@ -21,10 +21,13 @@ // @ts-expect-error -- this global exists if we're in the Edge Runtime | ||
if (!secret || !secret.trim()) { | ||
return false | ||
return { isValid: false, studioUrl: null } | ||
} | ||
const result = await client.fetch<FetchSecretQueryResponse>( | ||
const result = await client.fetch< | ||
FetchSecretQueryResponse, | ||
FetchSecretQueryParams | ||
>( | ||
fetchSecretQuery, | ||
{ secret } satisfies FetchSecretQueryParams, | ||
{ secret: secret }, | ||
{ | ||
tag, | ||
// @ts-expect-error -- the `cache` option is valid, but not in the types when NextJS typings aren't installed | ||
// In CloudFlare Workers we can't pass the cache header | ||
...(!disableCacheNoStore ? { cache: 'no-store' } : undefined), | ||
@@ -34,5 +37,5 @@ }, | ||
if (!result?._id || !result?._updatedAt || !result?.secret) { | ||
return false | ||
return { isValid: false, studioUrl: null } | ||
} | ||
return secret === result.secret | ||
return { isValid: secret === result.secret, studioUrl: result.studioUrl } | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
107665
852
178