@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 +150 -0
- package/dist/app.d.ts +64 -0
- package/dist/app.js +184 -0
- package/dist/embedLocations.d.ts +21 -0
- package/dist/embedLocations.js +83 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.js +22 -0
- package/dist/host.d.ts +44 -0
- package/dist/host.js +91 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -0
- package/dist/protocol.d.ts +93 -0
- package/dist/protocol.js +44 -0
- package/dist/scopes.d.ts +25 -0
- package/dist/scopes.js +129 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.js +9 -0
- package/package.json +50 -0
- package/src/app.ts +265 -0
- package/src/embedLocations.ts +101 -0
- package/src/events.ts +39 -0
- package/src/host.ts +138 -0
- package/src/index.ts +32 -0
- package/src/protocol.ts +104 -0
- package/src/scopes.ts +158 -0
- package/src/types.ts +115 -0
package/dist/host.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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 { envelope, isBridgeEnvelope } from './protocol';
|
|
13
|
+
export function createHostBridge(options) {
|
|
14
|
+
const { iframe, appOrigin, getContext, onApiRequest, onResize, onToast, onNavigate, onClose, onMessage, } = options;
|
|
15
|
+
// `srcdoc`/sandboxed iframes report a `null` origin; postMessage must use '*'.
|
|
16
|
+
const postTarget = appOrigin === 'null' ? '*' : appOrigin;
|
|
17
|
+
const send = (env) => {
|
|
18
|
+
var _a;
|
|
19
|
+
onMessage === null || onMessage === void 0 ? void 0 : onMessage('out', env);
|
|
20
|
+
(_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(env, postTarget);
|
|
21
|
+
};
|
|
22
|
+
const respond = (replyTo, type, payload) => {
|
|
23
|
+
send(envelope('response', type, { replyTo, ok: true, payload }));
|
|
24
|
+
};
|
|
25
|
+
const respondError = (replyTo, type, code, message) => {
|
|
26
|
+
send(envelope('response', type, { replyTo, ok: false, error: { code, message } }));
|
|
27
|
+
};
|
|
28
|
+
const handleMessage = async (event) => {
|
|
29
|
+
var _a;
|
|
30
|
+
if (event.source !== iframe.contentWindow)
|
|
31
|
+
return;
|
|
32
|
+
if (appOrigin !== '*' && appOrigin !== 'null' && event.origin !== appOrigin)
|
|
33
|
+
return;
|
|
34
|
+
if (!isBridgeEnvelope(event.data))
|
|
35
|
+
return;
|
|
36
|
+
const env = event.data;
|
|
37
|
+
if (env.kind !== 'request')
|
|
38
|
+
return;
|
|
39
|
+
onMessage === null || onMessage === void 0 ? void 0 : onMessage('in', env);
|
|
40
|
+
try {
|
|
41
|
+
switch (env.type) {
|
|
42
|
+
case 'handshake':
|
|
43
|
+
case 'context.get':
|
|
44
|
+
respond(env.id, env.type, getContext());
|
|
45
|
+
break;
|
|
46
|
+
case 'api.request': {
|
|
47
|
+
const res = await onApiRequest(env.payload, getContext());
|
|
48
|
+
respond(env.id, env.type, res);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'ui.resize':
|
|
52
|
+
// Fire-and-forget; the app does not await a response.
|
|
53
|
+
onResize === null || onResize === void 0 ? void 0 : onResize(env.payload.height);
|
|
54
|
+
break;
|
|
55
|
+
case 'ui.toast': {
|
|
56
|
+
const p = env.payload;
|
|
57
|
+
onToast === null || onToast === void 0 ? void 0 : onToast(p.message, (_a = p.severity) !== null && _a !== void 0 ? _a : 'info');
|
|
58
|
+
respond(env.id, env.type);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'host.navigate':
|
|
62
|
+
onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(env.payload.path);
|
|
63
|
+
respond(env.id, env.type);
|
|
64
|
+
break;
|
|
65
|
+
case 'host.close':
|
|
66
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
67
|
+
respond(env.id, env.type);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
respondError(env.id, env.type, 'unknown_method', `Unknown method: ${env.type}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : 'Host error';
|
|
75
|
+
respondError(env.id, env.type, 'host_error', message);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
window.addEventListener('message', handleMessage);
|
|
79
|
+
return {
|
|
80
|
+
pushContext(ctx) {
|
|
81
|
+
send(envelope('event', 'context.changed', { payload: ctx !== null && ctx !== void 0 ? ctx : getContext() }));
|
|
82
|
+
},
|
|
83
|
+
pushTheme(theme) {
|
|
84
|
+
send(envelope('event', 'theme.changed', { payload: theme }));
|
|
85
|
+
},
|
|
86
|
+
destroy() {
|
|
87
|
+
send(envelope('event', 'host.closing'));
|
|
88
|
+
window.removeEventListener('message', handleMessage);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rentaltide/app-sdk — Official SDK for building embedded apps on RentalTide.
|
|
3
|
+
*
|
|
4
|
+
* App developers import the app-side client:
|
|
5
|
+
* import { createApp } from '@rentaltide/app-sdk';
|
|
6
|
+
*
|
|
7
|
+
* The RentalTide host (and the developer sandbox) import the host bridge:
|
|
8
|
+
* import { createHostBridge } from '@rentaltide/app-sdk';
|
|
9
|
+
*/
|
|
10
|
+
export * from './types';
|
|
11
|
+
export * from './protocol';
|
|
12
|
+
export * from './scopes';
|
|
13
|
+
export * from './embedLocations';
|
|
14
|
+
export * from './events';
|
|
15
|
+
export { createApp, SDK_VERSION } from './app';
|
|
16
|
+
export type { CreateAppOptions, ApiCallOptions, RentalTideApp } from './app';
|
|
17
|
+
export { createHostBridge } from './host';
|
|
18
|
+
export type { CreateHostBridgeOptions, HostBridge } from './host';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rentaltide/app-sdk — Official SDK for building embedded apps on RentalTide.
|
|
3
|
+
*
|
|
4
|
+
* App developers import the app-side client:
|
|
5
|
+
* import { createApp } from '@rentaltide/app-sdk';
|
|
6
|
+
*
|
|
7
|
+
* The RentalTide host (and the developer sandbox) import the host bridge:
|
|
8
|
+
* import { createHostBridge } from '@rentaltide/app-sdk';
|
|
9
|
+
*/
|
|
10
|
+
// Data shapes
|
|
11
|
+
export * from './types';
|
|
12
|
+
// Wire protocol (envelopes, request/event maps, helpers)
|
|
13
|
+
export * from './protocol';
|
|
14
|
+
// OAuth scopes + the API-proxy scope policy
|
|
15
|
+
export * from './scopes';
|
|
16
|
+
// Embed-location registry
|
|
17
|
+
export * from './embedLocations';
|
|
18
|
+
// Webhook event catalog
|
|
19
|
+
export * from './events';
|
|
20
|
+
// App-side client (runs in the iframe)
|
|
21
|
+
export { createApp, SDK_VERSION } from './app';
|
|
22
|
+
// Host-side bridge (runs in RentalTide / the sandbox)
|
|
23
|
+
export { createHostBridge } from './host';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire protocol for the RentalTide App Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Host and embedded app communicate exclusively over `window.postMessage`.
|
|
5
|
+
* Every message is a {@link BridgeEnvelope} stamped with a namespace marker and
|
|
6
|
+
* protocol version so unrelated `message` events (and other postMessage users on
|
|
7
|
+
* the page) are ignored.
|
|
8
|
+
*/
|
|
9
|
+
import type { ApiRequest, ApiResponse, HostContext, ThemeTokens, ToastSeverity } from './types';
|
|
10
|
+
export declare const PROTOCOL_VERSION = "1";
|
|
11
|
+
/** Marker present on every bridge message; used to filter foreign messages. */
|
|
12
|
+
export declare const BRIDGE_NAMESPACE = "rentaltide-app-bridge";
|
|
13
|
+
export type MessageKind = 'request' | 'response' | 'event';
|
|
14
|
+
export interface BridgeError {
|
|
15
|
+
code: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
export interface BridgeEnvelope<P = unknown> {
|
|
19
|
+
/** Always {@link BRIDGE_NAMESPACE}. */
|
|
20
|
+
__rt: typeof BRIDGE_NAMESPACE;
|
|
21
|
+
/** Protocol version (see {@link PROTOCOL_VERSION}). */
|
|
22
|
+
v: string;
|
|
23
|
+
/** Unique message id used to correlate responses to requests. */
|
|
24
|
+
id: string;
|
|
25
|
+
kind: MessageKind;
|
|
26
|
+
/** Method name (requests/responses) or event name (events). */
|
|
27
|
+
type: string;
|
|
28
|
+
payload?: P;
|
|
29
|
+
/** Present on responses. */
|
|
30
|
+
ok?: boolean;
|
|
31
|
+
/** Present on failed responses. */
|
|
32
|
+
error?: BridgeError;
|
|
33
|
+
/** Id of the request a response is answering. */
|
|
34
|
+
replyTo?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* App → Host request methods, with their request and response payload types.
|
|
38
|
+
* This is the canonical reference for what an app may ask the host to do.
|
|
39
|
+
*/
|
|
40
|
+
export interface RequestMap {
|
|
41
|
+
handshake: {
|
|
42
|
+
req: {
|
|
43
|
+
appId?: string;
|
|
44
|
+
sdkVersion: string;
|
|
45
|
+
};
|
|
46
|
+
res: HostContext;
|
|
47
|
+
};
|
|
48
|
+
'context.get': {
|
|
49
|
+
req: void;
|
|
50
|
+
res: HostContext;
|
|
51
|
+
};
|
|
52
|
+
'api.request': {
|
|
53
|
+
req: ApiRequest;
|
|
54
|
+
res: ApiResponse;
|
|
55
|
+
};
|
|
56
|
+
'ui.toast': {
|
|
57
|
+
req: {
|
|
58
|
+
message: string;
|
|
59
|
+
severity?: ToastSeverity;
|
|
60
|
+
};
|
|
61
|
+
res: void;
|
|
62
|
+
};
|
|
63
|
+
'ui.resize': {
|
|
64
|
+
req: {
|
|
65
|
+
height: number;
|
|
66
|
+
};
|
|
67
|
+
res: void;
|
|
68
|
+
};
|
|
69
|
+
'host.navigate': {
|
|
70
|
+
req: {
|
|
71
|
+
path: string;
|
|
72
|
+
};
|
|
73
|
+
res: void;
|
|
74
|
+
};
|
|
75
|
+
'host.close': {
|
|
76
|
+
req: void;
|
|
77
|
+
res: void;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export type RequestType = keyof RequestMap;
|
|
81
|
+
/** Host → App events, with their payload types. */
|
|
82
|
+
export interface EventMap {
|
|
83
|
+
'context.changed': HostContext;
|
|
84
|
+
'theme.changed': ThemeTokens;
|
|
85
|
+
'host.closing': void;
|
|
86
|
+
}
|
|
87
|
+
export type EventType = keyof EventMap;
|
|
88
|
+
/** Narrowing type guard for incoming `message` events. */
|
|
89
|
+
export declare function isBridgeEnvelope(data: unknown): data is BridgeEnvelope;
|
|
90
|
+
/** Generates a correlation id (RFC4122 v4 when crypto is available). */
|
|
91
|
+
export declare function newId(): string;
|
|
92
|
+
/** Builds a well-formed envelope. */
|
|
93
|
+
export declare function envelope<P>(kind: MessageKind, type: string, parts?: Partial<BridgeEnvelope<P>>): BridgeEnvelope<P>;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire protocol for the RentalTide App Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Host and embedded app communicate exclusively over `window.postMessage`.
|
|
5
|
+
* Every message is a {@link BridgeEnvelope} stamped with a namespace marker and
|
|
6
|
+
* protocol version so unrelated `message` events (and other postMessage users on
|
|
7
|
+
* the page) are ignored.
|
|
8
|
+
*/
|
|
9
|
+
export const PROTOCOL_VERSION = '1';
|
|
10
|
+
/** Marker present on every bridge message; used to filter foreign messages. */
|
|
11
|
+
export const BRIDGE_NAMESPACE = 'rentaltide-app-bridge';
|
|
12
|
+
/** Narrowing type guard for incoming `message` events. */
|
|
13
|
+
export function isBridgeEnvelope(data) {
|
|
14
|
+
if (typeof data !== 'object' || data === null)
|
|
15
|
+
return false;
|
|
16
|
+
const env = data;
|
|
17
|
+
return (env.__rt === BRIDGE_NAMESPACE &&
|
|
18
|
+
typeof env.id === 'string' &&
|
|
19
|
+
typeof env.kind === 'string' &&
|
|
20
|
+
typeof env.type === 'string');
|
|
21
|
+
}
|
|
22
|
+
/** Generates a correlation id (RFC4122 v4 when crypto is available). */
|
|
23
|
+
export function newId() {
|
|
24
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
25
|
+
return crypto.randomUUID();
|
|
26
|
+
}
|
|
27
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
28
|
+
const r = (Math.random() * 16) | 0;
|
|
29
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
30
|
+
return v.toString(16);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Builds a well-formed envelope. */
|
|
34
|
+
export function envelope(kind, type, parts = {}) {
|
|
35
|
+
var _a;
|
|
36
|
+
return {
|
|
37
|
+
__rt: BRIDGE_NAMESPACE,
|
|
38
|
+
v: PROTOCOL_VERSION,
|
|
39
|
+
id: (_a = parts.id) !== null && _a !== void 0 ? _a : newId(),
|
|
40
|
+
kind,
|
|
41
|
+
type,
|
|
42
|
+
...parts,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/scopes.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth scope catalog and the default scope policy for the API proxy.
|
|
3
|
+
*
|
|
4
|
+
* The host enforces scopes on every proxied API call. The mapping below is the
|
|
5
|
+
* canonical default (also rendered in the developer docs and the app-creation
|
|
6
|
+
* form). The host may layer additional, stricter rules on top — but it should
|
|
7
|
+
* never grant access broader than what's declared here.
|
|
8
|
+
*/
|
|
9
|
+
import type { ApiMethod } from './types';
|
|
10
|
+
export declare const SCOPES: readonly ["read:bookings", "write:bookings", "read:customers", "write:customers", "read:inventory", "write:inventory", "read:pos", "write:pos", "read:transactions", "write:transactions", "read:analytics", "read:reports", "read:geofence", "write:geofence", "audio_calling", "webhooks:receive"];
|
|
11
|
+
export type Scope = (typeof SCOPES)[number];
|
|
12
|
+
export interface ScopeMeta {
|
|
13
|
+
scope: Scope;
|
|
14
|
+
label: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const SCOPE_CATALOG: ScopeMeta[];
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the scope a given API call requires. Returns `null` for paths that
|
|
20
|
+
* don't match any rule — callers should treat `null` as "deny unless explicitly
|
|
21
|
+
* allowed" rather than "no scope needed".
|
|
22
|
+
*/
|
|
23
|
+
export declare function requiredScope(method: ApiMethod, path: string): Scope | null;
|
|
24
|
+
/** Whether a granted scope set satisfies a required scope (default-deny on null). */
|
|
25
|
+
export declare function hasScope(granted: string[] | undefined, required: Scope | null): boolean;
|
package/dist/scopes.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth scope catalog and the default scope policy for the API proxy.
|
|
3
|
+
*
|
|
4
|
+
* The host enforces scopes on every proxied API call. The mapping below is the
|
|
5
|
+
* canonical default (also rendered in the developer docs and the app-creation
|
|
6
|
+
* form). The host may layer additional, stricter rules on top — but it should
|
|
7
|
+
* never grant access broader than what's declared here.
|
|
8
|
+
*/
|
|
9
|
+
export const SCOPES = [
|
|
10
|
+
'read:bookings',
|
|
11
|
+
'write:bookings',
|
|
12
|
+
'read:customers',
|
|
13
|
+
'write:customers',
|
|
14
|
+
'read:inventory',
|
|
15
|
+
'write:inventory',
|
|
16
|
+
'read:pos',
|
|
17
|
+
'write:pos',
|
|
18
|
+
'read:transactions',
|
|
19
|
+
'write:transactions',
|
|
20
|
+
'read:analytics',
|
|
21
|
+
'read:reports',
|
|
22
|
+
'read:geofence',
|
|
23
|
+
'write:geofence',
|
|
24
|
+
'audio_calling',
|
|
25
|
+
'webhooks:receive',
|
|
26
|
+
];
|
|
27
|
+
export const SCOPE_CATALOG = [
|
|
28
|
+
{
|
|
29
|
+
scope: 'read:bookings',
|
|
30
|
+
label: 'Read bookings',
|
|
31
|
+
description: 'View bookings, orders, and rental schedules.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
scope: 'write:bookings',
|
|
35
|
+
label: 'Manage bookings',
|
|
36
|
+
description: 'Create, update, and cancel bookings and orders.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
scope: 'read:customers',
|
|
40
|
+
label: 'Read customers',
|
|
41
|
+
description: 'View customer and renter profiles.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
scope: 'write:customers',
|
|
45
|
+
label: 'Manage customers',
|
|
46
|
+
description: 'Create and update customer records.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
scope: 'read:inventory',
|
|
50
|
+
label: 'Read inventory',
|
|
51
|
+
description: 'View inventory, assets, and availability.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
scope: 'write:inventory',
|
|
55
|
+
label: 'Manage inventory',
|
|
56
|
+
description: 'Update inventory, assets, and pricing.',
|
|
57
|
+
},
|
|
58
|
+
{ scope: 'read:pos', label: 'Read POS', description: 'View point-of-sale catalog and carts.' },
|
|
59
|
+
{ scope: 'write:pos', label: 'Manage POS', description: 'Create POS orders and carts.' },
|
|
60
|
+
{
|
|
61
|
+
scope: 'read:transactions',
|
|
62
|
+
label: 'Read transactions',
|
|
63
|
+
description: 'View payments and transaction history.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
scope: 'write:transactions',
|
|
67
|
+
label: 'Manage transactions',
|
|
68
|
+
description: 'Initiate payments and refunds.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
scope: 'read:analytics',
|
|
72
|
+
label: 'Read analytics',
|
|
73
|
+
description: 'View aggregated metrics and dashboards.',
|
|
74
|
+
},
|
|
75
|
+
{ scope: 'read:reports', label: 'Read reports', description: 'View and export reports.' },
|
|
76
|
+
{
|
|
77
|
+
scope: 'read:geofence',
|
|
78
|
+
label: 'Read locations',
|
|
79
|
+
description: 'View geofences and asset/staff GPS positions.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
scope: 'write:geofence',
|
|
83
|
+
label: 'Manage locations',
|
|
84
|
+
description: 'Create and update geofences / push telematics.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
scope: 'audio_calling',
|
|
88
|
+
label: 'Voice calls',
|
|
89
|
+
description: 'Initiate and manage voice calls (AI phone).',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
scope: 'webhooks:receive',
|
|
93
|
+
label: 'Receive webhooks',
|
|
94
|
+
description: 'Receive event notifications from RentalTide.',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
/** Ordered most-specific-first; the first match wins. */
|
|
98
|
+
const RESOURCE_RULES = [
|
|
99
|
+
{ pattern: /^\/(transactions|pos-transactions|payments|refunds)\b/, resource: 'transactions' },
|
|
100
|
+
{ pattern: /^\/(pos|carts)\b/, resource: 'pos' },
|
|
101
|
+
{ pattern: /^\/(bookings|orders|inventory-schedules|schedules|rentals)\b/, resource: 'bookings' },
|
|
102
|
+
{ pattern: /^\/(customers|renters|booking-customers)\b/, resource: 'customers' },
|
|
103
|
+
{ pattern: /^\/(inventory|pos-inventory|assets|availability)\b/, resource: 'inventory' },
|
|
104
|
+
{ pattern: /^\/(analytics|metrics|dashboard)\b/, resource: 'analytics', readOnly: true },
|
|
105
|
+
{ pattern: /^\/(reports|exports)\b/, resource: 'reports', readOnly: true },
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the scope a given API call requires. Returns `null` for paths that
|
|
109
|
+
* don't match any rule — callers should treat `null` as "deny unless explicitly
|
|
110
|
+
* allowed" rather than "no scope needed".
|
|
111
|
+
*/
|
|
112
|
+
export function requiredScope(method, path) {
|
|
113
|
+
const clean = (path.split('?')[0] || '').replace(/\/+$/, '') || '/';
|
|
114
|
+
const normalized = clean.startsWith('/') ? clean : `/${clean}`;
|
|
115
|
+
const rule = RESOURCE_RULES.find((r) => r.pattern.test(normalized));
|
|
116
|
+
if (!rule)
|
|
117
|
+
return null;
|
|
118
|
+
const isWrite = method !== 'GET' && !rule.readOnly;
|
|
119
|
+
const candidate = `${isWrite ? 'write' : 'read'}:${rule.resource}`;
|
|
120
|
+
return SCOPES.includes(candidate)
|
|
121
|
+
? candidate
|
|
122
|
+
: `read:${rule.resource}`;
|
|
123
|
+
}
|
|
124
|
+
/** Whether a granted scope set satisfies a required scope (default-deny on null). */
|
|
125
|
+
export function hasScope(granted, required) {
|
|
126
|
+
if (!required)
|
|
127
|
+
return false;
|
|
128
|
+
return Array.isArray(granted) && granted.includes(required);
|
|
129
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data shapes exchanged between the RentalTide host and an embedded app.
|
|
3
|
+
*
|
|
4
|
+
* These types are the public contract for app developers. They are intentionally
|
|
5
|
+
* conservative: the host only ever shares non-sensitive identifiers and the
|
|
6
|
+
* resource the user is currently looking at. Anything else must be fetched
|
|
7
|
+
* through the scoped {@link ApiRequest} proxy.
|
|
8
|
+
*/
|
|
9
|
+
/** Surfaces inside RentalTide where an app's iframe can be mounted. */
|
|
10
|
+
export type EmbedLocation = 'dashboard-widget' | 'order-details' | 'booking-details' | 'customer-profile' | 'inventory-detail' | 'asset-tracking' | 'checkout-flow' | 'pos-cart' | 'settings-panel';
|
|
11
|
+
export type ThemeMode = 'light' | 'dark';
|
|
12
|
+
/**
|
|
13
|
+
* The host's resolved design tokens. Embedded apps should consume these so they
|
|
14
|
+
* visually match the surrounding RentalTide UI (and react to dark-mode toggles).
|
|
15
|
+
*/
|
|
16
|
+
export interface ThemeTokens {
|
|
17
|
+
mode: ThemeMode;
|
|
18
|
+
primary: string;
|
|
19
|
+
primaryContrast: string;
|
|
20
|
+
/** Page background behind the embed surface. */
|
|
21
|
+
background: string;
|
|
22
|
+
/** Card / surface background. */
|
|
23
|
+
paper: string;
|
|
24
|
+
text: string;
|
|
25
|
+
textSecondary: string;
|
|
26
|
+
divider: string;
|
|
27
|
+
/** Base border radius in pixels. */
|
|
28
|
+
radius: number;
|
|
29
|
+
fontFamily: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HostInfo {
|
|
32
|
+
app: 'rentaltide';
|
|
33
|
+
environment: 'production' | 'sandbox';
|
|
34
|
+
hostVersion: string;
|
|
35
|
+
protocolVersion: string;
|
|
36
|
+
}
|
|
37
|
+
export interface AccountContext {
|
|
38
|
+
/** The RentalTide customer (business) the app is installed for. */
|
|
39
|
+
customerId: string;
|
|
40
|
+
businessName?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface LocationContext {
|
|
43
|
+
locationId: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
timezone?: string;
|
|
46
|
+
currency?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Non-sensitive details about the staff user currently viewing the embed. */
|
|
49
|
+
export interface UserContext {
|
|
50
|
+
id: string;
|
|
51
|
+
role: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
}
|
|
54
|
+
export type EmbedResourceType = 'booking' | 'order' | 'customer' | 'inventory' | 'asset' | 'cart';
|
|
55
|
+
/**
|
|
56
|
+
* The entity the user is currently looking at, when the embed location implies
|
|
57
|
+
* one (e.g. a booking on `booking-details`). `data` is a lightweight summary;
|
|
58
|
+
* use the API proxy for the full record.
|
|
59
|
+
*/
|
|
60
|
+
export type EmbedResource = {
|
|
61
|
+
type: EmbedResourceType;
|
|
62
|
+
id: string;
|
|
63
|
+
summary?: Record<string, unknown>;
|
|
64
|
+
} | null;
|
|
65
|
+
/** The full snapshot of host state handed to an app on handshake. */
|
|
66
|
+
export interface HostContext {
|
|
67
|
+
embedLocation: EmbedLocation;
|
|
68
|
+
host: HostInfo;
|
|
69
|
+
account: AccountContext;
|
|
70
|
+
location: LocationContext | null;
|
|
71
|
+
user: UserContext | null;
|
|
72
|
+
resource: EmbedResource;
|
|
73
|
+
theme: ThemeTokens;
|
|
74
|
+
locale: string;
|
|
75
|
+
/** OAuth scopes this installation was actually granted. */
|
|
76
|
+
grantedScopes: string[];
|
|
77
|
+
}
|
|
78
|
+
export type ApiMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
79
|
+
/** A request the app asks the host to proxy against the RentalTide API. */
|
|
80
|
+
export interface ApiRequest {
|
|
81
|
+
method: ApiMethod;
|
|
82
|
+
/** API path relative to the RentalTide API root, e.g. `/bookings/123`. */
|
|
83
|
+
path: string;
|
|
84
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
}
|
|
87
|
+
export interface ApiResponse<T = unknown> {
|
|
88
|
+
status: number;
|
|
89
|
+
ok: boolean;
|
|
90
|
+
data: T;
|
|
91
|
+
}
|
|
92
|
+
export type ToastSeverity = 'success' | 'info' | 'warning' | 'error';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data shapes exchanged between the RentalTide host and an embedded app.
|
|
3
|
+
*
|
|
4
|
+
* These types are the public contract for app developers. They are intentionally
|
|
5
|
+
* conservative: the host only ever shares non-sensitive identifiers and the
|
|
6
|
+
* resource the user is currently looking at. Anything else must be fetched
|
|
7
|
+
* through the scoped {@link ApiRequest} proxy.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rentaltide/app-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official SDK for building embedded apps on the RentalTide platform.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "RentalTide Inc.",
|
|
7
|
+
"homepage": "https://docs.rentaltide.com/developers/app-sdk/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/RentalTide/RentalTide.git",
|
|
11
|
+
"directory": "packages/RentalTide-App-SDK"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/RentalTide/RentalTide/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"rentaltide",
|
|
43
|
+
"app-sdk",
|
|
44
|
+
"oauth",
|
|
45
|
+
"embed",
|
|
46
|
+
"marketplace",
|
|
47
|
+
"postmessage"
|
|
48
|
+
],
|
|
49
|
+
"sideEffects": false
|
|
50
|
+
}
|