@realiizlabs/admin 0.9.1 → 0.10.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.
@@ -165,23 +165,130 @@ async function removeSiteUser(ctx, input) {
165
165
  if (target.error) throw target.error;
166
166
  const targetRole = target.data?.role ?? "editor";
167
167
  assertMayGrant(actorRole, input.siteId, targetRole);
168
+ if (targetRole === "owner") await assertNotLastOwner(svc, input.siteId);
168
169
  const del = await svc.from("site_users").delete().eq("user_id", input.userId).eq("site_id", input.siteId);
169
170
  if (del.error) throw del.error;
170
171
  }
172
+ async function updateSiteRole(ctx, input) {
173
+ const svc = createServiceClient(ctx);
174
+ const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);
175
+ assertMayGrant(actorRole, input.siteId, input.role);
176
+ const target = await svc.from("site_users").select("role").eq("user_id", input.userId).eq("site_id", input.siteId).maybeSingle();
177
+ if (target.error) throw target.error;
178
+ const current = target.data?.role;
179
+ if (!current) throw new IdentityError("That person isn't a member of this site.");
180
+ if (current === "owner") assertMayGrant(actorRole, input.siteId, "owner");
181
+ if (current === "owner" && input.role === "editor") await assertNotLastOwner(svc, input.siteId);
182
+ const upd = await svc.from("site_users").update({ role: input.role }).eq("user_id", input.userId).eq("site_id", input.siteId);
183
+ if (upd.error) throw upd.error;
184
+ }
185
+ async function assertNotLastOwner(svc, siteId) {
186
+ const owners = await svc.from("site_users").select("user_id").eq("site_id", siteId).eq("role", "owner");
187
+ if (owners.error) throw owners.error;
188
+ if ((owners.data ?? []).length <= 1) throw new IdentityError("A site needs at least one owner \u2014 make someone else an owner first.");
189
+ }
190
+ async function resendInvite(ctx, input) {
191
+ const svc = createServiceClient(ctx);
192
+ const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);
193
+ assertMayGrant(actorRole, input.siteId, "editor");
194
+ const email = input.email.trim().toLowerCase();
195
+ const again = await svc.auth.admin.inviteUserByEmail(email, { redirectTo: input.redirectTo, data: { site_id: input.siteId } });
196
+ if (!again.error) return { resent: true, message: `Invitation sent again to ${email}.` };
197
+ if (/already|exists|registered|confirmed/i.test(again.error.message)) {
198
+ return { resent: false, message: `${email} has already accepted \u2014 they can sign in with an email link any time.` };
199
+ }
200
+ throw new IdentityError(`Resend failed: ${again.error.message}`);
201
+ }
202
+
203
+ // src/auth/branding.ts
204
+ async function getSiteBranding(client, siteId) {
205
+ const { data, error } = await client.from("sites").select("name, logo_url, favicon_url, business_email, business_phone").eq("site_id", siteId).maybeSingle();
206
+ if (error) throw error;
207
+ if (!data) return null;
208
+ const r = data;
209
+ return { name: r.name, logoUrl: r.logo_url, faviconUrl: r.favicon_url, businessEmail: r.business_email, businessPhone: r.business_phone };
210
+ }
211
+ async function updateSiteBranding(client, siteId, patch) {
212
+ const row = {
213
+ site_id: siteId,
214
+ name: patch.name.trim(),
215
+ logo_url: patch.logoUrl ?? null,
216
+ favicon_url: patch.faviconUrl ?? null,
217
+ business_email: emptyToNull(patch.businessEmail),
218
+ business_phone: emptyToNull(patch.businessPhone)
219
+ };
220
+ const { error } = await client.from("sites").upsert(row, { onConflict: "site_id" });
221
+ if (error) throw error;
222
+ }
223
+ function emptyToNull(v) {
224
+ const s = (v ?? "").trim();
225
+ return s === "" ? null : s;
226
+ }
227
+
228
+ // src/auth/members.ts
229
+ async function listSiteMembers(client, siteId) {
230
+ const { data, error } = await client.rpc("site_members", { p_site_id: siteId });
231
+ if (error) {
232
+ if (error.code === "42501" || /not allowed/i.test(error.message)) throw new ForbiddenError(siteId, "owner", null);
233
+ throw error;
234
+ }
235
+ return (data ?? []).map((r) => ({
236
+ userId: r.user_id,
237
+ email: r.email,
238
+ name: r.name,
239
+ avatarUrl: r.avatar_url,
240
+ role: r.role === "owner" ? "owner" : "editor",
241
+ status: r.status === "active" ? "active" : "invited",
242
+ invitedAt: r.invited_at
243
+ }));
244
+ }
245
+
246
+ // src/auth/profile.ts
247
+ var MIN_PASSWORD_LENGTH = 10;
248
+ var PASSWORD_TOO_SHORT = "Use at least 10 characters.";
249
+ function profileOf(user) {
250
+ const m = user?.user_metadata ?? {};
251
+ const name = typeof m.name === "string" && m.name.trim() ? m.name.trim() : null;
252
+ const avatarUrl = typeof m.avatar_url === "string" && m.avatar_url ? m.avatar_url : null;
253
+ return { name, avatarUrl };
254
+ }
255
+ async function updateProfile(client, patch) {
256
+ const data = {};
257
+ if (patch.name !== void 0) data.name = (patch.name ?? "").trim().slice(0, 80);
258
+ if (patch.avatarUrl !== void 0) data.avatar_url = patch.avatarUrl ?? "";
259
+ const { data: res, error } = await client.auth.updateUser({ data });
260
+ if (error) throw new IdentityError(error.message);
261
+ return profileOf(res.user);
262
+ }
263
+ async function setPassword(client, password) {
264
+ if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) throw new IdentityError(PASSWORD_TOO_SHORT);
265
+ const { error } = await client.auth.updateUser({ password });
266
+ if (error) throw new IdentityError(error.message);
267
+ }
171
268
 
172
269
  exports.ForbiddenError = ForbiddenError;
173
270
  exports.IdentityError = IdentityError;
271
+ exports.MIN_PASSWORD_LENGTH = MIN_PASSWORD_LENGTH;
272
+ exports.PASSWORD_TOO_SHORT = PASSWORD_TOO_SHORT;
174
273
  exports.ROLE_RANK = ROLE_RANK;
175
274
  exports.UnauthenticatedError = UnauthenticatedError;
176
275
  exports.createIdentityClient = createIdentityClient;
177
276
  exports.getCurrentUser = getCurrentUser;
277
+ exports.getSiteBranding = getSiteBranding;
178
278
  exports.getSiteRole = getSiteRole;
179
279
  exports.handleAuthCallback = handleAuthCallback;
180
280
  exports.inviteUser = inviteUser;
281
+ exports.listSiteMembers = listSiteMembers;
282
+ exports.profileOf = profileOf;
181
283
  exports.removeSiteUser = removeSiteUser;
182
284
  exports.requireSiteUser = requireSiteUser;
285
+ exports.resendInvite = resendInvite;
183
286
  exports.roleAtLeast = roleAtLeast;
184
287
  exports.safeNextPath = safeNextPath;
288
+ exports.setPassword = setPassword;
185
289
  exports.signOut = signOut;
290
+ exports.updateProfile = updateProfile;
291
+ exports.updateSiteBranding = updateSiteBranding;
292
+ exports.updateSiteRole = updateSiteRole;
186
293
  //# sourceMappingURL=index.cjs.map
