@ticketlayer/backstage 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ticketlayer Ltd
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,148 @@
1
+ # @ticketlayer/backstage
2
+
3
+ TypeScript client for the Backstage API: every operation of the contract,
4
+ generated from `backstage-api/openapi.json`, plus the `embed` subpath that
5
+ mounts Backstage widgets in a partner's own page.
6
+
7
+ ## Install
8
+
9
+ The package is published as `@ticketlayer/backstage`. Until the first
10
+ release runs, the only version on npm is the old name
11
+ `@ticketlayer/backstage-sdk` 0.1.1; our own apps use a `file:` link to this
12
+ repository meanwhile. See the vault page `architecture/sdk-architecture.md`
13
+ for what is on npm.
14
+
15
+ ```bash
16
+ npm install @ticketlayer/backstage
17
+ ```
18
+
19
+ ## Client
20
+
21
+ ```ts
22
+ import { BackstageClient } from '@ticketlayer/backstage';
23
+
24
+ // Staff app (Next.js with Stagedoor OAuth): cookie mode, org from the subdomain
25
+ const backstage = new BackstageClient({ baseUrl: 'https://api.staging.t9r.dev', authMode: 'cookie' });
26
+
27
+ // Server or native app: bearer mode with a staff JWT, an organisation API key
28
+ // (tlak_...), an embed session or a device token; the org must be named
29
+ const server = new BackstageClient({
30
+ baseUrl: 'https://api.staging.t9r.dev',
31
+ authMode: 'bearer',
32
+ accessToken: () => currentToken, // a resolver is read on every request
33
+ organisationSlug: 'rowbot', // sent as X-Ticketlayer-Org
34
+ });
35
+
36
+ const events = await server.events.list();
37
+ ```
38
+
39
+ Every request carries `TL-Version` (the dated API version the client was
40
+ generated against) and JSend responses are unwrapped to their `data`. Errors
41
+ are `BackstageAPIError` with `code` and `statusCode`.
42
+
43
+ ## Develop
44
+
45
+ ```bash
46
+ npm ci
47
+ npm run generate:client # openapi.json -> src/generated/types.ts
48
+ npm run build # tsc -> dist/
49
+ npm test # node --test against dist/
50
+ ```
51
+
52
+ `src/client.ts` is generated by `backstage-sdk-tooling` (`tt sdk local`);
53
+ do not edit it by hand. Releases are driven by changesets and
54
+ `.github/workflows/release.yml`, which publishes to npm with provenance once
55
+ `NPM_TOKEN` is set. `CLAUDE.md` is the working agreement.
56
+
57
+ ## Embedding Backstage widgets in your app
58
+
59
+ `@ticketlayer/backstage/embed` mounts Backstage widgets (the events list, an
60
+ event editor, the orders list, an order) in an iframe on your own page. Your
61
+ users never log in to Backstage: your server mints an **embed session** for
62
+ one account with your organisation API key, and the widget runs as that
63
+ account with the scopes you chose. The subpath is framework-free and is not
64
+ loaded by the main entry.
65
+
66
+ The flow, as Rowbot uses it (one Ticketlayer organisation, one account per
67
+ club):
68
+
69
+ 1. **Server: mint a session.** With the organisation API key, call
70
+ `POST /embed/sessions` for the club's account. Keep the API key on the
71
+ server; only the short-lived session token reaches the browser.
72
+
73
+ ```ts
74
+ import { BackstageClient } from '@ticketlayer/backstage';
75
+
76
+ const backstage = new BackstageClient({
77
+ baseUrl: 'https://api.staging.t9r.dev',
78
+ accessToken: process.env.TICKETLAYER_ORG_API_KEY,
79
+ organisationSlug: 'rowbot',
80
+ });
81
+
82
+ // e.g. GET /api/ticketing/session on your server
83
+ const { token, expiresAt } = await backstage.embed.createSession({
84
+ accountId: club.ticketlayerAccountId,
85
+ scopes: ['events.read', 'events.write', 'orders.read'],
86
+ });
87
+ ```
88
+
89
+ 2. **Page: mount a widget.** Hand the token to the loader; it builds
90
+ `${baseUrl}/embed/<widget>?session=...&parent_origin=<your origin>`, accepts
91
+ messages only from the Backstage origin, and sizes the frame to its content.
92
+
93
+ ```ts
94
+ import { createBackstageEmbed } from '@ticketlayer/backstage/embed';
95
+
96
+ const embed = createBackstageEmbed({
97
+ baseUrl: 'https://backstage.staging.t9r.dev',
98
+ session: token,
99
+ // Called when the widget reports the session expired: mint a new one.
100
+ sessionProvider: async () => (await fetch('/api/ticketing/session').then((r) => r.json())).token,
101
+ onNavigate: (widget, params) => history.replaceState(null, '', `/ticketing/${widget}/${params.id ?? ''}`),
102
+ onEvent: (name, payload) => console.log(name, payload), // event.created, order.refund_requested, ...
103
+ onError: (err) => console.warn(err.code, err.message),
104
+ });
105
+
106
+ embed.mount(document.getElementById('ticketing')!, { widget: 'events' });
107
+ // later: embed.navigate('order', { id: 'ord_123' }); embed.unmount();
108
+ ```
109
+
110
+ 3. **Refresh on expiry.** Sessions are short-lived. When the widget gets a 401
111
+ (or its token's `exp` passes) it posts `tl:session-expired`; the loader calls
112
+ your `sessionProvider`, posts the new token back as `tl:session`, and the
113
+ widget resumes where it was. Without a provider, `onSessionExpired` fires
114
+ and the widget shows its expired state.
115
+
116
+ Widgets and params: `events` (`{ action?: 'new' }`), `event`
117
+ (`{ id, tab?: 'details' | 'tickets' | 'occurrences' }`), `orders`, `order`
118
+ (`{ id }`). Actions the session's scopes do not allow (for example editing
119
+ with only `events.read`) are hidden in the widget.
120
+
121
+ Messages from the widget (all carry `version: 1`): `tl:ready`, `tl:height`
122
+ `{ height }`, `tl:navigate` `{ widget, params }`, `tl:event` `{ name, payload }`,
123
+ `tl:session-expired`, `tl:error` `{ code, message }`. The only inbound message
124
+ is `tl:session` `{ token }`. The Backstage deployment must list your origin in
125
+ its `EMBED_FRAME_ANCESTORS` for the frame to load.
126
+
127
+ React example (the loader owns the iframe; React owns the container):
128
+
129
+ ```tsx
130
+ import { useEffect, useRef } from 'react';
131
+ import { createBackstageEmbed, type EmbedWidgetTarget } from '@ticketlayer/backstage/embed';
132
+
133
+ export function BackstageWidget({ token, target }: { token: string; target: EmbedWidgetTarget }) {
134
+ const ref = useRef<HTMLDivElement>(null);
135
+ useEffect(() => {
136
+ if (!ref.current) return;
137
+ const embed = createBackstageEmbed({
138
+ baseUrl: 'https://backstage.staging.t9r.dev',
139
+ session: token,
140
+ sessionProvider: () => fetch('/api/ticketing/session').then((r) => r.json()).then((s) => s.token),
141
+ });
142
+ embed.mount(ref.current, target);
143
+ return () => embed.unmount();
144
+ }, [token, target]);
145
+ return <div ref={ref} />;
146
+ }
147
+ // <BackstageWidget token={token} target={{ widget: 'order', params: { id: 'ord_123' } }} />
148
+ ```