@ingram-tech/luma 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/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +298 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ingram Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @ingram-tech/luma
|
|
2
|
+
|
|
3
|
+
TypeScript client for the [Luma](https://lu.ma) (lu.ma) public API.
|
|
4
|
+
|
|
5
|
+
Small, dependency-free, ESM-only. Wraps the endpoints needed to mirror a Luma
|
|
6
|
+
calendar — events, guests, people, coupons — and exposes typed escape hatches
|
|
7
|
+
for everything else.
|
|
8
|
+
|
|
9
|
+
> Unofficial. Not affiliated with or endorsed by Luma. "Luma" is a trademark of
|
|
10
|
+
> its respective owner.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun add @ingram-tech/luma
|
|
16
|
+
# or: npm install @ingram-tech/luma
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node.js 18+ (uses the global `fetch`). A Luma **API key** is needed —
|
|
20
|
+
available under *Calendar Settings → API* on a Luma Plus plan.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { LumaClient } from "@ingram-tech/luma";
|
|
26
|
+
|
|
27
|
+
const luma = new LumaClient({ apiKey: process.env.LUMA_API_KEY! });
|
|
28
|
+
// or: const luma = LumaClient.fromEnv(); // reads LUMA_API_KEY
|
|
29
|
+
|
|
30
|
+
// Iterate every event on a calendar (auto-paginates).
|
|
31
|
+
for await (const entry of luma.calendar.listEvents({ calendarApiId })) {
|
|
32
|
+
console.log(entry.event?.name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// …or collect them into an array.
|
|
36
|
+
const events = await luma.calendar.listAllEvents({ calendarApiId });
|
|
37
|
+
|
|
38
|
+
// Guests of an event.
|
|
39
|
+
const guests = await luma.events.listAllGuests({ eventApiId });
|
|
40
|
+
|
|
41
|
+
// A single guest, by email.
|
|
42
|
+
const guest = await luma.events.getGuest({ eventApiId, email: "a@b.com" });
|
|
43
|
+
|
|
44
|
+
// Approve a guest.
|
|
45
|
+
await luma.events.updateGuestStatus({
|
|
46
|
+
eventApiId,
|
|
47
|
+
guestApiId: guest.api_id,
|
|
48
|
+
status: "approved",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Coupons.
|
|
52
|
+
const coupon = await luma.calendar.createCoupon({
|
|
53
|
+
code: "SUMMIT-2027",
|
|
54
|
+
remainingCount: 1,
|
|
55
|
+
discount: { type: "amount", centsOff: 4000, currency: "EUR" },
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Pagination
|
|
60
|
+
|
|
61
|
+
Cursor-paginated methods (`listEvents`, `listPeople`, `listGuests`,
|
|
62
|
+
`listCoupons`) return an `AsyncGenerator` that follows `next_cursor`
|
|
63
|
+
automatically. Each has a `listAll…` sibling that drains it into an array.
|
|
64
|
+
`collect()` is exported for draining any async iterable.
|
|
65
|
+
|
|
66
|
+
### Escape hatches
|
|
67
|
+
|
|
68
|
+
The typed methods cover the endpoints this client is built around. For
|
|
69
|
+
anything not modelled, call the API directly — both methods are public:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// Any endpoint, typed by you.
|
|
73
|
+
const data = await luma.request<MyType>("/v1/event/get", {
|
|
74
|
+
query: { api_id: eventApiId },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Any cursor-paginated endpoint.
|
|
78
|
+
for await (const entry of luma.paginate<MyEntry>("/v1/some/list", { foo: "bar" })) {
|
|
79
|
+
// …
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`request` also accepts `fetchInit` for passing through framework-specific
|
|
84
|
+
options, e.g. Next.js cache hints:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await luma.request("/v1/calendar/list-events", {
|
|
88
|
+
query: { calendar_api_id },
|
|
89
|
+
fetchInit: { next: { revalidate: 300 } },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Errors
|
|
94
|
+
|
|
95
|
+
Non-2xx responses (and non-JSON bodies) throw `LumaApiError`, which carries the
|
|
96
|
+
`status`, raw `body`, and `path`, plus convenience getters:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { LumaApiError } from "@ingram-tech/luma";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await luma.calendar.createCoupon({ code: "DUP", discount: { type: "percent", percentOff: 10 } });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof LumaApiError && err.isDuplicateCouponCode) {
|
|
105
|
+
// coupon already exists — treat as idempotent
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`isAuthError` (401/403) and `isRateLimited` (429) are also provided.
|
|
111
|
+
|
|
112
|
+
## API coverage
|
|
113
|
+
|
|
114
|
+
| Area | Methods |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Calendar events | `calendar.listEvents`, `calendar.listAllEvents` |
|
|
117
|
+
| Calendar people | `calendar.listPeople`, `calendar.listAllPeople` |
|
|
118
|
+
| Coupons | `calendar.listCoupons`, `calendar.findCouponByCode`, `calendar.createCoupon` |
|
|
119
|
+
| Events | `events.get` |
|
|
120
|
+
| Guests | `events.listGuests`, `events.listAllGuests`, `events.getGuest`, `events.updateGuestStatus` |
|
|
121
|
+
| Anything else | `request`, `paginate` |
|
|
122
|
+
|
|
123
|
+
Luma does not publish a machine-readable schema. The calendar-events and
|
|
124
|
+
coupon paths are exercised in production; the guest and people paths are
|
|
125
|
+
modelled from Luma's public API documentation. Response objects carry an index
|
|
126
|
+
signature, so unmodelled fields are always reachable. PRs welcome.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
bun install
|
|
132
|
+
bun run ci # type-check, lint, test, build
|
|
133
|
+
bun run test # watch mode
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT © Ingram Technologies
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Luma public API.
|
|
3
|
+
*
|
|
4
|
+
* Luma does not publish a machine-readable schema, and several fields are
|
|
5
|
+
* optional or vary between plan tiers. Object types therefore carry an index
|
|
6
|
+
* signature so unmodelled fields remain accessible without a cast. When in
|
|
7
|
+
* doubt, reach for {@link import("./client").LumaClient.request} and type the
|
|
8
|
+
* response yourself.
|
|
9
|
+
*/
|
|
10
|
+
/** A cursor-paginated list response. */
|
|
11
|
+
interface LumaPaginatedResponse<T> {
|
|
12
|
+
entries: T[];
|
|
13
|
+
has_more?: boolean;
|
|
14
|
+
next_cursor?: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** Options shared by every cursor-paginated endpoint. */
|
|
17
|
+
interface LumaPaginationOptions {
|
|
18
|
+
/** Opaque cursor returned as `next_cursor` by a previous page. */
|
|
19
|
+
paginationCursor?: string;
|
|
20
|
+
/** Page size. Luma's default and maximum vary by endpoint. */
|
|
21
|
+
paginationLimit?: number;
|
|
22
|
+
}
|
|
23
|
+
interface LumaEventTag {
|
|
24
|
+
api_id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
interface LumaEventLocation {
|
|
29
|
+
type?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
address?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
interface LumaEvent {
|
|
35
|
+
api_id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
start_at: string;
|
|
39
|
+
end_at?: string;
|
|
40
|
+
timezone?: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
cover_url?: string;
|
|
43
|
+
visibility?: string;
|
|
44
|
+
location?: LumaEventLocation;
|
|
45
|
+
guest_limit?: number | null;
|
|
46
|
+
guest_count?: number;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An entry in `calendar/list-events`. Luma nests the event under `event` and
|
|
51
|
+
* places calendar-scoped metadata (tags) alongside it.
|
|
52
|
+
*/
|
|
53
|
+
interface LumaCalendarEntry {
|
|
54
|
+
api_id?: string;
|
|
55
|
+
event?: LumaEvent;
|
|
56
|
+
tags?: LumaEventTag[];
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
interface ListCalendarEventsOptions extends LumaPaginationOptions {
|
|
60
|
+
/** Calendar to list events for. Required. */
|
|
61
|
+
calendarApiId: string;
|
|
62
|
+
/** Only events starting at or after this instant. */
|
|
63
|
+
after?: Date | string;
|
|
64
|
+
/** Only events starting at or before this instant. */
|
|
65
|
+
before?: Date | string;
|
|
66
|
+
}
|
|
67
|
+
interface LumaPerson {
|
|
68
|
+
api_id: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
email?: string;
|
|
71
|
+
avatar_url?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface ListCalendarPeopleOptions extends LumaPaginationOptions {
|
|
75
|
+
calendarApiId: string;
|
|
76
|
+
}
|
|
77
|
+
type LumaGuestApprovalStatus = "approved" | "declined" | "pending_approval" | "waitlist" | "invited" | (string & {});
|
|
78
|
+
interface LumaGuest {
|
|
79
|
+
api_id: string;
|
|
80
|
+
name?: string;
|
|
81
|
+
email?: string;
|
|
82
|
+
user_api_id?: string;
|
|
83
|
+
approval_status?: LumaGuestApprovalStatus;
|
|
84
|
+
registered_at?: string;
|
|
85
|
+
checked_in_at?: string | null;
|
|
86
|
+
event_ticket?: {
|
|
87
|
+
api_id?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
/** Answers to the event's registration questions, when present. */
|
|
92
|
+
registration_answers?: Array<{
|
|
93
|
+
label?: string;
|
|
94
|
+
answer?: string;
|
|
95
|
+
question_id?: string;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
}>;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
}
|
|
100
|
+
/** An entry in `event/get-guests`. Luma nests the guest under `guest`. */
|
|
101
|
+
interface LumaGuestEntry {
|
|
102
|
+
api_id?: string;
|
|
103
|
+
guest?: LumaGuest;
|
|
104
|
+
[key: string]: unknown;
|
|
105
|
+
}
|
|
106
|
+
interface ListEventGuestsOptions extends LumaPaginationOptions {
|
|
107
|
+
eventApiId: string;
|
|
108
|
+
/** Filter by approval status, when supported by the endpoint. */
|
|
109
|
+
approvalStatus?: LumaGuestApprovalStatus;
|
|
110
|
+
}
|
|
111
|
+
interface GetEventGuestOptions {
|
|
112
|
+
eventApiId: string;
|
|
113
|
+
/** Look the guest up by their guest api_id… */
|
|
114
|
+
guestApiId?: string;
|
|
115
|
+
/** …or by the email they registered with. One of the two is required. */
|
|
116
|
+
email?: string;
|
|
117
|
+
}
|
|
118
|
+
interface UpdateGuestStatusOptions {
|
|
119
|
+
eventApiId: string;
|
|
120
|
+
guestApiId: string;
|
|
121
|
+
status: LumaGuestApprovalStatus;
|
|
122
|
+
}
|
|
123
|
+
interface LumaCoupon {
|
|
124
|
+
api_id: string;
|
|
125
|
+
code: string;
|
|
126
|
+
[key: string]: unknown;
|
|
127
|
+
}
|
|
128
|
+
interface LumaCouponEntry {
|
|
129
|
+
api_id?: string;
|
|
130
|
+
id?: string;
|
|
131
|
+
code?: string;
|
|
132
|
+
[key: string]: unknown;
|
|
133
|
+
}
|
|
134
|
+
interface CreateCalendarCouponInput {
|
|
135
|
+
code: string;
|
|
136
|
+
/** Number of times the coupon may be redeemed. */
|
|
137
|
+
remainingCount?: number;
|
|
138
|
+
validStartAt?: Date | string;
|
|
139
|
+
validEndAt?: Date | string;
|
|
140
|
+
discount: {
|
|
141
|
+
type: "percent";
|
|
142
|
+
/** Whole-percent value, e.g. `25` for 25% off. */
|
|
143
|
+
percentOff: number;
|
|
144
|
+
} | {
|
|
145
|
+
type: "amount";
|
|
146
|
+
/** Discount in the currency's minor unit, e.g. `4000` = €40. */
|
|
147
|
+
centsOff: number;
|
|
148
|
+
/** ISO 4217 code, lowercased by the client (e.g. `eur`). */
|
|
149
|
+
currency: string;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface LumaClientOptions {
|
|
154
|
+
/** Luma API key. Found under Calendar Settings → API on a Luma Plus plan. */
|
|
155
|
+
apiKey: string;
|
|
156
|
+
/** Override the API base URL. Defaults to {@link LUMA_API_BASE_URL}. */
|
|
157
|
+
baseUrl?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Fetch implementation to use. Defaults to the global `fetch`. Pass a
|
|
160
|
+
* custom one to inject Next.js cache hints, retries, or a test double.
|
|
161
|
+
*/
|
|
162
|
+
fetch?: typeof fetch;
|
|
163
|
+
}
|
|
164
|
+
/** Per-request options for {@link LumaClient.request}. */
|
|
165
|
+
interface LumaRequestInit {
|
|
166
|
+
method?: string;
|
|
167
|
+
/** Query parameters. `undefined`/`null` values are dropped; `Date`s are
|
|
168
|
+
* serialised as ISO strings. */
|
|
169
|
+
query?: Record<string, unknown>;
|
|
170
|
+
/** JSON request body. Sets `content-type: application/json`. */
|
|
171
|
+
body?: unknown;
|
|
172
|
+
signal?: AbortSignal;
|
|
173
|
+
/** Extra `fetch` init merged last — e.g. Next.js `{ next: { revalidate } }`. */
|
|
174
|
+
fetchInit?: RequestInit;
|
|
175
|
+
}
|
|
176
|
+
/** Drain an async generator into an array. */
|
|
177
|
+
declare const collect: <T>(source: AsyncIterable<T>) => Promise<T[]>;
|
|
178
|
+
/**
|
|
179
|
+
* Typed client for the Luma (lu.ma) public API.
|
|
180
|
+
*
|
|
181
|
+
* The typed resource methods cover the endpoints this client is built around;
|
|
182
|
+
* for anything not modelled here, {@link LumaClient.request} and
|
|
183
|
+
* {@link LumaClient.paginate} are public escape hatches that work against any
|
|
184
|
+
* endpoint.
|
|
185
|
+
*/
|
|
186
|
+
declare class LumaClient {
|
|
187
|
+
readonly baseUrl: string;
|
|
188
|
+
private readonly apiKey;
|
|
189
|
+
private readonly fetchImpl;
|
|
190
|
+
constructor(options: LumaClientOptions);
|
|
191
|
+
/**
|
|
192
|
+
* Construct a client from environment variables. Reads `LUMA_API_KEY` and,
|
|
193
|
+
* optionally, `LUMA_API_BASE_URL`.
|
|
194
|
+
*/
|
|
195
|
+
static fromEnv(env?: Record<string, string | undefined>): LumaClient;
|
|
196
|
+
/**
|
|
197
|
+
* Low-level request against any Luma endpoint. `path` is taken relative to
|
|
198
|
+
* the base URL, e.g. `/v1/event/get`.
|
|
199
|
+
*/
|
|
200
|
+
request<T>(path: string, init?: LumaRequestInit): Promise<T>;
|
|
201
|
+
/**
|
|
202
|
+
* Iterate a cursor-paginated endpoint, yielding each entry. Follows
|
|
203
|
+
* `next_cursor` while `has_more` is true.
|
|
204
|
+
*/
|
|
205
|
+
paginate<T>(path: string, query?: Record<string, unknown>): AsyncGenerator<T>;
|
|
206
|
+
readonly calendar: {
|
|
207
|
+
/** Iterate every event on a calendar. */
|
|
208
|
+
listEvents: (options: ListCalendarEventsOptions) => AsyncGenerator<LumaCalendarEntry, any, any>;
|
|
209
|
+
/** Collect every event on a calendar into an array. */
|
|
210
|
+
listAllEvents: (options: ListCalendarEventsOptions) => Promise<LumaCalendarEntry[]>;
|
|
211
|
+
/** Iterate every person who has interacted with a calendar. */
|
|
212
|
+
listPeople: (options: ListCalendarPeopleOptions) => AsyncGenerator<LumaPerson, any, any>;
|
|
213
|
+
/** Collect every person on a calendar into an array. */
|
|
214
|
+
listAllPeople: (options: ListCalendarPeopleOptions) => Promise<LumaPerson[]>;
|
|
215
|
+
/** Iterate every coupon on the calendar tied to the API key. */
|
|
216
|
+
listCoupons: () => AsyncGenerator<LumaCoupon, any, any>;
|
|
217
|
+
/** Find a calendar coupon by its code, or `null` if none matches. */
|
|
218
|
+
findCouponByCode: (code: string) => Promise<LumaCoupon | null>;
|
|
219
|
+
/** Create a calendar coupon. */
|
|
220
|
+
createCoupon: (input: CreateCalendarCouponInput) => Promise<LumaCoupon>;
|
|
221
|
+
};
|
|
222
|
+
readonly events: {
|
|
223
|
+
/** Fetch a single event by its `api_id`. */
|
|
224
|
+
get: (eventApiId: string) => Promise<LumaEvent>;
|
|
225
|
+
/** Iterate every guest of an event. */
|
|
226
|
+
listGuests: (options: ListEventGuestsOptions) => AsyncGenerator<LumaGuest, any, any>;
|
|
227
|
+
/** Collect every guest of an event into an array. */
|
|
228
|
+
listAllGuests: (options: ListEventGuestsOptions) => Promise<LumaGuest[]>;
|
|
229
|
+
/** Fetch a single guest, by guest `api_id` or by registration email. */
|
|
230
|
+
getGuest: (options: GetEventGuestOptions) => Promise<LumaGuest>;
|
|
231
|
+
/** Update a guest's approval status (approve, decline, waitlist…). */
|
|
232
|
+
updateGuestStatus: (options: UpdateGuestStatusOptions) => Promise<void>;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Base URL for the Luma public API. */
|
|
237
|
+
declare const LUMA_API_BASE_URL = "https://public-api.luma.com";
|
|
238
|
+
/** Header Luma expects the API key in. */
|
|
239
|
+
declare const LUMA_API_KEY_HEADER = "x-luma-api-key";
|
|
240
|
+
/** API version path segment prepended to every endpoint. */
|
|
241
|
+
declare const LUMA_API_VERSION = "v1";
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Thrown when the Luma API responds with a non-2xx status, or when a response
|
|
245
|
+
* body cannot be parsed as JSON.
|
|
246
|
+
*/
|
|
247
|
+
declare class LumaApiError extends Error {
|
|
248
|
+
/** HTTP status code returned by Luma (0 if the request never completed). */
|
|
249
|
+
readonly status: number;
|
|
250
|
+
/** Raw, untruncated response body. */
|
|
251
|
+
readonly body: string;
|
|
252
|
+
/** API path that produced the error, e.g. `/v1/event/get-guests`. */
|
|
253
|
+
readonly path: string;
|
|
254
|
+
constructor(params: {
|
|
255
|
+
message: string;
|
|
256
|
+
status: number;
|
|
257
|
+
body: string;
|
|
258
|
+
path: string;
|
|
259
|
+
});
|
|
260
|
+
/** True for 401/403 — the API key is missing, invalid, or lacks scope. */
|
|
261
|
+
get isAuthError(): boolean;
|
|
262
|
+
/** True for 429 — the caller is being rate limited. */
|
|
263
|
+
get isRateLimited(): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* True when Luma rejected a coupon create because the code already exists.
|
|
266
|
+
* Luma surfaces this as a 400/409 whose body mentions the code already
|
|
267
|
+
* existing; matched permissively so callers can treat it as idempotent.
|
|
268
|
+
*/
|
|
269
|
+
get isDuplicateCouponCode(): boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { type CreateCalendarCouponInput, type GetEventGuestOptions, LUMA_API_BASE_URL, LUMA_API_KEY_HEADER, LUMA_API_VERSION, type ListCalendarEventsOptions, type ListCalendarPeopleOptions, type ListEventGuestsOptions, LumaApiError, type LumaCalendarEntry, LumaClient, type LumaClientOptions, type LumaCoupon, type LumaCouponEntry, type LumaEvent, type LumaEventLocation, type LumaEventTag, type LumaGuest, type LumaGuestApprovalStatus, type LumaGuestEntry, type LumaPaginatedResponse, type LumaPaginationOptions, type LumaPerson, type LumaRequestInit, type UpdateGuestStatusOptions, collect };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var LUMA_API_BASE_URL = "https://public-api.luma.com";
|
|
3
|
+
var LUMA_API_KEY_HEADER = "x-luma-api-key";
|
|
4
|
+
var LUMA_API_VERSION = "v1";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var LumaApiError = class extends Error {
|
|
8
|
+
/** HTTP status code returned by Luma (0 if the request never completed). */
|
|
9
|
+
status;
|
|
10
|
+
/** Raw, untruncated response body. */
|
|
11
|
+
body;
|
|
12
|
+
/** API path that produced the error, e.g. `/v1/event/get-guests`. */
|
|
13
|
+
path;
|
|
14
|
+
constructor(params) {
|
|
15
|
+
super(params.message);
|
|
16
|
+
this.name = "LumaApiError";
|
|
17
|
+
this.status = params.status;
|
|
18
|
+
this.body = params.body;
|
|
19
|
+
this.path = params.path;
|
|
20
|
+
}
|
|
21
|
+
/** True for 401/403 — the API key is missing, invalid, or lacks scope. */
|
|
22
|
+
get isAuthError() {
|
|
23
|
+
return this.status === 401 || this.status === 403;
|
|
24
|
+
}
|
|
25
|
+
/** True for 429 — the caller is being rate limited. */
|
|
26
|
+
get isRateLimited() {
|
|
27
|
+
return this.status === 429;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when Luma rejected a coupon create because the code already exists.
|
|
31
|
+
* Luma surfaces this as a 400/409 whose body mentions the code already
|
|
32
|
+
* existing; matched permissively so callers can treat it as idempotent.
|
|
33
|
+
*/
|
|
34
|
+
get isDuplicateCouponCode() {
|
|
35
|
+
const lower = this.body.toLowerCase();
|
|
36
|
+
return (this.status === 400 || this.status === 409) && lower.includes("code") && (lower.includes("exist") || lower.includes("already"));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/client.ts
|
|
41
|
+
var serializeQueryValue = (value) => value instanceof Date ? value.toISOString() : String(value);
|
|
42
|
+
var toIso = (value) => value instanceof Date ? value.toISOString() : value;
|
|
43
|
+
var collect = async (source) => {
|
|
44
|
+
const out = [];
|
|
45
|
+
for await (const item of source) {
|
|
46
|
+
out.push(item);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
var LumaClient = class _LumaClient {
|
|
51
|
+
baseUrl;
|
|
52
|
+
apiKey;
|
|
53
|
+
fetchImpl;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
if (!options.apiKey) {
|
|
56
|
+
throw new Error("LumaClient requires an `apiKey`");
|
|
57
|
+
}
|
|
58
|
+
this.apiKey = options.apiKey;
|
|
59
|
+
this.baseUrl = options.baseUrl ?? LUMA_API_BASE_URL;
|
|
60
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
61
|
+
if (typeof fetchImpl !== "function") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"No global `fetch` available; pass one via LumaClientOptions.fetch"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
this.fetchImpl = fetchImpl;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Construct a client from environment variables. Reads `LUMA_API_KEY` and,
|
|
70
|
+
* optionally, `LUMA_API_BASE_URL`.
|
|
71
|
+
*/
|
|
72
|
+
static fromEnv(env = typeof process !== "undefined" ? process.env : {}) {
|
|
73
|
+
const apiKey = env.LUMA_API_KEY;
|
|
74
|
+
if (!apiKey) {
|
|
75
|
+
throw new Error("LUMA_API_KEY is not set");
|
|
76
|
+
}
|
|
77
|
+
return new _LumaClient({ apiKey, baseUrl: env.LUMA_API_BASE_URL });
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Low-level request against any Luma endpoint. `path` is taken relative to
|
|
81
|
+
* the base URL, e.g. `/v1/event/get`.
|
|
82
|
+
*/
|
|
83
|
+
async request(path, init = {}) {
|
|
84
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, this.baseUrl);
|
|
85
|
+
if (init.query) {
|
|
86
|
+
for (const [key, value] of Object.entries(init.query)) {
|
|
87
|
+
if (value !== void 0 && value !== null) {
|
|
88
|
+
url.searchParams.set(key, serializeQueryValue(value));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const headers = {
|
|
93
|
+
[LUMA_API_KEY_HEADER]: this.apiKey,
|
|
94
|
+
accept: "application/json"
|
|
95
|
+
};
|
|
96
|
+
let body;
|
|
97
|
+
if (init.body !== void 0) {
|
|
98
|
+
headers["content-type"] = "application/json";
|
|
99
|
+
body = JSON.stringify(init.body);
|
|
100
|
+
}
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await this.fetchImpl(url, {
|
|
104
|
+
method: init.method ?? "GET",
|
|
105
|
+
headers,
|
|
106
|
+
body,
|
|
107
|
+
signal: init.signal,
|
|
108
|
+
...init.fetchInit
|
|
109
|
+
});
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
throw new LumaApiError({
|
|
112
|
+
message: `Luma API request to ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
113
|
+
status: 0,
|
|
114
|
+
body: "",
|
|
115
|
+
path
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new LumaApiError({
|
|
121
|
+
message: `Luma API responded ${response.status} for ${path}`,
|
|
122
|
+
status: response.status,
|
|
123
|
+
body: text,
|
|
124
|
+
path
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (text.length === 0) {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new LumaApiError({
|
|
134
|
+
message: `Luma API returned a non-JSON body for ${path}`,
|
|
135
|
+
status: response.status,
|
|
136
|
+
body: text,
|
|
137
|
+
path
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Iterate a cursor-paginated endpoint, yielding each entry. Follows
|
|
143
|
+
* `next_cursor` while `has_more` is true.
|
|
144
|
+
*/
|
|
145
|
+
async *paginate(path, query = {}) {
|
|
146
|
+
let cursor;
|
|
147
|
+
for (let page = 0; page < 1e4; page += 1) {
|
|
148
|
+
const pageQuery = { ...query };
|
|
149
|
+
if (cursor) {
|
|
150
|
+
pageQuery.pagination_cursor = cursor;
|
|
151
|
+
}
|
|
152
|
+
const result = await this.request(path, {
|
|
153
|
+
query: pageQuery
|
|
154
|
+
});
|
|
155
|
+
for (const entry of result.entries ?? []) {
|
|
156
|
+
yield entry;
|
|
157
|
+
}
|
|
158
|
+
if (!result.has_more || !result.next_cursor) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
cursor = result.next_cursor;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ─── calendar ────────────────────────────────────────────────────────
|
|
165
|
+
calendar = {
|
|
166
|
+
/** Iterate every event on a calendar. */
|
|
167
|
+
listEvents: (options) => this.paginate("/v1/calendar/list-events", {
|
|
168
|
+
calendar_api_id: options.calendarApiId,
|
|
169
|
+
after: options.after,
|
|
170
|
+
before: options.before,
|
|
171
|
+
pagination_limit: options.paginationLimit,
|
|
172
|
+
pagination_cursor: options.paginationCursor
|
|
173
|
+
}),
|
|
174
|
+
/** Collect every event on a calendar into an array. */
|
|
175
|
+
listAllEvents: (options) => collect(this.calendar.listEvents(options)),
|
|
176
|
+
/** Iterate every person who has interacted with a calendar. */
|
|
177
|
+
listPeople: (options) => this.paginate("/v1/calendar/list-people", {
|
|
178
|
+
calendar_api_id: options.calendarApiId,
|
|
179
|
+
pagination_limit: options.paginationLimit,
|
|
180
|
+
pagination_cursor: options.paginationCursor
|
|
181
|
+
}),
|
|
182
|
+
/** Collect every person on a calendar into an array. */
|
|
183
|
+
listAllPeople: (options) => collect(this.calendar.listPeople(options)),
|
|
184
|
+
/** Iterate every coupon on the calendar tied to the API key. */
|
|
185
|
+
listCoupons: async function* () {
|
|
186
|
+
for await (const entry of this.paginate(
|
|
187
|
+
"/v1/calendar/coupons"
|
|
188
|
+
)) {
|
|
189
|
+
yield normalizeCoupon(entry);
|
|
190
|
+
}
|
|
191
|
+
}.bind(this),
|
|
192
|
+
/** Find a calendar coupon by its code, or `null` if none matches. */
|
|
193
|
+
findCouponByCode: async (code) => {
|
|
194
|
+
const target = code.toLowerCase();
|
|
195
|
+
for await (const coupon of this.calendar.listCoupons()) {
|
|
196
|
+
if (coupon.code.toLowerCase() === target) {
|
|
197
|
+
return coupon;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
},
|
|
202
|
+
/** Create a calendar coupon. */
|
|
203
|
+
createCoupon: async (input) => {
|
|
204
|
+
const discount = input.discount.type === "percent" ? {
|
|
205
|
+
discount_type: "percent",
|
|
206
|
+
percent_off: input.discount.percentOff
|
|
207
|
+
} : {
|
|
208
|
+
discount_type: "amount",
|
|
209
|
+
cents_off: input.discount.centsOff,
|
|
210
|
+
currency: input.discount.currency.toLowerCase()
|
|
211
|
+
};
|
|
212
|
+
const response = await this.request(
|
|
213
|
+
"/v1/calendar/coupons/create",
|
|
214
|
+
{
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: {
|
|
217
|
+
code: input.code,
|
|
218
|
+
remaining_count: input.remainingCount,
|
|
219
|
+
valid_start_at: input.validStartAt ? toIso(input.validStartAt) : void 0,
|
|
220
|
+
valid_end_at: input.validEndAt ? toIso(input.validEndAt) : void 0,
|
|
221
|
+
discount
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
return normalizeCoupon(response.coupon ?? {}, input.code);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// ─── events ──────────────────────────────────────────────────────────
|
|
229
|
+
events = {
|
|
230
|
+
/** Fetch a single event by its `api_id`. */
|
|
231
|
+
get: async (eventApiId) => {
|
|
232
|
+
const response = await this.request(
|
|
233
|
+
"/v1/event/get",
|
|
234
|
+
{ query: { api_id: eventApiId } }
|
|
235
|
+
);
|
|
236
|
+
return response.event ?? response;
|
|
237
|
+
},
|
|
238
|
+
/** Iterate every guest of an event. */
|
|
239
|
+
listGuests: (options) => async function* () {
|
|
240
|
+
for await (const entry of this.paginate(
|
|
241
|
+
"/v1/event/get-guests",
|
|
242
|
+
{
|
|
243
|
+
event_api_id: options.eventApiId,
|
|
244
|
+
approval_status: options.approvalStatus,
|
|
245
|
+
pagination_limit: options.paginationLimit,
|
|
246
|
+
pagination_cursor: options.paginationCursor
|
|
247
|
+
}
|
|
248
|
+
)) {
|
|
249
|
+
const guest = entry.guest ?? entry;
|
|
250
|
+
yield guest;
|
|
251
|
+
}
|
|
252
|
+
}.call(this),
|
|
253
|
+
/** Collect every guest of an event into an array. */
|
|
254
|
+
listAllGuests: (options) => collect(this.events.listGuests(options)),
|
|
255
|
+
/** Fetch a single guest, by guest `api_id` or by registration email. */
|
|
256
|
+
getGuest: async (options) => {
|
|
257
|
+
if (!options.guestApiId && !options.email) {
|
|
258
|
+
throw new Error("getGuest requires either `guestApiId` or `email`");
|
|
259
|
+
}
|
|
260
|
+
const response = await this.request(
|
|
261
|
+
"/v1/event/get-guest",
|
|
262
|
+
{
|
|
263
|
+
query: {
|
|
264
|
+
event_api_id: options.eventApiId,
|
|
265
|
+
api_id: options.guestApiId,
|
|
266
|
+
email: options.email
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
return response.guest ?? response;
|
|
271
|
+
},
|
|
272
|
+
/** Update a guest's approval status (approve, decline, waitlist…). */
|
|
273
|
+
updateGuestStatus: async (options) => {
|
|
274
|
+
await this.request("/v1/event/update-guest-status", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: {
|
|
277
|
+
event_api_id: options.eventApiId,
|
|
278
|
+
guest_api_id: options.guestApiId,
|
|
279
|
+
status: options.status
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
var normalizeCoupon = (entry, fallbackCode) => ({
|
|
286
|
+
...entry,
|
|
287
|
+
api_id: entry.api_id ?? entry.id ?? "",
|
|
288
|
+
code: entry.code ?? fallbackCode ?? ""
|
|
289
|
+
});
|
|
290
|
+
export {
|
|
291
|
+
LUMA_API_BASE_URL,
|
|
292
|
+
LUMA_API_KEY_HEADER,
|
|
293
|
+
LUMA_API_VERSION,
|
|
294
|
+
LumaApiError,
|
|
295
|
+
LumaClient,
|
|
296
|
+
collect
|
|
297
|
+
};
|
|
298
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/** Base URL for the Luma public API. */\nexport const LUMA_API_BASE_URL = \"https://public-api.luma.com\";\n\n/** Header Luma expects the API key in. */\nexport const LUMA_API_KEY_HEADER = \"x-luma-api-key\";\n\n/** API version path segment prepended to every endpoint. */\nexport const LUMA_API_VERSION = \"v1\";\n","/**\n * Thrown when the Luma API responds with a non-2xx status, or when a response\n * body cannot be parsed as JSON.\n */\nexport class LumaApiError extends Error {\n\t/** HTTP status code returned by Luma (0 if the request never completed). */\n\treadonly status: number;\n\t/** Raw, untruncated response body. */\n\treadonly body: string;\n\t/** API path that produced the error, e.g. `/v1/event/get-guests`. */\n\treadonly path: string;\n\n\tconstructor(params: {\n\t\tmessage: string;\n\t\tstatus: number;\n\t\tbody: string;\n\t\tpath: string;\n\t}) {\n\t\tsuper(params.message);\n\t\tthis.name = \"LumaApiError\";\n\t\tthis.status = params.status;\n\t\tthis.body = params.body;\n\t\tthis.path = params.path;\n\t}\n\n\t/** True for 401/403 — the API key is missing, invalid, or lacks scope. */\n\tget isAuthError(): boolean {\n\t\treturn this.status === 401 || this.status === 403;\n\t}\n\n\t/** True for 429 — the caller is being rate limited. */\n\tget isRateLimited(): boolean {\n\t\treturn this.status === 429;\n\t}\n\n\t/**\n\t * True when Luma rejected a coupon create because the code already exists.\n\t * Luma surfaces this as a 400/409 whose body mentions the code already\n\t * existing; matched permissively so callers can treat it as idempotent.\n\t */\n\tget isDuplicateCouponCode(): boolean {\n\t\tconst lower = this.body.toLowerCase();\n\t\treturn (\n\t\t\t(this.status === 400 || this.status === 409) &&\n\t\t\tlower.includes(\"code\") &&\n\t\t\t(lower.includes(\"exist\") || lower.includes(\"already\"))\n\t\t);\n\t}\n}\n","import { LUMA_API_BASE_URL, LUMA_API_KEY_HEADER } from \"./constants\";\nimport { LumaApiError } from \"./errors\";\nimport type {\n\tCreateCalendarCouponInput,\n\tGetEventGuestOptions,\n\tListCalendarEventsOptions,\n\tListCalendarPeopleOptions,\n\tListEventGuestsOptions,\n\tLumaCalendarEntry,\n\tLumaCoupon,\n\tLumaCouponEntry,\n\tLumaEvent,\n\tLumaGuest,\n\tLumaGuestEntry,\n\tLumaPaginatedResponse,\n\tLumaPerson,\n\tUpdateGuestStatusOptions,\n} from \"./types\";\n\nexport interface LumaClientOptions {\n\t/** Luma API key. Found under Calendar Settings → API on a Luma Plus plan. */\n\tapiKey: string;\n\t/** Override the API base URL. Defaults to {@link LUMA_API_BASE_URL}. */\n\tbaseUrl?: string;\n\t/**\n\t * Fetch implementation to use. Defaults to the global `fetch`. Pass a\n\t * custom one to inject Next.js cache hints, retries, or a test double.\n\t */\n\tfetch?: typeof fetch;\n}\n\n/** Per-request options for {@link LumaClient.request}. */\nexport interface LumaRequestInit {\n\tmethod?: string;\n\t/** Query parameters. `undefined`/`null` values are dropped; `Date`s are\n\t * serialised as ISO strings. */\n\tquery?: Record<string, unknown>;\n\t/** JSON request body. Sets `content-type: application/json`. */\n\tbody?: unknown;\n\tsignal?: AbortSignal;\n\t/** Extra `fetch` init merged last — e.g. Next.js `{ next: { revalidate } }`. */\n\tfetchInit?: RequestInit;\n}\n\nconst serializeQueryValue = (value: unknown): string =>\n\tvalue instanceof Date ? value.toISOString() : String(value);\n\nconst toIso = (value: Date | string): string =>\n\tvalue instanceof Date ? value.toISOString() : value;\n\n/** Drain an async generator into an array. */\nexport const collect = async <T>(source: AsyncIterable<T>): Promise<T[]> => {\n\tconst out: T[] = [];\n\tfor await (const item of source) {\n\t\tout.push(item);\n\t}\n\treturn out;\n};\n\n/**\n * Typed client for the Luma (lu.ma) public API.\n *\n * The typed resource methods cover the endpoints this client is built around;\n * for anything not modelled here, {@link LumaClient.request} and\n * {@link LumaClient.paginate} are public escape hatches that work against any\n * endpoint.\n */\nexport class LumaClient {\n\treadonly baseUrl: string;\n\tprivate readonly apiKey: string;\n\tprivate readonly fetchImpl: typeof fetch;\n\n\tconstructor(options: LumaClientOptions) {\n\t\tif (!options.apiKey) {\n\t\t\tthrow new Error(\"LumaClient requires an `apiKey`\");\n\t\t}\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.baseUrl = options.baseUrl ?? LUMA_API_BASE_URL;\n\t\tconst fetchImpl = options.fetch ?? globalThis.fetch;\n\t\tif (typeof fetchImpl !== \"function\") {\n\t\t\tthrow new Error(\n\t\t\t\t\"No global `fetch` available; pass one via LumaClientOptions.fetch\",\n\t\t\t);\n\t\t}\n\t\tthis.fetchImpl = fetchImpl;\n\t}\n\n\t/**\n\t * Construct a client from environment variables. Reads `LUMA_API_KEY` and,\n\t * optionally, `LUMA_API_BASE_URL`.\n\t */\n\tstatic fromEnv(\n\t\tenv: Record<string, string | undefined> = typeof process !== \"undefined\"\n\t\t\t? process.env\n\t\t\t: {},\n\t): LumaClient {\n\t\tconst apiKey = env.LUMA_API_KEY;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"LUMA_API_KEY is not set\");\n\t\t}\n\t\treturn new LumaClient({ apiKey, baseUrl: env.LUMA_API_BASE_URL });\n\t}\n\n\t/**\n\t * Low-level request against any Luma endpoint. `path` is taken relative to\n\t * the base URL, e.g. `/v1/event/get`.\n\t */\n\tasync request<T>(path: string, init: LumaRequestInit = {}): Promise<T> {\n\t\tconst url = new URL(path.startsWith(\"/\") ? path : `/${path}`, this.baseUrl);\n\t\tif (init.query) {\n\t\t\tfor (const [key, value] of Object.entries(init.query)) {\n\t\t\t\tif (value !== undefined && value !== null) {\n\t\t\t\t\turl.searchParams.set(key, serializeQueryValue(value));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t[LUMA_API_KEY_HEADER]: this.apiKey,\n\t\t\taccept: \"application/json\",\n\t\t};\n\t\tlet body: string | undefined;\n\t\tif (init.body !== undefined) {\n\t\t\theaders[\"content-type\"] = \"application/json\";\n\t\t\tbody = JSON.stringify(init.body);\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await this.fetchImpl(url, {\n\t\t\t\tmethod: init.method ?? \"GET\",\n\t\t\t\theaders,\n\t\t\t\tbody,\n\t\t\t\tsignal: init.signal,\n\t\t\t\t...init.fetchInit,\n\t\t\t});\n\t\t} catch (cause) {\n\t\t\tthrow new LumaApiError({\n\t\t\t\tmessage: `Luma API request to ${path} failed: ${\n\t\t\t\t\tcause instanceof Error ? cause.message : String(cause)\n\t\t\t\t}`,\n\t\t\t\tstatus: 0,\n\t\t\t\tbody: \"\",\n\t\t\t\tpath,\n\t\t\t});\n\t\t}\n\n\t\tconst text = await response.text();\n\t\tif (!response.ok) {\n\t\t\tthrow new LumaApiError({\n\t\t\t\tmessage: `Luma API responded ${response.status} for ${path}`,\n\t\t\t\tstatus: response.status,\n\t\t\t\tbody: text,\n\t\t\t\tpath,\n\t\t\t});\n\t\t}\n\t\tif (text.length === 0) {\n\t\t\treturn undefined as T;\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.parse(text) as T;\n\t\t} catch {\n\t\t\tthrow new LumaApiError({\n\t\t\t\tmessage: `Luma API returned a non-JSON body for ${path}`,\n\t\t\t\tstatus: response.status,\n\t\t\t\tbody: text,\n\t\t\t\tpath,\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Iterate a cursor-paginated endpoint, yielding each entry. Follows\n\t * `next_cursor` while `has_more` is true.\n\t */\n\tasync *paginate<T>(\n\t\tpath: string,\n\t\tquery: Record<string, unknown> = {},\n\t): AsyncGenerator<T> {\n\t\tlet cursor: string | null | undefined;\n\t\t// Hard stop so a misbehaving endpoint cannot loop forever.\n\t\tfor (let page = 0; page < 10_000; page += 1) {\n\t\t\tconst pageQuery = { ...query };\n\t\t\tif (cursor) {\n\t\t\t\tpageQuery.pagination_cursor = cursor;\n\t\t\t}\n\t\t\tconst result = await this.request<LumaPaginatedResponse<T>>(path, {\n\t\t\t\tquery: pageQuery,\n\t\t\t});\n\t\t\tfor (const entry of result.entries ?? []) {\n\t\t\t\tyield entry;\n\t\t\t}\n\t\t\tif (!result.has_more || !result.next_cursor) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcursor = result.next_cursor;\n\t\t}\n\t}\n\n\t// ─── calendar ────────────────────────────────────────────────────────\n\n\treadonly calendar = {\n\t\t/** Iterate every event on a calendar. */\n\t\tlistEvents: (options: ListCalendarEventsOptions) =>\n\t\t\tthis.paginate<LumaCalendarEntry>(\"/v1/calendar/list-events\", {\n\t\t\t\tcalendar_api_id: options.calendarApiId,\n\t\t\t\tafter: options.after,\n\t\t\t\tbefore: options.before,\n\t\t\t\tpagination_limit: options.paginationLimit,\n\t\t\t\tpagination_cursor: options.paginationCursor,\n\t\t\t}),\n\n\t\t/** Collect every event on a calendar into an array. */\n\t\tlistAllEvents: (options: ListCalendarEventsOptions) =>\n\t\t\tcollect(this.calendar.listEvents(options)),\n\n\t\t/** Iterate every person who has interacted with a calendar. */\n\t\tlistPeople: (options: ListCalendarPeopleOptions) =>\n\t\t\tthis.paginate<LumaPerson>(\"/v1/calendar/list-people\", {\n\t\t\t\tcalendar_api_id: options.calendarApiId,\n\t\t\t\tpagination_limit: options.paginationLimit,\n\t\t\t\tpagination_cursor: options.paginationCursor,\n\t\t\t}),\n\n\t\t/** Collect every person on a calendar into an array. */\n\t\tlistAllPeople: (options: ListCalendarPeopleOptions) =>\n\t\t\tcollect(this.calendar.listPeople(options)),\n\n\t\t/** Iterate every coupon on the calendar tied to the API key. */\n\t\tlistCoupons: async function* (this: LumaClient): AsyncGenerator<LumaCoupon> {\n\t\t\tfor await (const entry of this.paginate<LumaCouponEntry>(\n\t\t\t\t\"/v1/calendar/coupons\",\n\t\t\t)) {\n\t\t\t\tyield normalizeCoupon(entry);\n\t\t\t}\n\t\t}.bind(this),\n\n\t\t/** Find a calendar coupon by its code, or `null` if none matches. */\n\t\tfindCouponByCode: async (code: string): Promise<LumaCoupon | null> => {\n\t\t\tconst target = code.toLowerCase();\n\t\t\tfor await (const coupon of this.calendar.listCoupons()) {\n\t\t\t\tif (coupon.code.toLowerCase() === target) {\n\t\t\t\t\treturn coupon;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\n\t\t/** Create a calendar coupon. */\n\t\tcreateCoupon: async (input: CreateCalendarCouponInput): Promise<LumaCoupon> => {\n\t\t\tconst discount =\n\t\t\t\tinput.discount.type === \"percent\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tdiscount_type: \"percent\" as const,\n\t\t\t\t\t\t\tpercent_off: input.discount.percentOff,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tdiscount_type: \"amount\" as const,\n\t\t\t\t\t\t\tcents_off: input.discount.centsOff,\n\t\t\t\t\t\t\tcurrency: input.discount.currency.toLowerCase(),\n\t\t\t\t\t\t};\n\t\t\tconst response = await this.request<{ coupon?: LumaCouponEntry }>(\n\t\t\t\t\"/v1/calendar/coupons/create\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: {\n\t\t\t\t\t\tcode: input.code,\n\t\t\t\t\t\tremaining_count: input.remainingCount,\n\t\t\t\t\t\tvalid_start_at: input.validStartAt\n\t\t\t\t\t\t\t? toIso(input.validStartAt)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tvalid_end_at: input.validEndAt\n\t\t\t\t\t\t\t? toIso(input.validEndAt)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tdiscount,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn normalizeCoupon(response.coupon ?? {}, input.code);\n\t\t},\n\t};\n\n\t// ─── events ──────────────────────────────────────────────────────────\n\n\treadonly events = {\n\t\t/** Fetch a single event by its `api_id`. */\n\t\tget: async (eventApiId: string): Promise<LumaEvent> => {\n\t\t\tconst response = await this.request<{ event?: LumaEvent } & LumaEvent>(\n\t\t\t\t\"/v1/event/get\",\n\t\t\t\t{ query: { api_id: eventApiId } },\n\t\t\t);\n\t\t\treturn response.event ?? response;\n\t\t},\n\n\t\t/** Iterate every guest of an event. */\n\t\tlistGuests: (options: ListEventGuestsOptions) =>\n\t\t\tasync function* (this: LumaClient): AsyncGenerator<LumaGuest> {\n\t\t\t\tfor await (const entry of this.paginate<LumaGuestEntry>(\n\t\t\t\t\t\"/v1/event/get-guests\",\n\t\t\t\t\t{\n\t\t\t\t\t\tevent_api_id: options.eventApiId,\n\t\t\t\t\t\tapproval_status: options.approvalStatus,\n\t\t\t\t\t\tpagination_limit: options.paginationLimit,\n\t\t\t\t\t\tpagination_cursor: options.paginationCursor,\n\t\t\t\t\t},\n\t\t\t\t)) {\n\t\t\t\t\tconst guest = entry.guest ?? (entry as unknown as LumaGuest);\n\t\t\t\t\tyield guest;\n\t\t\t\t}\n\t\t\t}.call(this),\n\n\t\t/** Collect every guest of an event into an array. */\n\t\tlistAllGuests: (options: ListEventGuestsOptions) =>\n\t\t\tcollect(this.events.listGuests(options)),\n\n\t\t/** Fetch a single guest, by guest `api_id` or by registration email. */\n\t\tgetGuest: async (options: GetEventGuestOptions): Promise<LumaGuest> => {\n\t\t\tif (!options.guestApiId && !options.email) {\n\t\t\t\tthrow new Error(\"getGuest requires either `guestApiId` or `email`\");\n\t\t\t}\n\t\t\tconst response = await this.request<{ guest?: LumaGuest } & LumaGuest>(\n\t\t\t\t\"/v1/event/get-guest\",\n\t\t\t\t{\n\t\t\t\t\tquery: {\n\t\t\t\t\t\tevent_api_id: options.eventApiId,\n\t\t\t\t\t\tapi_id: options.guestApiId,\n\t\t\t\t\t\temail: options.email,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn response.guest ?? response;\n\t\t},\n\n\t\t/** Update a guest's approval status (approve, decline, waitlist…). */\n\t\tupdateGuestStatus: async (options: UpdateGuestStatusOptions): Promise<void> => {\n\t\t\tawait this.request(\"/v1/event/update-guest-status\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: {\n\t\t\t\t\tevent_api_id: options.eventApiId,\n\t\t\t\t\tguest_api_id: options.guestApiId,\n\t\t\t\t\tstatus: options.status,\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t};\n}\n\nconst normalizeCoupon = (\n\tentry: LumaCouponEntry,\n\tfallbackCode?: string,\n): LumaCoupon => ({\n\t...entry,\n\tapi_id: entry.api_id ?? entry.id ?? \"\",\n\tcode: entry.code ?? fallbackCode ?? \"\",\n});\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingram-tech/luma",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript client for the Luma (lu.ma) public API.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ingram Technologies",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"luma",
|
|
9
|
+
"lu.ma",
|
|
10
|
+
"events",
|
|
11
|
+
"api",
|
|
12
|
+
"client",
|
|
13
|
+
"sdk"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ingram-technologies/luma.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": "https://github.com/ingram-technologies/luma/issues",
|
|
20
|
+
"homepage": "https://github.com/ingram-technologies/luma#readme",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"prepack": "bun run build",
|
|
39
|
+
"lint": "biome check .",
|
|
40
|
+
"format": "biome format --write .",
|
|
41
|
+
"type-check": "tsc --noEmit",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:run": "vitest --run",
|
|
44
|
+
"ci": "bun run type-check && bun run lint && bun run test:run && bun run build"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@biomejs/biome": "^2.4.13",
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
}
|
|
56
|
+
}
|