@solumflow-app/crm-client 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.
@@ -0,0 +1,265 @@
1
+ /** What a key is allowed to reach. A key carries one or more of these. */
2
+ export type ApiScope = 'catalog:read' | 'events:read' | 'orders:write' | 'contacts:write' | 'forms:write';
3
+ export declare const ALL_API_SCOPES: readonly ApiScope[];
4
+ /** Everything an endpoint can be told about. A delivery names one. */
5
+ export type ApiWebhookEvent = 'product.changed' | 'product.deleted' | 'event.changed' | 'order.status_changed';
6
+ export declare const ALL_API_WEBHOOK_EVENTS: readonly ApiWebhookEvent[];
7
+ /** The `error.code` of a refusal. Match on this, never on the message -- the message is written for a person reading a log and may be reworded. */
8
+ export type ApiErrorCode = 'unauthorized' | 'forbidden' | 'feature_unavailable' | 'not_found' | 'invalid_request' | 'idempotency_key_reused' | 'request_in_progress' | 'rate_limited' | 'internal_error';
9
+ export declare const ALL_API_ERROR_CODES: readonly ApiErrorCode[];
10
+ /**
11
+ * One product as a stranger's website sees it.
12
+ *
13
+ * These interfaces are the public API's contract, not internal convenience
14
+ * types. They are served cross-origin to pages nobody here controls, and a
15
+ * customer's shop reads these exact keys — so a field removed or renamed breaks
16
+ * a site that will not be redeployed. Add fields; do not repurpose them.
17
+ *
18
+ * What is deliberately absent is as much the contract as what is here.
19
+ *
20
+ * - **`sku`** is the number this business uses to find the thing in its own
21
+ * stockroom. It says how many suppliers there are, which one a line came
22
+ * from, and often what was paid. `gtin` is the number printed on the box and
23
+ * is offered instead, in the detail: that one is meant to be public, and a
24
+ * shop needs it for a product feed.
25
+ * - **The stock count** never leaves. `inStock` answers the only question a
26
+ * visitor has, and the number itself is a business fact a competitor would
27
+ * like and a cached answer would get wrong within the minute.
28
+ * - **`unit_price_cents` raw** is not it either; see `priceFromCents`.
29
+ * - **Non-public custom fields.** `fields` carries only attributes a member
30
+ * marked `is_public`, which defaults to off. A field called "purchase price"
31
+ * stays home unless somebody says otherwise, once, per field.
32
+ *
33
+ * And one field that an event has and a product does not: **`url`**. An event
34
+ * is sold on a page this system hosts, so its listing can hand out a link. A
35
+ * product is sold on the customer's own site — that is the entire reason this
36
+ * API exists — and this system does not know what they called their product
37
+ * page. A guessed link is worse than none.
38
+ */
39
+ export interface PublicProductListItem {
40
+ id: string;
41
+ /** Always present: a product without an address cannot be published. */
42
+ slug: string;
43
+ name: string;
44
+ productType: 'digital' | 'physical';
45
+ /**
46
+ * The storage path, not a URL. The route turns it into a public URL, because
47
+ * only the route knows the origin its own storage is served from.
48
+ */
49
+ imagePath: string | null;
50
+ /**
51
+ * The cheapest way in, over the active price options — not the default one.
52
+ * A card that says "from €12" next to a product whose entry price is €12 and
53
+ * whose default is the €40 yearly plan is answering the question the visitor
54
+ * actually asked.
55
+ *
56
+ * Never null, unlike the same field on an event. An event without a ticket
57
+ * tier on sale genuinely has no price; a product always has `unit_price_cents`
58
+ * on its own row, so when no separate price option is active that column is
59
+ * the answer — including when it is `0`, which is a real price for something
60
+ * given away.
61
+ */
62
+ priceFromCents: number;
63
+ /** The struck-through price beside it, when the seller set one. */
64
+ compareAtCents: number | null;
65
+ currency: string;
66
+ /**
67
+ * Whether a visitor can buy it right now — never how many are left.
68
+ *
69
+ * True whenever the seller does not track stock at all, which is the common
70
+ * case and the honest answer for anything made to order. When they do track
71
+ * it, this counts what is on the shelf minus what other people already hold
72
+ * in a checkout, and it stays true for a seller who accepts backorders.
73
+ */
74
+ inStock: boolean;
75
+ /**
76
+ * The custom fields this account invented, keyed by the slug they chose, and
77
+ * only the ones marked public. An empty object when none are.
78
+ */
79
+ fields: Record<string, unknown>;
80
+ }
81
+ /**
82
+ * The same product on its own page, where more may be shown.
83
+ *
84
+ * The detail answers for things the listing hides — an unlisted product, one
85
+ * reachable by anyone holding the link — for the same reason the events detail
86
+ * answers for a cancelled evening: somebody was sent here on purpose, and a 404
87
+ * would be a lie. What it still refuses is anything unpublished.
88
+ */
89
+ export interface PublicProductDetail extends PublicProductListItem {
90
+ /** The one-line text that lands on a quote or an invoice row. */
91
+ description: string | null;
92
+ /** The long story, as sanitised HTML. Written by the seller, for this page. */
93
+ longDescription: string | null;
94
+ /** The barcode number: printed on the box, wanted by every product feed. */
95
+ gtin: string | null;
96
+ metaTitle: string | null;
97
+ metaDescription: string | null;
98
+ images: PublicProductImage[];
99
+ prices: PublicProductPrice[];
100
+ categories: PublicProductCategory[];
101
+ /** What the seller linked this to — a companion, a refill, a bigger model. */
102
+ related: PublicProductSummary[];
103
+ }
104
+ export interface PublicProductImage {
105
+ /** A path, turned into a URL by the route. Same reason as `imagePath`. */
106
+ path: string;
107
+ alt: string | null;
108
+ }
109
+ /**
110
+ * One way to buy the thing.
111
+ *
112
+ * `stripe_price_id` and `stripe_account_id` are absent and must stay absent:
113
+ * they name objects in the seller's payment account, and a checkout is started
114
+ * through this system's own order route, never by a stranger quoting an id.
115
+ */
116
+ export interface PublicProductPrice {
117
+ id: string;
118
+ kind: 'one_off' | 'recurring' | 'installment';
119
+ label: string | null;
120
+ amountCents: number;
121
+ /** What the whole thing costs when paid in parts; null for the other kinds. */
122
+ totalCents: number | null;
123
+ installmentCount: number | null;
124
+ recurringInterval: 'day' | 'week' | 'month' | 'year' | null;
125
+ recurringIntervalCount: number | null;
126
+ compareAtCents: number | null;
127
+ trialPeriodDays: number | null;
128
+ isDefault: boolean;
129
+ }
130
+ export interface PublicProductCategory {
131
+ id: string;
132
+ name: string;
133
+ }
134
+ export interface PublicProductSummary {
135
+ id: string;
136
+ slug: string;
137
+ name: string;
138
+ }
139
+ /**
140
+ * One event as a stranger's website sees it.
141
+ *
142
+ * This interface is the public API's contract, not an internal convenience
143
+ * type. It is served cross-origin to pages nobody here controls, and a widget
144
+ * on a customer's homepage reads these exact keys — so a field removed or
145
+ * renamed breaks a site that will not be redeployed. Add fields; do not
146
+ * repurpose them.
147
+ *
148
+ * What is deliberately absent is as much the contract as what is here: no
149
+ * description (a paragraph no card renders), no joining link (that is
150
+ * admission), no counts of who bought what, and nothing an organiser uses to
151
+ * manage the event.
152
+ */
153
+ export interface PublicEventListItem {
154
+ id: string;
155
+ slug: string;
156
+ title: string;
157
+ subtitle: string | null;
158
+ startsAt: string;
159
+ endsAt: string | null;
160
+ timezone: string;
161
+ locationName: string | null;
162
+ /**
163
+ * The storage path, not a URL. The route turns it into a public URL, because
164
+ * only the route knows the origin its own storage is served from.
165
+ */
166
+ coverImagePath: string | null;
167
+ /** Null when nothing is on sale — never 0, which is a real price. */
168
+ priceFromCents: number | null;
169
+ currency: string | null;
170
+ soldOut: boolean;
171
+ /** Null when the event has no ceiling to count down from. */
172
+ seatsLeft: number | null;
173
+ /** Path on the hosted site, so a widget can link straight to checkout. */
174
+ url: string;
175
+ /**
176
+ * The custom fields this account invented, keyed by the slug they chose, and
177
+ * only the ones marked public. An empty object when none are, which is every
178
+ * account until somebody turns one on.
179
+ *
180
+ * Added rather than repurposed, which is the rule this interface states at
181
+ * the top: a widget written before this field existed keeps working, because
182
+ * it simply never reads the key.
183
+ */
184
+ fields: Record<string, unknown>;
185
+ }
186
+ /**
187
+ * One event as the public API hands it out, on its own page.
188
+ *
189
+ * Not the same thing as `PublicEventView`, which feeds the detail page this
190
+ * system hosts — and the difference is the reason this type exists rather than
191
+ * the other one being reused. That view carries `online_url`: the link you join
192
+ * the evening by. On a page we host, behind a ticket, that is correct. Through
193
+ * an API that answers any holder of a read key, it is a free seat.
194
+ *
195
+ * It also carries `account_id`, which is ours and not the caller's business.
196
+ *
197
+ * So this is a separate, narrower projection, and everything in
198
+ * `PublicEventListItem` about adding rather than repurposing fields applies
199
+ * here too.
200
+ */
201
+ export interface PublicEventDetailItem extends PublicEventListItem {
202
+ /** The organiser's own text for the evening. */
203
+ description: string | null;
204
+ /**
205
+ * `on_sale`, `closed` or `cancelled` — never `draft`, which does not answer
206
+ * at all. A ticket holder has to be able to read that the evening is off, so
207
+ * this route replies for the last two where the listing withholds them.
208
+ */
209
+ status: 'on_sale' | 'closed' | 'cancelled';
210
+ doorsOpenAt: string | null;
211
+ salesStartAt: string | null;
212
+ salesEndAt: string | null;
213
+ /** The postal address, as the organiser entered it. */
214
+ address: unknown;
215
+ metaTitle: string | null;
216
+ metaDescription: string | null;
217
+ ticketTypes: PublicEventTicketType[];
218
+ /** Custom fields the account marked public, keyed by their own slug. */
219
+ fields: Record<string, unknown>;
220
+ }
221
+ /**
222
+ * One way in, as a stranger's page may show it.
223
+ *
224
+ * `reserved_count` and `sold_count` are absent: how many were sold is the
225
+ * organiser's figure, and `seatsLeft` already answers the only question a
226
+ * visitor has. The capacity behind it stays home for the same reason a stock
227
+ * count does.
228
+ */
229
+ export interface PublicEventTicketType {
230
+ id: string;
231
+ name: string;
232
+ description: string | null;
233
+ priceCents: number;
234
+ currency: string;
235
+ taxPercentage: number | null;
236
+ minPerOrder: number | null;
237
+ maxPerOrder: number | null;
238
+ salesStartAt: string | null;
239
+ salesEndAt: string | null;
240
+ /** Null when this tier has no ceiling to count down from. */
241
+ seatsLeft: number | null;
242
+ soldOut: boolean;
243
+ onSale: boolean;
244
+ }
245
+ /**
246
+ * The body of one delivery.
247
+ *
248
+ * IDS AND NOTHING ELSE, and the three reasons are worth keeping together. A
249
+ * message that arrives late still leads to the right state, because the
250
+ * receiver fetches the current row rather than trusting a snapshot from twenty
251
+ * minutes ago. A message can never hand out something the receiving key was
252
+ * not allowed to read, because it hands out nothing. And five hundred changes
253
+ * fit in a body of a few kilobytes instead of a few megabytes.
254
+ *
255
+ * `truncated` means the batch stopped collecting at its ceiling and `ids` is
256
+ * therefore incomplete: the receiver should refetch the whole collection. It
257
+ * is always present rather than only when true, so a receiver that reads it
258
+ * cannot mistake "absent" for "false" in the one direction that matters.
259
+ */
260
+ export interface ApiWebhookPayload {
261
+ type: ApiWebhookEvent;
262
+ occurredAt: string;
263
+ ids: string[];
264
+ truncated: boolean;
265
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ALL_API_ERROR_CODES: () => ALL_API_ERROR_CODES,
24
+ ALL_API_SCOPES: () => ALL_API_SCOPES,
25
+ ALL_API_WEBHOOK_EVENTS: () => ALL_API_WEBHOOK_EVENTS,
26
+ CrmApiError: () => CrmApiError,
27
+ createClient: () => createClient,
28
+ eventTag: () => eventTag,
29
+ eventsTag: () => eventsTag,
30
+ productTag: () => productTag,
31
+ productsTag: () => productsTag,
32
+ tagsForDelivery: () => tagsForDelivery
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/errors.ts
37
+ var CrmApiError = class extends Error {
38
+ constructor(input) {
39
+ super(input.message);
40
+ this.name = "CrmApiError";
41
+ this.code = input.code;
42
+ this.status = input.status;
43
+ this.fields = input.fields ?? [];
44
+ this.requestUrl = input.requestUrl;
45
+ }
46
+ /**
47
+ * Whether trying the same request again could plausibly work.
48
+ *
49
+ * A rate limit clears and a server error may be a blip; a rejected key and a
50
+ * malformed body will be refused just as firmly the second time. Write
51
+ * requests should be retried with the *same* idempotency key, which this
52
+ * client fills in for you, so a retry after a timeout cannot become a second
53
+ * order.
54
+ */
55
+ get retryable() {
56
+ return this.code === "rate_limited" || this.code === "internal_error";
57
+ }
58
+ };
59
+
60
+ // src/refusal.ts
61
+ async function readJson(response) {
62
+ try {
63
+ return await response.json();
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function refusalOf(status, body, requestUrl) {
69
+ const structured = body?.error;
70
+ if (structured && typeof structured === "object" && structured.code) {
71
+ return new CrmApiError({
72
+ code: structured.code,
73
+ message: structured.message ?? structured.code,
74
+ status,
75
+ fields: structured.fields ?? [],
76
+ requestUrl
77
+ });
78
+ }
79
+ const plain = body?.error;
80
+ return new CrmApiError({
81
+ code: codeForStatus(status),
82
+ message: typeof plain === "string" ? plain : `The request failed with ${status}.`,
83
+ status,
84
+ requestUrl
85
+ });
86
+ }
87
+ function codeForStatus(status) {
88
+ switch (status) {
89
+ case 400:
90
+ return "invalid_request";
91
+ case 401:
92
+ return "unauthorized";
93
+ case 403:
94
+ return "forbidden";
95
+ case 404:
96
+ return "not_found";
97
+ case 409:
98
+ return "request_in_progress";
99
+ case 429:
100
+ return "rate_limited";
101
+ default:
102
+ return "internal_error";
103
+ }
104
+ }
105
+
106
+ // src/tags.ts
107
+ var PREFIX = "crm";
108
+ function productsTag() {
109
+ return `${PREFIX}:products`;
110
+ }
111
+ function productTag(slugOrId) {
112
+ return `${PREFIX}:product:${slugOrId}`;
113
+ }
114
+ function eventsTag() {
115
+ return `${PREFIX}:events`;
116
+ }
117
+ function eventTag(idOrSlug) {
118
+ return `${PREFIX}:event:${idOrSlug}`;
119
+ }
120
+ function tagsForDelivery(input) {
121
+ const collection = input.type.startsWith("product.") ? productsTag() : input.type.startsWith("event.") ? eventsTag() : null;
122
+ if (collection === null) {
123
+ return [];
124
+ }
125
+ if (input.truncated) {
126
+ return [collection];
127
+ }
128
+ const item = input.type.startsWith("product.") ? productTag : eventTag;
129
+ return [collection, ...input.ids.map((id) => item(id))];
130
+ }
131
+
132
+ // src/client.ts
133
+ var KEY_FORM = /^crm[ps]_[A-Za-z0-9_-]{43}$/;
134
+ var SECRET_PREFIX = "crms_";
135
+ var BASE_PATH = "/api/public/v1";
136
+ var DEFAULT_REVALIDATE = 60;
137
+ function createClient(options) {
138
+ const apiKey = options.apiKey?.trim() ?? "";
139
+ if (!KEY_FORM.test(apiKey)) {
140
+ throw new Error(
141
+ "crm-client: the API key is not in the expected form. It starts with `crmp_` or `crms_` and is 48 characters long \u2014 a truncated paste looks exactly like a revoked key once it reaches the server."
142
+ );
143
+ }
144
+ if (apiKey.startsWith(SECRET_PREFIX) && inSomethingTheUserCanRead()) {
145
+ throw new Error(
146
+ "crm-client: a `crms_` key may not be used in a browser. It can write orders and read contacts, and anything a browser holds is public \u2014 including a service worker or a web worker, which is why this is refused there too. Read from a server component or a route handler, or issue a `crmp_` key for the parts of the catalogue a page reads directly."
147
+ );
148
+ }
149
+ const base = options.baseUrl?.replace(/\/+$/, "") ?? "";
150
+ if (!/^https?:\/\/[^/]+$/.test(base)) {
151
+ throw new Error(
152
+ `crm-client: baseUrl should be an origin and nothing more, such as "https://app.example.com". Received "${options.baseUrl}".`
153
+ );
154
+ }
155
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
156
+ const revalidate = options.revalidate ?? DEFAULT_REVALIDATE;
157
+ async function send(path, init, unwrap, notFoundIsNull) {
158
+ const url = `${base}${BASE_PATH}${path}`;
159
+ const response = await doFetch(url, {
160
+ ...init,
161
+ headers: {
162
+ accept: "application/json",
163
+ authorization: `Bearer ${apiKey}`,
164
+ ...init.headers
165
+ }
166
+ });
167
+ const body = await readJson(response);
168
+ if (!response.ok) {
169
+ const refusal = refusalOf(response.status, body, url);
170
+ if (notFoundIsNull && refusal.code === "not_found") {
171
+ return null;
172
+ }
173
+ throw refusal;
174
+ }
175
+ const answer = unwrap(body);
176
+ if (answer === void 0 || answer === null) {
177
+ throw new CrmApiError({
178
+ code: "internal_error",
179
+ message: "The API answered successfully with a body this client did not recognise. Either something between here and it replaced the answer, or this package is older than the route it called.",
180
+ status: response.status,
181
+ requestUrl: url
182
+ });
183
+ }
184
+ return answer;
185
+ }
186
+ function cached(tags) {
187
+ return { method: "GET", next: { tags, revalidate } };
188
+ }
189
+ const live = { method: "GET", cache: "no-store" };
190
+ function write(body, options2) {
191
+ return {
192
+ method: "POST",
193
+ cache: "no-store",
194
+ headers: {
195
+ "content-type": "application/json",
196
+ "idempotency-key": options2?.idempotencyKey ?? newIdempotencyKey()
197
+ },
198
+ body: JSON.stringify(body)
199
+ };
200
+ }
201
+ return {
202
+ async getProducts(listOptions) {
203
+ const query = new URLSearchParams();
204
+ if (listOptions?.limit !== void 0) {
205
+ query.set("limit", `${listOptions.limit}`);
206
+ }
207
+ if (listOptions?.cursor) {
208
+ query.set("cursor", listOptions.cursor);
209
+ }
210
+ if (listOptions?.category) {
211
+ query.set("category", listOptions.category);
212
+ }
213
+ const result = await send(
214
+ `/products${suffix(query)}`,
215
+ cached([productsTag()]),
216
+ (body) => listOrNothing(body, "data"),
217
+ false
218
+ );
219
+ return result;
220
+ },
221
+ getProduct(slugOrId) {
222
+ return send(
223
+ `/products/${encodeURIComponent(slugOrId)}`,
224
+ cached([productsTag(), productTag(slugOrId)]),
225
+ (body) => dataOf(body),
226
+ true
227
+ );
228
+ },
229
+ getAvailability(slugOrId) {
230
+ return send(
231
+ `/products/${encodeURIComponent(slugOrId)}/availability`,
232
+ live,
233
+ (body) => dataOf(body),
234
+ true
235
+ );
236
+ },
237
+ async getEvents(listOptions) {
238
+ const query = new URLSearchParams();
239
+ if (listOptions?.limit !== void 0) {
240
+ query.set("limit", `${listOptions.limit}`);
241
+ }
242
+ if (listOptions?.cursor) {
243
+ query.set("cursor", listOptions.cursor);
244
+ }
245
+ if (listOptions?.from) {
246
+ query.set("from", listOptions.from);
247
+ }
248
+ if (listOptions?.to) {
249
+ query.set("to", listOptions.to);
250
+ }
251
+ const result = await send(
252
+ `/events${suffix(query)}`,
253
+ cached([eventsTag()]),
254
+ (body) => listOrNothing(body, "events"),
255
+ false
256
+ );
257
+ return result;
258
+ },
259
+ getEvent(idOrSlug) {
260
+ return send(
261
+ `/events/${encodeURIComponent(idOrSlug)}`,
262
+ cached([eventsTag(), eventTag(idOrSlug)]),
263
+ (body) => dataOf(body),
264
+ true
265
+ );
266
+ },
267
+ getEventAvailability(eventId) {
268
+ return send(
269
+ `/events/${encodeURIComponent(eventId)}/availability`,
270
+ live,
271
+ (body) => body,
272
+ true
273
+ );
274
+ },
275
+ async submitOrder(input, writeOptions) {
276
+ const result = await send(
277
+ "/orders",
278
+ write(input, writeOptions),
279
+ (body) => dataOf(body),
280
+ false
281
+ );
282
+ return result;
283
+ },
284
+ async upsertContact(input, writeOptions) {
285
+ const result = await send(
286
+ "/contacts",
287
+ write(input, writeOptions),
288
+ (body) => dataOf(body),
289
+ false
290
+ );
291
+ return result;
292
+ },
293
+ async submitRequest(input, writeOptions) {
294
+ const result = await send(
295
+ "/requests",
296
+ write(input, writeOptions),
297
+ (body) => dataOf(body),
298
+ false
299
+ );
300
+ return result;
301
+ },
302
+ async submitForm(formId, input, writeOptions) {
303
+ const result = await send(
304
+ `/forms/${encodeURIComponent(formId)}/submissions`,
305
+ write(input, writeOptions),
306
+ (body) => dataOf(body),
307
+ false
308
+ );
309
+ return result;
310
+ }
311
+ };
312
+ }
313
+ function suffix(query) {
314
+ const rendered = query.toString();
315
+ return rendered ? `?${rendered}` : "";
316
+ }
317
+ function dataOf(body) {
318
+ return body?.data;
319
+ }
320
+ function listOrNothing(body, key) {
321
+ const listing = body;
322
+ return Array.isArray(listing?.[key]) ? listing : void 0;
323
+ }
324
+ function newIdempotencyKey() {
325
+ const source = globalThis.crypto;
326
+ if (typeof source?.randomUUID === "function") {
327
+ return source.randomUUID();
328
+ }
329
+ if (typeof source?.getRandomValues === "function") {
330
+ const bytes = source.getRandomValues(new Uint8Array(16));
331
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
332
+ }
333
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
334
+ }
335
+ function inSomethingTheUserCanRead() {
336
+ if (typeof window !== "undefined") {
337
+ return true;
338
+ }
339
+ const scope = globalThis;
340
+ return typeof scope.importScripts === "function";
341
+ }
342
+
343
+ // src/generated/api-types.ts
344
+ var ALL_API_SCOPES = [
345
+ "catalog:read",
346
+ "events:read",
347
+ "orders:write",
348
+ "contacts:write",
349
+ "forms:write"
350
+ ];
351
+ var ALL_API_WEBHOOK_EVENTS = [
352
+ "product.changed",
353
+ "product.deleted",
354
+ "event.changed",
355
+ "order.status_changed"
356
+ ];
357
+ var ALL_API_ERROR_CODES = [
358
+ "unauthorized",
359
+ "forbidden",
360
+ "feature_unavailable",
361
+ "not_found",
362
+ "invalid_request",
363
+ "idempotency_key_reused",
364
+ "request_in_progress",
365
+ "rate_limited",
366
+ "internal_error"
367
+ ];
368
+ // Annotate the CommonJS export names for ESM import in node:
369
+ 0 && (module.exports = {
370
+ ALL_API_ERROR_CODES,
371
+ ALL_API_SCOPES,
372
+ ALL_API_WEBHOOK_EVENTS,
373
+ CrmApiError,
374
+ createClient,
375
+ eventTag,
376
+ eventsTag,
377
+ productTag,
378
+ productsTag,
379
+ tagsForDelivery
380
+ });
381
+ //# sourceMappingURL=index.cjs.map