@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/src/scopes.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
|
|
10
|
+
import type { ApiMethod } from './types';
|
|
11
|
+
|
|
12
|
+
export const SCOPES = [
|
|
13
|
+
'read:bookings',
|
|
14
|
+
'write:bookings',
|
|
15
|
+
'read:customers',
|
|
16
|
+
'write:customers',
|
|
17
|
+
'read:inventory',
|
|
18
|
+
'write:inventory',
|
|
19
|
+
'read:pos',
|
|
20
|
+
'write:pos',
|
|
21
|
+
'read:transactions',
|
|
22
|
+
'write:transactions',
|
|
23
|
+
'read:analytics',
|
|
24
|
+
'read:reports',
|
|
25
|
+
'read:geofence',
|
|
26
|
+
'write:geofence',
|
|
27
|
+
'audio_calling',
|
|
28
|
+
'webhooks:receive',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type Scope = (typeof SCOPES)[number];
|
|
32
|
+
|
|
33
|
+
export interface ScopeMeta {
|
|
34
|
+
scope: Scope;
|
|
35
|
+
label: string;
|
|
36
|
+
description: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SCOPE_CATALOG: ScopeMeta[] = [
|
|
40
|
+
{
|
|
41
|
+
scope: 'read:bookings',
|
|
42
|
+
label: 'Read bookings',
|
|
43
|
+
description: 'View bookings, orders, and rental schedules.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
scope: 'write:bookings',
|
|
47
|
+
label: 'Manage bookings',
|
|
48
|
+
description: 'Create, update, and cancel bookings and orders.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
scope: 'read:customers',
|
|
52
|
+
label: 'Read customers',
|
|
53
|
+
description: 'View customer and renter profiles.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
scope: 'write:customers',
|
|
57
|
+
label: 'Manage customers',
|
|
58
|
+
description: 'Create and update customer records.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
scope: 'read:inventory',
|
|
62
|
+
label: 'Read inventory',
|
|
63
|
+
description: 'View inventory, assets, and availability.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
scope: 'write:inventory',
|
|
67
|
+
label: 'Manage inventory',
|
|
68
|
+
description: 'Update inventory, assets, and pricing.',
|
|
69
|
+
},
|
|
70
|
+
{ scope: 'read:pos', label: 'Read POS', description: 'View point-of-sale catalog and carts.' },
|
|
71
|
+
{ scope: 'write:pos', label: 'Manage POS', description: 'Create POS orders and carts.' },
|
|
72
|
+
{
|
|
73
|
+
scope: 'read:transactions',
|
|
74
|
+
label: 'Read transactions',
|
|
75
|
+
description: 'View payments and transaction history.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
scope: 'write:transactions',
|
|
79
|
+
label: 'Manage transactions',
|
|
80
|
+
description: 'Initiate payments and refunds.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
scope: 'read:analytics',
|
|
84
|
+
label: 'Read analytics',
|
|
85
|
+
description: 'View aggregated metrics and dashboards.',
|
|
86
|
+
},
|
|
87
|
+
{ scope: 'read:reports', label: 'Read reports', description: 'View and export reports.' },
|
|
88
|
+
{
|
|
89
|
+
scope: 'read:geofence',
|
|
90
|
+
label: 'Read locations',
|
|
91
|
+
description: 'View geofences and asset/staff GPS positions.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
scope: 'write:geofence',
|
|
95
|
+
label: 'Manage locations',
|
|
96
|
+
description: 'Create and update geofences / push telematics.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
scope: 'audio_calling',
|
|
100
|
+
label: 'Voice calls',
|
|
101
|
+
description: 'Initiate and manage voice calls (AI phone).',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
scope: 'webhooks:receive',
|
|
105
|
+
label: 'Receive webhooks',
|
|
106
|
+
description: 'Receive event notifications from RentalTide.',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
type ScopeResource =
|
|
111
|
+
| 'bookings'
|
|
112
|
+
| 'customers'
|
|
113
|
+
| 'inventory'
|
|
114
|
+
| 'pos'
|
|
115
|
+
| 'transactions'
|
|
116
|
+
| 'analytics'
|
|
117
|
+
| 'reports';
|
|
118
|
+
|
|
119
|
+
interface ResourceRule {
|
|
120
|
+
pattern: RegExp;
|
|
121
|
+
resource: ScopeResource;
|
|
122
|
+
/** Some resources are read-only (no write scope exists). */
|
|
123
|
+
readOnly?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Ordered most-specific-first; the first match wins. */
|
|
127
|
+
const RESOURCE_RULES: ResourceRule[] = [
|
|
128
|
+
{ pattern: /^\/(transactions|pos-transactions|payments|refunds)\b/, resource: 'transactions' },
|
|
129
|
+
{ pattern: /^\/(pos|carts)\b/, resource: 'pos' },
|
|
130
|
+
{ pattern: /^\/(bookings|orders|inventory-schedules|schedules|rentals)\b/, resource: 'bookings' },
|
|
131
|
+
{ pattern: /^\/(customers|renters|booking-customers)\b/, resource: 'customers' },
|
|
132
|
+
{ pattern: /^\/(inventory|pos-inventory|assets|availability)\b/, resource: 'inventory' },
|
|
133
|
+
{ pattern: /^\/(analytics|metrics|dashboard)\b/, resource: 'analytics', readOnly: true },
|
|
134
|
+
{ pattern: /^\/(reports|exports)\b/, resource: 'reports', readOnly: true },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the scope a given API call requires. Returns `null` for paths that
|
|
139
|
+
* don't match any rule — callers should treat `null` as "deny unless explicitly
|
|
140
|
+
* allowed" rather than "no scope needed".
|
|
141
|
+
*/
|
|
142
|
+
export function requiredScope(method: ApiMethod, path: string): Scope | null {
|
|
143
|
+
const clean = (path.split('?')[0] || '').replace(/\/+$/, '') || '/';
|
|
144
|
+
const normalized = clean.startsWith('/') ? clean : `/${clean}`;
|
|
145
|
+
const rule = RESOURCE_RULES.find((r) => r.pattern.test(normalized));
|
|
146
|
+
if (!rule) return null;
|
|
147
|
+
const isWrite = method !== 'GET' && !rule.readOnly;
|
|
148
|
+
const candidate = `${isWrite ? 'write' : 'read'}:${rule.resource}` as Scope;
|
|
149
|
+
return (SCOPES as readonly string[]).includes(candidate)
|
|
150
|
+
? candidate
|
|
151
|
+
: (`read:${rule.resource}` as Scope);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether a granted scope set satisfies a required scope (default-deny on null). */
|
|
155
|
+
export function hasScope(granted: string[] | undefined, required: Scope | null): boolean {
|
|
156
|
+
if (!required) return false;
|
|
157
|
+
return Array.isArray(granted) && granted.includes(required);
|
|
158
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
|
|
10
|
+
/** Surfaces inside RentalTide where an app's iframe can be mounted. */
|
|
11
|
+
export type EmbedLocation =
|
|
12
|
+
| 'dashboard-widget'
|
|
13
|
+
| 'order-details'
|
|
14
|
+
| 'booking-details'
|
|
15
|
+
| 'customer-profile'
|
|
16
|
+
| 'inventory-detail'
|
|
17
|
+
| 'asset-tracking'
|
|
18
|
+
| 'checkout-flow'
|
|
19
|
+
| 'pos-cart'
|
|
20
|
+
| 'settings-panel';
|
|
21
|
+
|
|
22
|
+
export type ThemeMode = 'light' | 'dark';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The host's resolved design tokens. Embedded apps should consume these so they
|
|
26
|
+
* visually match the surrounding RentalTide UI (and react to dark-mode toggles).
|
|
27
|
+
*/
|
|
28
|
+
export interface ThemeTokens {
|
|
29
|
+
mode: ThemeMode;
|
|
30
|
+
primary: string;
|
|
31
|
+
primaryContrast: string;
|
|
32
|
+
/** Page background behind the embed surface. */
|
|
33
|
+
background: string;
|
|
34
|
+
/** Card / surface background. */
|
|
35
|
+
paper: string;
|
|
36
|
+
text: string;
|
|
37
|
+
textSecondary: string;
|
|
38
|
+
divider: string;
|
|
39
|
+
/** Base border radius in pixels. */
|
|
40
|
+
radius: number;
|
|
41
|
+
fontFamily: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HostInfo {
|
|
45
|
+
app: 'rentaltide';
|
|
46
|
+
environment: 'production' | 'sandbox';
|
|
47
|
+
hostVersion: string;
|
|
48
|
+
protocolVersion: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AccountContext {
|
|
52
|
+
/** The RentalTide customer (business) the app is installed for. */
|
|
53
|
+
customerId: string;
|
|
54
|
+
businessName?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LocationContext {
|
|
58
|
+
locationId: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
timezone?: string;
|
|
61
|
+
currency?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Non-sensitive details about the staff user currently viewing the embed. */
|
|
65
|
+
export interface UserContext {
|
|
66
|
+
id: string;
|
|
67
|
+
role: string;
|
|
68
|
+
name?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type EmbedResourceType = 'booking' | 'order' | 'customer' | 'inventory' | 'asset' | 'cart';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The entity the user is currently looking at, when the embed location implies
|
|
75
|
+
* one (e.g. a booking on `booking-details`). `data` is a lightweight summary;
|
|
76
|
+
* use the API proxy for the full record.
|
|
77
|
+
*/
|
|
78
|
+
export type EmbedResource = {
|
|
79
|
+
type: EmbedResourceType;
|
|
80
|
+
id: string;
|
|
81
|
+
summary?: Record<string, unknown>;
|
|
82
|
+
} | null;
|
|
83
|
+
|
|
84
|
+
/** The full snapshot of host state handed to an app on handshake. */
|
|
85
|
+
export interface HostContext {
|
|
86
|
+
embedLocation: EmbedLocation;
|
|
87
|
+
host: HostInfo;
|
|
88
|
+
account: AccountContext;
|
|
89
|
+
location: LocationContext | null;
|
|
90
|
+
user: UserContext | null;
|
|
91
|
+
resource: EmbedResource;
|
|
92
|
+
theme: ThemeTokens;
|
|
93
|
+
locale: string;
|
|
94
|
+
/** OAuth scopes this installation was actually granted. */
|
|
95
|
+
grantedScopes: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type ApiMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
99
|
+
|
|
100
|
+
/** A request the app asks the host to proxy against the RentalTide API. */
|
|
101
|
+
export interface ApiRequest {
|
|
102
|
+
method: ApiMethod;
|
|
103
|
+
/** API path relative to the RentalTide API root, e.g. `/bookings/123`. */
|
|
104
|
+
path: string;
|
|
105
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
106
|
+
body?: unknown;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ApiResponse<T = unknown> {
|
|
110
|
+
status: number;
|
|
111
|
+
ok: boolean;
|
|
112
|
+
data: T;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ToastSeverity = 'success' | 'info' | 'warning' | 'error';
|