@rentaltide/app-sdk 0.1.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 ADDED
@@ -0,0 +1,150 @@
1
+ # @rentaltide/app-sdk
2
+
3
+ The official SDK for building **embedded apps** on the RentalTide platform.
4
+
5
+ An embedded app is a web page you host. RentalTide loads it in a sandboxed
6
+ iframe at one of several **embed locations** (a booking page, the dashboard, the
7
+ POS cart, …). The SDK gives your app a typed, secure bridge to the host:
8
+
9
+ - **Context** — who the app is installed for, the current location, the resource
10
+ the user is looking at, the active theme, and the granted scopes.
11
+ - **Scoped API** — make RentalTide API calls proxied through the host. Your app
12
+ never handles raw tokens, and every call is checked against the scopes the
13
+ merchant granted.
14
+ - **Host UI** — show toasts, navigate the host, and auto-resize your iframe.
15
+
16
+ It works with any framework (or none) — it's plain DOM `postMessage` under the
17
+ hood, with zero runtime dependencies.
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @rentaltide/app-sdk
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```ts
30
+ import { createApp } from '@rentaltide/app-sdk';
31
+
32
+ const app = await createApp();
33
+
34
+ // 1. Read context the host shared with you
35
+ const { account, location, resource, theme, grantedScopes } = app.context;
36
+ console.log(`Installed for ${account.businessName} (${account.customerId})`);
37
+ if (resource?.type === 'booking') {
38
+ console.log(`Viewing booking ${resource.id}`);
39
+ }
40
+
41
+ // 2. Call the RentalTide API — proxied + scope-checked by the host
42
+ const res = await app.api.get('/bookings', { query: { limit: 5 } });
43
+ if (res.ok) renderBookings(res.data);
44
+
45
+ // 3. Drive the host UI
46
+ app.toast('Synced!', 'success');
47
+
48
+ // 4. React to changes (dark mode, user navigates to another booking)
49
+ app.onContextChange((ctx) => rerender(ctx));
50
+ app.onThemeChange((t) => applyTheme(t));
51
+
52
+ // 5. Tell the host you're done loading (also triggers an auto-resize)
53
+ app.ready();
54
+ ```
55
+
56
+ That's the entire surface most apps need.
57
+
58
+ ## The context object
59
+
60
+ ```ts
61
+ interface HostContext {
62
+ embedLocation: EmbedLocation; // where you're mounted
63
+ host: { app: 'rentaltide'; environment: 'production' | 'sandbox'; ... };
64
+ account: { customerId: string; businessName?: string };
65
+ location: { locationId: string; name?: string; timezone?: string } | null;
66
+ user: { id: string; role: string; name?: string } | null;
67
+ resource: { type: 'booking' | 'order' | 'customer' | ...; id: string } | null;
68
+ theme: ThemeTokens; // match the surrounding UI
69
+ locale: string;
70
+ grantedScopes: string[]; // what the merchant granted you
71
+ }
72
+ ```
73
+
74
+ Which `resource` you get depends on the embed location — see
75
+ `EMBED_LOCATIONS` in this package for the full registry.
76
+
77
+ ## Scopes
78
+
79
+ Declare the scopes your app needs when you register it. The host enforces them
80
+ on every proxied API call. The catalog and the default path → scope policy live
81
+ in this package:
82
+
83
+ ```ts
84
+ import { SCOPE_CATALOG, requiredScope, hasScope } from '@rentaltide/app-sdk';
85
+
86
+ requiredScope('GET', '/bookings/123'); // 'read:bookings'
87
+ requiredScope('POST', '/customers'); // 'write:customers'
88
+ ```
89
+
90
+ If your app calls an endpoint it wasn't granted, the host rejects the request
91
+ with a `403`-style error — handle it gracefully.
92
+
93
+ ## Theming
94
+
95
+ `app.context.theme` gives you the host's resolved design tokens (`mode`,
96
+ `primary`, `background`, `paper`, `text`, `radius`, `fontFamily`, …). Apply them
97
+ on load and re-apply in `onThemeChange` so your app follows the merchant's
98
+ light/dark preference seamlessly.
99
+
100
+ ## Local development
101
+
102
+ Run your app locally and preview it inside a simulated RentalTide using the
103
+ **developer sandbox** in the partner portal — point it at `http://localhost:5173`
104
+ (or wherever your dev server runs), pick an embed location, and watch the live
105
+ message log. See the [starter template](../RentalTide-App-Examples) for a
106
+ ready-to-run project.
107
+
108
+ ## API reference
109
+
110
+ | Method | Description |
111
+ | ---------------------------------------- | -------------------------------------------------- |
112
+ | `app.context` | Latest `HostContext` (updated in place). |
113
+ | `app.ready()` | Signal initial render complete. |
114
+ | `app.onContextChange(cb)` | Subscribe to context updates. Returns unsubscribe. |
115
+ | `app.onThemeChange(cb)` | Subscribe to theme updates. Returns unsubscribe. |
116
+ | `app.api.get/post/put/patch/delete(...)` | Scoped, host-proxied API calls. |
117
+ | `app.api.request(req)` | Low-level form taking a full `ApiRequest`. |
118
+ | `app.toast(message, severity?)` | Show a host snackbar. |
119
+ | `app.navigate(path)` | Navigate the host app. |
120
+ | `app.resize(height?)` | Report height (auto by default). |
121
+ | `app.refreshContext()` | Re-fetch context from the host. |
122
+ | `app.destroy()` | Tear down the bridge. |
123
+
124
+ ## Building the host (RentalTide internal)
125
+
126
+ The same package exports the host side of the bridge, used by RentalTide and the
127
+ sandbox to mount apps:
128
+
129
+ ```ts
130
+ import { createHostBridge } from '@rentaltide/app-sdk';
131
+
132
+ const bridge = createHostBridge({
133
+ iframe,
134
+ appOrigin, // validated on every message
135
+ getContext: () => hostContext,
136
+ onApiRequest: async (req, ctx) => {
137
+ // enforce ctx.grantedScopes, then proxy to the API
138
+ },
139
+ onToast,
140
+ onNavigate,
141
+ onResize,
142
+ });
143
+
144
+ bridge.pushContext(); // when host state changes
145
+ bridge.pushTheme(nextTheme); // on dark-mode toggle
146
+ ```
147
+
148
+ ---
149
+
150
+ MIT © RentalTide Inc.
package/dist/app.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * App-side client. Runs inside the embedded iframe and is the only thing most
3
+ * developers import:
4
+ *
5
+ * ```ts
6
+ * import { createApp } from '@rentaltide/app-sdk';
7
+ * const app = await createApp();
8
+ * console.log(app.context.account.customerId);
9
+ * const res = await app.api.get('/bookings?limit=5');
10
+ * app.toast('Loaded!', 'success');
11
+ * ```
12
+ */
13
+ import type { ApiRequest, ApiResponse, HostContext, ThemeTokens, ToastSeverity } from './types';
14
+ export declare const SDK_VERSION = "0.1.0";
15
+ export interface CreateAppOptions {
16
+ /**
17
+ * Origins of the RentalTide host(s) allowed to talk to this app. Defaults to
18
+ * the document referrer's origin. Set this in production for defense in depth.
19
+ */
20
+ hostOrigins?: string[];
21
+ /** Auto-report content height to the host so the iframe grows to fit. Default true. */
22
+ autoResize?: boolean;
23
+ /** Per-request timeout in ms. Default 15000. */
24
+ timeoutMs?: number;
25
+ /** Your app's client id, sent on handshake (optional, useful in logs). */
26
+ appId?: string;
27
+ }
28
+ export interface ApiCallOptions {
29
+ query?: Record<string, string | number | boolean | undefined>;
30
+ }
31
+ export interface RentalTideApp {
32
+ /** The latest host context. Updated in place as the host pushes changes. */
33
+ readonly context: HostContext;
34
+ /** Tell the host the app has finished its initial render. */
35
+ ready(): void;
36
+ /** Subscribe to context changes (e.g. user navigates to another booking). Returns an unsubscribe fn. */
37
+ onContextChange(cb: (ctx: HostContext) => void): () => void;
38
+ /** Subscribe to host theme changes (e.g. dark-mode toggle). Returns an unsubscribe fn. */
39
+ onThemeChange(cb: (theme: ThemeTokens) => void): () => void;
40
+ /** Scoped, host-proxied RentalTide API. The app never sees raw credentials. */
41
+ api: {
42
+ request<T = unknown>(req: ApiRequest): Promise<ApiResponse<T>>;
43
+ get<T = unknown>(path: string, opts?: ApiCallOptions): Promise<ApiResponse<T>>;
44
+ post<T = unknown>(path: string, body?: unknown, opts?: ApiCallOptions): Promise<ApiResponse<T>>;
45
+ put<T = unknown>(path: string, body?: unknown, opts?: ApiCallOptions): Promise<ApiResponse<T>>;
46
+ patch<T = unknown>(path: string, body?: unknown, opts?: ApiCallOptions): Promise<ApiResponse<T>>;
47
+ delete<T = unknown>(path: string, opts?: ApiCallOptions): Promise<ApiResponse<T>>;
48
+ };
49
+ /** Show a snackbar in the host UI. */
50
+ toast(message: string, severity?: ToastSeverity): void;
51
+ /** Ask the host to navigate to an in-app path. */
52
+ navigate(path: string): void;
53
+ /** Manually report height (or omit to measure the document). */
54
+ resize(height?: number): void;
55
+ /** Re-fetch the current context from the host. */
56
+ refreshContext(): Promise<HostContext>;
57
+ /** Tear down listeners and reject any in-flight requests. */
58
+ destroy(): void;
59
+ }
60
+ /**
61
+ * Initialize the app bridge. Resolves once the host handshake completes and the
62
+ * initial {@link HostContext} is available.
63
+ */
64
+ export declare function createApp(options?: CreateAppOptions): Promise<RentalTideApp>;
package/dist/app.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * App-side client. Runs inside the embedded iframe and is the only thing most
3
+ * developers import:
4
+ *
5
+ * ```ts
6
+ * import { createApp } from '@rentaltide/app-sdk';
7
+ * const app = await createApp();
8
+ * console.log(app.context.account.customerId);
9
+ * const res = await app.api.get('/bookings?limit=5');
10
+ * app.toast('Loaded!', 'success');
11
+ * ```
12
+ */
13
+ import { envelope, isBridgeEnvelope } from './protocol';
14
+ export const SDK_VERSION = '0.1.0';
15
+ function safeOrigin(url) {
16
+ try {
17
+ return url ? new URL(url).origin : '';
18
+ }
19
+ catch {
20
+ return '';
21
+ }
22
+ }
23
+ /**
24
+ * Initialize the app bridge. Resolves once the host handshake completes and the
25
+ * initial {@link HostContext} is available.
26
+ */
27
+ export async function createApp(options = {}) {
28
+ var _a, _b, _c;
29
+ if (typeof window === 'undefined' || window.parent === window) {
30
+ throw new Error('[rentaltide-app-sdk] createApp() must run inside the RentalTide host iframe.');
31
+ }
32
+ const timeoutMs = (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : 15000;
33
+ const autoResize = (_b = options.autoResize) !== null && _b !== void 0 ? _b : true;
34
+ const referrerOrigin = safeOrigin(document.referrer);
35
+ const allowedOrigins = (_c = options.hostOrigins) !== null && _c !== void 0 ? _c : (referrerOrigin ? [referrerOrigin] : []);
36
+ let hostOrigin = referrerOrigin || '*';
37
+ let context = null;
38
+ const pending = new Map();
39
+ const contextListeners = new Set();
40
+ const themeListeners = new Set();
41
+ const originAllowed = (origin) => allowedOrigins.length === 0 || allowedOrigins.includes(origin);
42
+ const post = (env) => {
43
+ window.parent.postMessage(env, hostOrigin);
44
+ };
45
+ const sendRequest = (type, payload) => {
46
+ const env = envelope('request', type, { payload });
47
+ return new Promise((resolve, reject) => {
48
+ const timer = setTimeout(() => {
49
+ pending.delete(env.id);
50
+ reject(new Error(`[rentaltide-app-sdk] "${type}" timed out after ${timeoutMs}ms`));
51
+ }, timeoutMs);
52
+ pending.set(env.id, { resolve: resolve, reject, timer });
53
+ post(env);
54
+ });
55
+ };
56
+ const reportHeight = () => {
57
+ var _a, _b;
58
+ const height = Math.ceil(((_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.scrollHeight) || ((_b = document.body) === null || _b === void 0 ? void 0 : _b.scrollHeight) || 0);
59
+ post(envelope('request', 'ui.resize', { payload: { height } }));
60
+ };
61
+ const handleMessage = (event) => {
62
+ var _a;
63
+ if (event.source !== window.parent)
64
+ return;
65
+ if (!isBridgeEnvelope(event.data))
66
+ return;
67
+ if (!originAllowed(event.origin))
68
+ return;
69
+ // Lock onto the first valid host origin so subsequent posts are targeted.
70
+ if (hostOrigin === '*')
71
+ hostOrigin = event.origin;
72
+ const env = event.data;
73
+ if (env.kind === 'response' && env.replyTo) {
74
+ const p = pending.get(env.replyTo);
75
+ if (!p)
76
+ return;
77
+ clearTimeout(p.timer);
78
+ pending.delete(env.replyTo);
79
+ if (env.ok === false) {
80
+ p.reject(new Error(((_a = env.error) === null || _a === void 0 ? void 0 : _a.message) || `Request "${env.type}" failed`));
81
+ }
82
+ else {
83
+ p.resolve(env.payload);
84
+ }
85
+ return;
86
+ }
87
+ if (env.kind === 'event') {
88
+ if (env.type === 'context.changed') {
89
+ context = env.payload;
90
+ contextListeners.forEach((cb) => cb(context));
91
+ }
92
+ else if (env.type === 'theme.changed') {
93
+ const theme = env.payload;
94
+ if (context)
95
+ context = { ...context, theme };
96
+ themeListeners.forEach((cb) => cb(theme));
97
+ }
98
+ else if (env.type === 'host.closing') {
99
+ destroy();
100
+ }
101
+ }
102
+ };
103
+ window.addEventListener('message', handleMessage);
104
+ let resizeObserver = null;
105
+ const destroy = () => {
106
+ window.removeEventListener('message', handleMessage);
107
+ resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
108
+ pending.forEach((p) => {
109
+ clearTimeout(p.timer);
110
+ p.reject(new Error('[rentaltide-app-sdk] app bridge destroyed'));
111
+ });
112
+ pending.clear();
113
+ contextListeners.clear();
114
+ themeListeners.clear();
115
+ };
116
+ // Handshake — establishes the host origin and initial context.
117
+ context = await sendRequest('handshake', {
118
+ appId: options.appId,
119
+ sdkVersion: SDK_VERSION,
120
+ });
121
+ if (autoResize && typeof ResizeObserver !== 'undefined') {
122
+ resizeObserver = new ResizeObserver(() => reportHeight());
123
+ resizeObserver.observe(document.documentElement);
124
+ reportHeight();
125
+ }
126
+ const buildReq = (method, path, body, opts) => ({ method, path, body, query: opts === null || opts === void 0 ? void 0 : opts.query });
127
+ const api = {
128
+ request(req) {
129
+ return sendRequest('api.request', req);
130
+ },
131
+ get(path, opts) {
132
+ return sendRequest('api.request', buildReq('GET', path, undefined, opts));
133
+ },
134
+ post(path, body, opts) {
135
+ return sendRequest('api.request', buildReq('POST', path, body, opts));
136
+ },
137
+ put(path, body, opts) {
138
+ return sendRequest('api.request', buildReq('PUT', path, body, opts));
139
+ },
140
+ patch(path, body, opts) {
141
+ return sendRequest('api.request', buildReq('PATCH', path, body, opts));
142
+ },
143
+ delete(path, opts) {
144
+ return sendRequest('api.request', buildReq('DELETE', path, undefined, opts));
145
+ },
146
+ };
147
+ return {
148
+ get context() {
149
+ return context;
150
+ },
151
+ ready() {
152
+ reportHeight();
153
+ },
154
+ onContextChange(cb) {
155
+ contextListeners.add(cb);
156
+ return () => contextListeners.delete(cb);
157
+ },
158
+ onThemeChange(cb) {
159
+ themeListeners.add(cb);
160
+ return () => themeListeners.delete(cb);
161
+ },
162
+ api,
163
+ toast(message, severity = 'info') {
164
+ sendRequest('ui.toast', { message, severity }).catch(() => undefined);
165
+ },
166
+ navigate(path) {
167
+ sendRequest('host.navigate', { path }).catch(() => undefined);
168
+ },
169
+ resize(height) {
170
+ if (typeof height === 'number') {
171
+ post(envelope('request', 'ui.resize', { payload: { height } }));
172
+ }
173
+ else {
174
+ reportHeight();
175
+ }
176
+ },
177
+ async refreshContext() {
178
+ const next = await sendRequest('context.get');
179
+ context = next;
180
+ return next;
181
+ },
182
+ destroy,
183
+ };
184
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Registry of every surface where an app can embed, with the resource the host
3
+ * provides there and sensible scope suggestions. Drives the app-creation form,
4
+ * the sandbox location picker, and the developer docs — one source of truth.
5
+ */
6
+ import type { EmbedLocation, EmbedResourceType } from './types';
7
+ import type { Scope } from './scopes';
8
+ export interface EmbedLocationMeta {
9
+ id: EmbedLocation;
10
+ label: string;
11
+ description: string;
12
+ /** Resource type the host provides in `context.resource` here (null if none). */
13
+ resourceType: EmbedResourceType | null;
14
+ /** Scopes an app embedding here will typically need. */
15
+ suggestedScopes: Scope[];
16
+ /** Rough shape of the embed surface, for layout hints. */
17
+ surface: 'sidebar' | 'panel' | 'card' | 'fullwidth';
18
+ }
19
+ export declare const EMBED_LOCATIONS: Record<EmbedLocation, EmbedLocationMeta>;
20
+ export declare const EMBED_LOCATION_LIST: EmbedLocationMeta[];
21
+ export declare function getEmbedLocationMeta(id: string): EmbedLocationMeta | undefined;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Registry of every surface where an app can embed, with the resource the host
3
+ * provides there and sensible scope suggestions. Drives the app-creation form,
4
+ * the sandbox location picker, and the developer docs — one source of truth.
5
+ */
6
+ export const EMBED_LOCATIONS = {
7
+ 'dashboard-widget': {
8
+ id: 'dashboard-widget',
9
+ label: 'Dashboard widget',
10
+ description: 'A card on the main operator dashboard. No specific resource in context.',
11
+ resourceType: null,
12
+ suggestedScopes: ['read:analytics', 'read:bookings'],
13
+ surface: 'card',
14
+ },
15
+ 'order-details': {
16
+ id: 'order-details',
17
+ label: 'Order details',
18
+ description: 'Sidebar panel on an order page. Receives the current order.',
19
+ resourceType: 'order',
20
+ suggestedScopes: ['read:bookings'],
21
+ surface: 'sidebar',
22
+ },
23
+ 'booking-details': {
24
+ id: 'booking-details',
25
+ label: 'Booking details',
26
+ description: 'Panel on a single booking. Receives the current booking.',
27
+ resourceType: 'booking',
28
+ suggestedScopes: ['read:bookings'],
29
+ surface: 'sidebar',
30
+ },
31
+ 'customer-profile': {
32
+ id: 'customer-profile',
33
+ label: 'Customer profile',
34
+ description: 'Panel on a customer profile. Receives the current customer.',
35
+ resourceType: 'customer',
36
+ suggestedScopes: ['read:customers'],
37
+ surface: 'panel',
38
+ },
39
+ 'inventory-detail': {
40
+ id: 'inventory-detail',
41
+ label: 'Inventory detail',
42
+ description: 'Panel on an inventory item. Receives the current inventory item.',
43
+ resourceType: 'inventory',
44
+ suggestedScopes: ['read:inventory'],
45
+ surface: 'panel',
46
+ },
47
+ 'asset-tracking': {
48
+ id: 'asset-tracking',
49
+ label: 'Asset tracking',
50
+ description: 'Panel on the asset/nav board. Receives the current asset.',
51
+ resourceType: 'asset',
52
+ suggestedScopes: ['read:inventory', 'read:bookings'],
53
+ surface: 'panel',
54
+ },
55
+ 'checkout-flow': {
56
+ id: 'checkout-flow',
57
+ label: 'Checkout flow',
58
+ description: 'Step injected into the booking checkout. Receives the in-progress cart.',
59
+ resourceType: 'cart',
60
+ suggestedScopes: ['read:bookings', 'read:pos'],
61
+ surface: 'fullwidth',
62
+ },
63
+ 'pos-cart': {
64
+ id: 'pos-cart',
65
+ label: 'POS cart',
66
+ description: 'Panel beside the point-of-sale cart. Receives the active cart.',
67
+ resourceType: 'cart',
68
+ suggestedScopes: ['read:pos', 'write:pos'],
69
+ surface: 'sidebar',
70
+ },
71
+ 'settings-panel': {
72
+ id: 'settings-panel',
73
+ label: 'Settings panel',
74
+ description: 'A configuration page reachable from the app-store entry. No resource.',
75
+ resourceType: null,
76
+ suggestedScopes: [],
77
+ surface: 'fullwidth',
78
+ },
79
+ };
80
+ export const EMBED_LOCATION_LIST = Object.values(EMBED_LOCATIONS);
81
+ export function getEmbedLocationMeta(id) {
82
+ return EMBED_LOCATIONS[id];
83
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Catalog of outbound webhook events RentalTide can send to your app.
3
+ *
4
+ * Register a webhook URL + secret and request the `webhooks:receive` scope to
5
+ * receive these. Each delivery is signed (see the developer docs for
6
+ * verification). This list mirrors the server's emitter.
7
+ */
8
+ export interface WebhookEventMeta {
9
+ event: string;
10
+ description: string;
11
+ }
12
+ export declare const WEBHOOK_EVENT_CATALOG: WebhookEventMeta[];
13
+ export declare const WEBHOOK_EVENTS: string[];
14
+ export type WebhookEventName = (typeof WEBHOOK_EVENT_CATALOG)[number]['event'];
15
+ /** Shape of every webhook request body RentalTide POSTs to your endpoint. */
16
+ export interface WebhookPayload<T = Record<string, unknown>> {
17
+ event: string;
18
+ /** ISO-8601 timestamp the payload was created. */
19
+ timestamp: string;
20
+ data: T;
21
+ }
package/dist/events.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Catalog of outbound webhook events RentalTide can send to your app.
3
+ *
4
+ * Register a webhook URL + secret and request the `webhooks:receive` scope to
5
+ * receive these. Each delivery is signed (see the developer docs for
6
+ * verification). This list mirrors the server's emitter.
7
+ */
8
+ export const WEBHOOK_EVENT_CATALOG = [
9
+ { event: 'booking.created', description: 'A new booking/order was created.' },
10
+ { event: 'booking.updated', description: 'A booking changed (notes, timing, add-ons, assets).' },
11
+ { event: 'booking.status_changed', description: 'A booking moved to a new status.' },
12
+ { event: 'booking.checked_in', description: 'A booking was checked in.' },
13
+ { event: 'booking.cancelled', description: 'A booking was cancelled.' },
14
+ { event: 'booking.rescheduled', description: 'A booking was rescheduled.' },
15
+ { event: 'payment.received', description: 'A payment was recorded against a booking.' },
16
+ { event: 'payment.refunded', description: 'A refund was issued.' },
17
+ { event: 'customer.created', description: 'A new customer was created.' },
18
+ { event: 'customer.updated', description: 'A customer record was updated.' },
19
+ { event: 'app.installed', description: 'Your app was installed by a merchant.' },
20
+ { event: 'app.uninstalled', description: 'Your app was uninstalled (deprovision here).' },
21
+ ];
22
+ export const WEBHOOK_EVENTS = WEBHOOK_EVENT_CATALOG.map((e) => e.event);
package/dist/host.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Host-side bridge. Runs in the RentalTide host (the main app and the developer
3
+ * sandbox) — one instance per embedded iframe. It answers the app's handshake,
4
+ * proxies scoped API calls, and forwards UI intents (toast/navigate/resize).
5
+ *
6
+ * Security model:
7
+ * - Only messages from this iframe's `contentWindow` are accepted.
8
+ * - Only messages from the declared `appOrigin` are accepted.
9
+ * - `onApiRequest` is responsible for enforcing the installation's granted
10
+ * scopes (see {@link requiredScope}/{@link hasScope} in `./scopes`).
11
+ */
12
+ import { BridgeEnvelope } from './protocol';
13
+ import type { ApiRequest, ApiResponse, HostContext, ThemeTokens, ToastSeverity } from './types';
14
+ export interface CreateHostBridgeOptions {
15
+ /** The iframe element the app is mounted in. */
16
+ iframe: HTMLIFrameElement;
17
+ /**
18
+ * The exact origin the app is served from (validated on every message).
19
+ * Use `'*'` only for trusted local sandboxes; never in production.
20
+ */
21
+ appOrigin: string;
22
+ /** Returns the current host context (called on handshake/context.get). */
23
+ getContext: () => HostContext;
24
+ /**
25
+ * Handle a proxied API request. MUST enforce the installation's granted
26
+ * scopes before performing the call.
27
+ */
28
+ onApiRequest: (req: ApiRequest, ctx: HostContext) => Promise<ApiResponse>;
29
+ onResize?: (height: number) => void;
30
+ onToast?: (message: string, severity: ToastSeverity) => void;
31
+ onNavigate?: (path: string) => void;
32
+ onClose?: () => void;
33
+ /** Optional observer of all traffic — used by the sandbox message log. */
34
+ onMessage?: (direction: 'in' | 'out', env: BridgeEnvelope) => void;
35
+ }
36
+ export interface HostBridge {
37
+ /** Push a fresh context to the app (defaults to `getContext()`). */
38
+ pushContext(ctx?: HostContext): void;
39
+ /** Notify the app that the host theme changed. */
40
+ pushTheme(theme: ThemeTokens): void;
41
+ /** Tear down the listener and tell the app it's closing. */
42
+ destroy(): void;
43
+ }
44
+ export declare function createHostBridge(options: CreateHostBridgeOptions): HostBridge;