@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 +21 -0
- package/README.md +148 -0
- package/dist/client.d.ts +4945 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2736 -0
- package/dist/embed/helpers.d.ts +62 -0
- package/dist/embed/helpers.d.ts.map +1 -0
- package/dist/embed/helpers.js +105 -0
- package/dist/embed/index.d.ts +27 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +118 -0
- package/dist/generated/types.d.ts +24028 -0
- package/dist/generated/types.d.ts.map +1 -0
- package/dist/generated/types.js +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/package.json +83 -0
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
|
+
```
|