187
294
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/client.ts","../../src/auth/session.ts","../../src/auth/errors.ts","../../src/auth/types.ts","../../src/auth/roles.ts","../../src/auth/callback.ts","../../src/auth/signout.ts","../../src/auth/service.ts"],"names":["createServerClient","createClient"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * createIdentityClient — a Supabase client bound to the request's cookies.\n *\n * Built per request from arguments. A Next 16 route does:\n *\n * const store = await cookies();\n * const client = createIdentityClient({ url, anonKey, cookies: store });\n *\n * No `next` import here. The CookieStore interface accepts either shape a\n * host might hand us: @supabase/ssr's { getAll, setAll }, or Next's\n * cookies() store, which has { getAll, set } — the session cookies are\n * written through whichever exists. (Observed 2026-09-08: with only setAll\n * supported, Next's store silently wrote nothing and every sign-in bounced.)\n */\n\nimport { createServerClient } from \"@supabase/ssr\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { IdentityContext } from \"./types\";\n\nexport type IdentityClient = SupabaseClient;\n\ntype CookieToSet = { name: string; value: string; options?: Record<string, unknown> };\n\nexport function createIdentityClient(ctx: IdentityContext): IdentityClient {\n const store = ctx.cookies;\n return createServerClient(ctx.url, ctx.anonKey, {\n cookies: {\n getAll: () => store.getAll(),\n setAll: (list: CookieToSet[]) => {\n // Server Components can't set cookies; @supabase/ssr documents that\n // swallowing the failure there is correct — middleware / route\n // handlers refresh the session instead.\n try {\n if (typeof store.setAll === \"function\") {\n void store.setAll(list);\n } else if (typeof store.set === \"function\") {\n for (const c of list) store.set(c.name, c.value, c.options);\n }\n } catch {\n /* read-only cookie store */\n }\n },\n },\n });\n}\n","/**\n * getCurrentUser — who is signed in, verified against the auth server.\n *\n * `getUser()` round-trips to Supabase and validates the JWT; `getSession()`\n * only decodes the cookie and is not trustworthy on the server. Always this.\n */\n\nimport type { IdentityClient } from \"./client\";\nimport type { IdentityUser } from \"./types\";\n\nexport async function getCurrentUser(client: IdentityClient): Promise<IdentityUser | null> {\n const { data, error } = await client.auth.getUser();\n if (error || !data.user) return null;\n return { id: data.user.id, email: data.user.email ?? null };\n}\n","/** Typed errors so a route can map them to 401 / 403 without string matching. */\n\nexport class IdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"IdentityError\";\n }\n}\n\n/** No session — send them to sign in. Maps to 401. */\nexport class UnauthenticatedError extends IdentityError {\n constructor() {\n super(\"Not signed in\");\n this.name = \"UnauthenticatedError\";\n }\n}\n\n/** Signed in, but not allowed to do this here. Maps to 403. */\nexport class ForbiddenError extends IdentityError {\n readonly siteId: string;\n readonly required: string;\n readonly actual: string | null;\n\n constructor(siteId: string, required: string, actual: string | null) {\n super(`Requires ${required} on site \"${siteId}\" (you are ${actual ?? \"not a member\"})`);\n this.name = \"ForbiddenError\";\n this.siteId = siteId;\n this.required = required;\n this.actual = actual;\n }\n}\n","/**\n * Types for the identity layer. All config arrives as arguments — never env.\n */\n\n/**\n * Either cookie-store shape a host can hand us:\n * - @supabase/ssr's CookieMethodsServer: { getAll, setAll }\n * - Next's `await cookies()` store: { getAll, set }\n * `setAll` is preferred when present; otherwise each cookie goes through `set`.\n */\nexport interface CookieStore {\n getAll(): { name: string; value: string }[] | Promise<{ name: string; value: string }[]>;\n setAll?(cookies: { name: string; value: string; options?: Record<string, unknown> }[]): void | Promise<void>;\n set?(name: string, value: string, options?: Record<string, unknown>): unknown;\n}\n\n/** Enough to read the signed-in user with the anon key + their cookies. */\nexport interface IdentityContext {\n url: string;\n anonKey: string;\n cookies: CookieStore;\n}\n\n/** Server-only. The service-role key bypasses RLS; the module checks roles in code first. */\nexport interface ServiceContext {\n url: string;\n serviceRoleKey: string;\n}\n\nexport type SiteRole = \"staff\" | \"owner\" | \"editor\";\n\nexport const ROLE_RANK: Record<SiteRole, number> = { editor: 1, owner: 2, staff: 3 };\n\nexport interface IdentityUser {\n id: string;\n email: string | null;\n}\n\nexport interface SiteMembership {\n userId: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n}\n","/**\n * Role resolution and the request gate.\n *\n * Two selects on the caller's OWN rows — both permitted by RLS with the anon\n * key, so this never needs the service role. Staff wins over any site role.\n */\n\nimport type { IdentityClient } from \"./client\";\nimport { ForbiddenError, UnauthenticatedError } from \"./errors\";\nimport { getCurrentUser } from \"./session\";\nimport { ROLE_RANK, type IdentityUser, type SiteRole } from \"./types\";\n\nexport async function getSiteRole(client: IdentityClient, siteId: string, userId?: string): Promise<SiteRole | null> {\n const uid = userId ?? (await getCurrentUser(client))?.id;\n if (!uid) return null;\n\n const [staff, member] = await Promise.all([\n client.from(\"staff\").select(\"user_id\").eq(\"user_id\", uid).maybeSingle(),\n client.from(\"site_users\").select(\"role\").eq(\"user_id\", uid).eq(\"site_id\", siteId).maybeSingle(),\n ]);\n\n if (staff.error) throw staff.error;\n if (member.error) throw member.error;\n\n if (staff.data) return \"staff\";\n const role = (member.data as { role?: string } | null)?.role;\n return role === \"owner\" || role === \"editor\" ? role : null;\n}\n\nexport function roleAtLeast(actual: SiteRole | null, minimum: SiteRole): boolean {\n return actual !== null && ROLE_RANK[actual] >= ROLE_RANK[minimum];\n}\n\nexport interface RequireOptions {\n /** Default \"editor\" — any member of the site. */\n minimumRole?: SiteRole;\n}\n\n/**\n * The gate. Throws UnauthenticatedError (→ sign in) or ForbiddenError (→ 403);\n * otherwise returns the user and their effective role for this site.\n */\nexport async function requireSiteUser(\n client: IdentityClient,\n siteId: string,\n opts: RequireOptions = {},\n): Promise<{ user: IdentityUser; role: SiteRole }> {\n const user = await getCurrentUser(client);\n if (!user) throw new UnauthenticatedError();\n\n const minimum = opts.minimumRole ?? \"editor\";\n const role = await getSiteRole(client, siteId, user.id);\n if (!roleAtLeast(role, minimum)) throw new ForbiddenError(siteId, minimum, role);\n\n return { user, role: role as SiteRole };\n}\n","/**\n * handleAuthCallback — the emailed magic link lands here with ?code=…&next=…\n *\n * Exchanges the PKCE code for a session (cookies are written through the\n * adapter the client was built with) and says where to send the user. The\n * `next` parameter is only honoured when it is a plain same-origin path —\n * \"//evil.example\" and \"https://…\" fall back, which closes the open-redirect\n * a naive startsWith(\"/\") check leaves open.\n */\n\nimport type { IdentityClient } from \"./client\";\n\nexport type CallbackResult =\n | { ok: true; redirectTo: string }\n | { ok: false; reason: \"missing_code\" | \"exchange_failed\"; message?: string };\n\nexport interface CallbackOptions {\n /** Where to go when `next` is absent or unsafe. Default \"/admin\". */\n fallbackPath?: string;\n}\n\nexport function safeNextPath(next: string | null | undefined, fallback: string): string {\n if (!next) return fallback;\n // Exactly one leading slash, then not another slash or backslash.\n if (!/^\\/(?![/\\\\])/.test(next)) return fallback;\n // No scheme smuggling or CR/LF.\n if (/[\\r\\n]/.test(next) || /^\\/[^?#]*:\\/\\//.test(next)) return fallback;\n return next;\n}\n\nexport async function handleAuthCallback(\n client: IdentityClient,\n requestUrl: string | URL,\n opts: CallbackOptions = {},\n): Promise<CallbackResult> {\n const fallback = opts.fallbackPath ?? \"/admin\";\n const url = typeof requestUrl === \"string\" ? new URL(requestUrl, \"http://placeholder.invalid\") : requestUrl;\n const code = url.searchParams.get(\"code\");\n if (!code) return { ok: false, reason: \"missing_code\" };\n\n const { error } = await client.auth.exchangeCodeForSession(code);\n if (error) return { ok: false, reason: \"exchange_failed\", message: error.message };\n\n return { ok: true, redirectTo: safeNextPath(url.searchParams.get(\"next\"), fallback) };\n}\n","/** signOut — clears this browser's session. The caller redirects afterwards. */\n\nimport type { IdentityClient } from \"./client\";\n\nexport async function signOut(client: IdentityClient): Promise<void> {\n const { error } = await client.auth.signOut({ scope: \"local\" });\n if (error) throw error;\n}\n","/**\n * The only file that touches the service-role key. Invite and remove.\n *\n * Authorization is checked HERE, in code, before the privileged client does\n * anything — RLS doesn't apply to the service role, so this is the guard:\n * staff → may grant owner or editor on any site\n * owner → may grant editor on their own site only\n * editor → may not invite\n *\n * `actor` is the caller's already-resolved identity (from requireSiteUser on\n * the anon client). Its role is re-resolved with the service client so a\n * caller can't lie about it.\n */\n\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport { ForbiddenError, IdentityError } from \"./errors\";\nimport type { ServiceContext, SiteRole } from \"./types\";\n\nexport type ServiceClient = SupabaseClient;\n\nexport function createServiceClient(ctx: ServiceContext): ServiceClient {\n return createClient(ctx.url, ctx.serviceRoleKey, {\n auth: { persistSession: false, autoRefreshToken: false },\n });\n}\n\nexport interface InviteInput {\n email: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n /** The signed-in user performing the invite. */\n actor: { id: string };\n /** Where the invite email's link lands — the site's /admin/auth/callback. */\n redirectTo: string;\n}\n\nexport interface InviteResult {\n userId: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n /** false when the auth user already existed and only the membership was added. */\n invitedByEmail: boolean;\n}\n\nasync function resolveActorRole(svc: ServiceClient, actorId: string, siteId: string): Promise<SiteRole | null> {\n const staff = await svc.from(\"staff\").select(\"user_id\").eq(\"user_id\", actorId).maybeSingle();\n if (staff.error) throw staff.error;\n if (staff.data) return \"staff\";\n const m = await svc.from(\"site_users\").select(\"role\").eq(\"user_id\", actorId).eq(\"site_id\", siteId).maybeSingle();\n if (m.error) throw m.error;\n const role = (m.data as { role?: string } | null)?.role;\n return role === \"owner\" || role === \"editor\" ? role : null;\n}\n\nfunction assertMayGrant(actorRole: SiteRole | null, siteId: string, granting: \"owner\" | \"editor\"): void {\n if (actorRole === \"staff\") return;\n if (actorRole === \"owner\" && granting === \"editor\") return;\n throw new ForbiddenError(siteId, granting === \"owner\" ? \"staff\" : \"owner\", actorRole);\n}\n\nexport async function inviteUser(ctx: ServiceContext, input: InviteInput): Promise<InviteResult> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n assertMayGrant(actorRole, input.siteId, input.role);\n\n const email = input.email.trim().toLowerCase();\n let userId: string | undefined;\n let invitedByEmail = false;\n\n const invite = await svc.auth.admin.inviteUserByEmail(email, {\n redirectTo: input.redirectTo,\n data: { site_id: input.siteId, role: input.role },\n });\n\n if (!invite.error) {\n userId = invite.data.user.id;\n invitedByEmail = true;\n } else if (/already|exists|registered/i.test(invite.error.message)) {\n // Existing auth user (works for another site, or is staff): just add the row.\n userId = await findUserIdByEmail(svc, email);\n if (!userId) throw new IdentityError(`Could not find existing user ${email}`);\n } else {\n throw new IdentityError(`Invite failed: ${invite.error.message}`);\n }\n\n const upsert = await svc\n .from(\"site_users\")\n .upsert({ user_id: userId, site_id: input.siteId, role: input.role, invited_by: input.actor.id }, { onConflict: \"user_id,site_id\" });\n if (upsert.error) throw upsert.error;\n\n return { userId, siteId: input.siteId, role: input.role, invitedByEmail };\n}\n\nasync function findUserIdByEmail(svc: ServiceClient, email: string): Promise<string | undefined> {\n // listUsers is paged; identity holds tens of users, not thousands.\n for (let page = 1; page <= 20; page++) {\n const { data, error } = await svc.auth.admin.listUsers({ page, perPage: 200 });\n if (error) throw error;\n const hit = data.users.find((u) => u.email?.toLowerCase() === email);\n if (hit) return hit.id;\n if (data.users.length < 200) break;\n }\n return undefined;\n}\n\nexport interface RemoveInput {\n userId: string;\n siteId: string;\n actor: { id: string };\n}\n\n/** Removes the membership only. The auth user survives (they may belong to other sites). */\nexport async function removeSiteUser(ctx: ServiceContext, input: RemoveInput): Promise<void> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n\n const target = await svc.from(\"site_users\").select(\"role\").eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId).maybeSingle();\n if (target.error) throw target.error;\n const targetRole = ((target.data as { role?: string } | null)?.role ?? \"editor\") as \"owner\" | \"editor\";\n assertMayGrant(actorRole, input.siteId, targetRole);\n\n const del = await svc.from(\"site_users\").delete().eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId);\n if (del.error) throw del.error;\n}\n"]}
1
+ {"version":3,"sources":["../../src/auth/client.ts","../../src/auth/session.ts","../../src/auth/errors.ts","../../src/auth/types.ts","../../src/auth/roles.ts","../../src/auth/callback.ts","../../src/auth/signout.ts","../../src/auth/service.ts","../../src/auth/branding.ts","../../src/auth/members.ts","../../src/auth/profile.ts"],"names":["createServerClient","createClient"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * createIdentityClient — a Supabase client bound to the request's cookies.\n *\n * Built per request from arguments. A Next 16 route does:\n *\n * const store = await cookies();\n * const client = createIdentityClient({ url, anonKey, cookies: store });\n *\n * No `next` import here. The CookieStore interface accepts either shape a\n * host might hand us: @supabase/ssr's { getAll, setAll }, or Next's\n * cookies() store, which has { getAll, set } — the session cookies are\n * written through whichever exists. (Observed 2026-09-08: with only setAll\n * supported, Next's store silently wrote nothing and every sign-in bounced.)\n */\n\nimport { createServerClient } from \"@supabase/ssr\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { IdentityContext } from \"./types\";\n\nexport type IdentityClient = SupabaseClient;\n\ntype CookieToSet = { name: string; value: string; options?: Record<string, unknown> };\n\nexport function createIdentityClient(ctx: IdentityContext): IdentityClient {\n const store = ctx.cookies;\n return createServerClient(ctx.url, ctx.anonKey, {\n cookies: {\n getAll: () => store.getAll(),\n setAll: (list: CookieToSet[]) => {\n // Server Components can't set cookies; @supabase/ssr documents that\n // swallowing the failure there is correct — middleware / route\n // handlers refresh the session instead.\n try {\n if (typeof store.setAll === \"function\") {\n void store.setAll(list);\n } else if (typeof store.set === \"function\") {\n for (const c of list) store.set(c.name, c.value, c.options);\n }\n } catch {\n /* read-only cookie store */\n }\n },\n },\n });\n}\n","/**\n * getCurrentUser — who is signed in, verified against the auth server.\n *\n * `getUser()` round-trips to Supabase and validates the JWT; `getSession()`\n * only decodes the cookie and is not trustworthy on the server. Always this.\n */\n\nimport type { IdentityClient } from \"./client\";\nimport type { IdentityUser } from \"./types\";\n\nexport async function getCurrentUser(client: IdentityClient): Promise<IdentityUser | null> {\n const { data, error } = await client.auth.getUser();\n if (error || !data.user) return null;\n return { id: data.user.id, email: data.user.email ?? null };\n}\n","/** Typed errors so a route can map them to 401 / 403 without string matching. */\n\nexport class IdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"IdentityError\";\n }\n}\n\n/** No session — send them to sign in. Maps to 401. */\nexport class UnauthenticatedError extends IdentityError {\n constructor() {\n super(\"Not signed in\");\n this.name = \"UnauthenticatedError\";\n }\n}\n\n/** Signed in, but not allowed to do this here. Maps to 403. */\nexport class ForbiddenError extends IdentityError {\n readonly siteId: string;\n readonly required: string;\n readonly actual: string | null;\n\n constructor(siteId: string, required: string, actual: string | null) {\n super(`Requires ${required} on site \"${siteId}\" (you are ${actual ?? \"not a member\"})`);\n this.name = \"ForbiddenError\";\n this.siteId = siteId;\n this.required = required;\n this.actual = actual;\n }\n}\n","/**\n * Types for the identity layer. All config arrives as arguments — never env.\n */\n\n/**\n * Either cookie-store shape a host can hand us:\n * - @supabase/ssr's CookieMethodsServer: { getAll, setAll }\n * - Next's `await cookies()` store: { getAll, set }\n * `setAll` is preferred when present; otherwise each cookie goes through `set`.\n */\nexport interface CookieStore {\n getAll(): { name: string; value: string }[] | Promise<{ name: string; value: string }[]>;\n setAll?(cookies: { name: string; value: string; options?: Record<string, unknown> }[]): void | Promise<void>;\n set?(name: string, value: string, options?: Record<string, unknown>): unknown;\n}\n\n/** Enough to read the signed-in user with the anon key + their cookies. */\nexport interface IdentityContext {\n url: string;\n anonKey: string;\n cookies: CookieStore;\n}\n\n/** Server-only. The service-role key bypasses RLS; the module checks roles in code first. */\nexport interface ServiceContext {\n url: string;\n serviceRoleKey: string;\n}\n\nexport type SiteRole = \"staff\" | \"owner\" | \"editor\";\n\nexport const ROLE_RANK: Record<SiteRole, number> = { editor: 1, owner: 2, staff: 3 };\n\nexport interface IdentityUser {\n id: string;\n email: string | null;\n}\n\nexport interface SiteMembership {\n userId: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n}\n","/**\n * Role resolution and the request gate.\n *\n * Two selects on the caller's OWN rows — both permitted by RLS with the anon\n * key, so this never needs the service role. Staff wins over any site role.\n */\n\nimport type { IdentityClient } from \"./client\";\nimport { ForbiddenError, UnauthenticatedError } from \"./errors\";\nimport { getCurrentUser } from \"./session\";\nimport { ROLE_RANK, type IdentityUser, type SiteRole } from \"./types\";\n\nexport async function getSiteRole(client: IdentityClient, siteId: string, userId?: string): Promise<SiteRole | null> {\n const uid = userId ?? (await getCurrentUser(client))?.id;\n if (!uid) return null;\n\n const [staff, member] = await Promise.all([\n client.from(\"staff\").select(\"user_id\").eq(\"user_id\", uid).maybeSingle(),\n client.from(\"site_users\").select(\"role\").eq(\"user_id\", uid).eq(\"site_id\", siteId).maybeSingle(),\n ]);\n\n if (staff.error) throw staff.error;\n if (member.error) throw member.error;\n\n if (staff.data) return \"staff\";\n const role = (member.data as { role?: string } | null)?.role;\n return role === \"owner\" || role === \"editor\" ? role : null;\n}\n\nexport function roleAtLeast(actual: SiteRole | null, minimum: SiteRole): boolean {\n return actual !== null && ROLE_RANK[actual] >= ROLE_RANK[minimum];\n}\n\nexport interface RequireOptions {\n /** Default \"editor\" — any member of the site. */\n minimumRole?: SiteRole;\n}\n\n/**\n * The gate. Throws UnauthenticatedError (→ sign in) or ForbiddenError (→ 403);\n * otherwise returns the user and their effective role for this site.\n */\nexport async function requireSiteUser(\n client: IdentityClient,\n siteId: string,\n opts: RequireOptions = {},\n): Promise<{ user: IdentityUser; role: SiteRole }> {\n const user = await getCurrentUser(client);\n if (!user) throw new UnauthenticatedError();\n\n const minimum = opts.minimumRole ?? \"editor\";\n const role = await getSiteRole(client, siteId, user.id);\n if (!roleAtLeast(role, minimum)) throw new ForbiddenError(siteId, minimum, role);\n\n return { user, role: role as SiteRole };\n}\n","/**\n * handleAuthCallback — the emailed magic link lands here with ?code=…&next=…\n *\n * Exchanges the PKCE code for a session (cookies are written through the\n * adapter the client was built with) and says where to send the user. The\n * `next` parameter is only honoured when it is a plain same-origin path —\n * \"//evil.example\" and \"https://…\" fall back, which closes the open-redirect\n * a naive startsWith(\"/\") check leaves open.\n */\n\nimport type { IdentityClient } from \"./client\";\n\nexport type CallbackResult =\n | { ok: true; redirectTo: string }\n | { ok: false; reason: \"missing_code\" | \"exchange_failed\"; message?: string };\n\nexport interface CallbackOptions {\n /** Where to go when `next` is absent or unsafe. Default \"/admin\". */\n fallbackPath?: string;\n}\n\nexport function safeNextPath(next: string | null | undefined, fallback: string): string {\n if (!next) return fallback;\n // Exactly one leading slash, then not another slash or backslash.\n if (!/^\\/(?![/\\\\])/.test(next)) return fallback;\n // No scheme smuggling or CR/LF.\n if (/[\\r\\n]/.test(next) || /^\\/[^?#]*:\\/\\//.test(next)) return fallback;\n return next;\n}\n\nexport async function handleAuthCallback(\n client: IdentityClient,\n requestUrl: string | URL,\n opts: CallbackOptions = {},\n): Promise<CallbackResult> {\n const fallback = opts.fallbackPath ?? \"/admin\";\n const url = typeof requestUrl === \"string\" ? new URL(requestUrl, \"http://placeholder.invalid\") : requestUrl;\n const code = url.searchParams.get(\"code\");\n if (!code) return { ok: false, reason: \"missing_code\" };\n\n const { error } = await client.auth.exchangeCodeForSession(code);\n if (error) return { ok: false, reason: \"exchange_failed\", message: error.message };\n\n return { ok: true, redirectTo: safeNextPath(url.searchParams.get(\"next\"), fallback) };\n}\n","/** signOut — clears this browser's session. The caller redirects afterwards. */\n\nimport type { IdentityClient } from \"./client\";\n\nexport async function signOut(client: IdentityClient): Promise<void> {\n const { error } = await client.auth.signOut({ scope: \"local\" });\n if (error) throw error;\n}\n","/**\n * The only file that touches the service-role key. Invite and remove.\n *\n * Authorization is checked HERE, in code, before the privileged client does\n * anything — RLS doesn't apply to the service role, so this is the guard:\n * staff → may grant owner or editor on any site\n * owner → may grant editor on their own site only\n * editor → may not invite\n *\n * `actor` is the caller's already-resolved identity (from requireSiteUser on\n * the anon client). Its role is re-resolved with the service client so a\n * caller can't lie about it.\n */\n\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport { ForbiddenError, IdentityError } from \"./errors\";\nimport type { ServiceContext, SiteRole } from \"./types\";\n\nexport type ServiceClient = SupabaseClient;\n\nexport function createServiceClient(ctx: ServiceContext): ServiceClient {\n return createClient(ctx.url, ctx.serviceRoleKey, {\n auth: { persistSession: false, autoRefreshToken: false },\n });\n}\n\nexport interface InviteInput {\n email: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n /** The signed-in user performing the invite. */\n actor: { id: string };\n /** Where the invite email's link lands — the site's /admin/auth/callback. */\n redirectTo: string;\n}\n\nexport interface InviteResult {\n userId: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n /** false when the auth user already existed and only the membership was added. */\n invitedByEmail: boolean;\n}\n\nasync function resolveActorRole(svc: ServiceClient, actorId: string, siteId: string): Promise<SiteRole | null> {\n const staff = await svc.from(\"staff\").select(\"user_id\").eq(\"user_id\", actorId).maybeSingle();\n if (staff.error) throw staff.error;\n if (staff.data) return \"staff\";\n const m = await svc.from(\"site_users\").select(\"role\").eq(\"user_id\", actorId).eq(\"site_id\", siteId).maybeSingle();\n if (m.error) throw m.error;\n const role = (m.data as { role?: string } | null)?.role;\n return role === \"owner\" || role === \"editor\" ? role : null;\n}\n\nfunction assertMayGrant(actorRole: SiteRole | null, siteId: string, granting: \"owner\" | \"editor\"): void {\n if (actorRole === \"staff\") return;\n if (actorRole === \"owner\" && granting === \"editor\") return;\n throw new ForbiddenError(siteId, granting === \"owner\" ? \"staff\" : \"owner\", actorRole);\n}\n\nexport async function inviteUser(ctx: ServiceContext, input: InviteInput): Promise<InviteResult> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n assertMayGrant(actorRole, input.siteId, input.role);\n\n const email = input.email.trim().toLowerCase();\n let userId: string | undefined;\n let invitedByEmail = false;\n\n const invite = await svc.auth.admin.inviteUserByEmail(email, {\n redirectTo: input.redirectTo,\n data: { site_id: input.siteId, role: input.role },\n });\n\n if (!invite.error) {\n userId = invite.data.user.id;\n invitedByEmail = true;\n } else if (/already|exists|registered/i.test(invite.error.message)) {\n // Existing auth user (works for another site, or is staff): just add the row.\n userId = await findUserIdByEmail(svc, email);\n if (!userId) throw new IdentityError(`Could not find existing user ${email}`);\n } else {\n throw new IdentityError(`Invite failed: ${invite.error.message}`);\n }\n\n const upsert = await svc\n .from(\"site_users\")\n .upsert({ user_id: userId, site_id: input.siteId, role: input.role, invited_by: input.actor.id }, { onConflict: \"user_id,site_id\" });\n if (upsert.error) throw upsert.error;\n\n return { userId, siteId: input.siteId, role: input.role, invitedByEmail };\n}\n\nasync function findUserIdByEmail(svc: ServiceClient, email: string): Promise<string | undefined> {\n // listUsers is paged; identity holds tens of users, not thousands.\n for (let page = 1; page <= 20; page++) {\n const { data, error } = await svc.auth.admin.listUsers({ page, perPage: 200 });\n if (error) throw error;\n const hit = data.users.find((u) => u.email?.toLowerCase() === email);\n if (hit) return hit.id;\n if (data.users.length < 200) break;\n }\n return undefined;\n}\n\nexport interface RemoveInput {\n userId: string;\n siteId: string;\n actor: { id: string };\n}\n\n/** Removes the membership only. The auth user survives (they may belong to other sites). */\nexport async function removeSiteUser(ctx: ServiceContext, input: RemoveInput): Promise<void> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n\n const target = await svc.from(\"site_users\").select(\"role\").eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId).maybeSingle();\n if (target.error) throw target.error;\n const targetRole = ((target.data as { role?: string } | null)?.role ?? \"editor\") as \"owner\" | \"editor\";\n assertMayGrant(actorRole, input.siteId, targetRole);\n if (targetRole === \"owner\") await assertNotLastOwner(svc, input.siteId);\n\n const del = await svc.from(\"site_users\").delete().eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId);\n if (del.error) throw del.error;\n}\n\nexport interface RoleInput {\n userId: string;\n siteId: string;\n role: \"owner\" | \"editor\";\n actor: { id: string };\n}\n\n/**\n * Change a member's role. Same grant rules as invite (staff anything; an owner\n * may only make people editors on their own site), plus: the last owner of a\n * site can never be demoted, otherwise nobody could manage it.\n */\nexport async function updateSiteRole(ctx: ServiceContext, input: RoleInput): Promise<void> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n assertMayGrant(actorRole, input.siteId, input.role);\n\n const target = await svc.from(\"site_users\").select(\"role\").eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId).maybeSingle();\n if (target.error) throw target.error;\n const current = (target.data as { role?: string } | null)?.role;\n if (!current) throw new IdentityError(\"That person isn't a member of this site.\");\n // Demoting an owner is an owner-level act — an owner may not do it to a peer.\n if (current === \"owner\") assertMayGrant(actorRole, input.siteId, \"owner\");\n if (current === \"owner\" && input.role === \"editor\") await assertNotLastOwner(svc, input.siteId);\n\n const upd = await svc.from(\"site_users\").update({ role: input.role }).eq(\"user_id\", input.userId).eq(\"site_id\", input.siteId);\n if (upd.error) throw upd.error;\n}\n\nexport async function assertNotLastOwner(svc: ServiceClient, siteId: string): Promise<void> {\n const owners = await svc.from(\"site_users\").select(\"user_id\").eq(\"site_id\", siteId).eq(\"role\", \"owner\");\n if (owners.error) throw owners.error;\n if ((owners.data ?? []).length <= 1) throw new IdentityError(\"A site needs at least one owner — make someone else an owner first.\");\n}\n\nexport interface ResendInput {\n email: string;\n siteId: string;\n actor: { id: string };\n redirectTo: string;\n}\n\n/** Send the invite email again. An already-active user gets no email — they can just sign in. */\nexport async function resendInvite(ctx: ServiceContext, input: ResendInput): Promise<{ resent: boolean; message: string }> {\n const svc = createServiceClient(ctx);\n const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);\n assertMayGrant(actorRole, input.siteId, \"editor\");\n\n const email = input.email.trim().toLowerCase();\n const again = await svc.auth.admin.inviteUserByEmail(email, { redirectTo: input.redirectTo, data: { site_id: input.siteId } });\n if (!again.error) return { resent: true, message: `Invitation sent again to ${email}.` };\n if (/already|exists|registered|confirmed/i.test(again.error.message)) {\n return { resent: false, message: `${email} has already accepted — they can sign in with an email link any time.` };\n }\n throw new IdentityError(`Resend failed: ${again.error.message}`);\n}\n","/**\n * Per-site branding — the Business tab. Reads and writes public.sites through\n * the signed-in user's client, so RLS decides who may (members read; owners\n * and staff write). Nothing here needs the service role.\n */\n\nimport type { IdentityClient } from \"./client\";\n\nexport interface SiteBranding {\n name: string;\n logoUrl: string | null;\n faviconUrl: string | null;\n businessEmail: string | null;\n businessPhone: string | null;\n}\n\nexport type BrandingPatch = Partial<SiteBranding> & { name: string };\n\ninterface Row {\n name: string;\n logo_url: string | null;\n favicon_url: string | null;\n business_email: string | null;\n business_phone: string | null;\n}\n\nexport async function getSiteBranding(client: IdentityClient, siteId: string): Promise<SiteBranding | null> {\n const { data, error } = await client.from(\"sites\").select(\"name, logo_url, favicon_url, business_email, business_phone\").eq(\"site_id\", siteId).maybeSingle();\n if (error) throw error;\n if (!data) return null;\n const r = data as Row;\n return { name: r.name, logoUrl: r.logo_url, faviconUrl: r.favicon_url, businessEmail: r.business_email, businessPhone: r.business_phone };\n}\n\n/** Upsert the row. RLS refuses anyone but the site's owners or staff. */\nexport async function updateSiteBranding(client: IdentityClient, siteId: string, patch: BrandingPatch): Promise<void> {\n const row = {\n site_id: siteId,\n name: patch.name.trim(),\n logo_url: patch.logoUrl ?? null,\n favicon_url: patch.faviconUrl ?? null,\n business_email: emptyToNull(patch.businessEmail),\n business_phone: emptyToNull(patch.businessPhone),\n };\n const { error } = await client.from(\"sites\").upsert(row, { onConflict: \"site_id\" });\n if (error) throw error;\n}\n\nfunction emptyToNull(v: string | null | undefined): string | null {\n const s = (v ?? \"\").trim();\n return s === \"\" ? null : s;\n}\n","/**\n * The Team roster — public.site_members(site_id), a SECURITY DEFINER function\n * that returns rows only to that site's owners or staff (editors get a 42501).\n * Read with the signed-in user's client; writes are in service.ts.\n */\n\nimport type { IdentityClient } from \"./client\";\nimport { ForbiddenError } from \"./errors\";\n\nexport interface SiteMember {\n userId: string;\n email: string;\n name: string | null;\n avatarUrl: string | null;\n role: \"owner\" | \"editor\";\n /** \"invited\" until the person has signed in once. */\n status: \"invited\" | \"active\";\n invitedAt: string;\n}\n\ninterface Row {\n user_id: string;\n email: string;\n name: string | null;\n avatar_url: string | null;\n role: string;\n status: string;\n invited_at: string;\n}\n\nexport async function listSiteMembers(client: IdentityClient, siteId: string): Promise<SiteMember[]> {\n const { data, error } = await client.rpc(\"site_members\", { p_site_id: siteId });\n if (error) {\n if (error.code === \"42501\" || /not allowed/i.test(error.message)) throw new ForbiddenError(siteId, \"owner\", null);\n throw error;\n }\n return ((data ?? []) as Row[]).map((r) => ({\n userId: r.user_id,\n email: r.email,\n name: r.name,\n avatarUrl: r.avatar_url,\n role: r.role === \"owner\" ? \"owner\" : \"editor\",\n status: r.status === \"active\" ? \"active\" : \"invited\",\n invitedAt: r.invited_at,\n }));\n}\n","/**\n * My Profile and Change Password — the signed-in user editing themselves,\n * through their own client (auth.updateUser). Name and photo live in user\n * metadata; the photo file itself is in the avatars bucket (uploaded by the\n * host, path avatars/{uid}/…).\n */\n\nimport type { IdentityClient } from \"./client\";\nimport { IdentityError } from \"./errors\";\n\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const PASSWORD_TOO_SHORT = \"Use at least 10 characters.\";\n\nexport interface ProfilePatch {\n name?: string | null;\n avatarUrl?: string | null;\n}\n\nexport interface Profile {\n name: string | null;\n avatarUrl: string | null;\n}\n\nexport function profileOf(user: { user_metadata?: Record<string, unknown> | null } | null | undefined): Profile {\n const m = user?.user_metadata ?? {};\n const name = typeof m.name === \"string\" && m.name.trim() ? m.name.trim() : null;\n const avatarUrl = typeof m.avatar_url === \"string\" && m.avatar_url ? m.avatar_url : null;\n return { name, avatarUrl };\n}\n\nexport async function updateProfile(client: IdentityClient, patch: ProfilePatch): Promise<Profile> {\n const data: Record<string, unknown> = {};\n if (patch.name !== undefined) data.name = (patch.name ?? \"\").trim().slice(0, 80);\n if (patch.avatarUrl !== undefined) data.avatar_url = patch.avatarUrl ?? \"\";\n const { data: res, error } = await client.auth.updateUser({ data });\n if (error) throw new IdentityError(error.message);\n return profileOf(res.user);\n}\n\n/** Sets (or replaces) a password. Magic links keep working alongside it. */\nexport async function setPassword(client: IdentityClient, password: string): Promise<void> {\n if (typeof password !== \"string\" || password.length < MIN_PASSWORD_LENGTH) throw new IdentityError(PASSWORD_TOO_SHORT);\n const { error } = await client.auth.updateUser({ password });\n if (error) throw new IdentityError(error.message);\n}\n"]}
@@ -167,6 +167,95 @@ interface RemoveInput {
167
167
  }
