@relume_io/partner-embed 0.0.0-stage → 2.0.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
@@ -1,3 +1,137 @@
1
- # Temporary Holding Version
1
+ # @relume_io/partner-embed
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The frontend SDK for Relume partners. One call puts the Relume builder on your
4
+ page; a few event handlers tie it to your wallet. You never touch the iframe
5
+ or `postMessage`.
6
+
7
+ It is the browser side of the
8
+ [Relume Partner Protocol v1](../../docs/partner-protocol/v1.md) (sections 4
9
+ and 11). Pair it with [`@relume_io/partner-node`](../node) on your backend,
10
+ which mounts the session route this SDK calls. The
11
+ [partner docs](https://apis.relume.io/nova/docs/partners) walk through both.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @relume_io/partner-embed
17
+ ```
18
+
19
+ ```ts
20
+ import { Relume } from '@relume_io/partner-embed';
21
+ ```
22
+
23
+ Or with a script tag, which defines `window.Relume`. Keep the `integrity`
24
+ hash: the browser refuses the file if it changes.
25
+
26
+ ```html
27
+ <script
28
+ src="https://cdn.jsdelivr.net/npm/@relume_io/partner-embed@2.0.0/dist/relume-embed.global.js"
29
+ integrity="sha384-iueomFw1nX3F6SSp4SUXW//Q1Db/alXwrfGm1Mi2FnzWhTNEX2vzFZjp4yJjEY96"
30
+ crossorigin="anonymous"
31
+ ></script>
32
+ ```
33
+
34
+ No runtime dependencies. About 6 KB minified.
35
+
36
+ ## Quickstart
37
+
38
+ ```ts
39
+ const relume = Relume.init('#relume', 'pk_live_…');
40
+
41
+ relume.onTopUp(async ({ required, balance }) => {
42
+ await openCheckout(required - (balance ?? 0)); // your checkout
43
+ relume.refreshCredits(); // the builder re-reads the balance
44
+ });
45
+ relume.onCreditsChanged(({ balance }) => setHeaderBalance(balance));
46
+ relume.onExport(({ siteId, downloadUrl }) => toast('Your site is ready'));
47
+ relume.onClose(() => router.back());
48
+ relume.onError(({ code, message }) => report(code, message));
49
+ ```
50
+
51
+ The builder fills `#relume`, so give it a size (for example
52
+ `height: calc(100vh - 64px)`). Below 640px wide it covers the whole viewport
53
+ instead; pass `fullScreenOnMobile: false` to keep it in the container. Your
54
+ page needs a `<meta name="viewport" content="width=device-width">` for phones
55
+ to report their real width.
56
+
57
+ [`examples/index.html`](./examples/index.html) is a whole partner page.
58
+
59
+ ## What it does for you
60
+
61
+ - Creates the iframe on `https://embed.relume.ai/?pk=…` and sizes it.
62
+ - Calls your session route (`GET /relume/session`, mounted by
63
+ `@relume_io/partner-node`) whenever the builder asks: when it opens, before
64
+ the session expires and after a 401. The builder renews without a reload.
65
+ It sends a `Relume-Embed: 1` header, which the session route requires, so
66
+ another site can't get a session without a CORS preflight.
67
+ - Trusts your page. Any script on it can call your session route, as the SDK
68
+ does, and get a session for the signed-in user. It can also answer the
69
+ builder's session request itself, since the builder can't tell its message
70
+ from the SDK's: a client secret minted for another of your users opens the
71
+ builder as that user. Load only scripts you trust on pages that embed the
72
+ builder.
73
+ - Accepts messages only from the builder's own window on the embed origin, and
74
+ posts only to that origin. Messages from other scripts or frames are ignored.
75
+ - Agrees a protocol version with the builder, so either side can change its
76
+ messages without breaking the other.
77
+ - Checks your page's origin against the publishable key's allowed origins. The
78
+ browser refuses the frame on any other origin, and the frame can't say why,
79
+ so the SDK fires `onError({ code: 'origin_not_allowed' })` and removes it.
80
+
81
+ ## `Relume.init(target, publishableKey, options?)`
82
+
83
+ `target` is a selector or an element. Throws a `TypeError` if it matches
84
+ nothing or the key isn't a `pk_test_…`/`pk_live_…` key.
85
+
86
+ | Option | Default | What it does |
87
+ | -------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `sessionUrl` | `/relume/session` on your page | Your session route. On another origin it is fetched with credentials and the `Relume-Embed` header, so its CORS must allow your page's origin exactly (never `*`, never the request's `Origin`) and that header. |
89
+ | `fullScreenOnMobile` | `true` | Cover the viewport below 640px. |
90
+ | `embedUrl` | `https://embed.relume.ai` | The builder's host. Relume gives you staging's (`https://embed-staging.relume.ai`). |
91
+ | `apiUrl` | inferred from `embedUrl` | Relume's API, for the allowed-origins check. Known for Relume's hosts; without one the check is skipped and the builder's own applies. |
92
+
93
+ ## Events
94
+
95
+ Each `on…` method adds a listener and returns a function that removes it.
96
+
97
+ | Event | Payload | When |
98
+ | ------------------ | ----------------------------------- | ---------------------------------------------------------------------------------- |
99
+ | `onTopUp` | `{ required, balance }` | A charge was declined. `required` is the action's price in your credits. |
100
+ | `onCreditsChanged` | `{ balance }` | The balance the builder shows changed. Not fired when your wallet didn't answer. |
101
+ | `onExport` | `{ siteId, exportId, downloadUrl }` | An export is ready. |
102
+ | `onClose` | none | The user closed the builder. |
103
+ | `onError` | `{ code, message }` | The builder couldn't start or carry on. Logged to the console when nobody listens. |
104
+
105
+ Error codes: `session_failed`, `user_deactivated`, `user_suspended`,
106
+ `partner_suspended`, `origin_not_allowed`, `test_cap_reached` and
107
+ `charge_failed` ([protocol section 11](../../docs/partner-protocol/v1.md#embed-error-codes)).
108
+ New codes may be added; treat an unknown one like `session_failed`.
109
+
110
+ A handler that throws is logged and doesn't stop the others.
111
+
112
+ ## Methods
113
+
114
+ - `open({ site })` opens one of the user's sites (a `siteId` from the partner
115
+ API or `onExport`). The builder reloads on that site.
116
+ - `refreshCredits()` asks the builder to re-read the balance from your wallet
117
+ (`credits.get`), then fires `onCreditsChanged` if it changed. Call it after a
118
+ top-up.
119
+ - `destroy()` removes the iframe and every listener, and stops any pending
120
+ session request. The instance does nothing afterwards; call `Relume.init`
121
+ again to show the builder again.
122
+
123
+ ## Developing
124
+
125
+ ```sh
126
+ npm ci
127
+ npm run typecheck
128
+ npm test # unit tests (happy-dom) and the example's size
129
+ npm run test:dist # builds, then loads the ESM and script-tag output
130
+ ```
131
+
132
+ The `postMessage` messages live in [`src/protocol.ts`](./src/protocol.ts) and
133
+ mirror `frontend/base/embed/embed_protocol.ts` in the Relume app. Change both
134
+ together.
135
+
136
+ Releases go out from CI on a `partner-sdk/v*` tag, together with
137
+ `@relume_io/partner-node`: see [Releasing](../README.md#releasing).
@@ -0,0 +1,78 @@
1
+ /** Embed error codes (protocol section 11). New codes may be added. */
2
+ type EmbedErrorCode = 'session_failed' | 'user_deactivated' | 'user_suspended' | 'partner_suspended' | 'origin_not_allowed' | 'test_cap_reached' | 'charge_failed';
3
+
4
+ declare const DEFAULT_EMBED_URL = "https://embed.relume.ai";
5
+ declare const DEFAULT_SESSION_URL = "/relume/session";
6
+ /** On every session request; `@relume_io/partner-node` refuses one without it. */
7
+ declare const EMBED_HEADER = "Relume-Embed";
8
+ declare const EMBED_HEADER_VALUE = "1";
9
+ interface RelumeEmbedOptions {
10
+ /**
11
+ * Your session route, mounted by `@relume_io/partner-node`. Defaults to
12
+ * `/relume/session` on this page's origin. On another origin it is fetched
13
+ * with credentials (cookies), so it must allow this page's origin in CORS.
14
+ */
15
+ sessionUrl?: string;
16
+ /** Cover the viewport on narrow screens. Default true. */
17
+ fullScreenOnMobile?: boolean;
18
+ /** The builder's host. Defaults to production; Relume gives you staging's. */
19
+ embedUrl?: string;
20
+ /**
21
+ * Relume's API, used to explain a refused frame. Inferred from `embedUrl`
22
+ * for Relume's own hosts; without one the check is skipped.
23
+ */
24
+ apiUrl?: string;
25
+ }
26
+ interface TopUpEvent {
27
+ /** The action's price, in your credits. */
28
+ required: number;
29
+ /** The balance your wallet last reported, or null if it didn't answer. */
30
+ balance: number | null;
31
+ }
32
+ interface CreditsChangedEvent {
33
+ balance: number;
34
+ }
35
+ interface ExportEvent {
36
+ siteId: string;
37
+ exportId: string;
38
+ downloadUrl: string;
39
+ }
40
+ interface EmbedError {
41
+ /** One of EmbedErrorCode; treat an unknown code like `session_failed`. */
42
+ code: EmbedErrorCode | (string & {});
43
+ message: string;
44
+ }
45
+ interface OpenOptions {
46
+ /** A site id, from the partner API or an earlier `onExport`. */
47
+ site: string;
48
+ }
49
+ type Listener<T> = (event: T) => void;
50
+ type Unsubscribe = () => void;
51
+ interface RelumeEmbed {
52
+ /** A charge was declined: show your checkout, then call refreshCredits(). */
53
+ onTopUp(listener: Listener<TopUpEvent>): Unsubscribe;
54
+ /** The balance the builder shows changed. */
55
+ onCreditsChanged(listener: Listener<CreditsChangedEvent>): Unsubscribe;
56
+ /** An export is ready to download. */
57
+ onExport(listener: Listener<ExportEvent>): Unsubscribe;
58
+ /** The user closed the builder. */
59
+ onClose(listener: Listener<void>): Unsubscribe;
60
+ /** The builder couldn't start or carry on. Logged when nobody listens. */
61
+ onError(listener: Listener<EmbedError>): Unsubscribe;
62
+ /** Opens one of the user's sites. Reloads the builder. */
63
+ open(options: OpenOptions): void;
64
+ /** Re-reads the balance from your wallet, e.g. after a top-up. */
65
+ refreshCredits(): void;
66
+ /** Removes the iframe and every listener. The instance is then inert. */
67
+ destroy(): void;
68
+ }
69
+
70
+ declare const Relume: {
71
+ /**
72
+ * Puts the Relume builder in `target` (a selector or an element) for the
73
+ * signed-in user, whose session comes from your session route.
74
+ */
75
+ init(target: string | Element, publishableKey: string, options?: RelumeEmbedOptions): RelumeEmbed;
76
+ };
77
+
78
+ export { type CreditsChangedEvent, DEFAULT_EMBED_URL, DEFAULT_SESSION_URL, EMBED_HEADER, EMBED_HEADER_VALUE, type EmbedError, type EmbedErrorCode, type ExportEvent, type OpenOptions, Relume, type RelumeEmbed, type RelumeEmbedOptions, type TopUpEvent };
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
1
+ // src/protocol.ts
2
+ var PROTOCOL_VERSIONS = [1];
3
+ var SDK_SOURCE = "relume-partner-embed";
4
+ var APP_SOURCE = "relume-embed";
5
+ function encodeSdkMessage(message) {
6
+ return { source: SDK_SOURCE, ...message };
7
+ }
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ var isString = (value) => typeof value === "string";
12
+ var isBalance = (value) => value === null || typeof value === "number" && Number.isFinite(value);
13
+ function parseAppMessage(data, version) {
14
+ if (!isRecord(data) || data.source !== APP_SOURCE) return null;
15
+ if (version !== null && data.version !== version) return null;
16
+ switch (data.type) {
17
+ case "ready":
18
+ return typeof data.version === "number" && PROTOCOL_VERSIONS.includes(data.version) ? { type: "ready", version: data.version } : null;
19
+ case "session.request":
20
+ return { type: "session.request" };
21
+ case "credits.changed":
22
+ return isBalance(data.balance) ? { type: "credits.changed", balance: data.balance } : null;
23
+ case "topup.required":
24
+ return typeof data.required === "number" && isBalance(data.balance) ? {
25
+ type: "topup.required",
26
+ required: data.required,
27
+ balance: data.balance
28
+ } : null;
29
+ case "export.ready":
30
+ return isString(data.siteId) && isString(data.exportId) && isString(data.downloadUrl) ? {
31
+ type: "export.ready",
32
+ siteId: data.siteId,
33
+ exportId: data.exportId,
34
+ downloadUrl: data.downloadUrl
35
+ } : null;
36
+ case "close":
37
+ return { type: "close" };
38
+ case "error":
39
+ return isString(data.code) ? {
40
+ type: "error",
41
+ code: data.code,
42
+ message: isString(data.message) ? data.message : data.code
43
+ } : null;
44
+ default:
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // src/embed.ts
50
+ var DEFAULT_EMBED_URL = "https://embed.relume.ai";
51
+ var DEFAULT_SESSION_URL = "/relume/session";
52
+ var EMBED_HEADER = "Relume-Embed";
53
+ var EMBED_HEADER_VALUE = "1";
54
+ var API_URL_FOR_EMBED = {
55
+ "https://embed.relume.ai": "https://apis.relume.io/nova",
56
+ "https://embed-staging.relume.ai": "https://staging-apis.relume.io/nova"
57
+ };
58
+ var HELLO_RETRY_MS = 200;
59
+ var HELLO_MAX_RETRY_MS = 3200;
60
+ var HELLO_TIMEOUT_MS = 2e4;
61
+ var MOBILE_QUERY = "(max-width: 640px)";
62
+ var PUBLISHABLE_KEY = /^pk_(?:test|live)_[A-Za-z0-9]+$/;
63
+ var MAX_ERROR_CODE_LENGTH = 64;
64
+ function originOf(url, name) {
65
+ let parsed;
66
+ try {
67
+ parsed = new URL(url);
68
+ } catch {
69
+ throw new TypeError(`Relume.init: ${name} must be an absolute URL`);
70
+ }
71
+ return parsed.origin;
72
+ }
73
+ function isRecord2(value) {
74
+ return typeof value === "object" && value !== null && !Array.isArray(value);
75
+ }
76
+ function errorCodeOf(body) {
77
+ if (!isRecord2(body) || !isRecord2(body.error)) return null;
78
+ const { code } = body.error;
79
+ return typeof code === "string" && code.length > 0 && code.length <= MAX_ERROR_CODE_LENGTH ? code : null;
80
+ }
81
+ function createEmbed(target, publishableKey, options, env) {
82
+ const { window: win } = env;
83
+ const container = typeof target === "string" ? win.document.querySelector(target) : target;
84
+ if (container === null) {
85
+ throw new TypeError(`Relume.init: no element matches "${String(target)}"`);
86
+ }
87
+ if (!PUBLISHABLE_KEY.test(publishableKey)) {
88
+ throw new TypeError(
89
+ "Relume.init: the publishable key must look like pk_test_\u2026 or pk_live_\u2026"
90
+ );
91
+ }
92
+ const embedOrigin = originOf(
93
+ options.embedUrl ?? DEFAULT_EMBED_URL,
94
+ "embedUrl"
95
+ );
96
+ const apiUrl = (options.apiUrl ?? API_URL_FOR_EMBED[embedOrigin] ?? "").replace(/\/+$/, "");
97
+ const sessionUrl = new URL(
98
+ options.sessionUrl ?? DEFAULT_SESSION_URL,
99
+ win.location.href
100
+ );
101
+ const pageOrigin = win.location.origin;
102
+ const credentials = sessionUrl.origin === pageOrigin ? "same-origin" : "include";
103
+ const listeners = {
104
+ topUp: /* @__PURE__ */ new Set(),
105
+ creditsChanged: /* @__PURE__ */ new Set(),
106
+ export: /* @__PURE__ */ new Set(),
107
+ close: /* @__PURE__ */ new Set(),
108
+ error: /* @__PURE__ */ new Set()
109
+ };
110
+ const aborter = new AbortController();
111
+ let destroyed = false;
112
+ let blocked = false;
113
+ let generation = 0;
114
+ let version = null;
115
+ let awaitingLoad = true;
116
+ let helloTimer = null;
117
+ let helloDeadline = null;
118
+ let lastBalance = null;
119
+ const iframe = win.document.createElement("iframe");
120
+ iframe.title = "Relume website builder";
121
+ iframe.allow = "clipboard-read; clipboard-write; fullscreen";
122
+ iframe.style.border = "0";
123
+ iframe.style.display = "block";
124
+ const mobile = options.fullScreenOnMobile === false || typeof win.matchMedia !== "function" ? null : win.matchMedia(MOBILE_QUERY);
125
+ const applySize = () => {
126
+ const edge = mobile?.matches === true ? "0" : "";
127
+ Object.assign(iframe.style, {
128
+ position: edge && "fixed",
129
+ top: edge,
130
+ right: edge,
131
+ bottom: edge,
132
+ left: edge,
133
+ zIndex: edge && "2147483647",
134
+ width: "100%",
135
+ height: "100%"
136
+ });
137
+ };
138
+ applySize();
139
+ mobile?.addEventListener("change", applySize);
140
+ function emit(set, event) {
141
+ for (const listener of [...set]) {
142
+ try {
143
+ listener(event);
144
+ } catch (error) {
145
+ console.error("[Relume] An event handler threw", error);
146
+ }
147
+ }
148
+ }
149
+ function fail(code, message) {
150
+ if (destroyed) return;
151
+ if (listeners.error.size === 0) {
152
+ console.error(`[Relume] ${code}: ${message}`);
153
+ return;
154
+ }
155
+ emit(listeners.error, { code, message });
156
+ }
157
+ function post(message) {
158
+ iframe.contentWindow?.postMessage(encodeSdkMessage(message), embedOrigin);
159
+ }
160
+ function stopHello() {
161
+ if (helloTimer !== null) win.clearTimeout(helloTimer);
162
+ if (helloDeadline !== null) win.clearTimeout(helloDeadline);
163
+ helloTimer = null;
164
+ helloDeadline = null;
165
+ }
166
+ function load(path) {
167
+ stopHello();
168
+ generation += 1;
169
+ version = null;
170
+ awaitingLoad = true;
171
+ const url = new URL(path, embedOrigin);
172
+ url.searchParams.set("pk", publishableKey);
173
+ iframe.src = url.toString();
174
+ helloDeadline = win.setTimeout(() => {
175
+ stopHello();
176
+ fail("session_failed", "The builder didn't answer.");
177
+ }, HELLO_TIMEOUT_MS);
178
+ }
179
+ function sayHello(retryIn) {
180
+ post({ type: "hello", versions: PROTOCOL_VERSIONS });
181
+ helloTimer = win.setTimeout(
182
+ () => sayHello(Math.min(retryIn * 2, HELLO_MAX_RETRY_MS)),
183
+ retryIn
184
+ );
185
+ }
186
+ const onLoad = () => {
187
+ if (version !== null || helloDeadline === null) return;
188
+ awaitingLoad = false;
189
+ if (helloTimer !== null) win.clearTimeout(helloTimer);
190
+ sayHello(HELLO_RETRY_MS);
191
+ };
192
+ iframe.addEventListener("load", onLoad);
193
+ async function answerSessionRequest(forGeneration) {
194
+ let message;
195
+ try {
196
+ const response = await env.fetch(sessionUrl, {
197
+ credentials,
198
+ cache: "no-store",
199
+ // A custom header: cross-origin, the browser must preflight it, so
200
+ // the route can tell this from a link or form on another site.
201
+ headers: {
202
+ accept: "application/json",
203
+ [EMBED_HEADER]: EMBED_HEADER_VALUE
204
+ },
205
+ signal: aborter.signal
206
+ });
207
+ const body = await response.json().catch(() => null);
208
+ const clientSecret = isRecord2(body) ? body.clientSecret : void 0;
209
+ if (response.ok && typeof clientSecret === "string" && clientSecret) {
210
+ message = { type: "session", clientSecret };
211
+ } else {
212
+ message = {
213
+ type: "session.error",
214
+ code: errorCodeOf(body) ?? (response.status === 401 ? "not_signed_in" : "session_failed")
215
+ };
216
+ }
217
+ } catch {
218
+ if (destroyed) return;
219
+ message = { type: "session.error", code: "session_failed" };
220
+ }
221
+ if (destroyed || forGeneration !== generation) return;
222
+ post(message);
223
+ }
224
+ function handle(message) {
225
+ if (version === null) {
226
+ if (awaitingLoad) return;
227
+ if (message.type === "ready") {
228
+ stopHello();
229
+ version = message.version;
230
+ } else if (message.type === "error") {
231
+ stopHello();
232
+ fail(message.code, message.message);
233
+ }
234
+ return;
235
+ }
236
+ switch (message.type) {
237
+ case "ready":
238
+ return;
239
+ case "session.request":
240
+ void answerSessionRequest(generation);
241
+ return;
242
+ case "credits.changed":
243
+ if (message.balance !== null && message.balance !== lastBalance) {
244
+ lastBalance = message.balance;
245
+ emit(listeners.creditsChanged, { balance: message.balance });
246
+ }
247
+ return;
248
+ case "topup.required":
249
+ emit(listeners.topUp, {
250
+ required: message.required,
251
+ balance: message.balance
252
+ });
253
+ return;
254
+ case "export.ready":
255
+ emit(listeners.export, {
256
+ siteId: message.siteId,
257
+ exportId: message.exportId,
258
+ downloadUrl: message.downloadUrl
259
+ });
260
+ return;
261
+ case "close":
262
+ emit(listeners.close, void 0);
263
+ return;
264
+ case "error":
265
+ fail(message.code, message.message);
266
+ return;
267
+ }
268
+ }
269
+ const onMessage = (event) => {
270
+ if (event.origin !== embedOrigin || event.source === null || event.source !== iframe.contentWindow) {
271
+ return;
272
+ }
273
+ const message = parseAppMessage(event.data, version);
274
+ if (message !== null) handle(message);
275
+ };
276
+ win.addEventListener("message", onMessage);
277
+ async function checkOrigin() {
278
+ if (!apiUrl) return;
279
+ let response;
280
+ let body;
281
+ try {
282
+ response = await env.fetch(
283
+ `${apiUrl}/embed/partner?pk=${encodeURIComponent(publishableKey)}`,
284
+ { credentials: "omit", signal: aborter.signal }
285
+ );
286
+ body = await response.json();
287
+ } catch {
288
+ return;
289
+ }
290
+ if (destroyed) return;
291
+ const code = errorCodeOf(body);
292
+ if (code === "unknown_publishable_key" || code === "partner_suspended") {
293
+ block(
294
+ code === "partner_suspended" ? "partner_suspended" : "session_failed",
295
+ code === "partner_suspended" ? "This partner has been suspended." : "The publishable key is unknown, revoked or expired."
296
+ );
297
+ return;
298
+ }
299
+ const allowed = response.ok && isRecord2(body) && Array.isArray(body.allowedOrigins) ? body.allowedOrigins : null;
300
+ if (allowed !== null && !allowed.includes(pageOrigin)) {
301
+ block(
302
+ "origin_not_allowed",
303
+ `${pageOrigin} isn't an allowed origin for this publishable key.`
304
+ );
305
+ }
306
+ }
307
+ function teardown() {
308
+ stopHello();
309
+ aborter.abort();
310
+ win.removeEventListener("message", onMessage);
311
+ iframe.removeEventListener("load", onLoad);
312
+ mobile?.removeEventListener("change", applySize);
313
+ iframe.remove();
314
+ }
315
+ function block(code, message) {
316
+ blocked = true;
317
+ teardown();
318
+ fail(code, message);
319
+ }
320
+ const subscribe = (set) => (listener) => {
321
+ if (destroyed) return () => void 0;
322
+ set.add(listener);
323
+ return () => {
324
+ set.delete(listener);
325
+ };
326
+ };
327
+ load("/");
328
+ container.append(iframe);
329
+ void checkOrigin();
330
+ return {
331
+ onTopUp: subscribe(listeners.topUp),
332
+ onCreditsChanged: subscribe(listeners.creditsChanged),
333
+ onExport: subscribe(listeners.export),
334
+ onClose: subscribe(listeners.close),
335
+ onError: subscribe(listeners.error),
336
+ open({ site }) {
337
+ if (typeof site !== "string" || site.length === 0) {
338
+ throw new TypeError("open: site must be a site id");
339
+ }
340
+ if (destroyed || blocked) return;
341
+ load(`/app/site/${encodeURIComponent(site)}`);
342
+ },
343
+ refreshCredits() {
344
+ if (destroyed || version === null) return;
345
+ post({ type: "credits.refresh" });
346
+ },
347
+ destroy() {
348
+ if (destroyed) return;
349
+ destroyed = true;
350
+ teardown();
351
+ for (const set of Object.values(listeners)) set.clear();
352
+ }
353
+ };
354
+ }
355
+
356
+ // src/index.ts
357
+ var Relume = {
358
+ /**
359
+ * Puts the Relume builder in `target` (a selector or an element) for the
360
+ * signed-in user, whose session comes from your session route.
361
+ */
362
+ init(target, publishableKey, options = {}) {
363
+ return createEmbed(target, publishableKey, options, {
364
+ window,
365
+ fetch: (input, init) => window.fetch(input, init)
366
+ });
367
+ }
368
+ };
369
+ export {
370
+ DEFAULT_EMBED_URL,
371
+ DEFAULT_SESSION_URL,
372
+ EMBED_HEADER,
373
+ EMBED_HEADER_VALUE,
374
+ Relume
375
+ };
376
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protocol.ts","../src/embed.ts","../src/index.ts"],"sourcesContent":["// The postMessage protocol with the builder in the iframe. It is internal to\n// Relume and this SDK, and mirrors frontend/base/embed/embed_protocol.ts in\n// the Relume app: change both together. Partners see only the events built\n// on it (docs/partner-protocol/v1.md section 11).\n\n/** Versions this SDK speaks, newest first. Offered in `hello`. */\nexport const PROTOCOL_VERSIONS: readonly number[] = [1];\n\nexport const SDK_SOURCE = 'relume-partner-embed';\nexport const APP_SOURCE = 'relume-embed';\n\n/** Embed error codes (protocol section 11). New codes may be added. */\nexport type EmbedErrorCode =\n | 'session_failed'\n | 'user_deactivated'\n | 'user_suspended'\n | 'partner_suspended'\n | 'origin_not_allowed'\n | 'test_cap_reached'\n | 'charge_failed';\n\n/** What the SDK tells the app. */\nexport type SdkMessage =\n /** Repeated until the app answers `ready`. */\n | { type: 'hello'; versions: readonly number[] }\n | { type: 'session'; clientSecret: string }\n /** The session route failed; `code` is its `error.code` when it had one. */\n | { type: 'session.error'; code: string }\n /** Re-read the balance (`credits.get`), answered with `credits.changed`. */\n | { type: 'credits.refresh' };\n\n/** What the app tells the SDK. */\nexport type AppMessage =\n | { type: 'ready'; version: number }\n | { type: 'session.request' }\n | { type: 'credits.changed'; balance: number | null }\n | { type: 'topup.required'; required: number; balance: number | null }\n | {\n type: 'export.ready';\n siteId: string;\n exportId: string;\n downloadUrl: string;\n }\n | { type: 'close' }\n /** `code` is a string, not EmbedErrorCode: the app may add codes first. */\n | { type: 'error'; code: string; message: string };\n\nexport function encodeSdkMessage(message: SdkMessage): Record<string, unknown> {\n return { source: SDK_SOURCE, ...message };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nconst isString = (value: unknown): value is string => typeof value === 'string';\nconst isBalance = (value: unknown): value is number | null =>\n value === null || (typeof value === 'number' && Number.isFinite(value));\n\n/**\n * The app's message, or null for anything else: other scripts' messages, a\n * malformed one, or one for a version other than `version` once agreed.\n */\nexport function parseAppMessage(\n data: unknown,\n version: number | null,\n): AppMessage | null {\n if (!isRecord(data) || data.source !== APP_SOURCE) return null;\n if (version !== null && data.version !== version) return null;\n switch (data.type) {\n case 'ready':\n // Only a version we offered in hello.\n return typeof data.version === 'number' &&\n PROTOCOL_VERSIONS.includes(data.version)\n ? { type: 'ready', version: data.version }\n : null;\n case 'session.request':\n return { type: 'session.request' };\n case 'credits.changed':\n return isBalance(data.balance)\n ? { type: 'credits.changed', balance: data.balance }\n : null;\n case 'topup.required':\n return typeof data.required === 'number' && isBalance(data.balance)\n ? {\n type: 'topup.required',\n required: data.required,\n balance: data.balance,\n }\n : null;\n case 'export.ready':\n return isString(data.siteId) &&\n isString(data.exportId) &&\n isString(data.downloadUrl)\n ? {\n type: 'export.ready',\n siteId: data.siteId,\n exportId: data.exportId,\n downloadUrl: data.downloadUrl,\n }\n : null;\n case 'close':\n return { type: 'close' };\n case 'error':\n return isString(data.code)\n ? {\n type: 'error',\n code: data.code,\n message: isString(data.message) ? data.message : data.code,\n }\n : null;\n default:\n return null;\n }\n}\n","import {\n encodeSdkMessage,\n parseAppMessage,\n PROTOCOL_VERSIONS,\n type AppMessage,\n type EmbedErrorCode,\n type SdkMessage,\n} from './protocol.ts';\n\nexport const DEFAULT_EMBED_URL = 'https://embed.relume.ai';\nexport const DEFAULT_SESSION_URL = '/relume/session';\n/** On every session request; `@relume_io/partner-node` refuses one without it. */\nexport const EMBED_HEADER = 'Relume-Embed';\nexport const EMBED_HEADER_VALUE = '1';\n\n/** The Relume API each embed host talks to, for the allowed-origins check. */\nconst API_URL_FOR_EMBED: Record<string, string> = {\n 'https://embed.relume.ai': 'https://apis.relume.io/nova',\n 'https://embed-staging.relume.ai': 'https://staging-apis.relume.io/nova',\n};\n\n/** The first wait before `hello` is repeated; it doubles up to the max. */\nexport const HELLO_RETRY_MS = 200;\nexport const HELLO_MAX_RETRY_MS = 3_200;\n/** The builder gives up on the handshake after 15 seconds; so do we, later. */\nexport const HELLO_TIMEOUT_MS = 20_000;\n/** Below this width the builder covers the whole viewport. */\nexport const MOBILE_QUERY = '(max-width: 640px)';\n\nconst PUBLISHABLE_KEY = /^pk_(?:test|live)_[A-Za-z0-9]+$/;\nconst MAX_ERROR_CODE_LENGTH = 64;\n\nexport interface RelumeEmbedOptions {\n /**\n * Your session route, mounted by `@relume_io/partner-node`. Defaults to\n * `/relume/session` on this page's origin. On another origin it is fetched\n * with credentials (cookies), so it must allow this page's origin in CORS.\n */\n sessionUrl?: string;\n /** Cover the viewport on narrow screens. Default true. */\n fullScreenOnMobile?: boolean;\n /** The builder's host. Defaults to production; Relume gives you staging's. */\n embedUrl?: string;\n /**\n * Relume's API, used to explain a refused frame. Inferred from `embedUrl`\n * for Relume's own hosts; without one the check is skipped.\n */\n apiUrl?: string;\n}\n\nexport interface TopUpEvent {\n /** The action's price, in your credits. */\n required: number;\n /** The balance your wallet last reported, or null if it didn't answer. */\n balance: number | null;\n}\n\nexport interface CreditsChangedEvent {\n balance: number;\n}\n\nexport interface ExportEvent {\n siteId: string;\n exportId: string;\n downloadUrl: string;\n}\n\nexport interface EmbedError {\n /** One of EmbedErrorCode; treat an unknown code like `session_failed`. */\n code: EmbedErrorCode | (string & {});\n message: string;\n}\n\nexport interface OpenOptions {\n /** A site id, from the partner API or an earlier `onExport`. */\n site: string;\n}\n\ntype Listener<T> = (event: T) => void;\ntype Unsubscribe = () => void;\n\nexport interface RelumeEmbed {\n /** A charge was declined: show your checkout, then call refreshCredits(). */\n onTopUp(listener: Listener<TopUpEvent>): Unsubscribe;\n /** The balance the builder shows changed. */\n onCreditsChanged(listener: Listener<CreditsChangedEvent>): Unsubscribe;\n /** An export is ready to download. */\n onExport(listener: Listener<ExportEvent>): Unsubscribe;\n /** The user closed the builder. */\n onClose(listener: Listener<void>): Unsubscribe;\n /** The builder couldn't start or carry on. Logged when nobody listens. */\n onError(listener: Listener<EmbedError>): Unsubscribe;\n /** Opens one of the user's sites. Reloads the builder. */\n open(options: OpenOptions): void;\n /** Re-reads the balance from your wallet, e.g. after a top-up. */\n refreshCredits(): void;\n /** Removes the iframe and every listener. The instance is then inert. */\n destroy(): void;\n}\n\n/** What the embed needs from the page; tests pass a fake. */\nexport interface EmbedEnvironment {\n window: Window;\n fetch: typeof fetch;\n}\n\ninterface Listeners {\n topUp: Set<Listener<TopUpEvent>>;\n creditsChanged: Set<Listener<CreditsChangedEvent>>;\n export: Set<Listener<ExportEvent>>;\n close: Set<Listener<void>>;\n error: Set<Listener<EmbedError>>;\n}\n\nfunction originOf(url: string, name: string): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new TypeError(`Relume.init: ${name} must be an absolute URL`);\n }\n return parsed.origin;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** `error.code` from a `{ error: { code } }` body, when it has one. */\nfunction errorCodeOf(body: unknown): string | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code } = body.error;\n return typeof code === 'string' &&\n code.length > 0 &&\n code.length <= MAX_ERROR_CODE_LENGTH\n ? code\n : null;\n}\n\nexport function createEmbed(\n target: string | Element,\n publishableKey: string,\n options: RelumeEmbedOptions,\n env: EmbedEnvironment,\n): RelumeEmbed {\n const { window: win } = env;\n const container =\n typeof target === 'string' ? win.document.querySelector(target) : target;\n if (container === null) {\n throw new TypeError(`Relume.init: no element matches \"${String(target)}\"`);\n }\n if (!PUBLISHABLE_KEY.test(publishableKey)) {\n throw new TypeError(\n 'Relume.init: the publishable key must look like pk_test_… or pk_live_…',\n );\n }\n\n const embedOrigin = originOf(\n options.embedUrl ?? DEFAULT_EMBED_URL,\n 'embedUrl',\n );\n const apiUrl = (\n options.apiUrl ??\n API_URL_FOR_EMBED[embedOrigin] ??\n ''\n ).replace(/\\/+$/, '');\n const sessionUrl = new URL(\n options.sessionUrl ?? DEFAULT_SESSION_URL,\n win.location.href,\n );\n const pageOrigin = win.location.origin;\n // Cookies go to a cross-origin session route only when asked for, which\n // `sessionUrl` on another origin is.\n const credentials: RequestCredentials =\n sessionUrl.origin === pageOrigin ? 'same-origin' : 'include';\n\n const listeners: Listeners = {\n topUp: new Set(),\n creditsChanged: new Set(),\n export: new Set(),\n close: new Set(),\n error: new Set(),\n };\n const aborter = new AbortController();\n let destroyed = false;\n // Origin check failed: the browser refuses the frame, so it is gone.\n let blocked = false;\n // Bumped on every load of the frame, so answers meant for a page that has\n // since been replaced (open()) are dropped rather than posted to the next.\n let generation = 0;\n let version: number | null = null;\n // Between load() and the new page's load event, any `ready` comes from the\n // page being replaced.\n let awaitingLoad = true;\n let helloTimer: ReturnType<Window['setTimeout']> | null = null;\n let helloDeadline: ReturnType<Window['setTimeout']> | null = null;\n let lastBalance: number | null = null;\n\n const iframe = win.document.createElement('iframe');\n iframe.title = 'Relume website builder';\n iframe.allow = 'clipboard-read; clipboard-write; fullscreen';\n iframe.style.border = '0';\n iframe.style.display = 'block';\n\n const mobile =\n options.fullScreenOnMobile === false || typeof win.matchMedia !== 'function'\n ? null\n : win.matchMedia(MOBILE_QUERY);\n const applySize = () => {\n // Fixed to all four edges, so the height follows the visible viewport\n // as mobile browser bars come and go.\n const edge = mobile?.matches === true ? '0' : '';\n Object.assign(iframe.style, {\n position: edge && 'fixed',\n top: edge,\n right: edge,\n bottom: edge,\n left: edge,\n zIndex: edge && '2147483647',\n width: '100%',\n height: '100%',\n });\n };\n applySize();\n mobile?.addEventListener('change', applySize);\n\n function emit<T>(set: Set<Listener<T>>, event: T): void {\n for (const listener of [...set]) {\n try {\n listener(event);\n } catch (error) {\n // A partner handler that throws must not stop the builder.\n console.error('[Relume] An event handler threw', error);\n }\n }\n }\n\n function fail(code: EmbedErrorCode | (string & {}), message: string): void {\n if (destroyed) return;\n if (listeners.error.size === 0) {\n console.error(`[Relume] ${code}: ${message}`);\n return;\n }\n emit(listeners.error, { code, message });\n }\n\n function post(message: SdkMessage): void {\n iframe.contentWindow?.postMessage(encodeSdkMessage(message), embedOrigin);\n }\n\n function stopHello(): void {\n if (helloTimer !== null) win.clearTimeout(helloTimer);\n if (helloDeadline !== null) win.clearTimeout(helloDeadline);\n helloTimer = null;\n helloDeadline = null;\n }\n\n function load(path: string): void {\n stopHello();\n generation += 1;\n version = null;\n awaitingLoad = true;\n const url = new URL(path, embedOrigin);\n url.searchParams.set('pk', publishableKey);\n iframe.src = url.toString();\n helloDeadline = win.setTimeout(() => {\n stopHello();\n fail('session_failed', \"The builder didn't answer.\");\n }, HELLO_TIMEOUT_MS);\n }\n\n // Hello waits for the new page: until then the frame still holds the page\n // being replaced, which would answer ready for it, and posting to a frame\n // on another origin only logs errors on the partner's console. The builder\n // listens before its load event, so repeats are a fallback that backs off.\n function sayHello(retryIn: number): void {\n post({ type: 'hello', versions: PROTOCOL_VERSIONS });\n helloTimer = win.setTimeout(\n () => sayHello(Math.min(retryIn * 2, HELLO_MAX_RETRY_MS)),\n retryIn,\n );\n }\n\n const onLoad = () => {\n // Ready already, or given up on this load.\n if (version !== null || helloDeadline === null) return;\n awaitingLoad = false;\n if (helloTimer !== null) win.clearTimeout(helloTimer);\n sayHello(HELLO_RETRY_MS);\n };\n iframe.addEventListener('load', onLoad);\n\n async function answerSessionRequest(forGeneration: number): Promise<void> {\n let message: SdkMessage;\n try {\n const response = await env.fetch(sessionUrl, {\n credentials,\n cache: 'no-store',\n // A custom header: cross-origin, the browser must preflight it, so\n // the route can tell this from a link or form on another site.\n headers: {\n accept: 'application/json',\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n },\n signal: aborter.signal,\n });\n const body: unknown = await response.json().catch(() => null);\n const clientSecret = isRecord(body) ? body.clientSecret : undefined;\n if (response.ok && typeof clientSecret === 'string' && clientSecret) {\n message = { type: 'session', clientSecret };\n } else {\n // The builder turns this into onError: the code when it is an embed\n // error code (user_deactivated, …), otherwise session_failed.\n message = {\n type: 'session.error',\n code:\n errorCodeOf(body) ??\n (response.status === 401 ? 'not_signed_in' : 'session_failed'),\n };\n }\n } catch {\n if (destroyed) return;\n message = { type: 'session.error', code: 'session_failed' };\n }\n if (destroyed || forGeneration !== generation) return;\n post(message);\n }\n\n function handle(message: AppMessage): void {\n if (version === null) {\n if (awaitingLoad) return;\n if (message.type === 'ready') {\n stopHello();\n version = message.version;\n } else if (message.type === 'error') {\n // The builder speaks no version we offered.\n stopHello();\n fail(message.code, message.message);\n }\n return;\n }\n switch (message.type) {\n case 'ready':\n return;\n case 'session.request':\n void answerSessionRequest(generation);\n return;\n case 'credits.changed':\n if (message.balance !== null && message.balance !== lastBalance) {\n lastBalance = message.balance;\n emit(listeners.creditsChanged, { balance: message.balance });\n }\n return;\n case 'topup.required':\n emit(listeners.topUp, {\n required: message.required,\n balance: message.balance,\n });\n return;\n case 'export.ready':\n emit(listeners.export, {\n siteId: message.siteId,\n exportId: message.exportId,\n downloadUrl: message.downloadUrl,\n });\n return;\n case 'close':\n emit(listeners.close, undefined);\n return;\n case 'error':\n fail(message.code, message.message);\n return;\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n // Only the builder, in our frame, on the embed origin. A message from any\n // other window or origin (other scripts, other frames) is ignored.\n if (\n event.origin !== embedOrigin ||\n event.source === null ||\n event.source !== iframe.contentWindow\n ) {\n return;\n }\n const message = parseAppMessage(event.data, version);\n if (message !== null) handle(message);\n };\n win.addEventListener('message', onMessage);\n\n // The browser refuses the frame when this page's origin isn't allowed\n // (frame-ancestors), and the frame can't say why, so ask the API.\n async function checkOrigin(): Promise<void> {\n if (!apiUrl) return;\n let response: Response;\n let body: unknown;\n try {\n response = await env.fetch(\n `${apiUrl}/embed/partner?pk=${encodeURIComponent(publishableKey)}`,\n { credentials: 'omit', signal: aborter.signal },\n );\n body = await response.json();\n } catch {\n // Unreachable: the builder's own check still stands.\n return;\n }\n if (destroyed) return;\n const code = errorCodeOf(body);\n if (code === 'unknown_publishable_key' || code === 'partner_suspended') {\n block(\n code === 'partner_suspended' ? 'partner_suspended' : 'session_failed',\n code === 'partner_suspended'\n ? 'This partner has been suspended.'\n : 'The publishable key is unknown, revoked or expired.',\n );\n return;\n }\n const allowed =\n response.ok && isRecord(body) && Array.isArray(body.allowedOrigins)\n ? body.allowedOrigins\n : null;\n if (allowed !== null && !allowed.includes(pageOrigin)) {\n block(\n 'origin_not_allowed',\n `${pageOrigin} isn't an allowed origin for this publishable key.`,\n );\n }\n }\n\n // Everything but the partner's handlers, which a blocked embed still needs\n // for the error it is about to report.\n function teardown(): void {\n stopHello();\n aborter.abort();\n win.removeEventListener('message', onMessage);\n iframe.removeEventListener('load', onLoad);\n mobile?.removeEventListener('change', applySize);\n iframe.remove();\n }\n\n function block(code: EmbedErrorCode, message: string): void {\n blocked = true;\n teardown();\n fail(code, message);\n }\n\n const subscribe =\n <T>(set: Set<Listener<T>>) =>\n (listener: Listener<T>): Unsubscribe => {\n if (destroyed) return () => undefined;\n set.add(listener);\n return () => {\n set.delete(listener);\n };\n };\n\n // src before append, so the frame never loads about:blank first.\n load('/');\n container.append(iframe);\n void checkOrigin();\n\n return {\n onTopUp: subscribe(listeners.topUp),\n onCreditsChanged: subscribe(listeners.creditsChanged),\n onExport: subscribe(listeners.export),\n onClose: subscribe(listeners.close),\n onError: subscribe(listeners.error),\n\n open({ site }) {\n if (typeof site !== 'string' || site.length === 0) {\n throw new TypeError('open: site must be a site id');\n }\n if (destroyed || blocked) return;\n load(`/app/site/${encodeURIComponent(site)}`);\n },\n\n refreshCredits() {\n // Before the handshake the builder hasn't read a balance yet; it will.\n if (destroyed || version === null) return;\n post({ type: 'credits.refresh' });\n },\n\n destroy() {\n if (destroyed) return;\n destroyed = true;\n teardown();\n for (const set of Object.values(listeners)) set.clear();\n },\n };\n}\n","import {\n createEmbed,\n type RelumeEmbed,\n type RelumeEmbedOptions,\n} from './embed.ts';\n\nexport const Relume = {\n /**\n * Puts the Relume builder in `target` (a selector or an element) for the\n * signed-in user, whose session comes from your session route.\n */\n init(\n target: string | Element,\n publishableKey: string,\n options: RelumeEmbedOptions = {},\n ): RelumeEmbed {\n return createEmbed(target, publishableKey, options, {\n window,\n fetch: (input, init) => window.fetch(input, init),\n });\n },\n};\n\nexport {\n DEFAULT_EMBED_URL,\n DEFAULT_SESSION_URL,\n EMBED_HEADER,\n EMBED_HEADER_VALUE,\n type CreditsChangedEvent,\n type EmbedError,\n type ExportEvent,\n type OpenOptions,\n type RelumeEmbed,\n type RelumeEmbedOptions,\n type TopUpEvent,\n} from './embed.ts';\nexport type { EmbedErrorCode } from './protocol.ts';\n"],"mappings":"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","names":["isRecord"]}
@@ -0,0 +1,2 @@
1
+ "use strict";(()=>{var k=[1],J="relume-partner-embed",K="relume-embed";function N(r){return{source:J,...r}}function ee(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}var y=r=>typeof r=="string",F=r=>r===null||typeof r=="number"&&Number.isFinite(r);function W(r,t){if(!ee(r)||r.source!==K||t!==null&&r.version!==t)return null;switch(r.type){case"ready":return typeof r.version=="number"&&k.includes(r.version)?{type:"ready",version:r.version}:null;case"session.request":return{type:"session.request"};case"credits.changed":return F(r.balance)?{type:"credits.changed",balance:r.balance}:null;case"topup.required":return typeof r.required=="number"&&F(r.balance)?{type:"topup.required",required:r.required,balance:r.balance}:null;case"export.ready":return y(r.siteId)&&y(r.exportId)&&y(r.downloadUrl)?{type:"export.ready",siteId:r.siteId,exportId:r.exportId,downloadUrl:r.downloadUrl}:null;case"close":return{type:"close"};case"error":return y(r.code)?{type:"error",code:r.code,message:y(r.message)?r.message:r.code}:null;default:return null}}var re="https://embed.relume.ai",ne="/relume/session",te="Relume-Embed",oe="1",se={"https://embed.relume.ai":"https://apis.relume.io/nova","https://embed-staging.relume.ai":"https://staging-apis.relume.io/nova"},ie=200,le=3200,de=2e4,ue="(max-width: 640px)",ce=/^pk_(?:test|live)_[A-Za-z0-9]+$/,pe=64;function ae(r,t){let d;try{d=new URL(r)}catch{throw new TypeError(`Relume.init: ${t} must be an absolute URL`)}return d.origin}function _(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function z(r){if(!_(r)||!_(r.error))return null;let{code:t}=r.error;return typeof t=="string"&&t.length>0&&t.length<=pe?t:null}function V(r,t,d,m){let{window:o}=m,C=typeof r=="string"?o.document.querySelector(r):r;if(C===null)throw new TypeError(`Relume.init: no element matches "${String(r)}"`);if(!ce.test(t))throw new TypeError("Relume.init: the publishable key must look like pk_test_\u2026 or pk_live_\u2026");let h=ae(d.embedUrl??re,"embedUrl"),M=(d.apiUrl??se[h]??"").replace(/\/+$/,""),A=new URL(d.sessionUrl??ne,o.location.href),R=o.location.origin,X=A.origin===R?"same-origin":"include",s={topUp:new Set,creditsChanged:new Set,export:new Set,close:new Set,error:new Set},v=new AbortController,u=!1,I=!1,U=0,p=null,x=!0,a=null,b=null,q=null,i=o.document.createElement("iframe");i.title="Relume website builder",i.allow="clipboard-read; clipboard-write; fullscreen",i.style.border="0",i.style.display="block";let L=d.fullScreenOnMobile===!1||typeof o.matchMedia!="function"?null:o.matchMedia(ue),S=()=>{let e=L?.matches===!0?"0":"";Object.assign(i.style,{position:e&&"fixed",top:e,right:e,bottom:e,left:e,zIndex:e&&"2147483647",width:"100%",height:"100%"})};S(),L?.addEventListener("change",S);function E(e,n){for(let l of[...e])try{l(n)}catch(c){console.error("[Relume] An event handler threw",c)}}function w(e,n){if(!u){if(s.error.size===0){console.error(`[Relume] ${e}: ${n}`);return}E(s.error,{code:e,message:n})}}function O(e){i.contentWindow?.postMessage(N(e),h)}function f(){a!==null&&o.clearTimeout(a),b!==null&&o.clearTimeout(b),a=null,b=null}function D(e){f(),U+=1,p=null,x=!0;let n=new URL(e,h);n.searchParams.set("pk",t),i.src=n.toString(),b=o.setTimeout(()=>{f(),w("session_failed","The builder didn't answer.")},de)}function B(e){O({type:"hello",versions:k}),a=o.setTimeout(()=>B(Math.min(e*2,le)),e)}let H=()=>{p!==null||b===null||(x=!1,a!==null&&o.clearTimeout(a),B(ie))};i.addEventListener("load",H);async function G(e){let n;try{let l=await m.fetch(A,{credentials:X,cache:"no-store",headers:{accept:"application/json",[te]:oe},signal:v.signal}),c=await l.json().catch(()=>null),T=_(c)?c.clientSecret:void 0;l.ok&&typeof T=="string"&&T?n={type:"session",clientSecret:T}:n={type:"session.error",code:z(c)??(l.status===401?"not_signed_in":"session_failed")}}catch{if(u)return;n={type:"session.error",code:"session_failed"}}u||e!==U||O(n)}function Q(e){if(p===null){if(x)return;e.type==="ready"?(f(),p=e.version):e.type==="error"&&(f(),w(e.code,e.message));return}switch(e.type){case"ready":return;case"session.request":G(U);return;case"credits.changed":e.balance!==null&&e.balance!==q&&(q=e.balance,E(s.creditsChanged,{balance:e.balance}));return;case"topup.required":E(s.topUp,{required:e.required,balance:e.balance});return;case"export.ready":E(s.export,{siteId:e.siteId,exportId:e.exportId,downloadUrl:e.downloadUrl});return;case"close":E(s.close,void 0);return;case"error":w(e.code,e.message);return}}let $=e=>{if(e.origin!==h||e.source===null||e.source!==i.contentWindow)return;let n=W(e.data,p);n!==null&&Q(n)};o.addEventListener("message",$);async function Z(){if(!M)return;let e,n;try{e=await m.fetch(`${M}/embed/partner?pk=${encodeURIComponent(t)}`,{credentials:"omit",signal:v.signal}),n=await e.json()}catch{return}if(u)return;let l=z(n);if(l==="unknown_publishable_key"||l==="partner_suspended"){j(l==="partner_suspended"?"partner_suspended":"session_failed",l==="partner_suspended"?"This partner has been suspended.":"The publishable key is unknown, revoked or expired.");return}let c=e.ok&&_(n)&&Array.isArray(n.allowedOrigins)?n.allowedOrigins:null;c!==null&&!c.includes(R)&&j("origin_not_allowed",`${R} isn't an allowed origin for this publishable key.`)}function P(){f(),v.abort(),o.removeEventListener("message",$),i.removeEventListener("load",H),L?.removeEventListener("change",S),i.remove()}function j(e,n){I=!0,P(),w(e,n)}let g=e=>n=>u?()=>{}:(e.add(n),()=>{e.delete(n)});return D("/"),C.append(i),Z(),{onTopUp:g(s.topUp),onCreditsChanged:g(s.creditsChanged),onExport:g(s.export),onClose:g(s.close),onError:g(s.error),open({site:e}){if(typeof e!="string"||e.length===0)throw new TypeError("open: site must be a site id");u||I||D(`/app/site/${encodeURIComponent(e)}`)},refreshCredits(){u||p===null||O({type:"credits.refresh"})},destroy(){if(!u){u=!0,P();for(let e of Object.values(s))e.clear()}}}}var Y={init(r,t,d={}){return V(r,t,d,{window,fetch:(m,o)=>window.fetch(m,o)})}};Object.assign(globalThis,{Relume:Y});})();
2
+ //# sourceMappingURL=relume-embed.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protocol.ts","../src/embed.ts","../src/index.ts","../src/global.ts"],"sourcesContent":["// The postMessage protocol with the builder in the iframe. It is internal to\n// Relume and this SDK, and mirrors frontend/base/embed/embed_protocol.ts in\n// the Relume app: change both together. Partners see only the events built\n// on it (docs/partner-protocol/v1.md section 11).\n\n/** Versions this SDK speaks, newest first. Offered in `hello`. */\nexport const PROTOCOL_VERSIONS: readonly number[] = [1];\n\nexport const SDK_SOURCE = 'relume-partner-embed';\nexport const APP_SOURCE = 'relume-embed';\n\n/** Embed error codes (protocol section 11). New codes may be added. */\nexport type EmbedErrorCode =\n | 'session_failed'\n | 'user_deactivated'\n | 'user_suspended'\n | 'partner_suspended'\n | 'origin_not_allowed'\n | 'test_cap_reached'\n | 'charge_failed';\n\n/** What the SDK tells the app. */\nexport type SdkMessage =\n /** Repeated until the app answers `ready`. */\n | { type: 'hello'; versions: readonly number[] }\n | { type: 'session'; clientSecret: string }\n /** The session route failed; `code` is its `error.code` when it had one. */\n | { type: 'session.error'; code: string }\n /** Re-read the balance (`credits.get`), answered with `credits.changed`. */\n | { type: 'credits.refresh' };\n\n/** What the app tells the SDK. */\nexport type AppMessage =\n | { type: 'ready'; version: number }\n | { type: 'session.request' }\n | { type: 'credits.changed'; balance: number | null }\n | { type: 'topup.required'; required: number; balance: number | null }\n | {\n type: 'export.ready';\n siteId: string;\n exportId: string;\n downloadUrl: string;\n }\n | { type: 'close' }\n /** `code` is a string, not EmbedErrorCode: the app may add codes first. */\n | { type: 'error'; code: string; message: string };\n\nexport function encodeSdkMessage(message: SdkMessage): Record<string, unknown> {\n return { source: SDK_SOURCE, ...message };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nconst isString = (value: unknown): value is string => typeof value === 'string';\nconst isBalance = (value: unknown): value is number | null =>\n value === null || (typeof value === 'number' && Number.isFinite(value));\n\n/**\n * The app's message, or null for anything else: other scripts' messages, a\n * malformed one, or one for a version other than `version` once agreed.\n */\nexport function parseAppMessage(\n data: unknown,\n version: number | null,\n): AppMessage | null {\n if (!isRecord(data) || data.source !== APP_SOURCE) return null;\n if (version !== null && data.version !== version) return null;\n switch (data.type) {\n case 'ready':\n // Only a version we offered in hello.\n return typeof data.version === 'number' &&\n PROTOCOL_VERSIONS.includes(data.version)\n ? { type: 'ready', version: data.version }\n : null;\n case 'session.request':\n return { type: 'session.request' };\n case 'credits.changed':\n return isBalance(data.balance)\n ? { type: 'credits.changed', balance: data.balance }\n : null;\n case 'topup.required':\n return typeof data.required === 'number' && isBalance(data.balance)\n ? {\n type: 'topup.required',\n required: data.required,\n balance: data.balance,\n }\n : null;\n case 'export.ready':\n return isString(data.siteId) &&\n isString(data.exportId) &&\n isString(data.downloadUrl)\n ? {\n type: 'export.ready',\n siteId: data.siteId,\n exportId: data.exportId,\n downloadUrl: data.downloadUrl,\n }\n : null;\n case 'close':\n return { type: 'close' };\n case 'error':\n return isString(data.code)\n ? {\n type: 'error',\n code: data.code,\n message: isString(data.message) ? data.message : data.code,\n }\n : null;\n default:\n return null;\n }\n}\n","import {\n encodeSdkMessage,\n parseAppMessage,\n PROTOCOL_VERSIONS,\n type AppMessage,\n type EmbedErrorCode,\n type SdkMessage,\n} from './protocol.ts';\n\nexport const DEFAULT_EMBED_URL = 'https://embed.relume.ai';\nexport const DEFAULT_SESSION_URL = '/relume/session';\n/** On every session request; `@relume_io/partner-node` refuses one without it. */\nexport const EMBED_HEADER = 'Relume-Embed';\nexport const EMBED_HEADER_VALUE = '1';\n\n/** The Relume API each embed host talks to, for the allowed-origins check. */\nconst API_URL_FOR_EMBED: Record<string, string> = {\n 'https://embed.relume.ai': 'https://apis.relume.io/nova',\n 'https://embed-staging.relume.ai': 'https://staging-apis.relume.io/nova',\n};\n\n/** The first wait before `hello` is repeated; it doubles up to the max. */\nexport const HELLO_RETRY_MS = 200;\nexport const HELLO_MAX_RETRY_MS = 3_200;\n/** The builder gives up on the handshake after 15 seconds; so do we, later. */\nexport const HELLO_TIMEOUT_MS = 20_000;\n/** Below this width the builder covers the whole viewport. */\nexport const MOBILE_QUERY = '(max-width: 640px)';\n\nconst PUBLISHABLE_KEY = /^pk_(?:test|live)_[A-Za-z0-9]+$/;\nconst MAX_ERROR_CODE_LENGTH = 64;\n\nexport interface RelumeEmbedOptions {\n /**\n * Your session route, mounted by `@relume_io/partner-node`. Defaults to\n * `/relume/session` on this page's origin. On another origin it is fetched\n * with credentials (cookies), so it must allow this page's origin in CORS.\n */\n sessionUrl?: string;\n /** Cover the viewport on narrow screens. Default true. */\n fullScreenOnMobile?: boolean;\n /** The builder's host. Defaults to production; Relume gives you staging's. */\n embedUrl?: string;\n /**\n * Relume's API, used to explain a refused frame. Inferred from `embedUrl`\n * for Relume's own hosts; without one the check is skipped.\n */\n apiUrl?: string;\n}\n\nexport interface TopUpEvent {\n /** The action's price, in your credits. */\n required: number;\n /** The balance your wallet last reported, or null if it didn't answer. */\n balance: number | null;\n}\n\nexport interface CreditsChangedEvent {\n balance: number;\n}\n\nexport interface ExportEvent {\n siteId: string;\n exportId: string;\n downloadUrl: string;\n}\n\nexport interface EmbedError {\n /** One of EmbedErrorCode; treat an unknown code like `session_failed`. */\n code: EmbedErrorCode | (string & {});\n message: string;\n}\n\nexport interface OpenOptions {\n /** A site id, from the partner API or an earlier `onExport`. */\n site: string;\n}\n\ntype Listener<T> = (event: T) => void;\ntype Unsubscribe = () => void;\n\nexport interface RelumeEmbed {\n /** A charge was declined: show your checkout, then call refreshCredits(). */\n onTopUp(listener: Listener<TopUpEvent>): Unsubscribe;\n /** The balance the builder shows changed. */\n onCreditsChanged(listener: Listener<CreditsChangedEvent>): Unsubscribe;\n /** An export is ready to download. */\n onExport(listener: Listener<ExportEvent>): Unsubscribe;\n /** The user closed the builder. */\n onClose(listener: Listener<void>): Unsubscribe;\n /** The builder couldn't start or carry on. Logged when nobody listens. */\n onError(listener: Listener<EmbedError>): Unsubscribe;\n /** Opens one of the user's sites. Reloads the builder. */\n open(options: OpenOptions): void;\n /** Re-reads the balance from your wallet, e.g. after a top-up. */\n refreshCredits(): void;\n /** Removes the iframe and every listener. The instance is then inert. */\n destroy(): void;\n}\n\n/** What the embed needs from the page; tests pass a fake. */\nexport interface EmbedEnvironment {\n window: Window;\n fetch: typeof fetch;\n}\n\ninterface Listeners {\n topUp: Set<Listener<TopUpEvent>>;\n creditsChanged: Set<Listener<CreditsChangedEvent>>;\n export: Set<Listener<ExportEvent>>;\n close: Set<Listener<void>>;\n error: Set<Listener<EmbedError>>;\n}\n\nfunction originOf(url: string, name: string): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new TypeError(`Relume.init: ${name} must be an absolute URL`);\n }\n return parsed.origin;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** `error.code` from a `{ error: { code } }` body, when it has one. */\nfunction errorCodeOf(body: unknown): string | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code } = body.error;\n return typeof code === 'string' &&\n code.length > 0 &&\n code.length <= MAX_ERROR_CODE_LENGTH\n ? code\n : null;\n}\n\nexport function createEmbed(\n target: string | Element,\n publishableKey: string,\n options: RelumeEmbedOptions,\n env: EmbedEnvironment,\n): RelumeEmbed {\n const { window: win } = env;\n const container =\n typeof target === 'string' ? win.document.querySelector(target) : target;\n if (container === null) {\n throw new TypeError(`Relume.init: no element matches \"${String(target)}\"`);\n }\n if (!PUBLISHABLE_KEY.test(publishableKey)) {\n throw new TypeError(\n 'Relume.init: the publishable key must look like pk_test_… or pk_live_…',\n );\n }\n\n const embedOrigin = originOf(\n options.embedUrl ?? DEFAULT_EMBED_URL,\n 'embedUrl',\n );\n const apiUrl = (\n options.apiUrl ??\n API_URL_FOR_EMBED[embedOrigin] ??\n ''\n ).replace(/\\/+$/, '');\n const sessionUrl = new URL(\n options.sessionUrl ?? DEFAULT_SESSION_URL,\n win.location.href,\n );\n const pageOrigin = win.location.origin;\n // Cookies go to a cross-origin session route only when asked for, which\n // `sessionUrl` on another origin is.\n const credentials: RequestCredentials =\n sessionUrl.origin === pageOrigin ? 'same-origin' : 'include';\n\n const listeners: Listeners = {\n topUp: new Set(),\n creditsChanged: new Set(),\n export: new Set(),\n close: new Set(),\n error: new Set(),\n };\n const aborter = new AbortController();\n let destroyed = false;\n // Origin check failed: the browser refuses the frame, so it is gone.\n let blocked = false;\n // Bumped on every load of the frame, so answers meant for a page that has\n // since been replaced (open()) are dropped rather than posted to the next.\n let generation = 0;\n let version: number | null = null;\n // Between load() and the new page's load event, any `ready` comes from the\n // page being replaced.\n let awaitingLoad = true;\n let helloTimer: ReturnType<Window['setTimeout']> | null = null;\n let helloDeadline: ReturnType<Window['setTimeout']> | null = null;\n let lastBalance: number | null = null;\n\n const iframe = win.document.createElement('iframe');\n iframe.title = 'Relume website builder';\n iframe.allow = 'clipboard-read; clipboard-write; fullscreen';\n iframe.style.border = '0';\n iframe.style.display = 'block';\n\n const mobile =\n options.fullScreenOnMobile === false || typeof win.matchMedia !== 'function'\n ? null\n : win.matchMedia(MOBILE_QUERY);\n const applySize = () => {\n // Fixed to all four edges, so the height follows the visible viewport\n // as mobile browser bars come and go.\n const edge = mobile?.matches === true ? '0' : '';\n Object.assign(iframe.style, {\n position: edge && 'fixed',\n top: edge,\n right: edge,\n bottom: edge,\n left: edge,\n zIndex: edge && '2147483647',\n width: '100%',\n height: '100%',\n });\n };\n applySize();\n mobile?.addEventListener('change', applySize);\n\n function emit<T>(set: Set<Listener<T>>, event: T): void {\n for (const listener of [...set]) {\n try {\n listener(event);\n } catch (error) {\n // A partner handler that throws must not stop the builder.\n console.error('[Relume] An event handler threw', error);\n }\n }\n }\n\n function fail(code: EmbedErrorCode | (string & {}), message: string): void {\n if (destroyed) return;\n if (listeners.error.size === 0) {\n console.error(`[Relume] ${code}: ${message}`);\n return;\n }\n emit(listeners.error, { code, message });\n }\n\n function post(message: SdkMessage): void {\n iframe.contentWindow?.postMessage(encodeSdkMessage(message), embedOrigin);\n }\n\n function stopHello(): void {\n if (helloTimer !== null) win.clearTimeout(helloTimer);\n if (helloDeadline !== null) win.clearTimeout(helloDeadline);\n helloTimer = null;\n helloDeadline = null;\n }\n\n function load(path: string): void {\n stopHello();\n generation += 1;\n version = null;\n awaitingLoad = true;\n const url = new URL(path, embedOrigin);\n url.searchParams.set('pk', publishableKey);\n iframe.src = url.toString();\n helloDeadline = win.setTimeout(() => {\n stopHello();\n fail('session_failed', \"The builder didn't answer.\");\n }, HELLO_TIMEOUT_MS);\n }\n\n // Hello waits for the new page: until then the frame still holds the page\n // being replaced, which would answer ready for it, and posting to a frame\n // on another origin only logs errors on the partner's console. The builder\n // listens before its load event, so repeats are a fallback that backs off.\n function sayHello(retryIn: number): void {\n post({ type: 'hello', versions: PROTOCOL_VERSIONS });\n helloTimer = win.setTimeout(\n () => sayHello(Math.min(retryIn * 2, HELLO_MAX_RETRY_MS)),\n retryIn,\n );\n }\n\n const onLoad = () => {\n // Ready already, or given up on this load.\n if (version !== null || helloDeadline === null) return;\n awaitingLoad = false;\n if (helloTimer !== null) win.clearTimeout(helloTimer);\n sayHello(HELLO_RETRY_MS);\n };\n iframe.addEventListener('load', onLoad);\n\n async function answerSessionRequest(forGeneration: number): Promise<void> {\n let message: SdkMessage;\n try {\n const response = await env.fetch(sessionUrl, {\n credentials,\n cache: 'no-store',\n // A custom header: cross-origin, the browser must preflight it, so\n // the route can tell this from a link or form on another site.\n headers: {\n accept: 'application/json',\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n },\n signal: aborter.signal,\n });\n const body: unknown = await response.json().catch(() => null);\n const clientSecret = isRecord(body) ? body.clientSecret : undefined;\n if (response.ok && typeof clientSecret === 'string' && clientSecret) {\n message = { type: 'session', clientSecret };\n } else {\n // The builder turns this into onError: the code when it is an embed\n // error code (user_deactivated, …), otherwise session_failed.\n message = {\n type: 'session.error',\n code:\n errorCodeOf(body) ??\n (response.status === 401 ? 'not_signed_in' : 'session_failed'),\n };\n }\n } catch {\n if (destroyed) return;\n message = { type: 'session.error', code: 'session_failed' };\n }\n if (destroyed || forGeneration !== generation) return;\n post(message);\n }\n\n function handle(message: AppMessage): void {\n if (version === null) {\n if (awaitingLoad) return;\n if (message.type === 'ready') {\n stopHello();\n version = message.version;\n } else if (message.type === 'error') {\n // The builder speaks no version we offered.\n stopHello();\n fail(message.code, message.message);\n }\n return;\n }\n switch (message.type) {\n case 'ready':\n return;\n case 'session.request':\n void answerSessionRequest(generation);\n return;\n case 'credits.changed':\n if (message.balance !== null && message.balance !== lastBalance) {\n lastBalance = message.balance;\n emit(listeners.creditsChanged, { balance: message.balance });\n }\n return;\n case 'topup.required':\n emit(listeners.topUp, {\n required: message.required,\n balance: message.balance,\n });\n return;\n case 'export.ready':\n emit(listeners.export, {\n siteId: message.siteId,\n exportId: message.exportId,\n downloadUrl: message.downloadUrl,\n });\n return;\n case 'close':\n emit(listeners.close, undefined);\n return;\n case 'error':\n fail(message.code, message.message);\n return;\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n // Only the builder, in our frame, on the embed origin. A message from any\n // other window or origin (other scripts, other frames) is ignored.\n if (\n event.origin !== embedOrigin ||\n event.source === null ||\n event.source !== iframe.contentWindow\n ) {\n return;\n }\n const message = parseAppMessage(event.data, version);\n if (message !== null) handle(message);\n };\n win.addEventListener('message', onMessage);\n\n // The browser refuses the frame when this page's origin isn't allowed\n // (frame-ancestors), and the frame can't say why, so ask the API.\n async function checkOrigin(): Promise<void> {\n if (!apiUrl) return;\n let response: Response;\n let body: unknown;\n try {\n response = await env.fetch(\n `${apiUrl}/embed/partner?pk=${encodeURIComponent(publishableKey)}`,\n { credentials: 'omit', signal: aborter.signal },\n );\n body = await response.json();\n } catch {\n // Unreachable: the builder's own check still stands.\n return;\n }\n if (destroyed) return;\n const code = errorCodeOf(body);\n if (code === 'unknown_publishable_key' || code === 'partner_suspended') {\n block(\n code === 'partner_suspended' ? 'partner_suspended' : 'session_failed',\n code === 'partner_suspended'\n ? 'This partner has been suspended.'\n : 'The publishable key is unknown, revoked or expired.',\n );\n return;\n }\n const allowed =\n response.ok && isRecord(body) && Array.isArray(body.allowedOrigins)\n ? body.allowedOrigins\n : null;\n if (allowed !== null && !allowed.includes(pageOrigin)) {\n block(\n 'origin_not_allowed',\n `${pageOrigin} isn't an allowed origin for this publishable key.`,\n );\n }\n }\n\n // Everything but the partner's handlers, which a blocked embed still needs\n // for the error it is about to report.\n function teardown(): void {\n stopHello();\n aborter.abort();\n win.removeEventListener('message', onMessage);\n iframe.removeEventListener('load', onLoad);\n mobile?.removeEventListener('change', applySize);\n iframe.remove();\n }\n\n function block(code: EmbedErrorCode, message: string): void {\n blocked = true;\n teardown();\n fail(code, message);\n }\n\n const subscribe =\n <T>(set: Set<Listener<T>>) =>\n (listener: Listener<T>): Unsubscribe => {\n if (destroyed) return () => undefined;\n set.add(listener);\n return () => {\n set.delete(listener);\n };\n };\n\n // src before append, so the frame never loads about:blank first.\n load('/');\n container.append(iframe);\n void checkOrigin();\n\n return {\n onTopUp: subscribe(listeners.topUp),\n onCreditsChanged: subscribe(listeners.creditsChanged),\n onExport: subscribe(listeners.export),\n onClose: subscribe(listeners.close),\n onError: subscribe(listeners.error),\n\n open({ site }) {\n if (typeof site !== 'string' || site.length === 0) {\n throw new TypeError('open: site must be a site id');\n }\n if (destroyed || blocked) return;\n load(`/app/site/${encodeURIComponent(site)}`);\n },\n\n refreshCredits() {\n // Before the handshake the builder hasn't read a balance yet; it will.\n if (destroyed || version === null) return;\n post({ type: 'credits.refresh' });\n },\n\n destroy() {\n if (destroyed) return;\n destroyed = true;\n teardown();\n for (const set of Object.values(listeners)) set.clear();\n },\n };\n}\n","import {\n createEmbed,\n type RelumeEmbed,\n type RelumeEmbedOptions,\n} from './embed.ts';\n\nexport const Relume = {\n /**\n * Puts the Relume builder in `target` (a selector or an element) for the\n * signed-in user, whose session comes from your session route.\n */\n init(\n target: string | Element,\n publishableKey: string,\n options: RelumeEmbedOptions = {},\n ): RelumeEmbed {\n return createEmbed(target, publishableKey, options, {\n window,\n fetch: (input, init) => window.fetch(input, init),\n });\n },\n};\n\nexport {\n DEFAULT_EMBED_URL,\n DEFAULT_SESSION_URL,\n EMBED_HEADER,\n EMBED_HEADER_VALUE,\n type CreditsChangedEvent,\n type EmbedError,\n type ExportEvent,\n type OpenOptions,\n type RelumeEmbed,\n type RelumeEmbedOptions,\n type TopUpEvent,\n} from './embed.ts';\nexport type { EmbedErrorCode } from './protocol.ts';\n","// The script-tag build: `<script src=\".../relume-embed.global.js\">` defines\n// `window.Relume`, the same object the ESM build exports.\nimport { Relume } from './index.ts';\n\nObject.assign(globalThis, { Relume });\n"],"mappings":"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","names":["PROTOCOL_VERSIONS","SDK_SOURCE","APP_SOURCE","encodeSdkMessage","message","isRecord","value","isString","isBalance","parseAppMessage","data","version","DEFAULT_EMBED_URL","DEFAULT_SESSION_URL","EMBED_HEADER","EMBED_HEADER_VALUE","API_URL_FOR_EMBED","HELLO_RETRY_MS","HELLO_MAX_RETRY_MS","HELLO_TIMEOUT_MS","MOBILE_QUERY","PUBLISHABLE_KEY","MAX_ERROR_CODE_LENGTH","originOf","url","name","parsed","isRecord","value","errorCodeOf","body","code","createEmbed","target","publishableKey","options","env","win","container","embedOrigin","apiUrl","sessionUrl","pageOrigin","credentials","listeners","aborter","destroyed","blocked","generation","version","awaitingLoad","helloTimer","helloDeadline","lastBalance","iframe","mobile","applySize","edge","emit","set","event","listener","error","fail","message","post","encodeSdkMessage","stopHello","load","path","sayHello","retryIn","PROTOCOL_VERSIONS","onLoad","answerSessionRequest","forGeneration","response","clientSecret","handle","onMessage","parseAppMessage","checkOrigin","block","allowed","teardown","subscribe","site","Relume","target","publishableKey","options","createEmbed","input","init","Relume"]}
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@relume_io/partner-embed",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0",
4
+ "description": "Frontend SDK for Relume partners: put the Relume builder on your page with one call and a few event handlers.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://apis.relume.io/nova/docs/partners",
7
+ "type": "module",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./script": "./dist/relume-embed.global.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "unpkg": "./dist/relume-embed.global.js",
19
+ "jsdelivr": "./dist/relume-embed.global.js",
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "sideEffects": [
25
+ "./dist/relume-embed.global.js"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsup",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "node --test 'test/**/*.test.ts'",
34
+ "test:dist": "npm run build && node --test test-dist/*.test.mjs",
35
+ "prepublishOnly": "npm run typecheck && npm test && npm run test:dist"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.5",
39
+ "happy-dom": "^21.0.4",
40
+ "tsup": "^8.5.1",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }