1{2 "solution": "members",3 "requires": [4 "shared"5 ],6 "entrypoints": [7 "js/wix/auth.js",8 "js/wix/member-store.js",9 "js/wix/members.js"10 ],11 "files": {12 "js/wix/auth-core.js": "// Declarations of auth-core.ts: what this file exports, their arguments and what they return.\n
13 "js/wix/auth.js": "// Declarations of auth.ts: what this file exports, their arguments and what they return.\n// import { type Tokens } from \"./client.js\";\n// import { type LoginResult, type LoginState } from \"./auth-core.js\";\n// export type { LoginResult, LoginState };\n// export interface LoginOptions {\n// returnTo?: string;\n// }\n// export declare const PROFILE_ON_SIGNUP = true;\n// export declare function loggedInHint(): boolean;\n// export declare function loginMember(email: string, password: string, _options?: LoginOptions): Promise<LoginResult>;\n// export declare function registerMember(email: string, password: string, profile?: {\n// firstName?: string;\n// lastName?: string;\n// }, _options?: LoginOptions): Promise<LoginResult>;\n// export declare function verifyMemberEmail(verificationCode: string): Promise<LoginResult>;\n// export declare function logoutUrl(returnTo: string): Promise<string>;\n// export declare function logoutMember(returnTo?: string): Promise<void>;\n// export declare function memberTokensForSession(sessionToken: string): Promise<Tokens>;\n// Member authentication over REST — the twin of app/wix/members/auth.ts. Same exports, same\n// LoginResult; the state and error-code rules come from auth-core (the SAME file the SDK transport\n// uses). The custom credential flow, as the docs order it: login/register with the VISITOR token →\n// a one-shot sessionToken (5-minute life) → a redirect session that authorizes it (PKCE,\n// web_message) → an authorization code → POST /oauth2/token grantType authorization_code → MEMBER\n// tokens, adopted by ./client.js so every later call (the current-member read, a member's own\n// records) runs as the member and the refresh keeps the member. All from a page with the public\n// client id; nothing elevated. Porting: keep the paths and bodies; the exchange is three calls.\n// docs: https://dev.wix.com/docs/go-headless/authentication/members/custom-login-page/build-a-custom-login-page-rest.md\n// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md\nimport { WIX_CLIENT_ID } from \"./config.js\";\nimport { WixApiError, clearTokens, loadTokens, mintTokens, setTokens, wixRequest } from \"./client.js\";\nimport { NO_SESSION_ERROR, authOutcome, failure, loginErrorCode, registerErrorCode, toLoginResult, } from \"./auth-core.js\";\n/** register() carries first/last name on this transport. */\nexport const PROFILE_ON_SIGNUP = true;\nconst REDIRECT_SESSION = \"/headless/v1/redirect-session\";\n/** The stateToken of a login/registration that stopped at email verification — verifyMemberEmail continues it. */\nlet pendingStateToken = null;\n/** True when the persisted token set is a member's (no network) — the store skips the profile read when false. */\nexport function loggedInHint() {\n return loadTokens()?.role === \"member\";\n}\n// A SUCCESS carries the one-shot session token: exchange it for member tokens and adopt them. Other\n// states pass through; a verification state keeps its stateToken for verifyMemberEmail.\nasync function finish(raw) {\n const outcome = authOutcome(raw);\n if (outcome.state === \"SUCCESS\") {\n pendingStateToken = null;\n if (!outcome.sessionToken)\n return failure(NO_SESSION_ERROR);\n setTokens(await memberTokensForSession(outcome.sessionToken));\n return { state: \"SUCCESS\" };\n }\n if (outcome.stateToken)\n pendingStateToken = outcome.stateToken;\n return toLoginResult(outcome);\n}\nconst message = (e) => (e instanceof Error ? e.message : String(e));\n/**\n * Sign in with email + password. A wrong password or unknown email is a FAILURE result (errorCode\n * invalidPassword / invalidEmail, Wix's message in `error`), never a throw.\n * POST /_api/iam/authentication/v2/login { loginId: { email }, password } → { state, sessionToken?, stateToken? }\n */\nexport async function loginMember(email, password, _options = {}) {\n let raw;\n try {\n raw = await wixRequest(\"/_api/iam/authentication/v2/login\", { body: { loginId: { email }, password } });\n }\n catch (e) {\n return failure(message(e), e instanceof WixApiError ? loginErrorCode(e.code) : undefined);\n }\n return finish(raw);\n}\n/**\n * Sign up with email + password (+ first/last name). An email already registered is a FAILURE with\n * errorCode emailAlreadyExists. Whether the result is SUCCESS, EMAIL_VERIFICATION_REQUIRED, or\n * OWNER_APPROVAL_REQUIRED is the site's signup policy (dashboard), not the caller's.\n * POST /_api/iam/authentication/v2/register { loginId: { email }, password, profile?: { firstName, lastName } }\n */\nexport async function registerMember(email, password, profile, _options = {}) {\n let raw;\n try {\n raw = await wixRequest(\"/_api/iam/authentication/v2/register\", {\n body: { loginId: { email }, password, ...(profile ? { profile } : {}) },\n });\n }\n catch (e) {\n return failure(message(e), e instanceof WixApiError ? registerErrorCode(e.code, e.details) : undefined);\n }\n return finish(raw);\n}\n/**\n * Continue an EMAIL_VERIFICATION_REQUIRED flow with the code from the email; the state token of the\n * last login/registration is kept here. A FAILURE when nothing is waiting or the code is wrong.\n * POST /_api/iam/verification/v1/auth/verify { code, stateToken } → { state, sessionToken?, stateToken? }\n */\nexport async function verifyMemberEmail(verificationCode) {\n if (!pendingStateToken)\n return failure(\"No sign-in is waiting for a verification code.\");\n let raw;\n try {\n raw = await wixRequest(\"/_api/iam/verification/v1/auth/verify\", { body: { code: verificationCode, stateToken: pendingStateToken } });\n }\n catch (e) {\n return failure(message(e));\n }\n return finish(raw);\n}\n/**\n * The Wix logout URL for the current member — navigating the document there ends the Wix session\n * and lands on `returnTo` (an absolute URL). Called with the MEMBER token.\n * POST /headless/v1/redirect-session { logout: { clientId }, callbacks: { postFlowUrl } } → { redirectSession: { fullUrl } }\n */\nexport async function logoutUrl(returnTo) {\n const res = await wixRequest(REDIRECT_SESSION, { body: { logout: { clientId: WIX_CLIENT_ID }, callbacks: { postFlowUrl: returnTo } } });\n const url = res?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Logout couldn't start: no redirect URL returned.\");\n return url;\n}\n/** Log out: drop the member tokens (the next call is a fresh visitor) and navigate to Wix's logout, which returns to `returnTo`. */\nexport async function logoutMember(returnTo = \"/\") {\n const target = typeof window !== \"undefined\" ? new URL(returnTo, window.location.origin).href : returnTo;\n const url = await logoutUrl(target);\n clearTokens();\n if (typeof window !== \"undefined\")\n window.location.assign(url);\n}\n/**\n * sessionToken → member tokens. Three calls, as the docs order them:\n * 1. POST /headless/v1/redirect-session\n * { auth: { authRequest: { clientId, codeChallenge, codeChallengeMethod: \"S256\", responseMode: \"web_message\",\n * responseType: \"code\", scope: \"offline_access\", state, sessionToken }, prompt: \"none\" } }\n * → { redirectSession: { fullUrl } } (with the visitor token)\n * 2. fullUrl is a page whose script posts { code, state, error } to its parent — a hidden iframe in a\n * browser (its origin must be one of the OAuth app's allowed domains, or the message is dropped\n * and this times out); without a DOM (a server-side port, a Node check) the same page is fetched\n * and read.\n * 3. POST /oauth2/token { clientId, grantType: \"authorization_code\", code, codeVerifier }\n * → member access + refresh tokens (role \"member\"); the code is single-use.\n */\nexport async function memberTokensForSession(sessionToken) {\n const { verifier, challenge } = await pkce();\n const state = randomString();\n const res = await wixRequest(REDIRECT_SESSION, {\n body: {\n auth: {\n authRequest: {\n clientId: WIX_CLIENT_ID,\n codeChallenge: challenge,\n codeChallengeMethod: \"S256\",\n responseMode: \"web_message\",\n responseType: \"code\",\n scope: \"offline_access\",\n state,\n sessionToken,\n },\n prompt: \"none\",\n },\n },\n });\n const url = res?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Login couldn't finish: no authorization URL returned.\");\n const code = await authorizationCode(url, state);\n return mintTokens({ grantType: \"authorization_code\", code, codeVerifier: verifier }, \"member\");\n}\n// The authorize page posts { code, state, error }. Browser: a hidden iframe and a message listener\n// keyed on our state (the SDK's own mechanism). No DOM: fetch the page and read the same three values.\nasync function authorizationCode(url, state) {\n if (typeof document === \"undefined\") {\n const html = await (await fetch(url)).text();\n const field = (name) => html.match(new RegExp(`'${name}':\\\\s*'([^']*)'`))?.[1];\n const error = field(\"error\");\n if (error)\n throw new Error(`Login was refused: ${error}`);\n const code = field(\"code\");\n if (!code || field(\"state\") !== state)\n throw new Error(\"Login couldn't finish: no authorization code.\");\n return code;\n }\n return new Promise((resolve, reject) => {\n const frame = document.createElement(\"iframe\");\n frame.style.display = \"none\";\n const done = () => {\n window.removeEventListener(\"message\", onMessage);\n clearTimeout(timer);\n frame.remove();\n };\n const onMessage = (e) => {\n const data = e.data;\n if (!data || data.state !== state)\n return; // a message not meant for us\n done();\n if (data.error)\n reject(new Error(`Login was refused: ${data.error}`));\n else if (data.code)\n resolve(data.code);\n else\n reject(new Error(\"Login couldn't finish: no authorization code.\"));\n };\n const timer = setTimeout(() => {\n done();\n reject(new Error(\"Login timed out. Is this page's origin on the OAuth app's allowed domains?\"));\n }, 120_000);\n window.addEventListener(\"message\", onMessage);\n frame.src = url;\n document.body.appendChild(frame);\n });\n}\n// PKCE: a 43-char base64url verifier, its SHA-256 as the S256 challenge. Web Crypto (browser + Node).\nfunction base64url(bytes) {\n let s = \"\";\n for (const b of bytes)\n s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\nfunction randomString() {\n return base64url(crypto.getRandomValues(new Uint8Array(32)));\n}\nasync function pkce() {\n const verifier = randomString();\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)) };\n}\n",
14 "js/wix/member-store.js": "// Declarations of member-store.ts: what this file exports, their arguments and what they return.\n// import { type LoginOptions, type LoginResult } from \"./auth.js\";\n// import type { CurrentMember } from \"./members-types.js\";\n// export interface MemberState {\n// member: CurrentMember | null;\n// loggedIn: boolean;\n// loading: boolean;\n// error: string | null;\n// }\n// export declare function getMemberState(): MemberState;\n// export declare function hydrateMember(member: CurrentMember | null): void;\n// export declare function subscribeMember(fn: (state: MemberState) => void): () => void;\n// export declare function refreshMember(): Promise<void>;\n// export declare function login(email: string, password: string, options?: LoginOptions): Promise<LoginResult>;\n// export declare function register(email: string, password: string, profile?: {\n// firstName?: string;\n// lastName?: string;\n// }, options?: LoginOptions): Promise<LoginResult>;\n// export declare function verifyEmail(code: string): Promise<LoginResult>;\n// export declare function logout(returnTo?: string): Promise<void>;\n// Member session state — a module-scope store, deliberately NOT a React context.\n// A context can't span Astro islands (each island is its own React root); a module\n// singleton is shared by every island in the page bundle, and works identically in a\n// single-root SPA. Consume it through useMember() (hooks/), or subscribe directly.\nimport { fetchCurrentMember } from \"./members.js\";\nimport { loggedInHint, loginMember, logoutMember, registerMember, verifyMemberEmail, } from \"./auth.js\";\nconst EMPTY = {\n member: null,\n loggedIn: false,\n loading: true,\n error: null,\n};\nlet state = EMPTY;\nconst listeners = new Set();\nlet started = false;\nfunction setState(patch) {\n state = { ...state, ...patch };\n for (const l of listeners)\n l();\n}\nexport function getMemberState() {\n return state;\n}\n/** Adopt an SSR-resolved member (Astro island props) so no client re-fetch happens. */\nexport function hydrateMember(member) {\n if (started)\n return;\n started = true;\n state = { member, loggedIn: member !== null, loading: false, error: null };\n}\nexport function subscribeMember(fn) {\n const listener = () => { try {\n fn(getMemberState());\n }\n catch (e) {\n console.error(\"member-store listener failed\", e);\n } };\n listeners.add(listener);\n // First subscriber triggers the initial session read (browser only — SSR renders `loading`).\n if (!started && typeof window !== \"undefined\") {\n started = true;\n void refreshMember();\n }\n return () => listeners.delete(listener);\n}\n/** Re-read the session — call after anything that may have changed it. */\nexport async function refreshMember() {\n setState({ loading: true, error: null });\n const hint = loggedInHint();\n if (hint === false) {\n setState({ member: null, loggedIn: false, loading: false });\n return;\n }\n // hint true → read the profile (null = profile data missing, still logged in).\n // hint null (ambient) → the member read IS the session check: null = anonymous.\n const member = await fetchCurrentMember();\n setState({ member, loggedIn: hint ?? member !== null, loading: false });\n}\nasync function runLogin(action) {\n try {\n const result = await action();\n // Managed Astro: the session is written by the callback the browser is about to visit; navigate\n // and do nothing else here (the page unloads). Manual: the tokens are already in place — re-read.\n if (result.state === \"SUCCESS\" && result.redirectUrl) {\n window.location.assign(result.redirectUrl);\n return result;\n }\n if (result.state === \"SUCCESS\")\n await refreshMember();\n if (result.state === \"FAILURE\")\n setState({\n error: result.error ?? result.errorCode ?? \"Could not log in.\",\n });\n return result;\n }\n catch (e) {\n setState({ error: e instanceof Error ? e.message : String(e) });\n throw e;\n }\n}\n/**\n * Submit the branded in-app sign-in form. `returnTo` is where a SUCCESS lands (managed Astro: the\n * callback's destination; the caller navigates itself otherwise).\n */\nexport function login(email, password, options) {\n return runLogin(() => loginMember(email, password, options));\n}\n/** Submit the branded in-app sign-up form (names only where PROFILE_ON_SIGNUP is true). */\nexport function register(email, password, profile, options) {\n return runLogin(() => registerMember(email, password, profile, options));\n}\n/** Finish an email-verification branch from the same in-app form. */\nexport function verifyEmail(code) {\n return runLogin(() => verifyMemberEmail(code));\n}\n/** Log out (navigates away); local state resets for the instant before the redirect lands. */\nexport async function logout(returnTo) {\n setState({ member: null, loggedIn: false });\n try {\n await logoutMember(returnTo);\n }\n catch (e) {\n setState({ error: e instanceof Error ? e.message : String(e) });\n throw e;\n }\n}\n",
15 "js/wix/members-core.js": "// Declarations of members-core.ts: what this file exports, their arguments and what they return.\n// import type { CurrentMember } from \"./members-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export declare const MEMBER_FIELDSETS: readonly [\"FULL\"];\n// export declare const memberId: (raw: Raw | undefined) => string;\n// export declare function toCurrentMember(raw: Raw, imgSrc: ImgSrc): CurrentMember;\n// Requested on the current-member read. FULL → loginEmail, status, the contact's names, the profile\n// (nickname, photo), and the dates; the default PUBLIC fieldset has no login email and no names.\nexport const MEMBER_FIELDSETS = [\"FULL\"];\n/** The member's id on either transport; \"\" when the raw object isn't a member. */\nexport const memberId = (raw) => raw?._id ?? raw?.id ?? \"\";\n/**\n * Raw member → CurrentMember. displayName is the first non-empty of nickname, \"first last\", the\n * login email's local part, \"Member\". photoUrl is \"\" without a profile photo. memberSince is the\n * creation date as \"YYYY-MM-DD\" (\"\" when unknown).\n */\nexport function toCurrentMember(raw, imgSrc) {\n const nickname = raw.profile?.nickname ?? \"\";\n const firstName = raw.contact?.firstName ?? \"\";\n const lastName = raw.contact?.lastName ?? \"\";\n const loginEmail = raw.loginEmail ?? \"\";\n const fullName = [firstName, lastName].filter(Boolean).join(\" \");\n const created = raw._createdDate ?? raw.createdDate;\n return {\n id: memberId(raw),\n loginEmail,\n displayName: nickname || fullName || loginEmail.split(\"@\")[0] || \"Member\",\n firstName,\n lastName,\n nickname,\n photoUrl: imgSrc(raw.profile?.photo, 200, 200),\n contactId: raw.contactId ?? \"\",\n memberSince: created ? new Date(created).toISOString().slice(0, 10) : \"\",\n };\n}\n",
16 "js/wix/members-types.js": "// Declarations of members-types.ts: what this file exports, their arguments and what they return.\n// /** The logged-in member as the header/account surfaces need it. */\n// export interface CurrentMember {\n// id: string;\n// /** Email the member logs in with (\"\" when the fieldset hides it). */\n// loginEmail: string;\n// /** Best display name: nickname → first+last → the login email's local part. */\n// displayName: string;\n// firstName: string;\n// lastName: string;\n// nickname: string;\n// /** Resolved https URL (\"\" when the member has no photo). */\n// photoUrl: string;\n// /** Contact-list id — the key for member-owned CMS/content lookups. */\n// contactId: string;\n// /** \"YYYY-MM-DD\" the membership was created (\"\" when unknown). */\n// memberSince: string;\n// }\n// Members DTOs — the serializable shapes every hook, component, and page consumes.\n// Plain JSON: safe as Astro island props or across server/client boundaries. The photo is a\n// resolved https URL.\nexport {};\n",
17 "js/wix/members.js": "// Declarations of members.ts: what this file exports, their arguments and what they return.\n// import type { CurrentMember } from \"./members-types.js\";\n// export declare function fetchCurrentMember(): Promise<CurrentMember | null>;\n// Current-member read over REST — the twin of app/wix/members/members.ts. Same export, same DTO;\n// the mapper comes from members-core (the SAME file the SDK transport uses, deployed flat next to\n// this one by deploy.mjs --stack static), so this file is only the transport: one GET. It runs with\n// whatever token ./client.js holds — a member's after a rest/auth.ts login → the profile; a\n// visitor's → 403 → null. Porting: keep the path and the fieldsets, port the core once.\n// docs: https://dev.wix.com/docs/api-reference/crm/members-contacts/members/member-management/members/get-my-member.md\nimport { wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { MEMBER_FIELDSETS, memberId, toCurrentMember } from \"./members-core.js\";\n/**\n * The current member, or null: for a visitor (the call is refused — \"Missing site member id\"), for\n * a logged-in member whose site lacks the Members Area app (no profile), and on any failure —\n * anonymous is a normal state, never an error.\n * GET /members/v1/members/my?fieldsets=FULL → { member: { id, loginEmail, contactId, contact: { firstName, lastName }, profile: { nickname, photo? }, createdDate, … } }\n */\nexport async function fetchCurrentMember() {\n try {\n const res = await wixRequest(\"/members/v1/members/my\", { method: \"GET\", query: { fieldsets: MEMBER_FIELDSETS } });\n const raw = res?.member;\n return raw && memberId(raw) ? toCurrentMember(raw, imgSrc) : null;\n }\n catch {\n return null;\n }\n}\n"