168
168
  /** Removes the membership only. The auth user survives (they may belong to other sites). */
169
169
  declare function removeSiteUser(ctx: ServiceContext, input: RemoveInput): Promise<void>;
170
+ interface RoleInput {
171
+ userId: string;
172
+ siteId: string;
173
+ role: "owner" | "editor";
174
+ actor: {
175
+ id: string;
176
+ };
177
+ }
178
+ /**
179
+ * Change a member's role. Same grant rules as invite (staff anything; an owner
180
+ * may only make people editors on their own site), plus: the last owner of a
181
+ * site can never be demoted, otherwise nobody could manage it.
182
+ */
183
+ declare function updateSiteRole(ctx: ServiceContext, input: RoleInput): Promise<void>;
184
+ interface ResendInput {
185
+ email: string;
186
+ siteId: string;
187
+ actor: {
188
+ id: string;
189
+ };
190
+ redirectTo: string;
191
+ }
192
+ /** Send the invite email again. An already-active user gets no email — they can just sign in. */
193
+ declare function resendInvite(ctx: ServiceContext, input: ResendInput): Promise<{
194
+ resent: boolean;
195
+ message: string;
196
+ }>;
197
+
198
+ /**
199
+ * Per-site branding — the Business tab. Reads and writes public.sites through
200
+ * the signed-in user's client, so RLS decides who may (members read; owners
201
+ * and staff write). Nothing here needs the service role.
202
+ */
203
+
204
+ interface SiteBranding {
205
+ name: string;
206
+ logoUrl: string | null;
207
+ faviconUrl: string | null;
208
+ businessEmail: string | null;
209
+ businessPhone: string | null;
210
+ }
211
+ type BrandingPatch = Partial<SiteBranding> & {
212
+ name: string;
213
+ };
214
+ declare function getSiteBranding(client: IdentityClient, siteId: string): Promise<SiteBranding | null>;
215
+ /** Upsert the row. RLS refuses anyone but the site's owners or staff. */
216
+ declare function updateSiteBranding(client: IdentityClient, siteId: string, patch: BrandingPatch): Promise<void>;
217
+
218
+ /**
219
+ * The Team roster — public.site_members(site_id), a SECURITY DEFINER function
220
+ * that returns rows only to that site's owners or staff (editors get a 42501).
221
+ * Read with the signed-in user's client; writes are in service.ts.
222
+ */
223
+
224
+ interface SiteMember {
225
+ userId: string;
226
+ email: string;
227
+ name: string | null;
228
+ avatarUrl: string | null;
229
+ role: "owner" | "editor";
230
+ /** "invited" until the person has signed in once. */
231
+ status: "invited" | "active";
232
+ invitedAt: string;
233
+ }
234
+ declare function listSiteMembers(client: IdentityClient, siteId: string): Promise<SiteMember[]>;
235
+
236
+ /**
237
+ * My Profile and Change Password — the signed-in user editing themselves,
238
+ * through their own client (auth.updateUser). Name and photo live in user
239
+ * metadata; the photo file itself is in the avatars bucket (uploaded by the
240
+ * host, path avatars/{uid}/…).
241
+ */
242
+
243
+ declare const MIN_PASSWORD_LENGTH = 10;
244
+ declare const PASSWORD_TOO_SHORT = "Use at least 10 characters.";
245
+ interface ProfilePatch {
246
+ name?: string | null;
247
+ avatarUrl?: string | null;
248
+ }
249
+ interface Profile {
250
+ name: string | null;
251
+ avatarUrl: string | null;
252
+ }
253
+ declare function profileOf(user: {
254
+ user_metadata?: Record<string, unknown> | null;
255
+ } | null | undefined): Profile;
256
+ declare function updateProfile(client: IdentityClient, patch: ProfilePatch): Promise<Profile>;
257
+ /** Sets (or replaces) a password. Magic links keep working alongside it. */
258
+ declare function setPassword(client: IdentityClient, password: string): Promise<void>;
170
259
 
