@seatlayer/js 0.48.1 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,9 @@ Svelte → `onMount` / `onDestroy`. Angular → `ngAfterViewInit` / `ngOnDestroy
57
57
  `SeatManager` gives first-party dashboards and external platforms the same
58
58
  realtime organizer cockpit. Mint a short-lived, event-scoped `mse_` token from
59
59
  your backend, bound to the browser's exact origin and only the capabilities it
60
- needs. The shared surface owns Monitor, Inspect, Block/unblock, fullscreen,
60
+ needs. `SeatManager.token` accepts only that browser grant; passing a tenant
61
+ `sk_` secret to browser code is unsupported. Keep the grant in memory—never in
62
+ a URL, browser storage, or logs. The shared surface owns Monitor, Inspect, Block/unblock, fullscreen,
61
63
  presence, exact configured booked value, booking velocity, and a clearly explained
62
64
  **Booking momentum** overlay.
63
65
  Block mode exposes explicit multi-select category state and a searchable,
@@ -65,7 +67,7 @@ section-filtered blocked-inventory list, so an organizer can put specific seats
65
67
  back on sale without resetting the entire event.
66
68
 
67
69
  ```js
68
- import { SeatManager } from '@seatlayer/js';
70
+ import { SeatManager } from '@seatlayer/js/manager';
69
71
 
