@playaos/api-client 0.12.0 → 0.14.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/dist/index.js CHANGED
@@ -1,544 +1,548 @@
1
- // src/error.ts
1
+ //#region src/error.ts
2
2
  var ApiClientError = class extends Error {
3
- constructor(status, code, message, retryAfter) {
4
- super(message);
5
- this.status = status;
6
- this.code = code;
7
- this.retryAfter = retryAfter;
8
- this.name = "ApiClientError";
9
- }
10
- status;
11
- code;
12
- retryAfter;
3
+ status;
4
+ code;
5
+ retryAfter;
6
+ constructor(status, code, message, retryAfter) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ this.retryAfter = retryAfter;
11
+ this.name = "ApiClientError";
12
+ }
13
13
  };
14
+ /**
15
+ * Parse the `Retry-After` header into integer seconds. Only the delta-seconds
16
+ * form is honored; the HTTP-date form is ignored (returns undefined) since
17
+ * callers expect a seconds hint.
18
+ */
14
19
  function parseRetryAfter(res) {
15
- const header = res.headers?.get?.("retry-after")?.trim();
16
- return header && /^\d+$/.test(header) ? parseInt(header, 10) : void 0;
20
+ const header = res.headers?.get?.("retry-after")?.trim();
21
+ return header && /^\d+$/.test(header) ? parseInt(header, 10) : void 0;
17
22
  }
18
23
  async function parseError(res) {
19
- const retryAfter = parseRetryAfter(res);
20
- try {
21
- const body = await res.json();
22
- return new ApiClientError(res.status, body.code ?? "UNKNOWN", body.error ?? res.statusText, retryAfter);
23
- } catch {
24
- return new ApiClientError(res.status, "UNKNOWN", res.statusText, retryAfter);
25
- }
24
+ const retryAfter = parseRetryAfter(res);
25
+ try {
26
+ const body = await res.json();
27
+ return new ApiClientError(res.status, body.code ?? "UNKNOWN", body.error ?? res.statusText, retryAfter);
28
+ } catch {
29
+ return new ApiClientError(res.status, "UNKNOWN", res.statusText, retryAfter);
30
+ }
31
+ }
32
+ //#endregion
33
+ //#region src/client.ts
34
+ function v1Headers(auth) {
35
+ const headers = { Authorization: `Bearer ${auth.apiKey}` };
36
+ if (auth.memberToken) headers["X-PlayaOS-Member-Token"] = auth.memberToken;
37
+ return headers;
26
38
  }
27
-
28
- // src/client.ts
29
39
  function buildUrl(base, path, params) {
30
- const url = new URL(`${base}/api/v1${path}`);
31
- if (params) {
32
- for (const [k, v] of Object.entries(params)) {
33
- if (v !== void 0 && v !== false) url.searchParams.set(k, String(v));
34
- }
35
- }
36
- return url.toString();
40
+ const url = new URL(`${base}/api/v1${path}`);
41
+ if (params) {
42
+ for (const [k, v] of Object.entries(params)) if (v !== void 0 && v !== false) url.searchParams.set(k, String(v));
43
+ }
44
+ return url.toString();
37
45
  }
38
46
  function buildEmbedUrl(base, path) {
39
- return `${base}/api/embed/v1${path}`;
47
+ return `${base}/api/embed/v1${path}`;
40
48
  }
41
49
  function withCampKey(url, campKey) {
42
- const embedUrl = new URL(url);
43
- embedUrl.searchParams.set("camp_key", campKey);
44
- return embedUrl.toString();
50
+ const embedUrl = new URL(url);
51
+ embedUrl.searchParams.set("camp_key", campKey);
52
+ return embedUrl.toString();
45
53
  }
46
54
  async function getEmbed(url, campKey, signal) {
47
- const res = await fetch(withCampKey(url, campKey), {
48
- headers: { "X-Camp-Key": campKey },
49
- signal
50
- });
51
- if (!res.ok) throw await parseError(res);
52
- return res.json();
55
+ const res = await fetch(withCampKey(url, campKey), {
56
+ headers: { "X-Camp-Key": campKey },
57
+ signal
58
+ });
59
+ if (!res.ok) throw await parseError(res);
60
+ return res.json();
53
61
  }
54
62
  async function getEmbedAuthed(url, campKey, opts) {
55
- const headers = { "X-Camp-Key": campKey };
56
- if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
57
- const res = await fetch(withCampKey(url, campKey), { headers, signal: opts?.signal });
58
- if (!res.ok) throw await parseError(res);
59
- return res.json();
63
+ const headers = { "X-Camp-Key": campKey };
64
+ if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
65
+ const res = await fetch(withCampKey(url, campKey), {
66
+ headers,
67
+ signal: opts?.signal
68
+ });
69
+ if (!res.ok) throw await parseError(res);
70
+ return res.json();
60
71
  }
