Covers user.ts (User), cookie_manage.ts (cookie primitives), simple.ts (session helpers), and sandbox.ts (disposable test companies). See the Introduction for DataStructure<T>, SimpleResult<T>, EventData<T>, and QueryRequest<T>, which User builds on.
The account-level login — email/password credentials, a global profile, and cross-company query methods. This is distinct from Member (company/member/member.ts, see Members): a User is one row in the top-level users table; a Memberis that account's (or an unlinked invitee's) profile row within a single company. A User may have zero, one, or many Memberrows (one per company it's joined); Member.userID is the optional link back. Member has no roleID — company staff/role assignment is a Staff concern (see Company), not a User or Member field.
User extends DataStructure<UserData>, cached in a static Map keyed by id like every other domain class.
| Field | Type | Notes |
|---|---|---|
id | string | |
email | string | |
firstName? | string | |
lastName? | string | |
isAdmin? | boolean | present on LazyUserDataalready — no separate "full data" gate for it |
stripeCustomerID? | string | |
mobilePhone? / homePhone? / workPhone? | string | |
addressLine1? / addressLine2? / city? / state? / country? / postalCode? | string | |
birthday? | string | date-only; save() normalizes it to YYYY-MM-DD via toISOString().split("T")[0] before sending |
gender? | MemberGender | shared enum from types.ts ("male"|"female"|"non_binary"|"other"|"prefer_not_to_say") — same type Member uses, use MemberGenderOptions for display labels |
emergencyName? / emergencyRelationship? / emergencyPhone? / emergencyEmail? | string | |
photo? | string | storage key, not a URL — resolve via GetPhoto() |
createdAt / updatedAt | Date | full-data only |
LazyUserData is just { id, email, firstName?, lastName?, isAdmin? } — the subset returned by lazy fetches.
new User(id: string); // lazy — triggers preloadData(true)
new User(data: LazyUserData | UserData, lazy?); // pre-seeded, e.g. from a query resultUser.create(data: CreateUserRequest): Promise<SimpleResult<EventData>>CreateUserRequest = { firstName?, lastName?, email, password }. Hits POST /auth/register with needUser: false (no session required — this is how a session begins). Does not log the caller in — follow with User.Login().
User.Login(data: LoginUserRequest): Promise<boolean>LoginUserRequest = { email, password }. POST /auth/login, needUser: false. On status === 200 with a returned expireTime, stores it via SetCookie("ss-token-expire", ...); with a returned id, also stores it via SetCookie("ss-user-id", ...) (see the cookie-origin note below). Returns a plain boolean, not a SimpleResult — check the return value, not a status code.
user.save(): Promise<SimpleResult<EventData>> // PATCH /users
user.delete(): Promise<SimpleResult<EventData>> // DELETE /users?ids={id}
user.SetPassword(newPassword: string): Promise<SimpleResult<EventData>> // PATCH /users/password, no current password requireduser.UploadPhoto(file: File | Blob, contentType = "image/jpeg"): Promise<SimpleResult<EventData>>
user.GetPhoto(): Promise<string | undefined>Requests a presigned URL from POST /users/photo ({ type: "user-photo", objectID, contentType }), PUTs the file, then saves the returned key onto photo via save(). GetPhoto() re-posts to the same /users/photo endpoint with { key } and returns data.publicUrl. This is an account-level, bespoke endpoint — distinct from the generic MediaResourceType system (helper.ts's UploadMedia/DownloadMedia) used for companies, members, products, etc. (see the Introduction's Media Uploads section).
user.GetListOfCompanies(): Promise<SimpleResult<Company[]>>GET /users/companies?userID={id} — every company this account has a Member row in, i.e. what the account /dashboard lists to switch into.
user.idLink(): Promise<string | undefined>POST /users/idLink— issues a short-lived token for the account's digital-ID/QR check-in flow.
Seven methods query this account's records across every company it belongs to, via account-level /users/{resource}/queryendpoints, then wrap each raw row in its domain class scoped to the row's own companyID:
user.notifications(opts: { limit?, offset?, lazy?, isRead? }): Promise<{ status, data: Notification[], total?, unreadCount? }>
user.visits(opts: QueryRequest): Promise<{ status, data: Visit[], total? }>
user.invoices(opts: QueryRequest): Promise<{ status, data: Invoice[], total? }>
user.subscriptions(opts: QueryRequest): Promise<{ status, data: Subscription[], total? }>
user.checkouts(opts: QueryRequest): Promise<{ status, data: Checkout[], total? }>
user.payments(opts: QueryRequest): Promise<{ status, data: Payment[], total? }>
user.bookings(opts: QueryRequest): Promise<{ status, data: Booking[], total? }>
user.appointments(opts: QueryRequest): Promise<{ status, data: Appointment[], total? }>Each POSTs to its own /users/{resource}/query endpoint, then does result.data?.filter((r) => r.companyID).map((r) => new X(r, new Company(r.companyID))) — rows with no companyIDare silently dropped since they can't be constructed into their domain class. notifications is the odd one out: GET-style options object instead of a QueryRequest, and it also returns unreadCount.
GetActiveUser(): Promise<User | undefined>Reads ss-user-id; if present, returns a memoized User instance (module-level currentUservariable, recreated only if the cookie's id changes from the last call). Returns undefined if unset, or on any thrown error (caught and logged).
LogoutActiveUser(): booleanDeleteCookie("ss-user-id") then also calls LogoutOfActiveCompany() — clears the account session and any active company session together. Returns false (instead of throwing) on error.
GetActiveCompany(): Promise<Company | undefined>Same memoization pattern as GetActiveUser(), keyed off ss-company-id (module-level activeCompany variable).
LogoutOfActiveCompany(): booleanDeleteCookie("ss-company-id") only — leaves the account-level login intact. This is the one to call when a user backs out of a company back to the account dashboard without logging out entirely.
interface MeData {
memberID: string;
roleID: string | null;
firstName: string;
permissions: Permission[];
}
getMe(companyID: string): Promise<MeData>GET /company/{companyID}/members/me— resolves the signed-in user's Member/Staff identity within one company: their memberID, roleID (null if they're a plain member with no staff/role grant), display firstName, and resolved Permission[] (see Management's role.ts and nav.tsx's loadPermissions() in the app for how this feeds nav gating). Requires authentication (ss-user-id cookie) — unlike GetActiveUser()/GetActiveCompany(), it throws (Error(result.message ?? "getMe failed.")) on a non-200/no-data response rather than returning undefined, so callers should wrap it in try/catch.
Set-Cookie response on /auth/login (ss-access-token, ss-refresh-token, etc.) lands in the browser under the API's own origin. When the web app is hosted on a different origin than the API (e.g. the Next.js app on Vercel calling a separately hosted backend), document.cookieon the app's own pages can never see those — cookie visibility is scoped to the responding origin, not attached across origins, regardless of SameSite/Secure. That's fine for the HttpOnly auth cookies — the browser attaches those automatically to future requests to the API domain — but ss-user-id is a client-side bookkeeping cookie this SDK reads directly via document.cookie (GetActiveUser(), APIRequest's needUser gate), so User.Login() writes it into the app's owncookie jar explicitly, from the response body's idfield, instead of assuming the backend's cross-origin cookie is visible. If a login appears to succeed but the app immediately bounces back to the login page, check whether ss-user-id actually landed in document.cookie (Application/Storage tab) versus only showing up in the /auth/login response's Set-Cookieheaders (Network tab) — the latter alone won't establish a session on web.Based only on what the code above actually supports — no invented convenience wrapper exists that chains these steps for you:
import { User } from "zensile-sdk/user";
import { GetActiveUser, GetActiveCompany } from "zensile-sdk/simple";
// 1. Register (optional — skip straight to Login if the account exists)
const created = await User.create({ email, password, firstName, lastName });
if (created.status !== 200) throw new Error(created.message);
// 2. Authenticate — sets ss-token-expire and ss-user-id (see cookie-origin note above)
const ok = await User.Login({ email, password });
if (!ok) throw new Error("Login failed");
// 3. Resolve the now-active User
const user = await GetActiveUser(); // memoized, undefined if the cookie didn't take
// 4. List companies this account belongs to and select one
const { data: companies } = await user!.GetListOfCompanies();
await companies?.[0]?.SetActiveCompany(); // sets ss-company-id
// 5. Resolve the now-active Company, and this account's role/permissions within it
const company = await GetActiveCompany();
const me = await getMe(company!.id); // throws on failure — wrap in try/catchDisposable demo/test companies — seed data plus an admin login — for exploring the app without touching real data. Not a DataStructure subclass; all standalone functions.
interface SandboxMember { name: string; email: string; }
interface SandboxData {
companyID: string;
adminEmail: string;
adminPassword: string;
expiresAt: string;
members: SandboxMember[];
}CreateSandbox(): Promise<SimpleResult<SandboxData>>POST /v1/sandbox (called via a raw fetch, not APIRequest — bypasses the needUser/retry/timeout machinery entirely, though it still sends credentials: "include"). Provisions a new sandbox company with seed data and an admin login for the current session.
GetSandbox(): Promise<SimpleResult<SandboxData>>GET /sandbox (via APIRequest, unlike the other four functions in this file) — fetches the current session's active sandbox, if any.
DeleteUserSandbox(companyID: string): Promise<SimpleResult<void>>DELETE /sandbox/{companyID} (via APIRequest) — deletes the calling user's own sandbox company.
DeleteSandbox(secret: string, companyID: string): Promise<SimpleResult<void>>
CleanupSandboxes(secret: string): Promise<SimpleResult<void>>Both bypass APIRequest and use raw fetch with an x-sandbox-secret: {secret}header instead of the normal cookie-session auth — this is the SDK's one deliberate escape hatch from the standard user-session model, for ops/admin tooling that manages sandboxes across all users. DeleteSandbox removes any sandbox company by id; CleanupSandboxes bulk-removes every expired sandbox in one call (POST /v1/sandbox/cleanup).
Member, distinct from the account-level User covered here.Role/Permission and the module list that getMe()'s permissions resolves against.