@xeonr/renderer-sdk 1.0.4
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/dist/client.d.ts +132 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +360 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +78 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +27 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useRendererClient.d.ts +61 -0
- package/dist/react/useRendererClient.d.ts.map +1 -0
- package/dist/react/useRendererClient.js +96 -0
- package/dist/react/useRendererClient.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/client.ts +426 -0
- package/src/index.ts +43 -0
- package/src/protocol.ts +144 -0
- package/src/react/index.ts +2 -0
- package/src/react/useRendererClient.ts +145 -0
- package/src/types.ts +98 -0
- package/tsconfig.json +18 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HostInitMessage,
|
|
3
|
+
HostMessage,
|
|
4
|
+
HostThemeUpdateMessage,
|
|
5
|
+
HostTokenRefreshMessage,
|
|
6
|
+
HostGenerateTokenResultMessage,
|
|
7
|
+
IframeHistoryPushMessage,
|
|
8
|
+
IframeHistoryReplaceMessage,
|
|
9
|
+
} from './protocol.js';
|
|
10
|
+
import { isHostMessage } from './protocol.js';
|
|
11
|
+
import type { InitPayload, RendererApiAdapter, RendererConfig, RendererScope, RenderingType } from './types.js';
|
|
12
|
+
|
|
13
|
+
export interface RendererClientOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Expected origin of the host application.
|
|
16
|
+
* If set, messages from other origins are silently ignored.
|
|
17
|
+
* If omitted, all origins are accepted (suitable for local dev).
|
|
18
|
+
*/
|
|
19
|
+
targetOrigin?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type InitCallback = (payload: InitPayload) => void;
|
|
23
|
+
type ThemeCallback = (theme: 'light' | 'dark') => void;
|
|
24
|
+
type TokenCallback = (token: string, expiresAt: number) => void;
|
|
25
|
+
type NavigateCallback = (path: string) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Client SDK for custom renderers running inside a sandboxed iframe.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { RendererClient } from '@xeonr/renderer-sdk';
|
|
33
|
+
*
|
|
34
|
+
* const client = new RendererClient();
|
|
35
|
+
*
|
|
36
|
+
* client.onInit((payload) => {
|
|
37
|
+
* console.log('Received scope:', payload.scope);
|
|
38
|
+
* console.log('Token:', payload.token);
|
|
39
|
+
* // Bootstrap your app here
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* client.onThemeChange((theme) => {
|
|
43
|
+
* document.documentElement.setAttribute('data-theme', theme);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class RendererClient {
|
|
48
|
+
private targetOrigin: string;
|
|
49
|
+
private initCallbacks: InitCallback[] = [];
|
|
50
|
+
private themeCallbacks: ThemeCallback[] = [];
|
|
51
|
+
private tokenCallbacks: TokenCallback[] = [];
|
|
52
|
+
private navigateCallbacks: NavigateCallback[] = [];
|
|
53
|
+
|
|
54
|
+
private currentToken: string | null = null;
|
|
55
|
+
private currentTokenExpiresAt: number | null = null;
|
|
56
|
+
private currentScope: RendererScope | null = null;
|
|
57
|
+
private currentRenderingType: RenderingType | null = null;
|
|
58
|
+
private currentConfig: RendererConfig | null = null;
|
|
59
|
+
private currentTheme: 'light' | 'dark' = 'light';
|
|
60
|
+
private currentApiBaseUrl: string | null = null;
|
|
61
|
+
private currentPath: string = '/';
|
|
62
|
+
|
|
63
|
+
private listener: ((event: MessageEvent) => void) | null = null;
|
|
64
|
+
private hashChangeListener: (() => void) | null = null;
|
|
65
|
+
private historyPatched = false;
|
|
66
|
+
private originalPushState: typeof history.pushState | null = null;
|
|
67
|
+
private originalReplaceState: typeof history.replaceState | null = null;
|
|
68
|
+
private destroyed = false;
|
|
69
|
+
private suppressHashChange = false;
|
|
70
|
+
private pendingTokenResolvers: Array<(result: { token: string; expiresAt: number }) => void> = [];
|
|
71
|
+
private pendingGenerateTokenResolvers: Map<string, { resolve: (result: { accepted: boolean }) => void; reject: (error: Error) => void }> = new Map();
|
|
72
|
+
private apiAdapter: RendererApiAdapter | null = null;
|
|
73
|
+
|
|
74
|
+
constructor(options?: RendererClientOptions) {
|
|
75
|
+
this.targetOrigin = options?.targetOrigin ?? '*';
|
|
76
|
+
this.setup();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Lifecycle
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/** Register a callback for when the host sends the init message. */
|
|
84
|
+
onInit(cb: InitCallback): () => void {
|
|
85
|
+
this.initCallbacks.push(cb);
|
|
86
|
+
return () => {
|
|
87
|
+
this.initCallbacks = this.initCallbacks.filter(c => c !== cb);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Receive from host
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/** Register a callback for host theme changes. */
|
|
96
|
+
onThemeChange(cb: ThemeCallback): () => void {
|
|
97
|
+
this.themeCallbacks.push(cb);
|
|
98
|
+
return () => {
|
|
99
|
+
this.themeCallbacks = this.themeCallbacks.filter(c => c !== cb);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Register a callback for token refreshes from the host. */
|
|
104
|
+
onTokenRefresh(cb: TokenCallback): () => void {
|
|
105
|
+
this.tokenCallbacks.push(cb);
|
|
106
|
+
return () => {
|
|
107
|
+
this.tokenCallbacks = this.tokenCallbacks.filter(c => c !== cb);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Register a callback for hash-based navigation changes. */
|
|
112
|
+
onNavigate(cb: NavigateCallback): () => void {
|
|
113
|
+
this.navigateCallbacks.push(cb);
|
|
114
|
+
return () => {
|
|
115
|
+
this.navigateCallbacks = this.navigateCallbacks.filter(c => c !== cb);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Send to host
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** Request the host to open a specific upload. */
|
|
124
|
+
openUpload(uploadId: string): void {
|
|
125
|
+
this.postToHost({ type: 'uplim:openUpload', uploadId });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Request a fresh integration access token from the host. */
|
|
129
|
+
requestToken(): void {
|
|
130
|
+
this.postToHost({ type: 'uplim:tokenRequest' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Request a fresh token and wait for the response. */
|
|
134
|
+
requestTokenAsync(): Promise<{ token: string; expiresAt: number }> {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
this.pendingTokenResolvers.push(resolve);
|
|
137
|
+
this.postToHost({ type: 'uplim:tokenRequest' });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Request the host to close this renderer (modal mode). */
|
|
142
|
+
close(): void {
|
|
143
|
+
this.postToHost({ type: 'uplim:close' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Request the host to prompt the user to generate a long-lived token.
|
|
148
|
+
* The token is displayed to the user but never returned to the renderer.
|
|
149
|
+
* Resolves with `{ accepted: true }` if the user accepts, or `{ accepted: false }` if rejected.
|
|
150
|
+
*/
|
|
151
|
+
generateToken(opts: { reason: string; duration: 'forever' | string }): Promise<{ accepted: boolean }> {
|
|
152
|
+
const requestId = crypto.randomUUID();
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
this.pendingGenerateTokenResolvers.set(requestId, { resolve, reject });
|
|
155
|
+
this.postToHost({
|
|
156
|
+
type: 'uplim:generateToken',
|
|
157
|
+
requestId,
|
|
158
|
+
reason: opts.reason,
|
|
159
|
+
duration: opts.duration,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Getters
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/** Get the current integration access token. */
|
|
169
|
+
getToken(): string | null {
|
|
170
|
+
return this.currentToken;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Get the token expiry as a Unix timestamp (ms). */
|
|
174
|
+
getTokenExpiresAt(): number | null {
|
|
175
|
+
return this.currentTokenExpiresAt;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Check if the current token is expired or about to expire (within 30s). */
|
|
179
|
+
isTokenExpired(): boolean {
|
|
180
|
+
if (!this.currentTokenExpiresAt) return true;
|
|
181
|
+
return Date.now() >= this.currentTokenExpiresAt - 30_000;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Get the current scope. */
|
|
185
|
+
getScope(): RendererScope | null {
|
|
186
|
+
return this.currentScope;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Get the current rendering type. */
|
|
190
|
+
getRenderingType(): RenderingType | null {
|
|
191
|
+
return this.currentRenderingType;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Get the renderer config from config.json. */
|
|
195
|
+
getConfig(): RendererConfig | null {
|
|
196
|
+
return this.currentConfig;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Get the current theme. */
|
|
200
|
+
getTheme(): 'light' | 'dark' {
|
|
201
|
+
return this.currentTheme;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Get the current hash path (e.g. '/settings/advanced'). */
|
|
205
|
+
getPath(): string {
|
|
206
|
+
return this.currentPath;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Returns an API adapter compatible with `getUploadClientWithEnv()` from `@xeonr/uploads-sdk`.
|
|
211
|
+
* Handles authentication and automatic token refresh via the host bridge.
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* import { getUploadClientWithEnv } from '@xeonr/uploads-sdk/api/base';
|
|
215
|
+
* import { BucketUploadsService } from '@xeonr/uploads-protocol/uplim/api/v1/uploads_pb';
|
|
216
|
+
*
|
|
217
|
+
* const adapter = client.getApiAdapter();
|
|
218
|
+
* const uploadsClient = getUploadClientWithEnv(BucketUploadsService, adapter);
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
getApiAdapter(): RendererApiAdapter {
|
|
222
|
+
if (this.apiAdapter) return this.apiAdapter;
|
|
223
|
+
|
|
224
|
+
// Use a getter so hostname reflects the latest apiBaseUrl (set on init)
|
|
225
|
+
const self = this;
|
|
226
|
+
this.apiAdapter = {
|
|
227
|
+
get hostname() { return self.currentApiBaseUrl ?? undefined; },
|
|
228
|
+
tokenHelper: async () => this.currentToken,
|
|
229
|
+
onAuthenticationExpired: async (retryCount: number) => {
|
|
230
|
+
if (retryCount > 0) return false;
|
|
231
|
+
const result = await this.requestTokenAsync();
|
|
232
|
+
return !!result.token;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return this.apiAdapter;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Cleanup
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/** Remove all listeners and stop the client. */
|
|
244
|
+
destroy(): void {
|
|
245
|
+
this.destroyed = true;
|
|
246
|
+
if (this.listener) {
|
|
247
|
+
window.removeEventListener('message', this.listener);
|
|
248
|
+
this.listener = null;
|
|
249
|
+
}
|
|
250
|
+
if (this.hashChangeListener) {
|
|
251
|
+
window.removeEventListener('hashchange', this.hashChangeListener);
|
|
252
|
+
this.hashChangeListener = null;
|
|
253
|
+
}
|
|
254
|
+
if (this.historyPatched) {
|
|
255
|
+
if (this.originalPushState) window.history.pushState = this.originalPushState;
|
|
256
|
+
if (this.originalReplaceState) window.history.replaceState = this.originalReplaceState;
|
|
257
|
+
this.historyPatched = false;
|
|
258
|
+
}
|
|
259
|
+
this.initCallbacks = [];
|
|
260
|
+
this.themeCallbacks = [];
|
|
261
|
+
this.tokenCallbacks = [];
|
|
262
|
+
this.navigateCallbacks = [];
|
|
263
|
+
for (const [, pending] of this.pendingGenerateTokenResolvers) {
|
|
264
|
+
pending.reject(new Error('Client destroyed'));
|
|
265
|
+
}
|
|
266
|
+
this.pendingGenerateTokenResolvers.clear();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Internal
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
private setup(): void {
|
|
274
|
+
this.listener = (event: MessageEvent) => {
|
|
275
|
+
if (this.destroyed) return;
|
|
276
|
+
if (this.targetOrigin !== '*' && event.origin !== this.targetOrigin) return;
|
|
277
|
+
if (!isHostMessage(event.data)) return;
|
|
278
|
+
|
|
279
|
+
this.handleMessage(event.data);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
window.addEventListener('message', this.listener);
|
|
283
|
+
|
|
284
|
+
// Tell the host we're ready
|
|
285
|
+
this.postToHost({ type: 'uplim:ready' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private handleMessage(message: HostMessage): void {
|
|
289
|
+
switch (message.type) {
|
|
290
|
+
case 'uplim:init':
|
|
291
|
+
this.handleInit(message);
|
|
292
|
+
break;
|
|
293
|
+
case 'uplim:theme':
|
|
294
|
+
this.handleThemeUpdate(message);
|
|
295
|
+
break;
|
|
296
|
+
case 'uplim:token':
|
|
297
|
+
this.handleTokenRefresh(message);
|
|
298
|
+
break;
|
|
299
|
+
case 'uplim:generateTokenResult':
|
|
300
|
+
this.handleGenerateTokenResult(message);
|
|
301
|
+
break;
|
|
302
|
+
case 'uplim:historyBack':
|
|
303
|
+
window.history.back();
|
|
304
|
+
break;
|
|
305
|
+
case 'uplim:historyForward':
|
|
306
|
+
window.history.forward();
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private handleInit(message: HostInitMessage): void {
|
|
312
|
+
const { payload } = message;
|
|
313
|
+
this.currentToken = payload.token;
|
|
314
|
+
this.currentTokenExpiresAt = payload.tokenExpiresAt;
|
|
315
|
+
this.currentScope = payload.scope;
|
|
316
|
+
this.currentRenderingType = payload.renderingType;
|
|
317
|
+
this.currentConfig = payload.config;
|
|
318
|
+
this.currentTheme = payload.theme;
|
|
319
|
+
this.currentApiBaseUrl = payload.apiBaseUrl;
|
|
320
|
+
|
|
321
|
+
// Apply initial path from host URL fragment
|
|
322
|
+
if (payload.initialPath) {
|
|
323
|
+
this.suppressHashChange = true;
|
|
324
|
+
window.location.hash = payload.initialPath;
|
|
325
|
+
this.currentPath = payload.initialPath;
|
|
326
|
+
this.suppressHashChange = false;
|
|
327
|
+
} else {
|
|
328
|
+
this.currentPath = this.getHashPath();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Start observing hash changes
|
|
332
|
+
this.setupHashTracking();
|
|
333
|
+
|
|
334
|
+
for (const cb of this.initCallbacks) {
|
|
335
|
+
cb(payload);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Tell the host we've processed init and are ready to be displayed
|
|
339
|
+
this.postToHost({ type: 'uplim:ack' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private handleThemeUpdate(message: HostThemeUpdateMessage): void {
|
|
343
|
+
this.currentTheme = message.theme;
|
|
344
|
+
for (const cb of this.themeCallbacks) {
|
|
345
|
+
cb(message.theme);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private handleTokenRefresh(message: HostTokenRefreshMessage): void {
|
|
350
|
+
this.currentToken = message.token;
|
|
351
|
+
this.currentTokenExpiresAt = message.tokenExpiresAt;
|
|
352
|
+
|
|
353
|
+
// Resolve any pending requestTokenAsync() calls
|
|
354
|
+
const resolvers = this.pendingTokenResolvers;
|
|
355
|
+
this.pendingTokenResolvers = [];
|
|
356
|
+
for (const resolve of resolvers) {
|
|
357
|
+
resolve({ token: message.token, expiresAt: message.tokenExpiresAt });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const cb of this.tokenCallbacks) {
|
|
361
|
+
cb(message.token, message.tokenExpiresAt);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private handleGenerateTokenResult(message: HostGenerateTokenResultMessage): void {
|
|
366
|
+
const pending = this.pendingGenerateTokenResolvers.get(message.requestId);
|
|
367
|
+
if (pending) {
|
|
368
|
+
this.pendingGenerateTokenResolvers.delete(message.requestId);
|
|
369
|
+
pending.resolve({ accepted: message.accepted });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private getHashPath(): string {
|
|
374
|
+
const hash = window.location.hash;
|
|
375
|
+
if (!hash || hash === '#') return '/';
|
|
376
|
+
// Strip leading '#' (and optional leading '#/')
|
|
377
|
+
return hash.startsWith('#/') ? hash.slice(1) : hash.slice(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private onHashChanged(isPush: boolean): void {
|
|
381
|
+
if (this.destroyed || this.suppressHashChange) return;
|
|
382
|
+
|
|
383
|
+
const newPath = this.getHashPath();
|
|
384
|
+
if (newPath === this.currentPath) return;
|
|
385
|
+
|
|
386
|
+
this.currentPath = newPath;
|
|
387
|
+
|
|
388
|
+
const msg: IframeHistoryPushMessage | IframeHistoryReplaceMessage = isPush
|
|
389
|
+
? { type: 'uplim:historyPush', path: newPath }
|
|
390
|
+
: { type: 'uplim:historyReplace', path: newPath };
|
|
391
|
+
this.postToHost(msg);
|
|
392
|
+
|
|
393
|
+
for (const cb of this.navigateCallbacks) {
|
|
394
|
+
cb(newPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private setupHashTracking(): void {
|
|
399
|
+
if (this.hashChangeListener) return;
|
|
400
|
+
|
|
401
|
+
// Listen for direct hash changes (e.g. window.location.hash = '...' or <a href="#...">)
|
|
402
|
+
this.hashChangeListener = () => this.onHashChanged(true);
|
|
403
|
+
window.addEventListener('hashchange', this.hashChangeListener);
|
|
404
|
+
|
|
405
|
+
// Patch pushState/replaceState — hash routers use these instead of setting
|
|
406
|
+
// window.location.hash directly, and they don't fire hashchange.
|
|
407
|
+
this.originalPushState = window.history.pushState.bind(window.history);
|
|
408
|
+
this.originalReplaceState = window.history.replaceState.bind(window.history);
|
|
409
|
+
|
|
410
|
+
const self = this;
|
|
411
|
+
window.history.pushState = function (...args: Parameters<typeof history.pushState>) {
|
|
412
|
+
self.originalPushState!(...args);
|
|
413
|
+
self.onHashChanged(true);
|
|
414
|
+
};
|
|
415
|
+
window.history.replaceState = function (...args: Parameters<typeof history.replaceState>) {
|
|
416
|
+
self.originalReplaceState!(...args);
|
|
417
|
+
self.onHashChanged(false);
|
|
418
|
+
};
|
|
419
|
+
this.historyPatched = true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private postToHost(message: object): void {
|
|
423
|
+
if (this.destroyed) return;
|
|
424
|
+
window.parent.postMessage(message, this.targetOrigin);
|
|
425
|
+
}
|
|
426
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { RendererClient } from './client.js';
|
|
2
|
+
export type { RendererClientOptions } from './client.js';
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
RendererConfig,
|
|
6
|
+
RendererPermission,
|
|
7
|
+
RendererScope,
|
|
8
|
+
RendererBucketScope,
|
|
9
|
+
RendererFolderScope,
|
|
10
|
+
RendererUploadScope,
|
|
11
|
+
RendererVirtualFileScope,
|
|
12
|
+
RenderingType,
|
|
13
|
+
InitPayload,
|
|
14
|
+
RendererApiAdapter,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
MESSAGE_PREFIX,
|
|
19
|
+
PROTOCOL_VERSION,
|
|
20
|
+
isRendererMessage,
|
|
21
|
+
isHostMessage,
|
|
22
|
+
isIframeMessage,
|
|
23
|
+
} from './protocol.js';
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
HostMessage,
|
|
27
|
+
HostInitMessage,
|
|
28
|
+
HostThemeUpdateMessage,
|
|
29
|
+
HostTokenRefreshMessage,
|
|
30
|
+
HostGenerateTokenResultMessage,
|
|
31
|
+
HostHistoryBackMessage,
|
|
32
|
+
HostHistoryForwardMessage,
|
|
33
|
+
IframeMessage,
|
|
34
|
+
IframeReadyMessage,
|
|
35
|
+
IframeOpenUploadMessage,
|
|
36
|
+
IframeTokenRequestMessage,
|
|
37
|
+
IframeCloseMessage,
|
|
38
|
+
IframeInitAckMessage,
|
|
39
|
+
IframeGenerateTokenMessage,
|
|
40
|
+
IframeHistoryPushMessage,
|
|
41
|
+
IframeHistoryReplaceMessage,
|
|
42
|
+
RendererMessage,
|
|
43
|
+
} from './protocol.js';
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { InitPayload } from './types.js';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Message namespace — all messages use this prefix
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export const MESSAGE_PREFIX = 'uplim:' as const;
|
|
7
|
+
export const PROTOCOL_VERSION = 1 as const;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Host → Iframe messages
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Sent after the iframe reports 'ready'. Contains everything the renderer needs to bootstrap. */
|
|
14
|
+
export interface HostInitMessage {
|
|
15
|
+
type: 'uplim:init';
|
|
16
|
+
payload: InitPayload;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Sent when the host theme changes. */
|
|
20
|
+
export interface HostThemeUpdateMessage {
|
|
21
|
+
type: 'uplim:theme';
|
|
22
|
+
theme: 'light' | 'dark';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sent proactively or in response to a token request. */
|
|
26
|
+
export interface HostTokenRefreshMessage {
|
|
27
|
+
type: 'uplim:token';
|
|
28
|
+
token: string;
|
|
29
|
+
tokenExpiresAt: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Sent in response to a generateToken request from the iframe. */
|
|
33
|
+
export interface HostGenerateTokenResultMessage {
|
|
34
|
+
type: 'uplim:generateTokenResult';
|
|
35
|
+
requestId: string;
|
|
36
|
+
accepted: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tell the iframe to go back in its history. */
|
|
40
|
+
export interface HostHistoryBackMessage {
|
|
41
|
+
type: 'uplim:historyBack';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tell the iframe to go forward in its history. */
|
|
45
|
+
export interface HostHistoryForwardMessage {
|
|
46
|
+
type: 'uplim:historyForward';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type HostMessage =
|
|
50
|
+
| HostInitMessage
|
|
51
|
+
| HostThemeUpdateMessage
|
|
52
|
+
| HostTokenRefreshMessage
|
|
53
|
+
| HostGenerateTokenResultMessage
|
|
54
|
+
| HostHistoryBackMessage
|
|
55
|
+
| HostHistoryForwardMessage;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Iframe → Host messages
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** Iframe is loaded and ready to receive the init message. */
|
|
62
|
+
export interface IframeReadyMessage {
|
|
63
|
+
type: 'uplim:ready';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Request the host to open a specific upload (e.g. in a preview modal). */
|
|
67
|
+
export interface IframeOpenUploadMessage {
|
|
68
|
+
type: 'uplim:openUpload';
|
|
69
|
+
uploadId: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Request a fresh delegated token. Host responds with HostTokenRefreshMessage. */
|
|
73
|
+
export interface IframeTokenRequestMessage {
|
|
74
|
+
type: 'uplim:tokenRequest';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Request the host to close the renderer (relevant for modal mode). */
|
|
78
|
+
export interface IframeCloseMessage {
|
|
79
|
+
type: 'uplim:close';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Acknowledge that init has been processed and the renderer is ready to be displayed. */
|
|
83
|
+
export interface IframeInitAckMessage {
|
|
84
|
+
type: 'uplim:ack';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Request the host to prompt the user to generate a long-lived token. */
|
|
88
|
+
export interface IframeGenerateTokenMessage {
|
|
89
|
+
type: 'uplim:generateToken';
|
|
90
|
+
requestId: string;
|
|
91
|
+
reason: string;
|
|
92
|
+
duration: 'forever' | string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Notify the host that the renderer navigated to a new hash path. */
|
|
96
|
+
export interface IframeHistoryPushMessage {
|
|
97
|
+
type: 'uplim:historyPush';
|
|
98
|
+
path: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Notify the host that the renderer replaced the current hash path. */
|
|
102
|
+
export interface IframeHistoryReplaceMessage {
|
|
103
|
+
type: 'uplim:historyReplace';
|
|
104
|
+
path: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type IframeMessage =
|
|
108
|
+
| IframeReadyMessage
|
|
109
|
+
| IframeOpenUploadMessage
|
|
110
|
+
| IframeTokenRequestMessage
|
|
111
|
+
| IframeCloseMessage
|
|
112
|
+
| IframeInitAckMessage
|
|
113
|
+
| IframeGenerateTokenMessage
|
|
114
|
+
| IframeHistoryPushMessage
|
|
115
|
+
| IframeHistoryReplaceMessage;
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Union type for any message
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export type RendererMessage = HostMessage | IframeMessage;
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Type guard helpers
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
export function isRendererMessage(data: unknown): data is RendererMessage {
|
|
126
|
+
return (
|
|
127
|
+
typeof data === 'object' &&
|
|
128
|
+
data !== null &&
|
|
129
|
+
'type' in data &&
|
|
130
|
+
typeof (data as { type: unknown }).type === 'string' &&
|
|
131
|
+
(data as { type: string }).type.startsWith(MESSAGE_PREFIX)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isHostMessage(data: unknown): data is HostMessage {
|
|
136
|
+
if (!isRendererMessage(data)) return false;
|
|
137
|
+
const t = data.type;
|
|
138
|
+
return t === 'uplim:init' || t === 'uplim:theme' || t === 'uplim:token' || t === 'uplim:generateTokenResult' || t === 'uplim:historyBack' || t === 'uplim:historyForward';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isIframeMessage(data: unknown): data is IframeMessage {
|
|
142
|
+
if (!isRendererMessage(data)) return false;
|
|
143
|
+
return !isHostMessage(data);
|
|
144
|
+
}
|