61
- async function request(url, apiKey, signal) {
62
- const res = await fetch(url, {
63
- headers: { Authorization: `Bearer ${apiKey}` },
64
- signal
65
- });
66
- if (!res.ok) throw await parseError(res);
67
- return res.json();
72
+ async function request(url, auth, signal) {
73
+ const res = await fetch(url, {
74
+ headers: v1Headers(auth),
75
+ signal
76
+ });
77
+ if (!res.ok) throw await parseError(res);
78
+ return res.json();
68
79
  }
69
- async function post(url, apiKey, body, signal) {
70
- const res = await fetch(url, {
71
- method: "POST",
72
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
73
- body: JSON.stringify(body),
74
- signal
75
- });
76
- if (!res.ok) throw await parseError(res);
77
- return res.json();
80
+ async function post(url, auth, body, signal) {
81
+ const res = await fetch(url, {
82
+ method: "POST",
83
+ headers: {
84
+ ...v1Headers(auth),
85
+ "Content-Type": "application/json"
86
+ },
87
+ body: JSON.stringify(body),
88
+ signal
89
+ });
90
+ if (!res.ok) throw await parseError(res);
91
+ return res.json();
92
+ }
93
+ /** v1 write helpers. `request`/`post` predate these; same auth, other verbs. */
94
+ async function sendV1(method, url, auth, body, signal) {
95
+ const headers = v1Headers(auth);
96
+ const init = {
97
+ method,
98
+ headers,
99
+ signal
100
+ };
101
+ if (body !== void 0) {
102
+ headers["Content-Type"] = "application/json";
103
+ init.body = JSON.stringify(body);
104
+ }
105
+ const res = await fetch(url, init);
106
+ if (!res.ok) throw await parseError(res);
107
+ return res.json();
78
108
  }
79
109
  async function postEmbed(url, campKey, body, opts) {
80
- const headers = {
81
- "X-Camp-Key": campKey,
82
- "Content-Type": "application/json"
83
- };
84
- if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
85
- const res = await fetch(withCampKey(url, campKey), {
86
- method: "POST",
87
- headers,
88
- body: JSON.stringify(body),
89
- signal: opts?.signal
90
- });
91
- if (!res.ok) throw await parseError(res);
92
- return res.json();
110
+ const headers = {
111
+ "X-Camp-Key": campKey,
112
+ "Content-Type": "application/json"
113
+ };
114
+ if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
115
+ const res = await fetch(withCampKey(url, campKey), {
116
+ method: "POST",
117
+ headers,
118
+ body: JSON.stringify(body),
119
+ signal: opts?.signal
120
+ });
121
+ if (!res.ok) throw await parseError(res);
122
+ return res.json();
93
123
  }
94
124
  async function putEmbed(url, campKey, body, opts) {
95
- const headers = {
96
- "X-Camp-Key": campKey,
97
- "Content-Type": "application/json"
98
- };
99
- if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
100
- const res = await fetch(withCampKey(url, campKey), {
101
- method: "PUT",
102
- headers,
103
- body: JSON.stringify(body),
104
- signal: opts?.signal
105
- });
106
- if (!res.ok) throw await parseError(res);
107
- return res.json();
125
+ const headers = {
126
+ "X-Camp-Key": campKey,
127
+ "Content-Type": "application/json"
128
+ };
129
+ if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
130
+ const res = await fetch(withCampKey(url, campKey), {
131
+ method: "PUT",
132
+ headers,
133
+ body: JSON.stringify(body),
134
+ signal: opts?.signal
135
+ });
136
+ if (!res.ok) throw await parseError(res);
137
+ return res.json();
108
138
  }
109
139
  async function deleteEmbed(url, campKey, opts) {
110
- const headers = { "X-Camp-Key": campKey };
111
- if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
112
- const res = await fetch(withCampKey(url, campKey), { method: "DELETE", headers, signal: opts?.signal });
113
- if (!res.ok) throw await parseError(res);
114
- return res.json();
140
+ const headers = { "X-Camp-Key": campKey };
141
+ if (opts?.accessToken) headers.Authorization = `Bearer ${opts.accessToken}`;
142
+ const res = await fetch(withCampKey(url, campKey), {
143
+ method: "DELETE",
144
+ headers,
145
+ signal: opts?.signal
146
+ });
147
+ if (!res.ok) throw await parseError(res);
148
+ return res.json();
115
149
  }
150
+ /**
151
+ * Bearer-only helper for /api/embed/v1/admin/* (PLA-489). The admin token is
152
+ * the sole credential (no X-Camp-Key — orgId comes from the JWT claims
153
+ * server-side). `method` can be GET, POST, PATCH, or PUT; `body` is optional
154
+ * (omitted for GET reads like getAnnotations).
155
+ */
116
156
  async function adminEmbed(method, url, accessToken, body, signal) {
117
- const headers = { Authorization: `Bearer ${accessToken}` };
118
- const init = { method, headers, signal };
119
- if (body !== void 0) {
120
- headers["Content-Type"] = "application/json";
121
- init.body = JSON.stringify(body);
122
- }
123
- const res = await fetch(url, init);
124
- if (!res.ok) throw await parseError(res);
125
- return res.json();
157
+ const headers = { Authorization: `Bearer ${accessToken}` };
158
+ const init = {
159
+ method,
160
+ headers,
161
+ signal
162
+ };
163
+ if (body !== void 0) {
164
+ headers["Content-Type"] = "application/json";
165
+ init.body = JSON.stringify(body);
166
+ }
167
+ const res = await fetch(url, init);
168
+ if (!res.ok) throw await parseError(res);
169
+ return res.json();
126
170
  }
127
- var PlayaOSClient = class {
128
- baseUrl;
129
- embedBaseUrl;
130
- apiKey;
131
- constructor(opts) {
132
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
133
- this.embedBaseUrl = (opts.embedBaseUrl ?? opts.baseUrl).replace(/\/+$/, "");
134
- this.apiKey = opts.apiKey;
135
- }
136
- members = {
137
- list: (params, opts) => request(buildUrl(this.baseUrl, "/members", params), this.apiKey, opts?.signal),
138
- get: (id, opts) => request(buildUrl(this.baseUrl, `/members/${encodeURIComponent(id)}`), this.apiKey, opts?.signal),
139
- /** Member-self profile update (PLA-700). Requires camp key + member Bearer token. */
140
- selfUpdate: (payload, opts) => putEmbed(buildEmbedUrl(this.embedBaseUrl, "/members/self"), this.apiKey, payload, opts),
141
- /** Member-self shift signup + cancel (PLA-705). Requires camp key + member Bearer. */
142
- shifts: {
143
- signup: (payload, opts) => putEmbed(buildEmbedUrl(this.embedBaseUrl, "/shifts/signup"), this.apiKey, payload, opts),
144
- cancel: (signupId, opts) => deleteEmbed(
145
- buildEmbedUrl(this.embedBaseUrl, `/shifts/signups/${encodeURIComponent(signupId)}`),
146
- this.apiKey,
147
- opts
148
- )
149
- }
150
- };
151
- applications = {
152
- list: (params, opts) => request(buildUrl(this.baseUrl, "/applications", params), this.apiKey, opts?.signal),
153
- // Anonymous external-camp submission against POST /api/embed/v1/applications
154
- // (PLA-481). Uses the X-Camp-Key header rather than Bearer auth. Pass
155
- // `accessToken` to additionally send a PlayaOS IdP JWT (PLA-573 path) so
156
- // the server trusts the verified profile claim and skips the email-based
157
- // bootstrap.
158
- create: (payload, opts) => postEmbed(buildEmbedUrl(this.embedBaseUrl, "/applications"), this.apiKey, payload, opts)
159
- };
160
- waivers = {
161
- /** Get the active waiver for an org for the current year. */
162
- active: (orgId, opts) => getEmbed(buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/active`), this.apiKey, opts?.signal),
163
- /** Sign a waiver. Requires X-Camp-Key; optional accessToken for Bearer path. */
164
- sign: (payload, opts) => postEmbed(buildEmbedUrl(this.embedBaseUrl, "/waivers/sign"), this.apiKey, payload, opts),
165
- /** Has this applicant/member signed the active waiver? Anchor: applicationId query or Bearer accessToken. */
166
- status: (orgId, opts) => {
167
- const query = opts?.applicationId ? `?applicationId=${encodeURIComponent(opts.applicationId)}` : "";
168
- return getEmbedAuthed(buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/status${query}`), this.apiKey, opts);
169
- },
170
- /** Get a short-lived download URL for a completed signature's PDF. Anchor: applicationId query or Bearer. */
171
- signedPdf: (orgId, signatureId, opts) => {
172
- const query = opts?.applicationId ? `?applicationId=${encodeURIComponent(opts.applicationId)}` : "";
173
- return getEmbedAuthed(
174
- buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/signatures/${encodeURIComponent(signatureId)}/pdf${query}`),
175
- this.apiKey,
176
- opts
177
- );
178
- }
179
- };
180
- dues = {
181
- list: (params, opts) => request(buildUrl(this.baseUrl, "/dues", params), this.apiKey, opts?.signal)
182
- };
183
- shifts = {
184
- list: (params, opts) => request(buildUrl(this.baseUrl, "/shifts", params), this.apiKey, opts?.signal)
185
- };
186
- org = {
187
- get: (opts) => request(buildUrl(this.baseUrl, "/org"), this.apiKey, opts?.signal)
188
- };
189
- // Public embed surface: active commitment definitions for the org's current
190
- // year, so the <ApplicationForm> can render + validate them (PLA-682). Uses
191
- // the X-Camp-Key (the apiKey doubles as the camp key), same as applications.create.
192
- commitments = {
193
- list: (opts) => getEmbed(buildEmbedUrl(this.embedBaseUrl, "/commitments"), this.apiKey, opts?.signal)
194
- };
195
- payments = {
196
- page: (memberId, duesCollectionId, opts) => post(buildUrl(this.baseUrl, "/payments/page"), this.apiKey, { memberId, duesCollectionId }, opts?.signal)
197
- };
198
- // Admin write APIs (PLA-489). Each method requires `accessToken` — a Supabase
199
- // JWT for a camp admin/super_admin. The org boundary is enforced server-side
200
- // from the JWT claims; these endpoints do not use the X-Camp-Key.
201
- admin = {
202
- applications: {
203
- /** Get application audit timeline (PLA-712). Requires admin Bearer token. */
204
- history: (id, opts) => adminEmbed(
205
- "GET",
206
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/history`),
207
- opts.accessToken,
208
- void 0,
209
- opts.signal
210
- ),
211
- transition: (id, payload, opts) => adminEmbed(
212
- "PATCH",
213
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}`),
214
- opts.accessToken,
215
- payload,
216
- opts.signal
217
- ),
218
- getAnnotations: (id, opts) => adminEmbed(
219
- "GET",
220
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/annotations`),
221
- opts.accessToken,
222
- void 0,
223
- opts.signal
224
- ),
225
- addNote: (id, payload, opts) => adminEmbed(
226
- "POST",
227
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/notes`),
228
- opts.accessToken,
229
- payload,
230
- opts.signal
231
- ),
232
- setTags: (id, payload, opts) => adminEmbed(
233
- "PUT",
234
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/tags`),
235
- opts.accessToken,
236
- payload,
237
- opts.signal
238
- ),
239
- setScore: (id, payload, opts) => adminEmbed(
240
- "PUT",
241
- buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/scores`),
242
- opts.accessToken,
243
- payload,
244
- opts.signal
245
- )
246
- },
247
- givebutter: {
248
- campaigns: {
249
- list: (opts) => adminEmbed(
250
- "GET",
251
- buildEmbedUrl(this.embedBaseUrl, "/admin/givebutter/campaigns"),
252
- opts.accessToken,
253
- void 0,
254
- opts.signal
255
- ),
256
- discountCodes: {
257
- list: (campaignId, opts) => adminEmbed(
258
- "GET",
259
- buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/discount-codes`),
260
- opts.accessToken,
261
- void 0,
262
- opts.signal
263
- ),
264
- create: (campaignId, payload, opts) => adminEmbed(
265
- "POST",
266
- buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/discount-codes`),
267
- opts.accessToken,
268
- payload,
269
- opts.signal
270
- )
271
- },
272
- tickets: {
273
- create: (campaignId, payload, opts) => adminEmbed(
274
- "POST",
275
- buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/tickets`),
276
- opts.accessToken,
277
- payload,
278
- opts.signal
279
- )
280
- }
281
- }
282
- },
283
- members: {
284
- create: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/members"), opts.accessToken, payload, opts.signal),
285
- update: (id, patch, opts) => adminEmbed(
286
- "PATCH",
287
- buildEmbedUrl(this.embedBaseUrl, `/admin/members/${encodeURIComponent(id)}`),
288
- opts.accessToken,
289
- patch,
290
- opts.signal
291
- ),
292
- deactivate: (id, opts) => adminEmbed(
293
- "PATCH",
294
- buildEmbedUrl(this.embedBaseUrl, `/admin/members/${encodeURIComponent(id)}`),
295
- opts.accessToken,
296
- { status: "inactive" },
297
- opts.signal
298
- )
299
- },
300
- announcements: {
301
- /** Send an email blast to camp members (PLA-706). Requires admin Bearer token. */
302
- send: (payload, opts) => adminEmbed(
303
- "POST",
304
- buildEmbedUrl(this.embedBaseUrl, "/admin/announcements/send"),
305
- opts.accessToken,
306
- payload,
307
- opts.signal
308
- )
309
- },
310
- dues: {
311
- /** Create a manual dues/payment record (PLA-708). Requires admin Bearer token. */
312
- create: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/dues"), opts.accessToken, payload, opts.signal),
313
- /** Update a dues/payment record (PLA-708). Requires admin Bearer token. */
314
- update: (id, payload, opts) => adminEmbed(
315
- "PATCH",
316
- buildEmbedUrl(this.embedBaseUrl, `/admin/dues/${encodeURIComponent(id)}`),
317
- opts.accessToken,
318
- payload,
319
- opts.signal
320
- )
321
- },
322
- org: {
323
- /** Update camp configuration settings (PLA-709). Requires admin Bearer token. */
324
- update: (payload, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, "/admin/org"), opts.accessToken, payload, opts.signal)
325
- },
326
- scheduling: {
327
- availability: {
328
- /** Get interview availability for an interviewer (PLA-713). Requires admin Bearer token. */
329
- get: (interviewerId, opts) => adminEmbed(
330
- "GET",
331
- buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/availability/${encodeURIComponent(interviewerId)}`),
332
- opts.accessToken,
333
- void 0,
334
- opts.signal
335
- ),
336
- /** Set interview availability for an interviewer (PLA-713). Requires admin Bearer token. */
337
- set: (interviewerId, payload, opts) => adminEmbed(
338
- "PUT",
339
- buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/availability/${encodeURIComponent(interviewerId)}`),
340
- opts.accessToken,
341
- payload,
342
- opts.signal
343
- )
344
- },
345
- calendar: {
346
- /** Get Google Calendar connection status for an interviewer (PLA-726). Requires admin Bearer. */
347
- status: (interviewerId, opts) => adminEmbed(
348
- "GET",
349
- buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/status`),
350
- opts.accessToken,
351
- void 0,
352
- opts.signal
353
- ),
354
- /** List available Google Calendars (PLA-726). Requires admin Bearer. */
355
- listCalendars: (interviewerId, opts) => adminEmbed(
356
- "GET",
357
- buildEmbedUrl(
358
- this.embedBaseUrl,
359
- `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/calendars`
360
- ),
361
- opts.accessToken,
362
- void 0,
363
- opts.signal
364
- ),
365
- /** Select which Google Calendar to use (PLA-726). Requires admin Bearer. */
366
- select: (interviewerId, payload, opts) => adminEmbed(
367
- "PUT",
368
- buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/select`),
369
- opts.accessToken,
370
- payload,
371
- opts.signal
372
- ),
373
- /** Disconnect Google Calendar from interviewer (PLA-726). Requires admin Bearer. */
374
- disconnect: (interviewerId, opts) => adminEmbed(
375
- "POST",
376
- buildEmbedUrl(
377
- this.embedBaseUrl,
378
- `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/disconnect`
379
- ),
380
- opts.accessToken,
381
- void 0,
382
- opts.signal
383
- )
384
- },
385
- bookingTokens: {
386
- /** Create a booking token for an application's interview link (PLA-714). Requires admin Bearer token. */
387
- create: (payload, opts) => adminEmbed(
388
- "POST",
389
- buildEmbedUrl(this.embedBaseUrl, "/admin/booking-tokens"),
390
- opts.accessToken,
391
- payload,
392
- opts.signal
393
- ),
394
- /** Get an existing booking token for an application (PLA-714). Requires admin Bearer token. */
395
- get: (applicationId, opts) => adminEmbed(
396
- "GET",
397
- buildEmbedUrl(this.embedBaseUrl, `/admin/booking-tokens/${encodeURIComponent(applicationId)}`),
398
- opts.accessToken,
399
- void 0,
400
- opts.signal
401
- )
402
- }
403
- }
404
- };
405
- // Public embed-booking endpoints (PLA-714). No auth — these are the
406
- // applicant-facing schedule surface using the hashed booking token.
407
- schedule = {
408
- /** List available interview slots for a booking token (PLA-714). Public. */
409
- getSlots: (token, opts) => {
410
- const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}`;
411
- return fetch(url, { signal: opts?.signal }).then(async (res) => {
412
- if (!res.ok) throw await parseError(res);
413
- return res.json();
414
- });
415
- },
416
- /** Book an interview slot (PLA-714). Public. */
417
- book: (token, payload, opts) => {
418
- const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/book`;
419
- return fetch(url, {
420
- method: "POST",
421
- headers: { "Content-Type": "application/json" },
422
- body: JSON.stringify(payload),
423
- signal: opts?.signal
424
- }).then(async (res) => {
425
- if (!res.ok) throw await parseError(res);
426
- return res.json();
427
- });
428
- },
429
- /** Cancel an interview booking (PLA-714). Public. */
430
- cancel: (token, opts) => {
431
- const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/cancel`;
432
- return fetch(url, { method: "POST", signal: opts?.signal }).then(async (res) => {
433
- if (!res.ok) throw await parseError(res);
434
- return res.json();
435
- });
436
- },
437
- /** Reschedule an interview booking (PLA-714). Public. */
438
- reschedule: (token, payload, opts) => {
439
- const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/reschedule`;
440
- return fetch(url, {
441
- method: "POST",
442
- headers: { "Content-Type": "application/json" },
443
- body: JSON.stringify(payload),
444
- signal: opts?.signal
445
- }).then(async (res) => {
446
- if (!res.ok) throw await parseError(res);
447
- return res.json();
448
- });
449
- }
450
- };
171
+ var PlayaOSClient = class PlayaOSClient {
172
+ baseUrl;
173
+ embedBaseUrl;
174
+ apiKey;
175
+ memberToken;
176
+ constructor(opts) {
177
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
178
+ this.embedBaseUrl = (opts.embedBaseUrl ?? opts.baseUrl).replace(/\/+$/, "");
179
+ this.apiKey = opts.apiKey;
180
+ this.memberToken = opts.memberToken;
181
+ }
182
+ /** Credentials for /api/v1 calls; the embed surface uses `apiKey` alone as the camp key. */
183
+ get auth() {
184
+ return {
185
+ apiKey: this.apiKey,
186
+ memberToken: this.memberToken
187
+ };
188
+ }
189
+ /**
190
+ * Derive a client that acts as one member (PLA-1195): same host and org key,
191
+ * plus the member's Supabase session JWT on every /api/v1 call. Use this in a
192
+ * server that holds one org key and serves many signed-in members.
193
+ */
194
+ withMember(memberToken) {
195
+ return new PlayaOSClient({
196
+ baseUrl: this.baseUrl,
197
+ embedBaseUrl: this.embedBaseUrl,
198
+ apiKey: this.apiKey,
199
+ memberToken
200
+ });
201
+ }
202
+ members = {
203
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/members", params), this.auth, opts?.signal),
204
+ /**
205
+ * Full profile (phone, emergency contact). Requires a member token; pass
206
+ * `"me"` for the acting member — non-admins may fetch only themselves.
207
+ */
208
+ get: (id, opts) => request(buildUrl(this.baseUrl, `/members/${encodeURIComponent(id)}`), this.auth, opts?.signal),
209
+ /** Member-self profile update (PLA-700). Requires camp key + member Bearer token. */
210
+ selfUpdate: (payload, opts) => putEmbed(buildEmbedUrl(this.embedBaseUrl, "/members/self"), this.apiKey, payload, opts),
211
+ /** Member-self shift signup + cancel (PLA-705). Requires camp key + member Bearer. */
212
+ shifts: {
213
+ signup: (payload, opts) => postEmbed(buildEmbedUrl(this.embedBaseUrl, "/shifts/signup"), this.apiKey, payload, opts),
214
+ cancel: (signupId, opts) => deleteEmbed(buildEmbedUrl(this.embedBaseUrl, `/shifts/signups/${encodeURIComponent(signupId)}`), this.apiKey, opts)
215
+ }
216
+ };
217
+ applications = {
218
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/applications", params), this.auth, opts?.signal),
219
+ create: (payload, opts) => postEmbed(buildEmbedUrl(this.embedBaseUrl, "/applications"), this.apiKey, payload, opts)
220
+ };
221
+ waivers = {
222
+ /** Get the active waiver for an org for the current year. */
223
+ active: (orgId, opts) => getEmbed(buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/active`), this.apiKey, opts?.signal),
224
+ /** Sign a waiver. Requires X-Camp-Key; optional accessToken for Bearer path. */
225
+ sign: (payload, opts) => postEmbed(buildEmbedUrl(this.embedBaseUrl, "/waivers/sign"), this.apiKey, payload, opts),
226
+ /** Has this applicant/member signed the active waiver? Anchor: applicationId query or Bearer accessToken. */
227
+ status: (orgId, opts) => {
228
+ const query = opts?.applicationId ? `?applicationId=${encodeURIComponent(opts.applicationId)}` : "";
229
+ return getEmbedAuthed(buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/status${query}`), this.apiKey, opts);
230
+ },
231
+ /** Get a short-lived download URL for a completed signature's PDF. Anchor: applicationId query or Bearer. */
232
+ signedPdf: (orgId, signatureId, opts) => {
233
+ const query = opts?.applicationId ? `?applicationId=${encodeURIComponent(opts.applicationId)}` : "";
234
+ return getEmbedAuthed(buildEmbedUrl(this.embedBaseUrl, `/waivers/${orgId}/signatures/${encodeURIComponent(signatureId)}/pdf${query}`), this.apiKey, opts);
235
+ }
236
+ };
237
+ dues = { list: (params, opts) => request(buildUrl(this.baseUrl, "/dues", params), this.auth, opts?.signal) };
238
+ shifts = { list: (params, opts) => request(buildUrl(this.baseUrl, "/shifts", params), this.auth, opts?.signal) };
239
+ org = { get: (opts) => request(buildUrl(this.baseUrl, "/org"), this.auth, opts?.signal) };
240
+ commitments = { list: (opts) => getEmbed(buildEmbedUrl(this.embedBaseUrl, "/commitments"), this.apiKey, opts?.signal) };
241
+ payments = {
242
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/payments", params), this.auth, opts?.signal),
243
+ /** Requires a member token. `memberId` may be omitted by a non-admin (defaults to themselves); admins must pass it. */
244
+ page: (memberId, duesCollectionId, opts) => post(buildUrl(this.baseUrl, "/payments/page"), this.auth, {
245
+ memberId,
246
+ duesCollectionId
247
+ }, opts?.signal)
248
+ };
249
+ accounts = { list: (params, opts) => request(buildUrl(this.baseUrl, "/accounts", params), this.auth, opts?.signal) };
250
+ contacts = { list: (params, opts) => request(buildUrl(this.baseUrl, "/contacts", params), this.auth, opts?.signal) };
251
+ notifications = { list: (params, opts) => request(buildUrl(this.baseUrl, "/notifications", params), this.auth, opts?.signal) };
252
+ documents = {
253
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/documents", params), this.auth, opts?.signal),
254
+ get: (id, opts) => request(buildUrl(this.baseUrl, `/documents/${encodeURIComponent(id)}`), this.auth, opts?.signal)
255
+ };
256
+ shelters = {
257
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/shelters", params), this.auth, opts?.signal),
258
+ get: (id, opts) => request(buildUrl(this.baseUrl, `/shelters/${encodeURIComponent(id)}`), this.auth, opts?.signal)
259
+ };
260
+ bikes = {
261
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/bikes", params), this.auth, opts?.signal),
262
+ get: (id, opts) => request(buildUrl(this.baseUrl, `/bikes/${encodeURIComponent(id)}`), this.auth, opts?.signal)
263
+ };
264
+ /**
265
+ * DustJunkie bike rentals (PLA-1180), passed through to the vendor. A camp
266
+ * without the integration gets 403; a DustJunkie failure is a 502.
267
+ */
268
+ bikeRentals = {
269
+ /** Bookable options with estimated totals for `delivery` (server default `camp_delivery`). Org key only. */
270
+ catalog: (params, opts) => request(buildUrl(this.baseUrl, "/bike-rentals/catalog", params), this.auth, opts?.signal),
271
+ /** The rental agreement the member must accept before `requestReservation`. Org key only. */
272
+ waiver: (opts) => request(buildUrl(this.baseUrl, "/bike-rentals/waiver"), this.auth, opts?.signal),
273
+ /**
274
+ * Requires a member token. Admins get the whole camp when `memberId` is
275
+ * omitted; any other role gets their own reservations and 403 naming someone else.
276
+ */
277
+ reservations: (params, opts) => request(buildUrl(this.baseUrl, "/bike-rentals/reservations", params), this.auth, opts?.signal),
278
+ /**
279
+ * Requires a member token; always requests for the acting member. 409 when
280
+ * the model is sold out, 400 when it is unknown or no longer bookable.
281
+ */
282
+ requestReservation: (payload, opts) => post(buildUrl(this.baseUrl, "/bike-rentals/requests"), this.auth, payload, opts?.signal),
283
+ /** Members who said they want a bike but have no reservation, for the active camp year. Org key only. */
284
+ interestedNotReserved: (opts) => request(buildUrl(this.baseUrl, "/bike-rentals/interested-not-reserved"), this.auth, opts?.signal)
285
+ };
286
+ tags = { list: (opts) => request(buildUrl(this.baseUrl, "/tags"), this.auth, opts?.signal) };
287
+ groups = {
288
+ list: (opts) => request(buildUrl(this.baseUrl, "/groups"), this.auth, opts?.signal),
289
+ get: (id, opts) => request(buildUrl(this.baseUrl, `/groups/${encodeURIComponent(id)}`), this.auth, opts?.signal),
290
+ create: (payload, opts) => post(buildUrl(this.baseUrl, "/groups"), this.auth, payload, opts?.signal)
291
+ };
292
+ /**
293
+ * Camp-scoped packing (PLA-1197). Every method is the acting member's own
294
+ * list — packing has no `memberId` to widen, so these require `memberToken`
295
+ * and 401 on an org key alone.
296
+ */
297
+ packing = {
298
+ /** The resolved list: curated base + camp layer + personal layer (ADR-071). */
299
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/packing", params), this.auth, opts?.signal),
300
+ /**
301
+ * Set the member's status for one item. The slug goes in the body, not the
302
+ * path: slugs are `${category}/${name}` and contain a `/`.
303
+ *
304
+ * Pass the same `shelter` the list was read under — validation filters by
305
+ * shelter, so an override omitted here rejects shelter-specific items.
306
+ */
307
+ setStatus: (payload, opts) => sendV1("PUT", buildUrl(this.baseUrl, "/packing/items/status"), this.auth, payload, opts?.signal),
308
+ personalItems: {
309
+ list: (params, opts) => request(buildUrl(this.baseUrl, "/packing/personal-items", params), this.auth, opts?.signal),
310
+ create: (payload, opts) => post(buildUrl(this.baseUrl, "/packing/personal-items"), this.auth, payload, opts?.signal),
311
+ /**
312
+ * Flip the item between `need` and `have`. It does **not** set `packed` —
313
+ * use `setStatus` for the full vocabulary. 404 if the item is not the
314
+ * acting member's.
315
+ */
316
+ toggle: (id, opts) => sendV1("PATCH", buildUrl(this.baseUrl, `/packing/personal-items/${encodeURIComponent(id)}`), this.auth, void 0, opts?.signal),
317
+ /** 404 if the item is not the acting member's. */
318
+ remove: (id, opts) => sendV1("DELETE", buildUrl(this.baseUrl, `/packing/personal-items/${encodeURIComponent(id)}`), this.auth, void 0, opts?.signal)
319
+ }
320
+ };
321
+ admin = {
322
+ applications: {
323
+ /**
324
+ * Get application audit timeline (PLA-712). Requires admin Bearer token.
325
+ *
326
+ * @deprecated The embed handler for this method never existed — calls
327
+ * always 404'd (see PLA-1199). Retained as a compile-time compatibility
328
+ * surface for published-consumer code; delegates to the canonical
329
+ * `GET /api/v1/audit/application-logs?applicationId=` (ADR-054), which
330
+ * reads the same `application_audit_logs` table. The `accessToken`
331
+ * argument is accepted but unused — the v1 surface authenticates with
332
+ * the org API key (`audit:read` scope) instead of an admin JWT. Remove
333
+ * in the next major version.
334
+ */
335
+ history: async (id, opts) => {
336
+ const rows = [];
337
+ let cursor = null;
338
+ do {
339
+ const page = await request(buildUrl(this.baseUrl, "/audit/application-logs", {
340
+ applicationId: id,
341
+ limit: 100,
342
+ ...cursor ? { cursor } : {}
343
+ }), this.auth, opts?.signal);
344
+ rows.push(...page.data);
345
+ cursor = page.nextCursor;
346
+ } while (cursor);
347
+ return { events: rows.map((row) => ({
348
+ id: row.id,
349
+ applicationId: row.applicationId,
350
+ actorId: row.actorUserId,
351
+ actorName: null,
352
+ type: row.action === "status_changed" ? "status_change" : row.action,
353
+ timestamp: row.createdAt ?? "",
354
+ metadata: row.details ?? void 0
355
+ })) };
356
+ },
357
+ transition: (id, payload, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}`), opts.accessToken, payload, opts.signal),
358
+ getAnnotations: (id, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/annotations`), opts.accessToken, void 0, opts.signal),
359
+ addNote: (id, payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/notes`), opts.accessToken, payload, opts.signal),
360
+ setTags: (id, payload, opts) => adminEmbed("PUT", buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/tags`), opts.accessToken, payload, opts.signal),
361
+ setScore: (id, payload, opts) => adminEmbed("PUT", buildEmbedUrl(this.embedBaseUrl, `/admin/applications/${encodeURIComponent(id)}/scores`), opts.accessToken, payload, opts.signal)
362
+ },
363
+ givebutter: { campaigns: {
364
+ list: (opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, "/admin/givebutter/campaigns"), opts.accessToken, void 0, opts.signal),
365
+ discountCodes: {
366
+ list: (campaignId, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/discount-codes`), opts.accessToken, void 0, opts.signal),
367
+ create: (campaignId, payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/discount-codes`), opts.accessToken, payload, opts.signal)
368
+ },
369
+ tickets: { create: (campaignId, payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, `/admin/givebutter/campaigns/${campaignId}/tickets`), opts.accessToken, payload, opts.signal) }
370
+ } },
371
+ members: {
372
+ create: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/members"), opts.accessToken, payload, opts.signal),
373
+ update: (id, patch, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, `/admin/members/${encodeURIComponent(id)}`), opts.accessToken, patch, opts.signal),
374
+ deactivate: (id, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, `/admin/members/${encodeURIComponent(id)}`), opts.accessToken, { status: "inactive" }, opts.signal)
375
+ },
376
+ announcements: {
377
+ /** Send an email blast to camp members (PLA-706). Requires admin Bearer token. */
378
+ send: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/announcements/send"), opts.accessToken, payload, opts.signal) },
379
+ dues: {
380
+ /** Create a manual dues/payment record (PLA-708). Requires admin Bearer token. */
381
+ create: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/dues"), opts.accessToken, payload, opts.signal),
382
+ /** Update a dues/payment record (PLA-708). Requires admin Bearer token. */
383
+ update: (id, payload, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, `/admin/dues/${encodeURIComponent(id)}`), opts.accessToken, payload, opts.signal)
384
+ },
385
+ org: {
386
+ /** Update camp configuration settings (PLA-709). Requires admin Bearer token. */
387
+ update: (payload, opts) => adminEmbed("PATCH", buildEmbedUrl(this.embedBaseUrl, "/admin/org"), opts.accessToken, payload, opts.signal) },
388
+ scheduling: {
389
+ availability: {
390
+ /** Get interview availability for an interviewer (PLA-713). Requires admin Bearer token. */
391
+ get: (interviewerId, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/availability/${encodeURIComponent(interviewerId)}`), opts.accessToken, void 0, opts.signal),
392
+ /** Set interview availability for an interviewer (PLA-713). Requires admin Bearer token. */
393
+ set: (interviewerId, payload, opts) => adminEmbed("PUT", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/availability/${encodeURIComponent(interviewerId)}`), opts.accessToken, payload, opts.signal)
394
+ },
395
+ calendar: {
396
+ /** Get Google Calendar connection status for an interviewer (PLA-726). Requires admin Bearer. */
397
+ status: (interviewerId, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/status`), opts.accessToken, void 0, opts.signal),
398
+ /** List available Google Calendars (PLA-726). Requires admin Bearer. */
399
+ listCalendars: (interviewerId, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/calendars`), opts.accessToken, void 0, opts.signal),
400
+ /** Select which Google Calendar to use (PLA-726). Requires admin Bearer. */
401
+ select: (interviewerId, payload, opts) => adminEmbed("PUT", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/select`), opts.accessToken, payload, opts.signal),
402
+ /** Disconnect Google Calendar from interviewer (PLA-726). Requires admin Bearer. */
403
+ disconnect: (interviewerId, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, `/admin/scheduling/calendar/${encodeURIComponent(interviewerId)}/disconnect`), opts.accessToken, void 0, opts.signal)
404
+ },
405
+ bookingTokens: {
406
+ /** Create a booking token for an application's interview link (PLA-714). Requires admin Bearer token. */
407
+ create: (payload, opts) => adminEmbed("POST", buildEmbedUrl(this.embedBaseUrl, "/admin/booking-tokens"), opts.accessToken, payload, opts.signal),
408
+ /** Get an existing booking token for an application (PLA-714). Requires admin Bearer token. */
409
+ get: (applicationId, opts) => adminEmbed("GET", buildEmbedUrl(this.embedBaseUrl, `/admin/booking-tokens/${encodeURIComponent(applicationId)}`), opts.accessToken, void 0, opts.signal)
410
+ }
411
+ }
412
+ };
413
+ schedule = {
414
+ /** List available interview slots for a booking token (PLA-714). Public. */
415
+ getSlots: (token, opts) => {
416
+ const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}`;
417
+ return fetch(url, { signal: opts?.signal }).then(async (res) => {
418
+ if (!res.ok) throw await parseError(res);
419
+ return res.json();
420
+ });
421
+ },
422
+ /** Book an interview slot (PLA-714). Public. */
423
+ book: (token, payload, opts) => {
424
+ const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/book`;
425
+ return fetch(url, {
426
+ method: "POST",
427
+ headers: { "Content-Type": "application/json" },
428
+ body: JSON.stringify(payload),
429
+ signal: opts?.signal
430
+ }).then(async (res) => {
431
+ if (!res.ok) throw await parseError(res);
432
+ return res.json();
433
+ });
434
+ },
435
+ /** Cancel an interview booking (PLA-714). Public. */
436
+ cancel: (token, opts) => {
437
+ const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/cancel`;
438
+ return fetch(url, {
439
+ method: "POST",
440
+ signal: opts?.signal
441
+ }).then(async (res) => {
442
+ if (!res.ok) throw await parseError(res);
443
+ return res.json();
444
+ });
445
+ },
446
+ /** Reschedule an interview booking (PLA-714). Public. */
447
+ reschedule: (token, payload, opts) => {
448
+ const url = `${this.embedBaseUrl}/api/embed/v1/schedule/${encodeURIComponent(token)}/reschedule`;
449
+ return fetch(url, {
450
+ method: "POST",
451
+ headers: { "Content-Type": "application/json" },
452
+ body: JSON.stringify(payload),
453
+ signal: opts?.signal
454
+ }).then(async (res) => {
455
+ if (!res.ok) throw await parseError(res);
456
+ return res.json();
457
+ });
458
+ }
459
+ };
451
460
  };
