@vuoma/sdk 0.1.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.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Vuoma SDK
2
+
3
+ Framework-neutral JavaScript and TypeScript client for the existing Vuoma v1
4
+ public organization and member account APIs. Works with a host-provided Fetch
5
+ implementation; it imports no React, Svelte, Expo or Supabase runtime. The package
6
+ does not store credentials, read environment variables, create accounts, or
7
+ grant access. This is a local package foundation; registry publication is a
8
+ separate step.
9
+
10
+ ```js
11
+ import { createVuomaClient } from '@vuoma/sdk'
12
+
13
+ const vuoma = createVuomaClient({
14
+ apiOrigin: 'http://localhost:5173', // Use your verified Vuoma HTTPS origin in production.
15
+ organizationSlug: 'your-studio',
16
+ fetch: globalThis.fetch,
17
+ })
18
+
19
+ const bootstrap = await vuoma.public.getBootstrap({ includeEvents: true })
20
+ const catalog = await vuoma.public.getCatalog()
21
+ const schedule = await vuoma.public.getSchedule({
22
+ from: '2026-10-07T00:00:00Z',
23
+ to: '2026-10-14T00:00:00Z',
24
+ })
25
+ ```
26
+
27
+ No key is needed for these public reads. Schedule timestamps are normalized to
28
+ UTC, and ranges must be positive and at most 63 days. The response preserves
29
+ `schedule.has_more`; when it is true, split the requested range and fetch each
30
+ part rather than displaying the first response as a complete schedule.
31
+
32
+ ## Member account setup
33
+
34
+ The host owns the current session. Supply an adapter that returns that session
35
+ or `null`, and install returned sign-in/refresh sessions in your host's existing
36
+ secure session boundary. The following transport example is for a controlled
37
+ server or native host; it does not establish a browser cookie session.
38
+
39
+ ```js
40
+ let session = null // Example in-memory host state; no SDK persistence.
41
+ const vuoma = createVuomaClient({
42
+ apiOrigin: 'http://localhost:5173',
43
+ organizationSlug: 'your-studio',
44
+ tokenAdapter: { getSession: () => session },
45
+ })
46
+
47
+ session = await vuoma.auth.signIn({ email, password })
48
+ const account = await vuoma.member.getProfile()
49
+ const commerce = await vuoma.member.getCommerce()
50
+ const updated = await vuoma.member.updateProfile({ first_name: 'Sam' })
51
+
52
+ session = await vuoma.auth.refresh({ refreshToken: session.refresh_token })
53
+ const previous = session
54
+ session = null // Immediately remove local access when signing out.
55
+ await vuoma.auth.signOut({ refreshToken: previous.refresh_token })
56
+ ```
57
+
58
+ For websites, keep access/refresh tokens in the site's server adapter and Secure
59
+ HttpOnly cookies. Browser components call the site's same-origin backend. The
60
+ existing Svelte account components and member-site adapter implement this
61
+ pattern. Never serialize a member session into page loader data. For React
62
+ Native, keep refresh tokens in Keychain/Keystore through a secure-storage adapter,
63
+ keep access tokens in memory, and refresh through the documented member route.
64
+ Refresh scheduling, retries and persistence remain host responsibilities.
65
+
66
+ The SDK uses `credentials: 'omit'`, prevents redirects, and sends member access
67
+ tokens only to its exact configured origin and organization. It rejects missing,
68
+ expired, cross-organization, platform-account, organization API, identity-only
69
+ OAuth and Supabase secret credentials in member calls. JWT decoding is a local
70
+ credential-kind check; only the API verifies signatures and current permissions.
71
+ Optionally configure `organizationId` for an additional JWT tenant check. Private
72
+ responses are discarded if the adapter reports an account change during a call.
73
+
74
+ Responses are checked for their v1 envelope, required display fields and relevant
75
+ organization/customer/range binding. Additional wire fields are preserved;
76
+ nested commerce records are typed as JSON records. These checks are not complete
77
+ business/financial validation, and do not replace the server's authorization or
78
+ canonical schemas. The configured response byte limit is checked after reading
79
+ the response text and against a supplied Content-Length header.
80
+
81
+ ## Keys and client IDs
82
+
83
+ - **Organization integration/delegated API keys** are confidential server
84
+ credentials. Never put them in a website bundle, native binary, URL or SDK
85
+ token adapter. They do not sign a customer in.
86
+ - **Registered OAuth client IDs** are public app identifiers. Existing shared
87
+ sign-in grants only `openid`, `profile` and `email`; those identity tokens do
88
+ not authorize this SDK's member account methods.
89
+ - **Supabase publishable keys** are public configuration for direct Supabase
90
+ clients. This SDK calls Vuoma's API and requires no Supabase key.
91
+ - **Member access/refresh tokens** belong to the signed-in organization customer.
92
+ Platform-account sessions are a separate boundary and are intentionally not
93
+ accepted by this member client.
94
+
95
+ This package adds no OAuth grant, CORS policy or backend permission. A third-party
96
+ website still needs its backend adapter. Independent app authorization and
97
+ app-scoped member grants are separate platform work; a public app ID alone must
98
+ never authorize private account data.
99
+
100
+ Supported routes are public `/api/v1/o/{slug}/member/{bootstrap,schedule,catalog}`;
101
+ member sign-in, profile GET/PATCH and commerce GET; plus the existing global
102
+ member refresh/sign-out routes. Signup, payments, booking mutations, uploads,
103
+ staff administration and platform-account sessions are outside this initial SDK.
104
+
105
+ ## Verification and package artifact
106
+
107
+ ```sh
108
+ npm test
109
+ npm run verify
110
+ ```
111
+
112
+ Verification tests the actual wire route/header/session boundaries and creates
113
+ an installable npm archive under `artifacts/`. It checks the archive's public
114
+ file list and loads the packed module in an isolated temporary directory.
115
+ Nothing is published and no registry authentication is attempted. The inherited
116
+ license remains `UNLICENSED`; `private: false` prepares packaging without granting
117
+ open-source licensing rights.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@vuoma/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Framework-neutral client for Vuoma public organization and member account APIs",
5
+ "type": "module",
6
+ "private": false,
7
+ "license": "UNLICENSED",
8
+ "files": ["src", "README.md"],
9
+ "exports": {
10
+ ".": { "types": "./src/index.d.ts", "import": "./src/index.js" }
11
+ },
12
+ "types": "./src/index.d.ts",
13
+ "sideEffects": false,
14
+ "scripts": {
15
+ "test": "node --test tests/*.test.mjs",
16
+ "verify": "npm test && node scripts/verify-package.mjs",
17
+ "prepack": "npm test"
18
+ },
19
+ "publishConfig": { "access": "public" },
20
+ "engines": { "node": ">=20" }
21
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,134 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
2
+ export interface ApiRecord { [key: string]: JsonValue }
3
+ export interface RequestOptions { signal?: AbortSignal }
4
+
5
+ /** Existing organization-member session wire shape; never an integration key. */
6
+ export interface MemberAuthSession {
7
+ access_token: string
8
+ refresh_token: string
9
+ expires_at: number
10
+ organization_slug: string
11
+ customer: { id: string; display_name: string; email: string }
12
+ }
13
+
14
+ export interface MemberTokenAdapter {
15
+ /** Return the host's current session, or null after sign-out/account change. */
16
+ getSession(): MemberAuthSession | null | Promise<MemberAuthSession | null>
17
+ }
18
+
19
+ export interface VuomaClientOptions {
20
+ /** Origin only. HTTPS required except HTTP loopback during local development. */
21
+ apiOrigin: string
22
+ organizationSlug: string
23
+ /** Optional stronger client-side tenant check against the member JWT claim. */
24
+ organizationId?: string
25
+ fetch?: typeof globalThis.fetch
26
+ tokenAdapter?: MemberTokenAdapter
27
+ /** Default 2 MiB; accepted range 1 KiB–16 MiB. */
28
+ maximumResponseBytes?: number
29
+ }
30
+
31
+ export interface PublicOrganization extends ApiRecord {
32
+ slug: string
33
+ name: string
34
+ }
35
+
36
+ /** Selected stable display fields; additional existing API fields are preserved. */
37
+ export interface PublicSession extends ApiRecord {
38
+ id: string
39
+ name: string
40
+ starts_at: string
41
+ ends_at: string
42
+ location: ApiRecord
43
+ }
44
+
45
+ export interface MemberBootstrap {
46
+ api_version: 1
47
+ generated_at: string
48
+ organization: PublicOrganization
49
+ locations: ApiRecord[]
50
+ schedule: { range_end: string; sessions: PublicSession[] }
51
+ features: ApiRecord
52
+ }
53
+
54
+ export interface MemberPublicSchedule {
55
+ api_version: 1
56
+ generated_at: string
57
+ schedule: { from: string; to: string; has_more: boolean; sessions: PublicSession[] }
58
+ }
59
+
60
+ export interface PublicOffer extends ApiRecord {
61
+ id: string
62
+ name: string
63
+ prices: ApiRecord[]
64
+ }
65
+
66
+ export interface MemberPublicCatalog {
67
+ api_version: 1
68
+ generated_at: string
69
+ purchases_available: boolean
70
+ credit_purchase?: ApiRecord | null
71
+ catalog: { offers: PublicOffer[] }
72
+ }
73
+
74
+ export interface MemberProfile {
75
+ api_version: 1
76
+ profile: {
77
+ customer_id: string
78
+ display_name: string
79
+ first_name: string | null
80
+ last_name: string | null
81
+ email: string | null
82
+ phone: string | null
83
+ birth_date: string | null
84
+ profile_photo: ApiRecord | null
85
+ }
86
+ }
87
+
88
+ export interface MemberProfileChanges {
89
+ first_name?: string | null
90
+ last_name?: string | null
91
+ phone?: string | null
92
+ birth_date?: string | null
93
+ }
94
+
95
+ /** Read-only snapshot. Nested commerce records retain the existing wire fields. */
96
+ export interface MemberCommerceSnapshot {
97
+ api_version: 1
98
+ generated_at: string
99
+ cancellation_notice_minutes: number
100
+ cancellation_policy?: ApiRecord | null
101
+ catalog: { offers: ApiRecord[] }
102
+ entitlements: ApiRecord[]
103
+ bookings: ApiRecord[]
104
+ orders: ApiRecord[]
105
+ }
106
+
107
+ export interface VuomaClient {
108
+ readonly public: {
109
+ getBootstrap(options?: RequestOptions & { includeEvents?: boolean }): Promise<MemberBootstrap>
110
+ /** Returns one server range. If has_more is true, split the range and request again. */
111
+ getSchedule(options: RequestOptions & { from: string; to: string; includeEvents?: boolean; fresh?: boolean }): Promise<MemberPublicSchedule>
112
+ getCatalog(options?: RequestOptions & { includeCreditPurchases?: boolean }): Promise<MemberPublicCatalog>
113
+ }
114
+ readonly auth: {
115
+ /** Returns credentials to the host; the SDK neither stores nor installs them. */
116
+ signIn(options: RequestOptions & { email: string; password: string }): Promise<MemberAuthSession>
117
+ refresh(options: RequestOptions & { refreshToken: string }): Promise<MemberAuthSession>
118
+ /** Clear host-held credentials even if remote sign-out needs a retry. */
119
+ signOut(options: RequestOptions & { refreshToken: string }): Promise<void>
120
+ }
121
+ readonly member: {
122
+ getProfile(options?: RequestOptions): Promise<MemberProfile>
123
+ updateProfile(changes: MemberProfileChanges, options?: RequestOptions): Promise<MemberProfile>
124
+ getCommerce(options?: RequestOptions): Promise<MemberCommerceSnapshot>
125
+ }
126
+ }
127
+
128
+ export class VuomaApiError extends Error {
129
+ readonly status: number
130
+ readonly code: string
131
+ constructor(status: number, code: string, message: string)
132
+ }
133
+
134
+ export function createVuomaClient(options: VuomaClientOptions): VuomaClient
package/src/index.js ADDED
@@ -0,0 +1,259 @@
1
+ // Public package: no environment reads, provider clients, persistence or secrets.
2
+ const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u
3
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
4
+ const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u
5
+ const maximumRange = 63 * 24 * 60 * 60 * 1000
6
+ const object = value => value !== null && typeof value === "object" && !Array.isArray(value)
7
+ const uuid = value => typeof value === "string" && uuidPattern.test(value)
8
+ const timestamp = value => typeof value === "string" && timestampPattern.test(value) && Number.isFinite(Date.parse(value)) &&
9
+ new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) === value.slice(0, 10)
10
+
11
+ export class VuomaApiError extends Error {
12
+ /** @param {number} status @param {string} code @param {string} message */
13
+ constructor(status, code, message) {
14
+ super(message)
15
+ this.name = "VuomaApiError"
16
+ this.status = status
17
+ this.code = code
18
+ }
19
+ }
20
+
21
+ function invalidResponse() {
22
+ return new VuomaApiError(502, "invalid_response", "Vuoma returned an invalid response.")
23
+ }
24
+ function invalidSession() {
25
+ return new VuomaApiError(401, "member_session_required", "Sign in to this organization to continue.")
26
+ }
27
+
28
+ /** Decode only to reject incorrect credential kinds. The API verifies authenticity. */
29
+ function memberClaims(token) {
30
+ if (typeof token !== "string" || token.length < 20 || token.length > 4096) throw invalidSession()
31
+ const segments = token.split(".")
32
+ if (segments.length !== 3 || segments.some(part => !/^[A-Za-z0-9_-]+$/u.test(part))) throw invalidSession()
33
+ try {
34
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
35
+ const encoded = segments[1].replace(/-/gu, "+").replace(/_/gu, "/")
36
+ if (encoded.length % 4 === 1) throw invalidSession()
37
+ let bits = 0, buffer = 0, escaped = ""
38
+ for (const character of encoded) {
39
+ buffer = (buffer << 6) | alphabet.indexOf(character)
40
+ bits += 6
41
+ if (bits >= 8) {
42
+ bits -= 8
43
+ escaped += `%${((buffer >> bits) & 255).toString(16).padStart(2, "0")}`
44
+ }
45
+ }
46
+ const claims = JSON.parse(decodeURIComponent(escaped))
47
+ if (!object(claims) || !uuid(claims.sub) || !uuid(claims.session_id) ||
48
+ claims.role !== "authenticated" || !object(claims.app_metadata) ||
49
+ claims.app_metadata.account_kind !== "organization_member" ||
50
+ !uuid(claims.app_metadata.organization_id) || !Number.isSafeInteger(claims.exp) ||
51
+ claims.exp <= Math.floor(Date.now() / 1000)) throw invalidSession()
52
+ return claims
53
+ } catch {
54
+ throw invalidSession()
55
+ }
56
+ }
57
+
58
+ function verifySession(value, organizationSlug, organizationId) {
59
+ if (!object(value) || value.organization_slug !== organizationSlug ||
60
+ !object(value.customer) || !uuid(value.customer.id) ||
61
+ typeof value.customer.display_name !== "string" || !value.customer.display_name ||
62
+ typeof value.customer.email !== "string" || !value.customer.email.includes("@") ||
63
+ !Number.isSafeInteger(value.expires_at) || value.expires_at <= Math.floor(Date.now() / 1000)) throw invalidSession()
64
+ const claims = memberClaims(value.access_token)
65
+ if (claims.exp !== value.expires_at || (organizationId && claims.app_metadata.organization_id !== organizationId)) throw invalidSession()
66
+ return { session: value, identity: `${organizationSlug}:${value.customer.id}:${claims.sub}:${claims.session_id}:${claims.app_metadata.organization_id}` }
67
+ }
68
+
69
+ function refreshToken(value) {
70
+ if (typeof value !== "string" || !value.length || value.length > 1024 || /[\s\u0000-\u001f\u007f]/u.test(value) ||
71
+ /^(?:chapi_|chsignin_|sb_secret_|sb_publishable_)/u.test(value)) throw invalidSession()
72
+ return value
73
+ }
74
+
75
+ function envelope(value) {
76
+ if (!object(value) || value.api_version !== 1) throw invalidResponse()
77
+ return value
78
+ }
79
+
80
+ function sessionResponse(value, organizationSlug, organizationId) {
81
+ try {
82
+ const { session } = verifySession(value, organizationSlug, organizationId)
83
+ refreshToken(session.refresh_token)
84
+ // Project the established wire session; never retain it inside the client.
85
+ return { access_token: session.access_token, refresh_token: session.refresh_token,
86
+ expires_at: session.expires_at, organization_slug: session.organization_slug,
87
+ customer: { id: session.customer.id, display_name: session.customer.display_name, email: session.customer.email } }
88
+ } catch {
89
+ throw invalidResponse()
90
+ }
91
+ }
92
+
93
+ function publicSessions(value) {
94
+ if (!Array.isArray(value) || value.some(item => !object(item) || !uuid(item.id) ||
95
+ typeof item.name !== "string" || !timestamp(item.starts_at) || !timestamp(item.ends_at) ||
96
+ Date.parse(item.ends_at) <= Date.parse(item.starts_at) || !object(item.location))) throw invalidResponse()
97
+ return value
98
+ }
99
+
100
+ function catalog(value) {
101
+ envelope(value)
102
+ if (!timestamp(value.generated_at) || typeof value.purchases_available !== "boolean" ||
103
+ !object(value.catalog) || !Array.isArray(value.catalog.offers) || value.catalog.offers.length > 100 ||
104
+ value.catalog.offers.some(offer => !object(offer) || !uuid(offer.id) || typeof offer.name !== "string" || !Array.isArray(offer.prices) || offer.prices.some(price => !object(price))) ||
105
+ (!value.purchases_available && value.catalog.offers.length)) throw invalidResponse()
106
+ return value
107
+ }
108
+
109
+ function utf8Length(text) {
110
+ let length = 0
111
+ for (const character of text) {
112
+ const code = character.codePointAt(0)
113
+ length += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4
114
+ }
115
+ return length
116
+ }
117
+
118
+ /**
119
+ * Create an isolated client. No implicit API host, cookies, environment or storage.
120
+ * @param {import('./index.js').VuomaClientOptions} options
121
+ * @returns {import('./index.js').VuomaClient}
122
+ */
123
+ export function createVuomaClient(options) {
124
+ if (!object(options) || typeof options.apiOrigin !== "string" || /[\s\\]/u.test(options.apiOrigin)) throw new TypeError("An explicit API origin is required.")
125
+ let origin
126
+ try {
127
+ const url = new URL(options.apiOrigin)
128
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
129
+ if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
130
+ url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error()
131
+ origin = url.origin
132
+ } catch { throw new TypeError("Use an HTTPS API origin, or HTTP on loopback for local development.") }
133
+ const organizationSlug = options.organizationSlug
134
+ if (typeof organizationSlug !== "string" || organizationSlug.length < 2 || organizationSlug.length > 80 || !slugPattern.test(organizationSlug)) throw new TypeError("Use an exact organization slug.")
135
+ const organizationId = options.organizationId
136
+ if (organizationId !== undefined && !uuid(organizationId)) throw new TypeError("Use a valid organization ID.")
137
+ const requestFetch = options.fetch ?? globalThis.fetch
138
+ if (typeof requestFetch !== "function") throw new TypeError("A fetch implementation is required.")
139
+ const tokenAdapter = options.tokenAdapter
140
+ if (tokenAdapter !== undefined && (!object(tokenAdapter) || typeof tokenAdapter.getSession !== "function")) throw new TypeError("Provide a member session adapter.")
141
+ const maximumBytes = options.maximumResponseBytes ?? 2 * 1024 * 1024
142
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1024 || maximumBytes > 16 * 1024 * 1024) throw new TypeError("Use a response limit between 1 KiB and 16 MiB.")
143
+ const memberRoot = `/api/v1/o/${organizationSlug}/member`
144
+
145
+ async function readSession() {
146
+ if (!tokenAdapter) throw invalidSession()
147
+ return verifySession(await tokenAdapter.getSession(), organizationSlug, organizationId)
148
+ }
149
+
150
+ async function request(path, { method = "GET", body, accessToken, signal, query, privateResponse = false, empty = false } = {}) {
151
+ const url = new URL(path, origin)
152
+ if (query) for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value)
153
+ const headers = { accept: "application/json" }
154
+ if (body !== undefined) headers["content-type"] = "application/json"
155
+ if (accessToken) headers.authorization = `Bearer ${accessToken}`
156
+ const response = await requestFetch(url.href, {
157
+ method, headers, signal, credentials: "omit", redirect: "error", referrerPolicy: "no-referrer",
158
+ cache: privateResponse ? "no-store" : "default",
159
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
160
+ })
161
+ // Custom fetch adapters must also honor the no-redirect destination boundary.
162
+ if (response.redirected || (response.url && response.url !== url.href)) throw invalidResponse()
163
+ const declared = Number(response.headers.get("content-length"))
164
+ if (Number.isFinite(declared) && declared > maximumBytes) throw invalidResponse()
165
+ const text = await response.text()
166
+ if (utf8Length(text) > maximumBytes) throw invalidResponse()
167
+ let value
168
+ try { value = text ? JSON.parse(text) : null } catch { if (response.ok) throw invalidResponse() }
169
+ if (!response.ok) {
170
+ const code = object(value) && typeof value.code === "string" && /^[a-z][a-z0-9_]{0,80}$/u.test(value.code) &&
171
+ !/^(?:chapi_|chsignin_|sb_secret_|sb_publishable_)/u.test(value.code) ? value.code : "api_request_failed"
172
+ const message = response.status === 401 ? "Sign in again to continue." : response.status === 403 ? "Your account cannot perform this operation." :
173
+ response.status === 429 ? "Too many requests. Try again shortly." : "The Vuoma request could not be completed."
174
+ throw new VuomaApiError(response.status, code, message)
175
+ }
176
+ if (empty) {
177
+ if (response.status !== 204 || text) throw invalidResponse()
178
+ return
179
+ }
180
+ if (!object(value)) throw invalidResponse()
181
+ return value
182
+ }
183
+
184
+ async function accountRequest(path, requestOptions = {}, parse = value => value) {
185
+ const initial = await readSession()
186
+ const value = await request(`${memberRoot}${path}`, { ...requestOptions, accessToken: initial.session.access_token, privateResponse: true })
187
+ const current = await readSession()
188
+ if (current.identity !== initial.identity) throw new VuomaApiError(401, "member_session_changed", "Your signed-in account changed. Try again.")
189
+ return parse(value, initial.session.customer.id)
190
+ }
191
+
192
+ function profile(value, customerId) {
193
+ envelope(value)
194
+ if (!object(value.profile) || value.profile.customer_id !== customerId || typeof value.profile.display_name !== "string") throw invalidResponse()
195
+ for (const field of ["first_name", "last_name", "email", "phone", "birth_date"]) {
196
+ if (value.profile[field] !== null && typeof value.profile[field] !== "string") throw invalidResponse()
197
+ }
198
+ if (value.profile.profile_photo !== null && !object(value.profile.profile_photo)) throw invalidResponse()
199
+ return value
200
+ }
201
+
202
+ return Object.freeze({
203
+ public: Object.freeze({
204
+ async getBootstrap({ includeEvents = false, signal } = {}) {
205
+ const value = envelope(await request(`${memberRoot}/bootstrap`, { signal, query: includeEvents ? { include_events: "true" } : undefined }))
206
+ if (!timestamp(value.generated_at) || !object(value.organization) || value.organization.slug !== organizationSlug ||
207
+ typeof value.organization.name !== "string" || !Array.isArray(value.locations) || value.locations.some(location => !object(location)) || !object(value.features) || !object(value.schedule) ||
208
+ !timestamp(value.schedule.range_end)) throw invalidResponse()
209
+ publicSessions(value.schedule.sessions)
210
+ return value
211
+ },
212
+ async getSchedule({ from, to, includeEvents = false, fresh = false, signal }) {
213
+ if (!timestamp(from) || !timestamp(to) || Date.parse(to) <= Date.parse(from) || Date.parse(to) - Date.parse(from) > maximumRange) throw new TypeError("Choose RFC 3339 timestamps spanning at most 63 days.")
214
+ from = new Date(from).toISOString()
215
+ to = new Date(to).toISOString()
216
+ const query = { from, to, ...(includeEvents ? { include_events: "true" } : {}), ...(fresh ? { refresh: "1" } : {}) }
217
+ const value = envelope(await request(`${memberRoot}/schedule`, { query, signal, privateResponse: fresh }))
218
+ if (!timestamp(value.generated_at) || !object(value.schedule) || value.schedule.from !== from || value.schedule.to !== to ||
219
+ typeof value.schedule.has_more !== "boolean") throw invalidResponse()
220
+ publicSessions(value.schedule.sessions)
221
+ if (value.schedule.sessions.length > 250) throw invalidResponse()
222
+ return value
223
+ },
224
+ async getCatalog({ includeCreditPurchases = false, signal } = {}) {
225
+ return catalog(await request(`${memberRoot}/catalog`, { signal, query: includeCreditPurchases ? { include_credit_purchases: "true" } : undefined }))
226
+ },
227
+ }),
228
+ auth: Object.freeze({
229
+ async signIn({ email, password, signal }) {
230
+ if (typeof email !== "string" || !email.trim() || email.length > 320 || typeof password !== "string" || !password || password.length > 1024) throw new TypeError("Provide an email address and password.")
231
+ return sessionResponse(await request(`${memberRoot}/auth/sign-in`, { method: "POST", body: { email, password }, signal, privateResponse: true }), organizationSlug, organizationId)
232
+ },
233
+ async refresh({ refreshToken: token, signal }) {
234
+ return sessionResponse(await request("/api/v1/member/auth/refresh", { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true }), organizationSlug, organizationId)
235
+ },
236
+ async signOut({ refreshToken: token, signal }) {
237
+ await request("/api/v1/member/auth/sign-out", { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true, empty: true })
238
+ },
239
+ }),
240
+ member: Object.freeze({
241
+ getProfile({ signal } = {}) { return accountRequest("/profile", { signal }, profile) },
242
+ updateProfile(changes, { signal } = {}) {
243
+ const allowed = new Set(["first_name", "last_name", "phone", "birth_date"])
244
+ if (!object(changes) || !Object.keys(changes).length || Object.entries(changes).some(([key, value]) =>
245
+ !allowed.has(key) || (value !== null && typeof value !== "string") || (typeof value === "string" && value.length > 320))) throw new TypeError("Provide supported member profile fields.")
246
+ return accountRequest("/profile", { method: "PATCH", body: changes, signal }, profile)
247
+ },
248
+ getCommerce({ signal } = {}) {
249
+ return accountRequest("/commerce", { signal }, value => {
250
+ envelope(value)
251
+ if (!timestamp(value.generated_at) || !object(value.catalog) || !Array.isArray(value.catalog.offers) || value.catalog.offers.some(offer => !object(offer)) ||
252
+ [value.entitlements, value.bookings, value.orders].some(records => !Array.isArray(records) || records.some(record => !object(record))) ||
253
+ !Number.isSafeInteger(value.cancellation_notice_minutes) || value.cancellation_notice_minutes < 0) throw invalidResponse()
254
+ return value
255
+ })
256
+ },
257
+ }),
258
+ })
259
+ }