@getrefino/onboarding 0.1.0-rc.4 → 0.1.0-rc.6
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/dist/apply.d.ts +16 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +19 -0
- package/dist/generated.d.ts +8 -0
- package/dist/generated.js +25 -5
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/integration-update.d.ts +83 -0
- package/dist/integration-update.js +281 -0
- package/dist/lockfiles.d.ts +73 -0
- package/dist/lockfiles.js +443 -0
- package/dist/plan.d.ts +1 -12
- package/dist/plan.js +2 -13
- package/dist/registry.d.ts +42 -0
- package/dist/registry.js +111 -0
- package/dist/semver.d.ts +14 -0
- package/dist/semver.js +64 -0
- package/dist/template-file.js +1 -1
- package/dist/templates-hosted.d.ts +7 -4
- package/dist/templates-hosted.js +319 -58
- package/dist/templates-legacy.d.ts +26 -1
- package/dist/templates-legacy.js +566 -1
- package/dist/templates.js +1 -1
- package/dist/verify.js +30 -0
- package/package.json +2 -2
|
@@ -15,11 +15,36 @@ export interface LegacySiteConfig {
|
|
|
15
15
|
readonly siteId: string;
|
|
16
16
|
readonly appUrl: string;
|
|
17
17
|
}
|
|
18
|
-
/** The last release that wrote the bodies below. */
|
|
18
|
+
/** The last release that wrote the rc.1 bodies below. */
|
|
19
19
|
export declare const RC1 = "0.1.0-rc.1";
|
|
20
|
+
/**
|
|
21
|
+
* The last release that wrote the rc.4 bodies below (unchanged since rc.2).
|
|
22
|
+
* If rc.4 is ever published with the update-aware bodies instead, this label
|
|
23
|
+
* is still harmless: it only names the `from` version in upgrade reports.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RC4 = "0.1.0-rc.4";
|
|
26
|
+
/** The release that wrote the rc.5 bodies below: the published 0.1.0-rc.5. */
|
|
27
|
+
export declare const RC5 = "0.1.0-rc.5";
|
|
20
28
|
/** 0.1.0-rc.1 refino/refino-site.ts: the two public constants, before REFINO_INSTALL. */
|
|
21
29
|
export declare const REFINO_SITE_RC1: (config: LegacySiteConfig) => string;
|
|
22
30
|
/** 0.1.0-rc.1 refino/refino-client.ts: before reportEditorEvent and the install facts. */
|
|
23
31
|
export declare const REFINO_CLIENT_RC1 = "/**\n * Browser-side Refino editor authorization for this site. No secrets here:\n * the site is a public client (OAuth 2.1 with PKCE). The editor token lives\n * in sessionStorage only (never localStorage, never a URL), is scoped to\n * this site, and Refino re-checks the account's entitlement on every load\n * and save. Never import this from server code.\n */\nimport { REFINO_APP_URL, REFINO_SITE_ID } from \"./refino-site\";\n\nconst PKCE_KEY = \"refino.pkce\";\nconst SESSION_KEY = \"refino.editor\";\nconst CALLBACK_PATH = \"/edit\";\n\nexport interface EditorSession {\n readonly token: string;\n /** Unix seconds. */\n readonly expiresAt: number;\n readonly siteId: string;\n}\n\ninterface PendingAuthorization {\n readonly verifier: string;\n readonly state: string;\n readonly returnTo: string;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction randomToken(bytes: number): string {\n const buffer = new Uint8Array(bytes);\n crypto.getRandomValues(buffer);\n return base64url(buffer);\n}\n\nasync function sha256(text: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text));\n return base64url(new Uint8Array(digest));\n}\n\nfunction storage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\nfunction readJson<T>(key: string): T | null {\n const raw = storage()?.getItem(key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Only a path on this site, never another origin. */\nexport function safeReturnTo(value: string | null | undefined): string {\n if (!value || !value.startsWith(\"/\") || value.startsWith(\"//\") || value.startsWith(\"/\\\\\") || value.length > 512) return \"/\";\n return value;\n}\n\nexport function editorEndpoint(): string {\n return `${REFINO_APP_URL}/api/sites/${REFINO_SITE_ID}/copy`;\n}\n\nexport function callbackUrl(): string {\n return `${window.location.origin}${CALLBACK_PATH}`;\n}\n\n/** The current editor session, or null when there is none or it expired. */\nexport function readEditorSession(nowSeconds: number = Math.floor(Date.now() / 1000)): EditorSession | null {\n const session = readJson<EditorSession>(SESSION_KEY);\n if (!session || typeof session.token !== \"string\" || session.siteId !== REFINO_SITE_ID || typeof session.expiresAt !== \"number\") return null;\n if (session.expiresAt <= nowSeconds) {\n storage()?.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n}\n\nexport function clearEditorSession(): void {\n storage()?.removeItem(SESSION_KEY);\n storage()?.removeItem(PKCE_KEY);\n}\n\n/** Start the flow: remember the PKCE verifier and state, then go to Refino. */\nexport async function beginAuthorization(returnTo: string): Promise<void> {\n const verifier = randomToken(32);\n const state = randomToken(16);\n const pending: PendingAuthorization = { verifier, state, returnTo: safeReturnTo(returnTo) };\n storage()?.setItem(PKCE_KEY, JSON.stringify(pending));\n const params = new URLSearchParams({\n site_id: REFINO_SITE_ID,\n redirect_uri: callbackUrl(),\n code_challenge: await sha256(verifier),\n code_challenge_method: \"S256\",\n state,\n });\n window.location.assign(`${REFINO_APP_URL}/editor/authorize?${params.toString()}`);\n}\n\nexport type AuthorizationResult = { readonly ok: true; readonly returnTo: string } | { readonly ok: false; readonly error: string };\n\n/**\n * Finish the flow on /edit?code&state: check the state, exchange the code\n * with the verifier this browser kept, store the session, and scrub the\n * code from the URL. The token is never placed in a URL.\n */\nexport async function completeAuthorization(search: string = window.location.search): Promise<AuthorizationResult> {\n const params = new URLSearchParams(search);\n const code = params.get(\"code\");\n const state = params.get(\"state\");\n const pending = readJson<PendingAuthorization>(PKCE_KEY);\n storage()?.removeItem(PKCE_KEY);\n if (typeof window !== \"undefined\" && window.history.replaceState) window.history.replaceState(null, \"\", window.location.pathname);\n if (!code || !state) return { ok: false, error: \"Refino did not return an authorization code.\" };\n if (!pending || pending.state !== state) return { ok: false, error: \"Sign-in was interrupted (state mismatch). Start again.\" };\n\n let response: Response;\n try {\n response = await fetch(`${REFINO_APP_URL}/api/editor/token`, {\n method: \"POST\",\n mode: \"cors\",\n credentials: \"omit\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({ grant_type: \"authorization_code\", code, code_verifier: pending.verifier, site_id: REFINO_SITE_ID, redirect_uri: callbackUrl() }),\n });\n } catch {\n return { ok: false, error: \"Could not reach Refino to finish signing in.\" };\n }\n const body = (await response.json().catch(() => null)) as { ok?: boolean; token?: string; expiresAt?: number; siteId?: string; error?: { message?: string } } | null;\n if (!response.ok || !body?.ok || typeof body.token !== \"string\" || typeof body.expiresAt !== \"number\" || body.siteId !== REFINO_SITE_ID) {\n return { ok: false, error: body?.error?.message ?? \"Refino did not accept the authorization.\" };\n }\n const session: EditorSession = { token: body.token, expiresAt: body.expiresAt, siteId: body.siteId };\n storage()?.setItem(SESSION_KEY, JSON.stringify(session));\n return { ok: true, returnTo: pending.returnTo };\n}\n";
|
|
24
32
|
/** 0.1.0-rc.1 refino/copy-editing.tsx: before onEvent={reportEditorEvent}. */
|
|
25
33
|
export declare const COPY_EDITING_HOSTED_RC1 = "\"use client\";\n\nimport type { CopyContent } from \"@getrefino/core\";\nimport { RefinoProvider } from \"@getrefino/react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nimport { clearEditorSession, editorEndpoint, readEditorSession } from \"./refino-client\";\nimport type { EditorSession } from \"./refino-client\";\n\ninterface CopyEditingProps {\n content: CopyContent;\n children: ReactNode;\n}\n\n/**\n * Hosted mode (Refino). Visitors get the plain site: the server never\n * decides edit mode, so pages can stay static. In the browser, an editor\n * session stored by /edit turns edit mode on; loads and saves go to Refino\n * with the session's bearer token, and Refino checks the site's\n * entitlement on every request.\n */\nexport function CopyEditing({ content, children }: CopyEditingProps) {\n const [session, setSession] = useState<EditorSession | null>(null);\n\n useEffect(() => {\n // Read the session only after mount so server-rendered HTML never differs from the visitor's.\n setSession(readEditorSession());\n }, []);\n\n // Read at request time (the provider keeps its options from the first render), so the\n // token is always the one currently stored and never sent once the session is cleared.\n const headers = useCallback((): Record<string, string> => {\n const current = readEditorSession();\n return current ? { authorization: `Bearer ${current.token}` } : {};\n }, []);\n\n const onExit = useCallback(async () => {\n clearEditorSession();\n window.location.assign(\"/\");\n }, []);\n\n return (\n <RefinoProvider content={content} editing={session !== null} endpoint={editorEndpoint()} headers={headers} signInHref=\"/edit\" onExit={onExit}>\n {children}\n </RefinoProvider>\n );\n}\n";
|
|
34
|
+
/** 0.1.0-rc.4 refino/refino-client.ts: before refinoUpdates, and before reportEditorEvent carried the editing surface. */
|
|
35
|
+
export declare const REFINO_CLIENT_RC4 = "/**\n * Browser-side Refino editor authorization for this site. No secrets here:\n * the site is a public client (OAuth 2.1 with PKCE). The editor token lives\n * in sessionStorage only (never localStorage, never a URL), is scoped to\n * this site, and Refino re-checks the account's entitlement on every load\n * and save. Never import this from server code.\n */\nimport { REFINO_APP_URL, REFINO_INSTALL, REFINO_SITE_ID } from \"./refino-site\";\n\nconst PKCE_KEY = \"refino.pkce\";\nconst SESSION_KEY = \"refino.editor\";\nconst CALLBACK_PATH = \"/edit\";\n\nexport interface EditorSession {\n readonly token: string;\n /** Unix seconds. */\n readonly expiresAt: number;\n readonly siteId: string;\n}\n\ninterface PendingAuthorization {\n readonly verifier: string;\n readonly state: string;\n readonly returnTo: string;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction randomToken(bytes: number): string {\n const buffer = new Uint8Array(bytes);\n crypto.getRandomValues(buffer);\n return base64url(buffer);\n}\n\nasync function sha256(text: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text));\n return base64url(new Uint8Array(digest));\n}\n\nfunction storage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\nfunction readJson<T>(key: string): T | null {\n const raw = storage()?.getItem(key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Only a path on this site, never another origin. */\nexport function safeReturnTo(value: string | null | undefined): string {\n if (!value || !value.startsWith(\"/\") || value.startsWith(\"//\") || value.startsWith(\"/\\\\\") || value.length > 512) return \"/\";\n return value;\n}\n\nexport function editorEndpoint(): string {\n return `${REFINO_APP_URL}/api/sites/${REFINO_SITE_ID}/copy`;\n}\n\nexport function callbackUrl(): string {\n return `${window.location.origin}${CALLBACK_PATH}`;\n}\n\n/** The current editor session, or null when there is none or it expired. */\nexport function readEditorSession(nowSeconds: number = Math.floor(Date.now() / 1000)): EditorSession | null {\n const session = readJson<EditorSession>(SESSION_KEY);\n if (!session || typeof session.token !== \"string\" || session.siteId !== REFINO_SITE_ID || typeof session.expiresAt !== \"number\") return null;\n if (session.expiresAt <= nowSeconds) {\n storage()?.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n}\n\nexport function clearEditorSession(): void {\n storage()?.removeItem(SESSION_KEY);\n storage()?.removeItem(PKCE_KEY);\n}\n\n/** Start the flow: remember the PKCE verifier and state, then go to Refino. */\nexport async function beginAuthorization(returnTo: string): Promise<void> {\n const verifier = randomToken(32);\n const state = randomToken(16);\n const pending: PendingAuthorization = { verifier, state, returnTo: safeReturnTo(returnTo) };\n storage()?.setItem(PKCE_KEY, JSON.stringify(pending));\n const params = new URLSearchParams({\n site_id: REFINO_SITE_ID,\n redirect_uri: callbackUrl(),\n code_challenge: await sha256(verifier),\n code_challenge_method: \"S256\",\n state,\n });\n window.location.assign(`${REFINO_APP_URL}/editor/authorize?${params.toString()}`);\n}\n\nexport type AuthorizationResult = { readonly ok: true; readonly returnTo: string } | { readonly ok: false; readonly error: string };\n\n/**\n * Finish the flow on /edit?code&state: check the state, exchange the code\n * with the verifier this browser kept, store the session, and scrub the\n * code from the URL. The token is never placed in a URL.\n */\nexport async function completeAuthorization(search: string = window.location.search): Promise<AuthorizationResult> {\n const params = new URLSearchParams(search);\n const code = params.get(\"code\");\n const state = params.get(\"state\");\n const pending = readJson<PendingAuthorization>(PKCE_KEY);\n storage()?.removeItem(PKCE_KEY);\n if (typeof window !== \"undefined\" && window.history.replaceState) window.history.replaceState(null, \"\", window.location.pathname);\n if (!code || !state) return { ok: false, error: \"Refino did not return an authorization code.\" };\n if (!pending || pending.state !== state) return { ok: false, error: \"Sign-in was interrupted (state mismatch). Start again.\" };\n\n let response: Response;\n try {\n response = await fetch(`${REFINO_APP_URL}/api/editor/token`, {\n method: \"POST\",\n mode: \"cors\",\n credentials: \"omit\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n code_verifier: pending.verifier,\n site_id: REFINO_SITE_ID,\n redirect_uri: callbackUrl(),\n // The public install facts from refino-site.ts, sent once per\n // sign-in. Refino records them the first time and ignores them\n // afterwards. Remove this line and everything still works.\n install: REFINO_INSTALL,\n }),\n });\n } catch {\n return { ok: false, error: \"Could not reach Refino to finish signing in.\" };\n }\n const body = (await response.json().catch(() => null)) as { ok?: boolean; token?: string; expiresAt?: number; siteId?: string; error?: { message?: string } } | null;\n if (!response.ok || !body?.ok || typeof body.token !== \"string\" || typeof body.expiresAt !== \"number\" || body.siteId !== REFINO_SITE_ID) {\n return { ok: false, error: body?.error?.message ?? \"Refino did not accept the authorization.\" };\n }\n const session: EditorSession = { token: body.token, expiresAt: body.expiresAt, siteId: body.siteId };\n storage()?.setItem(SESSION_KEY, JSON.stringify(session));\n return { ok: true, returnTo: pending.returnTo };\n}\n\n/**\n * The three editor events this site reports, and the only ones it ever\n * sends. There is no analytics SDK on this site: no autocapture, no session\n * recording, no third-party script, no cookie. Each event is one small POST\n * to Refino carrying an event name and, for a revert, whether it was one\n * word or all of them \u2014 never your page, your copy, your visitors or your\n * repository. `edit_saved` is not in this list on purpose: a save is a\n * commit, and Refino records that on its own side where it cannot be faked\n * or lost.\n */\nconst REPORTABLE_EVENTS = [\"editor_opened\", \"edit_started\", \"edit_reverted\"] as const;\n\ntype ReportableEvent = (typeof REPORTABLE_EVENTS)[number];\n\ninterface EditorEventLike {\n readonly name: string;\n readonly properties: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Fire and forget. A failure here is invisible and harmless: nothing in the\n * editor waits for it, retries it, or behaves differently when it fails.\n */\nexport function reportEditorEvent(event: EditorEventLike): void {\n const session = readEditorSession();\n if (!session) return;\n if (!(REPORTABLE_EVENTS as readonly string[]).includes(event.name)) return;\n const name = event.name as ReportableEvent;\n const scope = event.properties.scope;\n try {\n void fetch(`${REFINO_APP_URL}/api/editor/events`, {\n method: \"POST\",\n mode: \"cors\",\n credentials: \"omit\",\n keepalive: true,\n headers: { \"content-type\": \"application/json\", authorization: `Bearer ${session.token}` },\n body: JSON.stringify(scope === \"one\" || scope === \"all\" ? { event: name, scope } : { event: name }),\n }).catch(() => {});\n } catch {\n // Never let reporting affect editing.\n }\n}\n";
|
|
36
|
+
/** 0.1.0-rc.4 refino/copy-editing.tsx: before updates={refinoUpdates}. */
|
|
37
|
+
export declare const COPY_EDITING_HOSTED_RC4 = "\"use client\";\n\nimport type { CopyContent } from \"@getrefino/core\";\nimport { RefinoProvider } from \"@getrefino/react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nimport { clearEditorSession, editorEndpoint, readEditorSession, reportEditorEvent } from \"./refino-client\";\nimport type { EditorSession } from \"./refino-client\";\n\ninterface CopyEditingProps {\n content: CopyContent;\n children: ReactNode;\n}\n\n/**\n * Hosted mode (Refino). Visitors get the plain site: the server never\n * decides edit mode, so pages can stay static. In the browser, an editor\n * session stored by /edit turns edit mode on; loads and saves go to Refino\n * with the session's bearer token, and Refino checks the site's\n * entitlement on every request.\n */\nexport function CopyEditing({ content, children }: CopyEditingProps) {\n const [session, setSession] = useState<EditorSession | null>(null);\n\n useEffect(() => {\n // Read the session only after mount so server-rendered HTML never differs from the visitor's.\n setSession(readEditorSession());\n }, []);\n\n // Read at request time (the provider keeps its options from the first render), so the\n // token is always the one currently stored and never sent once the session is cleared.\n const headers = useCallback((): Record<string, string> => {\n const current = readEditorSession();\n return current ? { authorization: `Bearer ${current.token}` } : {};\n }, []);\n\n const onExit = useCallback(async () => {\n clearEditorSession();\n window.location.assign(\"/\");\n }, []);\n\n return (\n <RefinoProvider content={content} editing={session !== null} endpoint={editorEndpoint()} headers={headers} signInHref=\"/edit\" onExit={onExit} onEvent={reportEditorEvent}>\n {children}\n </RefinoProvider>\n );\n}\n";
|
|
38
|
+
/**
|
|
39
|
+
* 0.1.0-rc.5 refino/refino-client.ts: before `resolveEditorPhase`, `signOut`
|
|
40
|
+
* and the hardened `safeReturnTo`. This is what every rc.5 site has on disk.
|
|
41
|
+
*/
|
|
42
|
+
export declare const REFINO_CLIENT_RC5 = "/**\n * Browser-side Refino editor authorization for this site. No secrets here:\n * the site is a public client (OAuth 2.1 with PKCE). The editor token lives\n * in sessionStorage only (never localStorage, never a URL), is scoped to\n * this site, and Refino re-checks the account's entitlement on every load\n * and save. Never import this from server code.\n */\nimport { REFINO_APP_URL, REFINO_INSTALL, REFINO_SITE_ID } from \"./refino-site\";\n\nconst PKCE_KEY = \"refino.pkce\";\nconst SESSION_KEY = \"refino.editor\";\nconst CALLBACK_PATH = \"/edit\";\n\nexport interface EditorSession {\n readonly token: string;\n /** Unix seconds. */\n readonly expiresAt: number;\n readonly siteId: string;\n}\n\ninterface PendingAuthorization {\n readonly verifier: string;\n readonly state: string;\n readonly returnTo: string;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction randomToken(bytes: number): string {\n const buffer = new Uint8Array(bytes);\n crypto.getRandomValues(buffer);\n return base64url(buffer);\n}\n\nasync function sha256(text: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text));\n return base64url(new Uint8Array(digest));\n}\n\nfunction storage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\nfunction readJson<T>(key: string): T | null {\n const raw = storage()?.getItem(key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Only a path on this site, never another origin. */\nexport function safeReturnTo(value: string | null | undefined): string {\n if (!value || !value.startsWith(\"/\") || value.startsWith(\"//\") || value.startsWith(\"/\\\\\") || value.length > 512) return \"/\";\n return value;\n}\n\nexport function editorEndpoint(): string {\n return `${REFINO_APP_URL}/api/sites/${REFINO_SITE_ID}/copy`;\n}\n\nexport function callbackUrl(): string {\n return `${window.location.origin}${CALLBACK_PATH}`;\n}\n\n/** The current editor session, or null when there is none or it expired. */\nexport function readEditorSession(nowSeconds: number = Math.floor(Date.now() / 1000)): EditorSession | null {\n const session = readJson<EditorSession>(SESSION_KEY);\n if (!session || typeof session.token !== \"string\" || session.siteId !== REFINO_SITE_ID || typeof session.expiresAt !== \"number\") return null;\n if (session.expiresAt <= nowSeconds) {\n storage()?.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n}\n\nexport function clearEditorSession(): void {\n storage()?.removeItem(SESSION_KEY);\n storage()?.removeItem(PKCE_KEY);\n}\n\n/** Start the flow: remember the PKCE verifier and state, then go to Refino. */\nexport async function beginAuthorization(returnTo: string): Promise<void> {\n const verifier = randomToken(32);\n const state = randomToken(16);\n const pending: PendingAuthorization = { verifier, state, returnTo: safeReturnTo(returnTo) };\n storage()?.setItem(PKCE_KEY, JSON.stringify(pending));\n const params = new URLSearchParams({\n site_id: REFINO_SITE_ID,\n redirect_uri: callbackUrl(),\n code_challenge: await sha256(verifier),\n code_challenge_method: \"S256\",\n state,\n });\n window.location.assign(`${REFINO_APP_URL}/editor/authorize?${params.toString()}`);\n}\n\nexport type AuthorizationResult = { readonly ok: true; readonly returnTo: string } | { readonly ok: false; readonly error: string };\n\n/**\n * Finish the flow on /edit?code&state: check the state, exchange the code\n * with the verifier this browser kept, store the session, and scrub the\n * code from the URL. The token is never placed in a URL.\n */\nexport async function completeAuthorization(search: string = window.location.search): Promise<AuthorizationResult> {\n const params = new URLSearchParams(search);\n const code = params.get(\"code\");\n const state = params.get(\"state\");\n const pending = readJson<PendingAuthorization>(PKCE_KEY);\n storage()?.removeItem(PKCE_KEY);\n if (typeof window !== \"undefined\" && window.history.replaceState) window.history.replaceState(null, \"\", window.location.pathname);\n if (!code || !state) return { ok: false, error: \"Refino did not return an authorization code.\" };\n if (!pending || pending.state !== state) return { ok: false, error: \"Sign-in was interrupted (state mismatch). Start again.\" };\n\n let response: Response;\n try {\n response = await fetch(`${REFINO_APP_URL}/api/editor/token`, {\n method: \"POST\",\n mode: \"cors\",\n credentials: \"omit\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n code_verifier: pending.verifier,\n site_id: REFINO_SITE_ID,\n redirect_uri: callbackUrl(),\n // The public install facts from refino-site.ts, sent once per\n // sign-in. Refino records them the first time and ignores them\n // afterwards. Remove this line and everything still works.\n install: REFINO_INSTALL,\n }),\n });\n } catch {\n return { ok: false, error: \"Could not reach Refino to finish signing in.\" };\n }\n const body = (await response.json().catch(() => null)) as { ok?: boolean; token?: string; expiresAt?: number; siteId?: string; error?: { message?: string } } | null;\n if (!response.ok || !body?.ok || typeof body.token !== \"string\" || typeof body.expiresAt !== \"number\" || body.siteId !== REFINO_SITE_ID) {\n return { ok: false, error: body?.error?.message ?? \"Refino did not accept the authorization.\" };\n }\n const session: EditorSession = { token: body.token, expiresAt: body.expiresAt, siteId: body.siteId };\n storage()?.setItem(SESSION_KEY, JSON.stringify(session));\n return { ok: true, returnTo: pending.returnTo };\n}\n\n/**\n * The three editor events this site reports, and the only ones it ever\n * sends. There is no analytics SDK on this site: no autocapture, no session\n * recording, no third-party script, no cookie. Each event is one small POST\n * to Refino carrying an event name and, for a revert, whether it was one\n * word or all of them \u2014 never your page, your copy, your visitors or your\n * repository. `edit_saved` is not in this list on purpose: a save is a\n * commit, and Refino records that on its own side where it cannot be faked\n * or lost.\n */\nconst REPORTABLE_EVENTS = [\"editor_opened\", \"edit_started\", \"edit_reverted\"] as const;\n\ntype ReportableEvent = (typeof REPORTABLE_EVENTS)[number];\n\ninterface EditorEventLike {\n readonly name: string;\n readonly properties: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Fire and forget. A failure here is invisible and harmless: nothing in the\n * editor waits for it, retries it, or behaves differently when it fails.\n */\nexport function reportEditorEvent(event: EditorEventLike): void {\n const session = readEditorSession();\n if (!session) return;\n if (!(REPORTABLE_EVENTS as readonly string[]).includes(event.name)) return;\n const name = event.name as ReportableEvent;\n const scope = event.properties.scope;\n try {\n void fetch(`${REFINO_APP_URL}/api/editor/events`, {\n method: \"POST\",\n mode: \"cors\",\n credentials: \"omit\",\n keepalive: true,\n headers: { \"content-type\": \"application/json\", authorization: `Bearer ${session.token}` },\n body: JSON.stringify(scope === \"one\" || scope === \"all\" ? { event: name, scope } : { event: name }),\n }).catch(() => {});\n } catch {\n // Never let reporting affect editing.\n }\n}\n\n/**\n * Updates to this site's Refino integration, offered in the editor\n * toolbar. Asking is read-only. Nothing changes unless the owner presses\n * \"Update Refino\": Refino then commits the new Refino version to this\n * repository, and the site's normal deployment takes it from there. The\n * editor checks every answer itself, so this stays a plain pipe.\n */\nexport const refinoUpdates = {\n check: (): Promise<unknown> => updateRequest(\"GET\"),\n apply: (version: string): Promise<unknown> => updateRequest(\"POST\", JSON.stringify({ version })),\n};\n\nasync function updateRequest(method: \"GET\" | \"POST\", body?: string): Promise<unknown> {\n const session = readEditorSession();\n if (!session) return null;\n const headers: Record<string, string> = { accept: \"application/json\", authorization: `Bearer ${session.token}` };\n if (body) headers[\"content-type\"] = \"application/json\";\n try {\n const response = await fetch(`${REFINO_APP_URL}/api/sites/${REFINO_SITE_ID}/update`, { method, mode: \"cors\", credentials: \"omit\", headers, body });\n return await response.json();\n } catch {\n // Unreachable or unreadable: the editor treats it as \"nothing to offer\", or as an unknown outcome.\n return null;\n }\n}\n";
|
|
43
|
+
/**
|
|
44
|
+
* refino/edit-page.tsx as every release up to and including 0.1.0-rc.5 wrote
|
|
45
|
+
* it (the body never changed after rc.1): the unstyled handoff screen, which
|
|
46
|
+
* a successful sign-in never landed on, and its "Sign in as someone else"
|
|
47
|
+
* button, which cleared the site's editor session but not the Refino one and
|
|
48
|
+
* so signed the same account straight back in.
|
|
49
|
+
*/
|
|
50
|
+
export declare const EDIT_PAGE_HOSTED_RC5 = "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { beginAuthorization, clearEditorSession, completeAuthorization, readEditorSession, safeReturnTo } from \"./refino-client\";\n\ntype Phase = { kind: \"loading\" } | { kind: \"signed-in\" } | { kind: \"error\"; message: string };\n\n/**\n * Owner entry point for Refino-hosted editing. Visiting /edit sends the\n * owner to Refino to sign in and authorize this site; Refino sends them\n * back here with a one-time code, which is exchanged for an editor session\n * and scrubbed from the URL. Restyle freely; keep the behaviour.\n */\nexport default function EditPage() {\n const [phase, setPhase] = useState<Phase>({ kind: \"loading\" });\n\n useEffect(() => {\n let cancelled = false;\n const params = new URLSearchParams(window.location.search);\n if (params.has(\"code\") || params.has(\"state\")) {\n void completeAuthorization().then((result) => {\n if (cancelled) return;\n if (result.ok) window.location.assign(result.returnTo);\n else setPhase({ kind: \"error\", message: result.error });\n });\n } else if (readEditorSession()) {\n setPhase({ kind: \"signed-in\" });\n } else if (params.get(\"error\")) {\n setPhase({ kind: \"error\", message: params.get(\"error\") ?? \"Sign-in failed.\" });\n } else {\n void beginAuthorization(safeReturnTo(params.get(\"return_to\")));\n }\n return () => {\n cancelled = true;\n };\n }, []);\n\n return (\n <main style={{ maxWidth: 420, margin: \"10vh auto\", padding: 24, fontFamily: \"system-ui, sans-serif\" }}>\n {phase.kind === \"loading\" && <p>Signing you in with Refino\u2026</p>}\n {phase.kind === \"signed-in\" && (\n <>\n <h1>Edit mode is on</h1>\n <p>Go to the site, click any text, and press Save when you are done.</p>\n <p>\n <a href=\"/\">Open the site</a>\n </p>\n <button\n type=\"button\"\n onClick={() => {\n clearEditorSession();\n setPhase({ kind: \"loading\" });\n void beginAuthorization(\"/\");\n }}\n >\n Sign in as someone else\n </button>\n </>\n )}\n {phase.kind === \"error\" && (\n <>\n <h1>Could not start editing</h1>\n <p role=\"alert\">{phase.message}</p>\n <button type=\"button\" onClick={() => void beginAuthorization(\"/\")}>\n Try again\n </button>\n </>\n )}\n </main>\n );\n}\n";
|