461
+ /** Convenience factory — equivalent to `new PlayaOSClient(opts)`. */
452
462
  function createClient(opts) {
453
- return new PlayaOSClient(opts);
463
+ return new PlayaOSClient(opts);
454
464
  }
455
-
456
- // src/exchange.ts
465
+ //#endregion
466
+ //#region src/exchange.ts
457
467
  async function exchangeCode(params) {
458
- const base = params.authBaseUrl ?? "https://auth.playaos.app";
459
- const isConfidential = params.clientSecret !== void 0;
460
- const res = await fetch(`${base}/api/auth/v1/exchange`, {
461
- method: "POST",
462
- headers: { "content-type": "application/json" },
463
- body: JSON.stringify({
464
- code: params.code,
465
- code_verifier: params.codeVerifier,
466
- client_id: params.clientId,
467
- ...isConfidential ? { client_secret: params.clientSecret } : {},
468
- redirect_uri: params.redirectUri
469
- })
470
- });
471
- let body;
472
- try {
473
- body = await res.json();
474
- } catch {
475
- throw new Error(`Exchange failed: non-JSON response (${res.status})`);
476
- }
477
- if (!res.ok) {
478
- const errorReason = typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" ? body.error : String(res.status);
479
- throw new Error(`Exchange failed: ${errorReason}`);
480
- }
481
- if (typeof body !== "object" || body === null || !("idToken" in body) || typeof body.idToken !== "string" || !("expiresAt" in body) || typeof body.expiresAt !== "string") {
482
- throw new Error("Exchange failed: malformed success response");
483
- }
484
- const refreshToken = "refreshToken" in body && typeof body.refreshToken === "string" ? body.refreshToken : void 0;
485
- if (isConfidential && refreshToken === void 0) {
486
- throw new Error("Exchange failed: confidential client response missing refreshToken");
487
- }
488
- return {
489
- idToken: body.idToken,
490
- expiresAt: body.expiresAt,
491
- ...refreshToken !== void 0 ? { refreshToken } : {}
492
- };
468
+ const base = params.authBaseUrl ?? "https://auth.playaos.app";
469
+ const isConfidential = params.clientSecret !== void 0;
470
+ const res = await fetch(`${base}/api/auth/v1/exchange`, {
471
+ method: "POST",
472
+ headers: { "content-type": "application/json" },
473
+ body: JSON.stringify({
474
+ code: params.code,
475
+ code_verifier: params.codeVerifier,
476
+ client_id: params.clientId,
477
+ ...isConfidential ? { client_secret: params.clientSecret } : {},
478
+ redirect_uri: params.redirectUri
479
+ })
480
+ });
481
+ let body;
482
+ try {
483
+ body = await res.json();
484
+ } catch {
485
+ throw new Error(`Exchange failed: non-JSON response (${res.status})`);
486
+ }
487
+ if (!res.ok) {
488
+ const errorReason = typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" ? body.error : String(res.status);
489
+ throw new Error(`Exchange failed: ${errorReason}`);
490
+ }
491
+ if (typeof body !== "object" || body === null || !("idToken" in body) || typeof body.idToken !== "string" || !("expiresAt" in body) || typeof body.expiresAt !== "string") throw new Error("Exchange failed: malformed success response");
492
+ const refreshToken = "refreshToken" in body && typeof body.refreshToken === "string" ? body.refreshToken : void 0;
493
+ if (isConfidential && refreshToken === void 0) throw new Error("Exchange failed: confidential client response missing refreshToken");
494
+ return {
495
+ idToken: body.idToken,
496
+ expiresAt: body.expiresAt,
497
+ ...refreshToken !== void 0 ? { refreshToken } : {}
498
+ };
493
499
  }