70
72
  const manager = new SeatManager({
71
73
  container: '#control-room',
@@ -78,6 +80,16 @@ const manager = new SeatManager({
78
80
  await manager.render();
79
81
  ```
80
82
 
83
+ The package root is the buyer runtime entry. Organizer JavaScript values—
84
+ `SeatManager`, `ChannelsMode`, channel planning helpers, and `ManageApi`—come
85
+ from `@seatlayer/js/manager`, keeping buyer imports free of control-room code.
86
+ Type-only organizer imports from the root remain supported for TypeScript
87
+ source compatibility, but new code should use the manager subpath consistently.
88
+
89
+ `SeatManager` does not expose a browser-side box-office booking method. Create
90
+ managed sales from trusted server code through the inventory booking API; the
91
+ cockpit's public methods are limited to operations it can actually complete.
92
+
81
93
  Switching tools and rotating tokens happen in place: the renderer, camera,
82
94
  selection, WebSocket, and DOM are not remounted. Give the container a height;
83
95
  the cockpit responds to its container rather than the browser viewport.
@@ -95,6 +107,8 @@ const detail = await inventory.retrieveBooking('ev_9f3a', page.bookings[0].booki
95
107
 
96
108
  These records contain labels, lifecycle activity and configured-value snapshots.
97
109
  They do not contain buyer, payment, ticket, email or refund data.
110
+ `ManageApi` retains `sk_` compatibility only for trusted server runtimes; browser
111
+ callers must use an event-scoped `mse_` grant.
98
112
 
99
113
  ## Embed the chart Designer
100
114
 
@@ -2,12 +2,11 @@ import {
2
2
  CHANNELS_CSS,
3
3
  ChannelsMode,
4
4
  bucketRowsHtml
5
- } from "./chunk-KNMZQZXR.js";
6
- import "./chunk-H4OJF6LE.js";
5
+ } from "./chunk-5URCWBHM.js";
7
6
  import "./chunk-HMLY7DHA.js";
8
7
  export {
9
8
  CHANNELS_CSS,
10
9
  ChannelsMode,
11
10
  bucketRowsHtml
12
11
  };
13
- //# sourceMappingURL=channelsMode-NCH5GYHN.js.map
12
+ //# sourceMappingURL=channelsMode-S4GEWCWS.js.map
@@ -1,7 +1,3 @@
1
- import {
2
- ManageApiError
3
- } from "./chunk-H4OJF6LE.js";
4
-
5
1
  // src/channelPlan.ts
6
2
  var PUBLIC_CHANNEL_ID = "public";
7
3
  var PUBLIC_CHANNEL_NAME = "Public sale";
@@ -355,6 +351,493 @@ function stateBadge(state) {
355
351
  return state === "builtin" ? "Built-in" : state === "active" ? "Active" : state === "paused" ? "Paused" : "Archived";
356
352
  }
357
353
 
354
+ // src/manageApi.ts
355
+ var ManageApiError = class extends Error {
356
+ constructor(status, message, code, conflicts, details, serverMessage) {
357
+ super(message);
358
+ this.name = "ManageApiError";
359
+ this.status = status;
360
+ this.code = code;
361
+ this.conflicts = conflicts;
362
+ this.details = details;
363
+ this.serverMessage = serverMessage;
364
+ }
365
+ };
366
+ async function parse(res) {
367
+ const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
368
+ const data = isJson ? await res.json().catch(() => null) : null;
369
+ if (!res.ok) {
370
+ const err = data;
371
+ throw new ManageApiError(
372
+ res.status,
373
+ err?.error ?? `request_failed_${res.status}`,
374
+ err?.code,
375
+ err?.conflicts,
376
+ err?.details,
377
+ typeof err?.message === "string" ? err.message : void 0
378
+ );
379
+ }
380
+ return data;
381
+ }
382
+ function record(value) {
383
+ return value && typeof value === "object" ? value : {};
384
+ }
385
+ function finite(primary, legacy, fallback = 0) {
386
+ if (typeof primary === "number" && Number.isFinite(primary)) return primary;
387
+ if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
388
+ return fallback;
389
+ }
390
+ function nullableFinite(primary, legacy) {
391
+ if (primary === null) return null;
392
+ if (typeof primary === "number" && Number.isFinite(primary)) return primary;
393
+ if (primary !== void 0) return null;
394
+ if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
395
+ return null;
396
+ }
397
+ function normalizeSection(value) {
398
+ const row = record(value);
399
+ const bookedValue = finite(row.bookedValue, row.bookedRevenue);
400
+ return { ...row, bookedValue, bookedRevenue: bookedValue };
401
+ }
402
+ function normalizeReportResult(value) {
403
+ const source = record(value);
404
+ const report = record(source.report);
405
+ const byCategory = Array.isArray(report.byCategory) ? report.byCategory.map((value2) => {
406
+ const row = record(value2);
407
+ const bookedValue = finite(row.bookedValue, row.bookedRevenue);
408
+ return { ...row, bookedValue, bookedRevenue: bookedValue };
409
+ }) : [];
410
+ const bySection = Array.isArray(report.bySection) ? report.bySection.map(normalizeSection) : void 0;
411
+ return {
412
+ ...source,
413
+ report: { ...report, byCategory, ...bySection ? { bySection } : {} }
414
+ };
415
+ }
416
+ function normalizeControlRoomSnapshot(value) {
417
+ const source = record(value);
418
+ const canonical = record(source.bookedValue);
419
+ const legacy = record(source.revenue);
420
+ const selected = Object.keys(canonical).length ? canonical : legacy;
421
+ const bySectionSource = Array.isArray(canonical.bySection) ? canonical.bySection : Array.isArray(legacy.bySection) ? legacy.bySection : [];
422
+ const bookedValue = {
423
+ ...selected,
424
+ gross: finite(canonical.gross, legacy.gross),
425
+ bySection: bySectionSource.map(normalizeSection)
426
+ };
427
+ const velocity = record(source.velocity);
428
+ const velocityRows = Array.isArray(velocity.bySection) ? velocity.bySection.map((value2) => {
429
+ const row = record(value2);
430
+ const rowValue = finite(row.bookedValue, row.grossRevenue);
431
+ return { ...row, bookedValue: rowValue, grossRevenue: rowValue };
432
+ }) : [];
433
+ return {
434
+ ...source,
435
+ bookedValue,
436
+ revenue: bookedValue,
437
+ velocity: { ...velocity, bySection: velocityRows }
438
+ };
439
+ }
440
+ function normalizeChannelReportResult(value) {
441
+ const source = record(value);
442
+ const report = record(source.report);
443
+ const includesBookedValue = typeof report.includesBookedValue === "boolean" ? report.includesBookedValue : report.includesRevenue === true;
444
+ const rows = Array.isArray(report.rows) ? report.rows.map((value2) => {
445
+ const row = record(value2);
446
+ const attribution = record(row.attribution);
447
+ const bookedValue = nullableFinite(attribution.bookedValue, attribution.revenue);
448
+ return {
449
+ ...row,
450
+ attribution: { ...attribution, bookedValue, revenue: bookedValue }
451
+ };
452
+ }) : [];
453
+ const totals = record(report.totals);
454
+ const totalBookedValue = nullableFinite(totals.bookedValue, totals.revenue);
455
+ return {
456
+ ...source,
457
+ report: {
458
+ ...report,
459
+ includesBookedValue,
460
+ includesRevenue: includesBookedValue,
461
+ rows,
462
+ totals: { ...totals, bookedValue: totalBookedValue, revenue: totalBookedValue }
463
+ }
464
+ };
465
+ }
466
+ function normalizeChannelReportLink(value) {
467
+ const link = record(value);
468
+ const includesBookedValue = typeof link.includesBookedValue === "boolean" ? link.includesBookedValue : link.includesRevenue === true;
469
+ return {
470
+ ...link,
471
+ includesBookedValue,
472
+ includesRevenue: includesBookedValue
473
+ };
474
+ }
475
+ var ManageApi = class {
476
+ constructor(apiBase, token) {
477
+ this.base = apiBase.replace(/\/+$/, "");
478
+ this.token = token;
479
+ }
480
+ /** Swap the Bearer token in place (SeatManager re-mints on 401). */
481
+ setToken(token) {
482
+ this.token = token;
483
+ }
484
+ auth(path, init = {}) {
485
+ const method = init.method ?? "GET";
486
+ const headers = { Authorization: `Bearer ${this.token}` };
487
+ let body;
488
+ if (init.body !== void 0) {
489
+ headers["Content-Type"] = "application/json";
490
+ body = JSON.stringify(init.body);
491
+ }
492
+ return fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" }).then((r) => parse(r));
493
+ }
494
+ async authBlob(path) {
495
+ const res = await fetch(`${this.base}${path}`, {
496
+ method: "GET",
497
+ headers: { Authorization: `Bearer ${this.token}` },
498
+ credentials: "omit"
499
+ });
500
+ if (!res.ok) await parse(res);
501
+ return res.blob();
502
+ }
503
+ // ---- realtime read ----
504
+ /** Event-pinned organizer geometry. A manage token is never sent to `/pub`. */
505
+ chart(key) {
506
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/chart`);
507
+ }
508
+ /** Authenticated bytes for an Event-scoped organizer chart asset. */
509
+ asset(key, asset) {
510
+ if (!/^[a-zA-Z0-9._-]+$/.test(asset)) {
511
+ return Promise.reject(new ManageApiError(404, "not_found", "not_found"));
512
+ }
513
+ return this.authBlob(
514
+ `/v1/events/${encodeURIComponent(key)}/assets/${encodeURIComponent(asset)}`
515
+ );
516
+ }
517
+ /**
518
+ * The ORGANIZER's seat map: physical state, token-authed.
519
+ *
520
+ * This used to read `/pub/events/:key/objects` with no credential, which
521
+ * answers with the BUYER projection — every unit the caller may not buy
522
+ * collapses to a neutral `blocked`. An anonymous caller may buy only Public
523
+ * sale inventory, so the cockpit rendered every channel-allocated seat as
524
+ * blocked and then computed its KPIs, sell-through and (worse) its
525
+ * block/unblock target sets from that. `/v1/events/:key/objects` returns the
526
+ * unprojected snapshot the control-room read model already trusts.
527
+ */
528
+ objects(key) {
529
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/objects`);
530
+ }
531
+ /**
532
+ * Exchange the manage token for a one-use organizer socket ticket.
533
+ *
534
+ * A browser `WebSocket` cannot send an Authorization header, so the socket's
535
+ * scope is established here, over ordinary HTTPS. Without it the DO treats a
536
+ * manager socket as an anonymous public buyer and projects its deltas — so a
537
+ * hold inside a private allocation is structurally suppressed and the map
538
+ * drifts away from the truth `objects()` just established.
539
+ *
540
+ * Tickets are single-redemption and expire in ~30s: mint one per connect.
541
+ */
542
+ subscribeTicket(key) {
543
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/subscribe-tickets`, { method: "POST" });
544
+ }
545
+ socketUrl(key) {
546
+ return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe?surface=manager`;
547
+ }
548
+ // ---- inventory writes (token) ----
549
+ /** Take FREE seats off sale in one batched call. Optional `releaseAt` (epoch
550
+ * ms, future) auto-returns them to sale; `reason` tags the block (M3 uses it).
551
+ * Throws ManageApiError 409 (conflicts) if any seat was just taken. */
552
+ block(key, labels, opts = {}) {
553
+ const body = { labels };
554
+ if (typeof opts.releaseAt === "number") body.releaseAt = opts.releaseAt;
555
+ if (opts.reason) body.reason = opts.reason;
556
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/block`, { method: "POST", body });
557
+ }
558
+ /** Return specific blocked seats to sale (one batched call). */
559
+ unblock(key, labels) {
560
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock`, { method: "POST", body: { labels } });
561
+ }
562
+ /** Return every blocked seat to sale; resolves with the freed count. */
563
+ unblockAll(key) {
564
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock-all`, { method: "POST" });
565
+ }
566
+ /** Cancel bookings — return BOOKED seats to free (credit not refunded).
567
+ * Guarded by the original booking reference. */
568
+ unbook(key, labels, bookingRef) {
569
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unbook`, { method: "POST", body: { labels, bookingRef } });
570
+ }
571
+ /** Set (ms, clamped 1–60 min server-side) or clear (null) the hold TTL. */
572
+ setHoldTtl(key, holdTtlMs) {
573
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
574
+ }
575
+ // ---- Platform inventory booking history (token) ----
576
+ /** Inventory lifecycle by stable integrator bookingRef. Absent on Managed. */
577
+ bookings(key, query = {}) {
578
+ const params = new URLSearchParams();
579
+ if (query.q) params.set("q", query.q);
580
+ if (query.state) params.set("state", query.state);
581
+ if (query.cursor) params.set("cursor", query.cursor);
582
+ if (query.limit != null) params.set("limit", String(query.limit));
583
+ const qs = params.toString();
584
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/bookings${qs ? `?${qs}` : ""}`);
585
+ }
586
+ /** Exact configured-value snapshot plus book/replay/cancellation audit. */
587
+ booking(key, bookingRef) {
588
+ return this.auth(
589
+ `/v1/events/${encodeURIComponent(key)}/bookings/${encodeURIComponent(bookingRef)}`
590
+ );
591
+ }
592
+ /** Alias matching the server SDK vocabulary. */
593
+ listBookings(key, query = {}) {
594
+ return this.bookings(key, query);
595
+ }
596
+ /** Alias matching the server SDK vocabulary. */
597
+ retrieveBooking(key, bookingRef) {
598
+ return this.booking(key, bookingRef);
599
+ }
600
+ // ---- availability windows (token) ----
601
+ /** The organizer's current per section/zone availability windows (needs
602
+ * `event:view`). Ids absent from `rules` are open / on sale. */
603
+ availability(key) {
604
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`);
605
+ }
606
+ /** Replace the availability windows for a set of section/zone ids (needs
607
+ * `event:block`). Ids absent from `rules` become open / on sale; a zone rule
608
+ * cascades to its sections. The worker derives each id's seat labels, so
609
+ * `labels` on the sent rules is best-effort. Resolves with the authoritative
610
+ * effective `hidden` set (a due rule may fire at once) and the server-cleaned
611
+ * `rules` map (fired timed/threshold windows dropped). */
612
+ setAvailability(key, rules) {
613
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`, { method: "POST", body: { rules } });
614
+ }
615
+ // ---- sales channels (token, capability-gated) ----
616
+ // Reads need `event:channels:view`, mutations `event:channels:manage`.
617
+ // `event:block` grants NEITHER (spec §10), so a Block-only cockpit token gets
618
+ // a 403 here and Channels mode never renders.
619
+ /** Allocation list with exact per-channel counts. `includeArchived` adds the
620
+ * read-only archived rows behind the rail's "Show archived" control. */
621
+ channels(key, opts = {}) {
622
+ const qs = opts.includeArchived ? "?includeArchived=1" : "";
623
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels${qs}`);
624
+ }
625
+ /** One page of the label → channel map that paints the allocation overlay.
626
+ * Paged by label; follow `nextAfterLabel` until it is null. */
627
+ channelAllocation(key, opts = {}) {
628
+ const params = new URLSearchParams();
629
+ if (opts.afterLabel) params.set("afterLabel", opts.afterLabel);
630
+ if (opts.limit != null) params.set("limit", String(opts.limit));
631
+ const qs = params.toString();
632
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/allocation${qs ? `?${qs}` : ""}`);
633
+ }
634
+ channelAudit(key, opts = {}) {
635
+ const params = new URLSearchParams();
636
+ if (opts.limit != null) params.set("limit", String(opts.limit));
637
+ if (opts.before != null) params.set("before", String(opts.before));
638
+ const qs = params.toString();
639
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/audit${qs ? `?${qs}` : ""}`);
640
+ }
641
+ createChannel(key, input) {
642
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels`, { method: "POST", body: input });
643
+ }
644
+ renameChannel(key, channelId, name) {
645
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}`, {
646
+ method: "PATCH",
647
+ body: { name }
648
+ });
649
+ }
650
+ setChannelPaused(key, channelId, paused) {
651
+ const path = paused ? "pause" : "unpause";
652
+ return this.auth(
653
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/${path}`,
654
+ { method: "POST", body: {} }
655
+ );
656
+ }
657
+ /** Archive with a mandatory destination for the remaining allocation.
658
+ * Throws ManageApiError 409 `channel_archive_blocked_by_holds` while any hold
659
+ * is live; `err.details` carries the exact counts + retry window. */
660
+ archiveChannel(key, channelId, destination) {
661
+ return this.auth(
662
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/archive`,
663
+ { method: "POST", body: { destination } }
664
+ );
665
+ }
666
+ /**
667
+ * Versioned Apply. A stale `assignmentVersion` mutates NOTHING and throws
668
+ * ManageApiError 409 `channel_assignment_conflict` — the caller keeps its
669
+ * selection and offers "Refresh and review". There is no dry-run: the review
670
+ * sheet previews locally, this call returns the authoritative buckets.
671
+ */
672
+ applyChannelAssignment(key, input) {
673
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/assignments`, {
674
+ method: "POST",
675
+ body: {
676
+ targetChannelId: input.targetChannelId || null,
677
+ labels: input.labels,
678
+ assignmentVersion: input.assignmentVersion
679
+ }
680
+ });
681
+ }
682
+ /**
683
+ * Read-only buyer projection for an audience (§8.6) — the SAME scoped server
684
+ * view the buyer SDK receives, never a local approximation.
685
+ *
686
+ * Ships on the access-hardening branch. Older workers 404/405 here; callers
687
+ * MUST feature-detect and quietly say the preview needs a newer server rather
688
+ * than faking a projection client-side.
689
+ */
690
+ channelPreview(key, channelIds, opts = {}) {
691
+ const params = new URLSearchParams();
692
+ if (channelIds.length) params.set("channelIds", channelIds.join(","));
693
+ if (opts.includePublic != null) params.set("includePublic", opts.includePublic ? "1" : "0");
694
+ const qs = params.toString();
695
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/preview${qs ? `?${qs}` : ""}`);
696
+ }
697
+ /**
698
+ * Choose which sale route this channel opens.
699
+ *
700
+ * Since the server's 2026-08-06 change this is AUTHORIZATION, not a label:
701
+ * exactly one of the four routes may mint buyer access for the channel and the
702
+ * other three refuse with 409 `channel_access_intent_forbids`. The default is
703
+ * `none`, which refuses all four — so a route has to be declared before any
704
+ * buyer-facing action on the channel can succeed.
705
+ *
706
+ * Switching the route while buyers are already inside the current one is
707
+ * refused with 409 `channel_intent_switch_blocked`, whose `details` name what
708
+ * is live (`liveAccessLinks`, `activeSessions`). Retry with
709
+ * `acknowledgeLiveAccess: true`: hosted links on the channel are revoked,
710
+ * while sessions already minted keep their holds and drain on their own.
711
+ * `intentSwitch` is present on the response ONLY when the switch disturbed
712
+ * something, so the ordinary case stays the two-key body it has always been.
713
+ */
714
+ setChannelAccessIntent(key, channelId, accessIntent, opts = {}) {
715
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}`, {
716
+ method: "PATCH",
717
+ body: {
718
+ accessIntent,
719
+ ...opts.acknowledgeLiveAccess ? { acknowledgeLiveAccess: true } : {},
720
+ ...opts.reason ? { reason: opts.reason } : {}
721
+ }
722
+ });
723
+ }
724
+ // ---- hosted access links (M8) ----
725
+ /**
726
+ * Mint a hosted access link. The 201 is the ONE and ONLY time `url` and
727
+ * `capability` exist outside the buyer's browser — SeatLayer keeps a hash, so
728
+ * there is no route, cache, or support escalation that can produce this string
729
+ * again. Callers must reveal it immediately and then let it go.
730
+ *
731
+ * Every omitted field takes the server's default: expiry = when the event
732
+ * starts, 100 redemptions, 4 seats per buyer, this channel's allocation only.
733
+ * Platform bounds are enforced server-side and reported as 422 with the rule
734
+ * spelled out in `ManageApiError.serverMessage`.
735
+ *
736
+ * NOT a side effect any more. This used to SET the channel's access intent to
737
+ * `hosted_link`; since 2026-08-06 it REQUIRES it, and a channel declaring any
738
+ * other route refuses with 409 `channel_access_intent_forbids`. Callers must
739
+ * declare the route first — `ChannelsMode` does exactly that before it
740
+ * creates, so a first buyer link on a fresh channel is still one gesture.
741
+ */
742
+ createAccessLink(key, channelId, input = {}) {
743
+ return this.auth(
744
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`,
745
+ { method: "POST", body: input }
746
+ );
747
+ }
748
+ /** Status only — label, expiry, redemptions, per-buyer cap, lineage, and the
749
+ * live session count. Never the url, never the capability. Needs `:view`. */
750
+ accessLinks(key, channelId) {
751
+ return this.auth(
752
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`
753
+ );
754
+ }
755
+ /**
756
+ * Rotate — the ONLY recovery for a link nobody kept. The old URL stops opening
757
+ * immediately and the response is a fresh one-time reveal.
758
+ *
759
+ * `endActiveSessions` is REQUIRED, not defaulted: the organizer must say
760
+ * whether buyers already inside finish their checkout or lose access now. The
761
+ * server answers 422 `end_active_sessions_required` if it is omitted, and that
762
+ * refusal is correct — a UI must not pick either branch on their behalf.
763
+ */
764
+ rotateAccessLink(key, channelId, linkId, endActiveSessions) {
765
+ return this.auth(
766
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}/rotate`,
767
+ { method: "POST", body: { endActiveSessions } }
768
+ );
769
+ }
770
+ /** Revoke. The link stops opening immediately; `endActiveSessions` decides
771
+ * whether the buyers already inside keep their sessions. */
772
+ revokeAccessLink(key, channelId, linkId, endActiveSessions = false) {
773
+ const qs = endActiveSessions ? "?endActiveSessions=1" : "";
774
+ return this.auth(
775
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}${qs}`,
776
+ { method: "DELETE" }
777
+ );
778
+ }
779
+ // ---- reports (token) ----
780
+ report(key) {
781
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/report`).then(normalizeReportResult);
782
+ }
783
+ controlRoom(key, windowMinutes = 15) {
784
+ return this.auth(
785
+ `/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`
786
+ ).then(normalizeControlRoomSnapshot);
787
+ }
788
+ /** Allocation beside immutable booking-time channel attribution. */
789
+ channelReport(key) {
790
+ return this.auth(
791
+ `/v1/events/${encodeURIComponent(key)}/channels/report`
792
+ ).then(normalizeChannelReportResult);
793
+ }
794
+ createChannelReportLink(key, channelId, input = {}) {
795
+ return this.auth(
796
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`,
797
+ { method: "POST", body: input }
798
+ ).then((value) => {
799
+ const reveal = record(value);
800
+ return { ...reveal, link: normalizeChannelReportLink(reveal.link) };
801
+ });
802
+ }
803
+ channelReportLinks(key, channelId) {
804
+ return this.auth(
805
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`
806
+ ).then((value) => {
807
+ const result = record(value);
808
+ return {
809
+ links: Array.isArray(result.links) ? result.links.map(normalizeChannelReportLink) : []
810
+ };
811
+ });
812
+ }
813
+ revokeChannelReportLink(key, channelId, linkId) {
814
+ return this.auth(
815
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links/${encodeURIComponent(linkId)}`,
816
+ { method: "DELETE" }
817
+ ).then((value) => {
818
+ const result = record(value);
819
+ return { ...result, link: normalizeChannelReportLink(result.link) };
820
+ });
821
+ }
822
+ log(key, opts = {}) {
823
+ const params = new URLSearchParams();
824
+ if (opts.limit != null) params.set("limit", String(opts.limit));
825
+ if (opts.before != null) params.set("before", String(opts.before));
826
+ const qs = params.toString();
827
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/log${qs ? `?${qs}` : ""}`);
828
+ }
829
+ /** CSV report as a Blob (Bearer auth can't ride a plain <a href>). Host builds
830
+ * an object URL for download. */
831
+ async reportCsv(key) {
832
+ const res = await fetch(`${this.base}/v1/events/${encodeURIComponent(key)}/report.csv`, {
833
+ headers: { Authorization: `Bearer ${this.token}` },
834
+ credentials: "omit"
835
+ });
836
+ if (!res.ok) throw new ManageApiError(res.status, `request_failed_${res.status}`);
837
+ return res.blob();
838
+ }
839
+ };
840
+
358
841
  // src/channelsMode.ts
359
842
  var POLL_MS = 3e4;
360
843
  var MAX_FLAGS = 8;
@@ -3167,6 +3650,8 @@ var ChannelsMode = class {
3167
3650
  };
3168
3651
 
3169
3652
  export {
3653
+ ManageApiError,
3654
+ ManageApi,
3170
3655
  PUBLIC_CHANNEL_ID,
3171
3656
  PUBLIC_CHANNEL_NAME,
3172
3657
  isPublicChannelId,
@@ -3195,4 +3680,4 @@ export {
3195
3680
  bucketRowsHtml,
3196
3681
  ChannelsMode
3197
3682
  };
3198
- //# sourceMappingURL=chunk-KNMZQZXR.js.map
3683
+ //# sourceMappingURL=chunk-5URCWBHM.js.map