171
260
  /** Typed errors so a route can map them to 401 / 403 without string matching. */
172
261
  declare class IdentityError extends Error {
@@ -184,4 +273,4 @@ declare class ForbiddenError extends IdentityError {
184
273
  constructor(siteId: string, required: string, actual: string | null);
185
274
  }
186
275
 
187
- export { type CallbackOptions, type CallbackResult, type CookieStore, ForbiddenError, type IdentityClient, type IdentityContext, IdentityError, type IdentityUser, type InviteInput, type InviteResult, ROLE_RANK, type RemoveInput, type RequireOptions, type ServiceContext, type SiteMembership, type SiteRole, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteRole, handleAuthCallback, inviteUser, removeSiteUser, requireSiteUser, roleAtLeast, safeNextPath, signOut };
276
+ export { type BrandingPatch, type CallbackOptions, type CallbackResult, type CookieStore, ForbiddenError, type IdentityClient, type IdentityContext, IdentityError, type IdentityUser, type InviteInput, type InviteResult, MIN_PASSWORD_LENGTH, PASSWORD_TOO_SHORT, type Profile, type ProfilePatch, ROLE_RANK, type RemoveInput, type RequireOptions, type ResendInput, type RoleInput, type ServiceContext, type SiteBranding, type SiteMember, type SiteMembership, type SiteRole, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteBranding, getSiteRole, handleAuthCallback, inviteUser, listSiteMembers, profileOf, removeSiteUser, requireSiteUser, resendInvite, roleAtLeast, safeNextPath, setPassword, signOut, updateProfile, updateSiteBranding, updateSiteRole };
@@ -167,6 +167,95 @@ interface RemoveInput {
167
167
  }
168
168
  /** Removes the membership only. The auth user survives (they may belong to other sites). */
169
169
  declare function removeSiteUser(ctx: ServiceContext, input: RemoveInput): Promise<void>;
170
+ interface RoleInput {
171
+ userId: string;
172
+ siteId: string;
173
+ role: "owner" | "editor";
174
+ actor: {
175
+ id: string;
176
+ };
177
+ }
178
+ /**
179
+ * Change a member's role. Same grant rules as invite (staff anything; an owner
180
+ * may only make people editors on their own site), plus: the last owner of a
181
+ * site can never be demoted, otherwise nobody could manage it.
182
+ */
183
+ declare function updateSiteRole(ctx: ServiceContext, input: RoleInput): Promise<void>;
184
+ interface ResendInput {
185
+ email: string;
186
+ siteId: string;
187
+ actor: {
188
+ id: string;
189
+ };
190
+ redirectTo: string;
191
+ }
192
+ /** Send the invite email again. An already-active user gets no email — they can just sign in. */
193
+ declare function resendInvite(ctx: ServiceContext, input: ResendInput): Promise<{
194
+ resent: boolean;
195
+ message: string;
196
+ }>;
197
+
198
+ /**
199
+ * Per-site branding — the Business tab. Reads and writes public.sites through
200
+ * the signed-in user's client, so RLS decides who may (members read; owners
201
+ * and staff write). Nothing here needs the service role.
202
+ */
203
+
204
+ interface SiteBranding {
205
+ name: string;
206
+ logoUrl: string | null;
207
+ faviconUrl: string | null;
208
+ businessEmail: string | null;
209
+ businessPhone: string | null;
210
+ }
211
+ type BrandingPatch = Partial<SiteBranding> & {
212
+ name: string;
213
+ };
214
+ declare function getSiteBranding(client: IdentityClient, siteId: string): Promise<SiteBranding | null>;
215
+ /** Upsert the row. RLS refuses anyone but the site's owners or staff. */
216
+ declare function updateSiteBranding(client: IdentityClient, siteId: string, patch: BrandingPatch): Promise<void>;
217
+
218
+ /**
219
+ * The Team roster — public.site_members(site_id), a SECURITY DEFINER function
220
+ * that returns rows only to that site's owners or staff (editors get a 42501).
221
+ * Read with the signed-in user's client; writes are in service.ts.
222
+ */
223
+
224
+ interface SiteMember {
225
+ userId: string;
226
+ email: string;
227
+ name: string | null;
228
+ avatarUrl: string | null;
229
+ role: "owner" | "editor";
230
+ /** "invited" until the person has signed in once. */
231
+ status: "invited" | "active";
232
+ invitedAt: string;
233
+ }
234
+ declare function listSiteMembers(client: IdentityClient, siteId: string): Promise<SiteMember[]>;
235
+
236
+ /**
237
+ * My Profile and Change Password — the signed-in user editing themselves,
238
+ * through their own client (auth.updateUser). Name and photo live in user
239
+ * metadata; the photo file itself is in the avatars bucket (uploaded by the
240
+ * host, path avatars/{uid}/…).
241
+ */
242
+
243
+ declare const MIN_PASSWORD_LENGTH = 10;
244
+ declare const PASSWORD_TOO_SHORT = "Use at least 10 characters.";
245
+ interface ProfilePatch {
246
+ name?: string | null;
247
+ avatarUrl?: string | null;
248
+ }
249
+ interface Profile {
250
+ name: string | null;
251
+ avatarUrl: string | null;
252
+ }
253
+ declare function profileOf(user: {
254
+ user_metadata?: Record<string, unknown> | null;
255
+ } | null | undefined): Profile;
256
+ declare function updateProfile(client: IdentityClient, patch: ProfilePatch): Promise<Profile>;
257
+ /** Sets (or replaces) a password. Magic links keep working alongside it. */
258
+ declare function setPassword(client: IdentityClient, password: string): Promise<void>;
170
259
 
171
260
  /** Typed errors so a route can map them to 401 / 403 without string matching. */
172
261
  declare class IdentityError extends Error {
@@ -184,4 +273,4 @@ declare class ForbiddenError extends IdentityError {
184
273
  constructor(siteId: string, required: string, actual: string | null);
185
274
  }
186
275
 
187
- export { type CallbackOptions, type CallbackResult, type CookieStore, ForbiddenError, type IdentityClient, type IdentityContext, IdentityError, type IdentityUser, type InviteInput, type InviteResult, ROLE_RANK, type RemoveInput, type RequireOptions, type ServiceContext, type SiteMembership, type SiteRole, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteRole, handleAuthCallback, inviteUser, removeSiteUser, requireSiteUser, roleAtLeast, safeNextPath, signOut };
276
+ export { type BrandingPatch, type CallbackOptions, type CallbackResult, type CookieStore, ForbiddenError, type IdentityClient, type IdentityContext, IdentityError, type IdentityUser, type InviteInput, type InviteResult, MIN_PASSWORD_LENGTH, PASSWORD_TOO_SHORT, type Profile, type ProfilePatch, ROLE_RANK, type RemoveInput, type RequireOptions, type ResendInput, type RoleInput, type ServiceContext, type SiteBranding, type SiteMember, type SiteMembership, type SiteRole, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteBranding, getSiteRole, handleAuthCallback, inviteUser, listSiteMembers, profileOf, removeSiteUser, requireSiteUser, resendInvite, roleAtLeast, safeNextPath, setPassword, signOut, updateProfile, updateSiteBranding, updateSiteRole };
@@ -163,10 +163,107 @@ async function removeSiteUser(ctx, input) {
163
163
  if (target.error) throw target.error;
164
164
  const targetRole = target.data?.role ?? "editor";
165
165
  assertMayGrant(actorRole, input.siteId, targetRole);
166
+ if (targetRole === "owner") await assertNotLastOwner(svc, input.siteId);
166
167
  const del = await svc.from("site_users").delete().eq("user_id", input.userId).eq("site_id", input.siteId);
167
168
  if (del.error) throw del.error;
168
169
  }
170
+ async function updateSiteRole(ctx, input) {
171
+ const svc = createServiceClient(ctx);
172
+ const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);
173
+ assertMayGrant(actorRole, input.siteId, input.role);
174
+ const target = await svc.from("site_users").select("role").eq("user_id", input.userId).eq("site_id", input.siteId).maybeSingle();
175
+ if (target.error) throw target.error;
176
+ const current = target.data?.role;
177
+ if (!current) throw new IdentityError("That person isn't a member of this site.");
178
+ if (current === "owner") assertMayGrant(actorRole, input.siteId, "owner");
179
+ if (current === "owner" && input.role === "editor") await assertNotLastOwner(svc, input.siteId);
180
+ const upd = await svc.from("site_users").update({ role: input.role }).eq("user_id", input.userId).eq("site_id", input.siteId);
181
+ if (upd.error) throw upd.error;
182
+ }
183
+ async function assertNotLastOwner(svc, siteId) {
184
+ const owners = await svc.from("site_users").select("user_id").eq("site_id", siteId).eq("role", "owner");
185
+ if (owners.error) throw owners.error;
186
+ if ((owners.data ?? []).length <= 1) throw new IdentityError("A site needs at least one owner \u2014 make someone else an owner first.");
187
+ }
188
+ async function resendInvite(ctx, input) {
189
+ const svc = createServiceClient(ctx);
190
+ const actorRole = await resolveActorRole(svc, input.actor.id, input.siteId);
191
+ assertMayGrant(actorRole, input.siteId, "editor");
192
+ const email = input.email.trim().toLowerCase();
193
+ const again = await svc.auth.admin.inviteUserByEmail(email, { redirectTo: input.redirectTo, data: { site_id: input.siteId } });
194
+ if (!again.error) return { resent: true, message: `Invitation sent again to ${email}.` };
195
+ if (/already|exists|registered|confirmed/i.test(again.error.message)) {
196
+ return { resent: false, message: `${email} has already accepted \u2014 they can sign in with an email link any time.` };
197
+ }
198
+ throw new IdentityError(`Resend failed: ${again.error.message}`);
199
+ }
200
+
201
+ // src/auth/branding.ts
202
+ async function getSiteBranding(client, siteId) {
203
+ const { data, error } = await client.from("sites").select("name, logo_url, favicon_url, business_email, business_phone").eq("site_id", siteId).maybeSingle();
204
+ if (error) throw error;
205
+ if (!data) return null;
206
+ const r = data;
207
+ return { name: r.name, logoUrl: r.logo_url, faviconUrl: r.favicon_url, businessEmail: r.business_email, businessPhone: r.business_phone };
208
+ }
209
+ async function updateSiteBranding(client, siteId, patch) {
210
+ const row = {
211
+ site_id: siteId,
212
+ name: patch.name.trim(),
213
+ logo_url: patch.logoUrl ?? null,
214
+ favicon_url: patch.faviconUrl ?? null,
215
+ business_email: emptyToNull(patch.businessEmail),
216
+ business_phone: emptyToNull(patch.businessPhone)
217
+ };
218
+ const { error } = await client.from("sites").upsert(row, { onConflict: "site_id" });
219
+ if (error) throw error;
220
+ }
221
+ function emptyToNull(v) {
222
+ const s = (v ?? "").trim();
223
+ return s === "" ? null : s;
224
+ }
225
+
226
+ // src/auth/members.ts
227
+ async function listSiteMembers(client, siteId) {
228
+ const { data, error } = await client.rpc("site_members", { p_site_id: siteId });
229
+ if (error) {
230
+ if (error.code === "42501" || /not allowed/i.test(error.message)) throw new ForbiddenError(siteId, "owner", null);
231
+ throw error;
232
+ }
233
+ return (data ?? []).map((r) => ({
234
+ userId: r.user_id,
235
+ email: r.email,
236
+ name: r.name,
237
+ avatarUrl: r.avatar_url,
238
+ role: r.role === "owner" ? "owner" : "editor",
239
+ status: r.status === "active" ? "active" : "invited",
240
+ invitedAt: r.invited_at
241
+ }));
242
+ }
243
+
244
+ // src/auth/profile.ts
245
+ var MIN_PASSWORD_LENGTH = 10;
246
+ var PASSWORD_TOO_SHORT = "Use at least 10 characters.";
247
+ function profileOf(user) {
248
+ const m = user?.user_metadata ?? {};
249
+ const name = typeof m.name === "string" && m.name.trim() ? m.name.trim() : null;
250
+ const avatarUrl = typeof m.avatar_url === "string" && m.avatar_url ? m.avatar_url : null;
251
+ return { name, avatarUrl };
252
+ }
253
+ async function updateProfile(client, patch) {
254
+ const data = {};
255
+ if (patch.name !== void 0) data.name = (patch.name ?? "").trim().slice(0, 80);
256
+ if (patch.avatarUrl !== void 0) data.avatar_url = patch.avatarUrl ?? "";
257
+ const { data: res, error } = await client.auth.updateUser({ data });
258
+ if (error) throw new IdentityError(error.message);
259
+ return profileOf(res.user);
260
+ }
261
+ async function setPassword(client, password) {
262
+ if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) throw new IdentityError(PASSWORD_TOO_SHORT);
263
+ const { error } = await client.auth.updateUser({ password });
264
+ if (error) throw new IdentityError(error.message);
265
+ }
169
266
 
170
- export { ForbiddenError, IdentityError, ROLE_RANK, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteRole, handleAuthCallback, inviteUser, removeSiteUser, requireSiteUser, roleAtLeast, safeNextPath, signOut };
267
+ export { ForbiddenError, IdentityError, MIN_PASSWORD_LENGTH, PASSWORD_TOO_SHORT, ROLE_RANK, UnauthenticatedError, createIdentityClient, getCurrentUser, getSiteBranding, getSiteRole, handleAuthCallback, inviteUser, listSiteMembers, profileOf, removeSiteUser, requireSiteUser, resendInvite, roleAtLeast, safeNextPath, setPassword, signOut, updateProfile, updateSiteBranding, updateSiteRole };
171
268
  //# sourceMappingURL=index.js.map
172
269
  //# sourceMappingURL=index.js.map