494
-
495
- // src/public-marketplace.ts
496
- var MAX_IMAGE_POSITION = 6;
500
+ //#endregion
501
+ //#region src/public-marketplace.ts
502
+ /** A listing holds at most this many images (matches the DB's position 1–6 check). */
503
+ const MAX_IMAGE_POSITION = 6;
497
504
  function createPublicMarketplaceClient(opts) {
498
- const base = opts.baseUrl.replace(/\/+$/, "");
499
- if (!base) {
500
- throw new Error("createPublicMarketplaceClient: baseUrl is required");
501
- }
502
- async function authedPost(path, body, signal) {
503
- const token = await opts.getToken();
504
- if (!token) throw new Error("Not signed in");
505
- const res = await fetch(`${base}/api/public/marketplace${path}`, {
506
- method: "POST",
507
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
508
- body: JSON.stringify(body),
509
- signal
510
- });
511
- if (!res.ok) throw await parseError(res);
512
- return res.json();
513
- }
514
- async function publicGet(path, signal) {
515
- const init = { signal, cache: "no-store" };
516
- const res = await fetch(`${base}/api/public/marketplace${path}`, init);
517
- if (!res.ok) throw await parseError(res);
518
- return res.json();
519
- }
520
- const seg = (listingId) => `/listings/${encodeURIComponent(listingId)}`;
521
- return {
522
- createListing: (input, signal) => authedPost("/listings", input, signal),
523
- getImageUploadUrl: (listingId, signal) => authedPost(`${seg(listingId)}/image-upload-url`, {}, signal),
524
- attachImage: (listingId, input, signal) => {
525
- if (!Number.isInteger(input.position) || input.position < 1 || input.position > MAX_IMAGE_POSITION) {
526
- return Promise.reject(new Error(`attachImage: position must be 1\u2013${MAX_IMAGE_POSITION}`));
527
- }
528
- return authedPost(`${seg(listingId)}/images`, input, signal);
529
- },
530
- listListingImages: (listingId, signal) => publicGet(`${seg(listingId)}/images`, signal),
531
- listThumbnails: (listingIds, signal) => publicGet(`/thumbnails?ids=${encodeURIComponent(listingIds.join(","))}`, signal),
532
- flagListing: (listingId, reason, signal) => authedPost(`${seg(listingId)}/flag`, { reason }, signal),
533
- replyToListing: (listingId, message, signal) => authedPost(`${seg(listingId)}/reply`, { message }, signal)
534
- };
505
+ const base = opts.baseUrl.replace(/\/+$/, "");
506
+ if (!base) throw new Error("createPublicMarketplaceClient: baseUrl is required");
507
+ async function authedPost(path, body, signal) {
508
+ const token = await opts.getToken();
509
+ if (!token) throw new Error("Not signed in");
510
+ const res = await fetch(`${base}/api/public/marketplace${path}`, {
511
+ method: "POST",
512
+ headers: {
513
+ Authorization: `Bearer ${token}`,
514
+ "Content-Type": "application/json"
515
+ },
516
+ body: JSON.stringify(body),
517
+ signal
518
+ });
519
+ if (!res.ok) throw await parseError(res);
520
+ return res.json();
521
+ }
522
+ async function publicGet(path, signal) {
523
+ const init = {
524
+ signal,
525
+ cache: "no-store"
526
+ };
527
+ const res = await fetch(`${base}/api/public/marketplace${path}`, init);
528
+ if (!res.ok) throw await parseError(res);
529
+ return res.json();
530
+ }
531
+ const seg = (listingId) => `/listings/${encodeURIComponent(listingId)}`;
532
+ return {
533
+ createListing: (input, signal) => authedPost("/listings", input, signal),
534
+ getImageUploadUrl: (listingId, signal) => authedPost(`${seg(listingId)}/image-upload-url`, {}, signal),
535
+ attachImage: (listingId, input, signal) => {
536
+ if (!Number.isInteger(input.position) || input.position < 1 || input.position > 6) return Promise.reject(/* @__PURE__ */ new Error(`attachImage: position must be 1–6`));
537
+ return authedPost(`${seg(listingId)}/images`, input, signal);
538
+ },
539
+ listListingImages: (listingId, signal) => publicGet(`${seg(listingId)}/images`, signal),
540
+ listThumbnails: (listingIds, signal) => publicGet(`/thumbnails?ids=${encodeURIComponent(listingIds.join(","))}`, signal),
541
+ flagListing: (listingId, reason, signal) => authedPost(`${seg(listingId)}/flag`, { reason }, signal),
542
+ replyToListing: (listingId, message, signal) => authedPost(`${seg(listingId)}/reply`, { message }, signal)
543
+ };
535
544
  }
536
- export {
537
- ApiClientError,
538
- MAX_IMAGE_POSITION,
539
- PlayaOSClient,
540
- createClient,
541
- createPublicMarketplaceClient,
542
- exchangeCode
543
- };
545
+ //#endregion
546
+ export { ApiClientError, MAX_IMAGE_POSITION, PlayaOSClient, createClient, createPublicMarketplaceClient, exchangeCode };
547
+
544
548
  //# sourceMappingURL=index.js.map