@relume_io/partner-embed 0.0.0-stage → 1.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 +128 -2
- package/dist/index.d.ts +75 -0
- package/dist/index.js +367 -0
- package/dist/index.js.map +1 -0
- package/dist/relume-embed.global.js +2 -0
- package/dist/relume-embed.global.js.map +1 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @relume_io/partner-embed
|
|
2
2
|
|
|
3
|
-
|
|
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@1.0.0/dist/relume-embed.global.js"
|
|
29
|
+
integrity="sha384-6oARnUlX7kgZVZFq38Xa1n2x9bNPVb4AjfO/XrJ3HaUxRy/ICJVW51VDjIlwwpdk"
|
|
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
|
+
- Accepts messages only from the builder's own window on the embed origin, and
|
|
66
|
+
posts only to that origin. Messages from other scripts or frames are ignored.
|
|
67
|
+
- Agrees a protocol version with the builder, so either side can change its
|
|
68
|
+
messages without breaking the other.
|
|
69
|
+
- Checks your page's origin against the publishable key's allowed origins. The
|
|
70
|
+
browser refuses the frame on any other origin, and the frame can't say why,
|
|
71
|
+
so the SDK fires `onError({ code: 'origin_not_allowed' })` and removes it.
|
|
72
|
+
|
|
73
|
+
## `Relume.init(target, publishableKey, options?)`
|
|
74
|
+
|
|
75
|
+
`target` is a selector or an element. Throws a `TypeError` if it matches
|
|
76
|
+
nothing or the key isn't a `pk_test_…`/`pk_live_…` key.
|
|
77
|
+
|
|
78
|
+
| Option | Default | What it does |
|
|
79
|
+
| -------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `sessionUrl` | `/relume/session` on your page | Your session route. On another origin it is fetched with credentials, so its CORS must allow your page's origin exactly (never `*`). |
|
|
81
|
+
| `fullScreenOnMobile` | `true` | Cover the viewport below 640px. |
|
|
82
|
+
| `embedUrl` | `https://embed.relume.ai` | The builder's host. Relume gives you staging's (`https://embed-staging.relume.ai`). |
|
|
83
|
+
| `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. |
|
|
84
|
+
|
|
85
|
+
## Events
|
|
86
|
+
|
|
87
|
+
Each `on…` method adds a listener and returns a function that removes it.
|
|
88
|
+
|
|
89
|
+
| Event | Payload | When |
|
|
90
|
+
| ------------------ | ----------------------------------- | ---------------------------------------------------------------------------------- |
|
|
91
|
+
| `onTopUp` | `{ required, balance }` | A charge was declined. `required` is the action's price in your credits. |
|
|
92
|
+
| `onCreditsChanged` | `{ balance }` | The balance the builder shows changed. Not fired when your wallet didn't answer. |
|
|
93
|
+
| `onExport` | `{ siteId, exportId, downloadUrl }` | An export is ready. |
|
|
94
|
+
| `onClose` | none | The user closed the builder. |
|
|
95
|
+
| `onError` | `{ code, message }` | The builder couldn't start or carry on. Logged to the console when nobody listens. |
|
|
96
|
+
|
|
97
|
+
Error codes: `session_failed`, `user_deactivated`, `user_suspended`,
|
|
98
|
+
`partner_suspended`, `origin_not_allowed`, `test_cap_reached` and
|
|
99
|
+
`charge_failed` ([protocol section 11](../../docs/partner-protocol/v1.md#embed-error-codes)).
|
|
100
|
+
New codes may be added; treat an unknown one like `session_failed`.
|
|
101
|
+
|
|
102
|
+
A handler that throws is logged and doesn't stop the others.
|
|
103
|
+
|
|
104
|
+
## Methods
|
|
105
|
+
|
|
106
|
+
- `open({ site })` opens one of the user's sites (a `siteId` from the partner
|
|
107
|
+
API or `onExport`). The builder reloads on that site.
|
|
108
|
+
- `refreshCredits()` asks the builder to re-read the balance from your wallet
|
|
109
|
+
(`credits.get`), then fires `onCreditsChanged` if it changed. Call it after a
|
|
110
|
+
top-up.
|
|
111
|
+
- `destroy()` removes the iframe and every listener, and stops any pending
|
|
112
|
+
session request. The instance does nothing afterwards; call `Relume.init`
|
|
113
|
+
again to show the builder again.
|
|
114
|
+
|
|
115
|
+
## Developing
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
npm ci
|
|
119
|
+
npm run typecheck
|
|
120
|
+
npm test # unit tests (happy-dom) and the example's size
|
|
121
|
+
npm run test:dist # builds, then loads the ESM and script-tag output
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The `postMessage` messages live in [`src/protocol.ts`](./src/protocol.ts) and
|
|
125
|
+
mirror `frontend/base/embed/embed_protocol.ts` in the Relume app. Change both
|
|
126
|
+
together.
|
|
127
|
+
|
|
128
|
+
Releases go out from CI on a `partner-sdk/v*` tag, together with
|
|
129
|
+
`@relume_io/partner-node`: see [Releasing](../README.md#releasing).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
interface RelumeEmbedOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Your session route, mounted by `@relume_io/partner-node`. Defaults to
|
|
9
|
+
* `/relume/session` on this page's origin. On another origin it is fetched
|
|
10
|
+
* with credentials (cookies), so it must allow this page's origin in CORS.
|
|
11
|
+
*/
|
|
12
|
+
sessionUrl?: string;
|
|
13
|
+
/** Cover the viewport on narrow screens. Default true. */
|
|
14
|
+
fullScreenOnMobile?: boolean;
|
|
15
|
+
/** The builder's host. Defaults to production; Relume gives you staging's. */
|
|
16
|
+
embedUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Relume's API, used to explain a refused frame. Inferred from `embedUrl`
|
|
19
|
+
* for Relume's own hosts; without one the check is skipped.
|
|
20
|
+
*/
|
|
21
|
+
apiUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
interface TopUpEvent {
|
|
24
|
+
/** The action's price, in your credits. */
|
|
25
|
+
required: number;
|
|
26
|
+
/** The balance your wallet last reported, or null if it didn't answer. */
|
|
27
|
+
balance: number | null;
|
|
28
|
+
}
|
|
29
|
+
interface CreditsChangedEvent {
|
|
30
|
+
balance: number;
|
|
31
|
+
}
|
|
32
|
+
interface ExportEvent {
|
|
33
|
+
siteId: string;
|
|
34
|
+
exportId: string;
|
|
35
|
+
downloadUrl: string;
|
|
36
|
+
}
|
|
37
|
+
interface EmbedError {
|
|
38
|
+
/** One of EmbedErrorCode; treat an unknown code like `session_failed`. */
|
|
39
|
+
code: EmbedErrorCode | (string & {});
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
interface OpenOptions {
|
|
43
|
+
/** A site id, from the partner API or an earlier `onExport`. */
|
|
44
|
+
site: string;
|
|
45
|
+
}
|
|
46
|
+
type Listener<T> = (event: T) => void;
|
|
47
|
+
type Unsubscribe = () => void;
|
|
48
|
+
interface RelumeEmbed {
|
|
49
|
+
/** A charge was declined: show your checkout, then call refreshCredits(). */
|
|
50
|
+
onTopUp(listener: Listener<TopUpEvent>): Unsubscribe;
|
|
51
|
+
/** The balance the builder shows changed. */
|
|
52
|
+
onCreditsChanged(listener: Listener<CreditsChangedEvent>): Unsubscribe;
|
|
53
|
+
/** An export is ready to download. */
|
|
54
|
+
onExport(listener: Listener<ExportEvent>): Unsubscribe;
|
|
55
|
+
/** The user closed the builder. */
|
|
56
|
+
onClose(listener: Listener<void>): Unsubscribe;
|
|
57
|
+
/** The builder couldn't start or carry on. Logged when nobody listens. */
|
|
58
|
+
onError(listener: Listener<EmbedError>): Unsubscribe;
|
|
59
|
+
/** Opens one of the user's sites. Reloads the builder. */
|
|
60
|
+
open(options: OpenOptions): void;
|
|
61
|
+
/** Re-reads the balance from your wallet, e.g. after a top-up. */
|
|
62
|
+
refreshCredits(): void;
|
|
63
|
+
/** Removes the iframe and every listener. The instance is then inert. */
|
|
64
|
+
destroy(): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare const Relume: {
|
|
68
|
+
/**
|
|
69
|
+
* Puts the Relume builder in `target` (a selector or an element) for the
|
|
70
|
+
* signed-in user, whose session comes from your session route.
|
|
71
|
+
*/
|
|
72
|
+
init(target: string | Element, publishableKey: string, options?: RelumeEmbedOptions): RelumeEmbed;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { type CreditsChangedEvent, DEFAULT_EMBED_URL, DEFAULT_SESSION_URL, type EmbedError, type EmbedErrorCode, type ExportEvent, type OpenOptions, Relume, type RelumeEmbed, type RelumeEmbedOptions, type TopUpEvent };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
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 API_URL_FOR_EMBED = {
|
|
53
|
+
"https://embed.relume.ai": "https://apis.relume.io/nova",
|
|
54
|
+
"https://embed-staging.relume.ai": "https://staging-apis.relume.io/nova"
|
|
55
|
+
};
|
|
56
|
+
var HELLO_RETRY_MS = 200;
|
|
57
|
+
var HELLO_MAX_RETRY_MS = 3200;
|
|
58
|
+
var HELLO_TIMEOUT_MS = 2e4;
|
|
59
|
+
var MOBILE_QUERY = "(max-width: 640px)";
|
|
60
|
+
var PUBLISHABLE_KEY = /^pk_(?:test|live)_[A-Za-z0-9]+$/;
|
|
61
|
+
var MAX_ERROR_CODE_LENGTH = 64;
|
|
62
|
+
function originOf(url, name) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(url);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new TypeError(`Relume.init: ${name} must be an absolute URL`);
|
|
68
|
+
}
|
|
69
|
+
return parsed.origin;
|
|
70
|
+
}
|
|
71
|
+
function isRecord2(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function errorCodeOf(body) {
|
|
75
|
+
if (!isRecord2(body) || !isRecord2(body.error)) return null;
|
|
76
|
+
const { code } = body.error;
|
|
77
|
+
return typeof code === "string" && code.length > 0 && code.length <= MAX_ERROR_CODE_LENGTH ? code : null;
|
|
78
|
+
}
|
|
79
|
+
function createEmbed(target, publishableKey, options, env) {
|
|
80
|
+
const { window: win } = env;
|
|
81
|
+
const container = typeof target === "string" ? win.document.querySelector(target) : target;
|
|
82
|
+
if (container === null) {
|
|
83
|
+
throw new TypeError(`Relume.init: no element matches "${String(target)}"`);
|
|
84
|
+
}
|
|
85
|
+
if (!PUBLISHABLE_KEY.test(publishableKey)) {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
"Relume.init: the publishable key must look like pk_test_\u2026 or pk_live_\u2026"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const embedOrigin = originOf(
|
|
91
|
+
options.embedUrl ?? DEFAULT_EMBED_URL,
|
|
92
|
+
"embedUrl"
|
|
93
|
+
);
|
|
94
|
+
const apiUrl = (options.apiUrl ?? API_URL_FOR_EMBED[embedOrigin] ?? "").replace(/\/+$/, "");
|
|
95
|
+
const sessionUrl = new URL(
|
|
96
|
+
options.sessionUrl ?? DEFAULT_SESSION_URL,
|
|
97
|
+
win.location.href
|
|
98
|
+
);
|
|
99
|
+
const pageOrigin = win.location.origin;
|
|
100
|
+
const credentials = sessionUrl.origin === pageOrigin ? "same-origin" : "include";
|
|
101
|
+
const listeners = {
|
|
102
|
+
topUp: /* @__PURE__ */ new Set(),
|
|
103
|
+
creditsChanged: /* @__PURE__ */ new Set(),
|
|
104
|
+
export: /* @__PURE__ */ new Set(),
|
|
105
|
+
close: /* @__PURE__ */ new Set(),
|
|
106
|
+
error: /* @__PURE__ */ new Set()
|
|
107
|
+
};
|
|
108
|
+
const aborter = new AbortController();
|
|
109
|
+
let destroyed = false;
|
|
110
|
+
let blocked = false;
|
|
111
|
+
let generation = 0;
|
|
112
|
+
let version = null;
|
|
113
|
+
let awaitingLoad = true;
|
|
114
|
+
let helloTimer = null;
|
|
115
|
+
let helloDeadline = null;
|
|
116
|
+
let lastBalance = null;
|
|
117
|
+
const iframe = win.document.createElement("iframe");
|
|
118
|
+
iframe.title = "Relume website builder";
|
|
119
|
+
iframe.allow = "clipboard-read; clipboard-write; fullscreen";
|
|
120
|
+
iframe.style.border = "0";
|
|
121
|
+
iframe.style.display = "block";
|
|
122
|
+
const mobile = options.fullScreenOnMobile === false || typeof win.matchMedia !== "function" ? null : win.matchMedia(MOBILE_QUERY);
|
|
123
|
+
const applySize = () => {
|
|
124
|
+
const edge = mobile?.matches === true ? "0" : "";
|
|
125
|
+
Object.assign(iframe.style, {
|
|
126
|
+
position: edge && "fixed",
|
|
127
|
+
top: edge,
|
|
128
|
+
right: edge,
|
|
129
|
+
bottom: edge,
|
|
130
|
+
left: edge,
|
|
131
|
+
zIndex: edge && "2147483647",
|
|
132
|
+
width: "100%",
|
|
133
|
+
height: "100%"
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
applySize();
|
|
137
|
+
mobile?.addEventListener("change", applySize);
|
|
138
|
+
function emit(set, event) {
|
|
139
|
+
for (const listener of [...set]) {
|
|
140
|
+
try {
|
|
141
|
+
listener(event);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("[Relume] An event handler threw", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function fail(code, message) {
|
|
148
|
+
if (destroyed) return;
|
|
149
|
+
if (listeners.error.size === 0) {
|
|
150
|
+
console.error(`[Relume] ${code}: ${message}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
emit(listeners.error, { code, message });
|
|
154
|
+
}
|
|
155
|
+
function post(message) {
|
|
156
|
+
iframe.contentWindow?.postMessage(encodeSdkMessage(message), embedOrigin);
|
|
157
|
+
}
|
|
158
|
+
function stopHello() {
|
|
159
|
+
if (helloTimer !== null) win.clearTimeout(helloTimer);
|
|
160
|
+
if (helloDeadline !== null) win.clearTimeout(helloDeadline);
|
|
161
|
+
helloTimer = null;
|
|
162
|
+
helloDeadline = null;
|
|
163
|
+
}
|
|
164
|
+
function load(path) {
|
|
165
|
+
stopHello();
|
|
166
|
+
generation += 1;
|
|
167
|
+
version = null;
|
|
168
|
+
awaitingLoad = true;
|
|
169
|
+
const url = new URL(path, embedOrigin);
|
|
170
|
+
url.searchParams.set("pk", publishableKey);
|
|
171
|
+
iframe.src = url.toString();
|
|
172
|
+
helloDeadline = win.setTimeout(() => {
|
|
173
|
+
stopHello();
|
|
174
|
+
fail("session_failed", "The builder didn't answer.");
|
|
175
|
+
}, HELLO_TIMEOUT_MS);
|
|
176
|
+
}
|
|
177
|
+
function sayHello(retryIn) {
|
|
178
|
+
post({ type: "hello", versions: PROTOCOL_VERSIONS });
|
|
179
|
+
helloTimer = win.setTimeout(
|
|
180
|
+
() => sayHello(Math.min(retryIn * 2, HELLO_MAX_RETRY_MS)),
|
|
181
|
+
retryIn
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const onLoad = () => {
|
|
185
|
+
if (version !== null || helloDeadline === null) return;
|
|
186
|
+
awaitingLoad = false;
|
|
187
|
+
if (helloTimer !== null) win.clearTimeout(helloTimer);
|
|
188
|
+
sayHello(HELLO_RETRY_MS);
|
|
189
|
+
};
|
|
190
|
+
iframe.addEventListener("load", onLoad);
|
|
191
|
+
async function answerSessionRequest(forGeneration) {
|
|
192
|
+
let message;
|
|
193
|
+
try {
|
|
194
|
+
const response = await env.fetch(sessionUrl, {
|
|
195
|
+
credentials,
|
|
196
|
+
cache: "no-store",
|
|
197
|
+
headers: { accept: "application/json" },
|
|
198
|
+
signal: aborter.signal
|
|
199
|
+
});
|
|
200
|
+
const body = await response.json().catch(() => null);
|
|
201
|
+
const clientSecret = isRecord2(body) ? body.clientSecret : void 0;
|
|
202
|
+
if (response.ok && typeof clientSecret === "string" && clientSecret) {
|
|
203
|
+
message = { type: "session", clientSecret };
|
|
204
|
+
} else {
|
|
205
|
+
message = {
|
|
206
|
+
type: "session.error",
|
|
207
|
+
code: errorCodeOf(body) ?? (response.status === 401 ? "not_signed_in" : "session_failed")
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
if (destroyed) return;
|
|
212
|
+
message = { type: "session.error", code: "session_failed" };
|
|
213
|
+
}
|
|
214
|
+
if (destroyed || forGeneration !== generation) return;
|
|
215
|
+
post(message);
|
|
216
|
+
}
|
|
217
|
+
function handle(message) {
|
|
218
|
+
if (version === null) {
|
|
219
|
+
if (awaitingLoad) return;
|
|
220
|
+
if (message.type === "ready") {
|
|
221
|
+
stopHello();
|
|
222
|
+
version = message.version;
|
|
223
|
+
} else if (message.type === "error") {
|
|
224
|
+
stopHello();
|
|
225
|
+
fail(message.code, message.message);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
switch (message.type) {
|
|
230
|
+
case "ready":
|
|
231
|
+
return;
|
|
232
|
+
case "session.request":
|
|
233
|
+
void answerSessionRequest(generation);
|
|
234
|
+
return;
|
|
235
|
+
case "credits.changed":
|
|
236
|
+
if (message.balance !== null && message.balance !== lastBalance) {
|
|
237
|
+
lastBalance = message.balance;
|
|
238
|
+
emit(listeners.creditsChanged, { balance: message.balance });
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
case "topup.required":
|
|
242
|
+
emit(listeners.topUp, {
|
|
243
|
+
required: message.required,
|
|
244
|
+
balance: message.balance
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
case "export.ready":
|
|
248
|
+
emit(listeners.export, {
|
|
249
|
+
siteId: message.siteId,
|
|
250
|
+
exportId: message.exportId,
|
|
251
|
+
downloadUrl: message.downloadUrl
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
case "close":
|
|
255
|
+
emit(listeners.close, void 0);
|
|
256
|
+
return;
|
|
257
|
+
case "error":
|
|
258
|
+
fail(message.code, message.message);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const onMessage = (event) => {
|
|
263
|
+
if (event.origin !== embedOrigin || event.source === null || event.source !== iframe.contentWindow) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const message = parseAppMessage(event.data, version);
|
|
267
|
+
if (message !== null) handle(message);
|
|
268
|
+
};
|
|
269
|
+
win.addEventListener("message", onMessage);
|
|
270
|
+
async function checkOrigin() {
|
|
271
|
+
if (!apiUrl) return;
|
|
272
|
+
let response;
|
|
273
|
+
let body;
|
|
274
|
+
try {
|
|
275
|
+
response = await env.fetch(
|
|
276
|
+
`${apiUrl}/embed/partner?pk=${encodeURIComponent(publishableKey)}`,
|
|
277
|
+
{ credentials: "omit", signal: aborter.signal }
|
|
278
|
+
);
|
|
279
|
+
body = await response.json();
|
|
280
|
+
} catch {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (destroyed) return;
|
|
284
|
+
const code = errorCodeOf(body);
|
|
285
|
+
if (code === "unknown_publishable_key" || code === "partner_suspended") {
|
|
286
|
+
block(
|
|
287
|
+
code === "partner_suspended" ? "partner_suspended" : "session_failed",
|
|
288
|
+
code === "partner_suspended" ? "This partner has been suspended." : "The publishable key is unknown, revoked or expired."
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const allowed = response.ok && isRecord2(body) && Array.isArray(body.allowedOrigins) ? body.allowedOrigins : null;
|
|
293
|
+
if (allowed !== null && !allowed.includes(pageOrigin)) {
|
|
294
|
+
block(
|
|
295
|
+
"origin_not_allowed",
|
|
296
|
+
`${pageOrigin} isn't an allowed origin for this publishable key.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function teardown() {
|
|
301
|
+
stopHello();
|
|
302
|
+
aborter.abort();
|
|
303
|
+
win.removeEventListener("message", onMessage);
|
|
304
|
+
iframe.removeEventListener("load", onLoad);
|
|
305
|
+
mobile?.removeEventListener("change", applySize);
|
|
306
|
+
iframe.remove();
|
|
307
|
+
}
|
|
308
|
+
function block(code, message) {
|
|
309
|
+
blocked = true;
|
|
310
|
+
teardown();
|
|
311
|
+
fail(code, message);
|
|
312
|
+
}
|
|
313
|
+
const subscribe = (set) => (listener) => {
|
|
314
|
+
if (destroyed) return () => void 0;
|
|
315
|
+
set.add(listener);
|
|
316
|
+
return () => {
|
|
317
|
+
set.delete(listener);
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
load("/");
|
|
321
|
+
container.append(iframe);
|
|
322
|
+
void checkOrigin();
|
|
323
|
+
return {
|
|
324
|
+
onTopUp: subscribe(listeners.topUp),
|
|
325
|
+
onCreditsChanged: subscribe(listeners.creditsChanged),
|
|
326
|
+
onExport: subscribe(listeners.export),
|
|
327
|
+
onClose: subscribe(listeners.close),
|
|
328
|
+
onError: subscribe(listeners.error),
|
|
329
|
+
open({ site }) {
|
|
330
|
+
if (typeof site !== "string" || site.length === 0) {
|
|
331
|
+
throw new TypeError("open: site must be a site id");
|
|
332
|
+
}
|
|
333
|
+
if (destroyed || blocked) return;
|
|
334
|
+
load(`/app/site/${encodeURIComponent(site)}`);
|
|
335
|
+
},
|
|
336
|
+
refreshCredits() {
|
|
337
|
+
if (destroyed || version === null) return;
|
|
338
|
+
post({ type: "credits.refresh" });
|
|
339
|
+
},
|
|
340
|
+
destroy() {
|
|
341
|
+
if (destroyed) return;
|
|
342
|
+
destroyed = true;
|
|
343
|
+
teardown();
|
|
344
|
+
for (const set of Object.values(listeners)) set.clear();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/index.ts
|
|
350
|
+
var Relume = {
|
|
351
|
+
/**
|
|
352
|
+
* Puts the Relume builder in `target` (a selector or an element) for the
|
|
353
|
+
* signed-in user, whose session comes from your session route.
|
|
354
|
+
*/
|
|
355
|
+
init(target, publishableKey, options = {}) {
|
|
356
|
+
return createEmbed(target, publishableKey, options, {
|
|
357
|
+
window,
|
|
358
|
+
fetch: (input, init) => window.fetch(input, init)
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
export {
|
|
363
|
+
DEFAULT_EMBED_URL,
|
|
364
|
+
DEFAULT_SESSION_URL,
|
|
365
|
+
Relume
|
|
366
|
+
};
|
|
367
|
+
//# 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\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 headers: { accept: 'application/json' },\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 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={"https://embed.relume.ai":"https://apis.relume.io/nova","https://embed-staging.relume.ai":"https://staging-apis.relume.io/nova"},oe=200,se=3200,ie=2e4,le="(max-width: 640px)",de=/^pk_(?:test|live)_[A-Za-z0-9]+$/,ue=64;function ce(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<=ue?t:null}function Y(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(!de.test(t))throw new TypeError("Relume.init: the publishable key must look like pk_test_\u2026 or pk_live_\u2026");let h=ce(d.embedUrl??re,"embedUrl"),M=(d.apiUrl??te[h]??"").replace(/\/+$/,""),A=new URL(d.sessionUrl??ne,o.location.href),v=o.location.origin,X=A.origin===v?"same-origin":"include",s={topUp:new Set,creditsChanged:new Set,export:new Set,close:new Set,error:new Set},R=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(le),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 f(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}f(s.error,{code:e,message:n})}}function O(e){i.contentWindow?.postMessage(N(e),h)}function g(){a!==null&&o.clearTimeout(a),b!==null&&o.clearTimeout(b),a=null,b=null}function D(e){g(),U+=1,p=null,x=!0;let n=new URL(e,h);n.searchParams.set("pk",t),i.src=n.toString(),b=o.setTimeout(()=>{g(),w("session_failed","The builder didn't answer.")},ie)}function $(e){O({type:"hello",versions:k}),a=o.setTimeout(()=>$(Math.min(e*2,se)),e)}let P=()=>{p!==null||b===null||(x=!1,a!==null&&o.clearTimeout(a),$(oe))};i.addEventListener("load",P);async function G(e){let n;try{let l=await m.fetch(A,{credentials:X,cache:"no-store",headers:{accept:"application/json"},signal:R.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"?(g(),p=e.version):e.type==="error"&&(g(),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,f(s.creditsChanged,{balance:e.balance}));return;case"topup.required":f(s.topUp,{required:e.required,balance:e.balance});return;case"export.ready":f(s.export,{siteId:e.siteId,exportId:e.exportId,downloadUrl:e.downloadUrl});return;case"close":f(s.close,void 0);return;case"error":w(e.code,e.message);return}}let j=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",j);async function Z(){if(!M)return;let e,n;try{e=await m.fetch(`${M}/embed/partner?pk=${encodeURIComponent(t)}`,{credentials:"omit",signal:R.signal}),n=await e.json()}catch{return}if(u)return;let l=z(n);if(l==="unknown_publishable_key"||l==="partner_suspended"){H(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(v)&&H("origin_not_allowed",`${v} isn't an allowed origin for this publishable key.`)}function B(){g(),R.abort(),o.removeEventListener("message",j),i.removeEventListener("load",P),L?.removeEventListener("change",S),i.remove()}function H(e,n){I=!0,B(),w(e,n)}let E=e=>n=>u?()=>{}:(e.add(n),()=>{e.delete(n)});return D("/"),C.append(i),Z(),{onTopUp:E(s.topUp),onCreditsChanged:E(s.creditsChanged),onExport:E(s.export),onClose:E(s.close),onError:E(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,B();for(let e of Object.values(s))e.clear()}}}}var V={init(r,t,d={}){return Y(r,t,d,{window,fetch:(m,o)=>window.fetch(m,o)})}};Object.assign(globalThis,{Relume:V});})();
|
|
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\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 headers: { accept: 'application/json' },\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 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","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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.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
|
+
}
|