@proveanything/smartlinks 1.3.8 → 1.3.9
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/cache.d.ts +32 -0
- package/dist/cache.js +125 -0
- package/dist/docs/API_SUMMARY.md +160 -1
- package/dist/docs/iframe-responder.md +463 -0
- package/dist/iframe.d.ts +2 -0
- package/dist/iframe.js +2 -0
- package/dist/iframeResponder.d.ts +93 -0
- package/dist/iframeResponder.js +549 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/types/iframeResponder.d.ts +112 -0
- package/dist/types/iframeResponder.js +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/docs/API_SUMMARY.md +160 -1
- package/docs/iframe-responder.md +463 -0
- package/package.json +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// IframeResponder - Parent-side iframe communication handler
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import * as cache from './cache';
|
|
5
|
+
import { collection } from './api/collection';
|
|
6
|
+
/**
|
|
7
|
+
* Parent-side iframe responder for SmartLinks microapp embedding.
|
|
8
|
+
*
|
|
9
|
+
* Handles all bidirectional communication with embedded iframes:
|
|
10
|
+
* - API proxy requests (with caching)
|
|
11
|
+
* - Authentication state synchronization
|
|
12
|
+
* - Deep linking / route changes
|
|
13
|
+
* - Resize management
|
|
14
|
+
* - Chunked file upload proxying
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const responder = new IframeResponder({
|
|
19
|
+
* collectionId: 'my-collection',
|
|
20
|
+
* appId: 'warranty',
|
|
21
|
+
* onAuthLogin: async (token, user) => {
|
|
22
|
+
* smartlinks.setBearerToken(token);
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* const src = await responder.attach(iframeElement);
|
|
27
|
+
* iframeElement.src = src;
|
|
28
|
+
*
|
|
29
|
+
* // Cleanup
|
|
30
|
+
* responder.destroy();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class IframeResponder {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.iframe = null;
|
|
36
|
+
this.uploads = new Map();
|
|
37
|
+
this.isInitialLoad = true;
|
|
38
|
+
this.messageHandler = null;
|
|
39
|
+
this.resizeHandler = null;
|
|
40
|
+
this.appUrl = null;
|
|
41
|
+
this.resolveReady = null;
|
|
42
|
+
this.options = options;
|
|
43
|
+
this.cache = options.cache || {};
|
|
44
|
+
// Create ready promise
|
|
45
|
+
this.ready = new Promise((resolve) => {
|
|
46
|
+
this.resolveReady = resolve;
|
|
47
|
+
});
|
|
48
|
+
// Start resolving app URL
|
|
49
|
+
this.resolveAppUrl().then(() => {
|
|
50
|
+
var _a;
|
|
51
|
+
(_a = this.resolveReady) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Attach to an iframe element.
|
|
56
|
+
* Returns the src URL to set on the iframe.
|
|
57
|
+
*/
|
|
58
|
+
async attach(iframe) {
|
|
59
|
+
await this.ready;
|
|
60
|
+
this.iframe = iframe;
|
|
61
|
+
// Set up message listener
|
|
62
|
+
this.messageHandler = this.handleMessage.bind(this);
|
|
63
|
+
window.addEventListener('message', this.messageHandler);
|
|
64
|
+
// Set up resize listener for viewport-based calculations
|
|
65
|
+
this.resizeHandler = this.calculateViewportHeight.bind(this);
|
|
66
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
67
|
+
window.addEventListener('orientationchange', this.resizeHandler);
|
|
68
|
+
return this.buildIframeSrc();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Update cached data (e.g., after user logs in).
|
|
72
|
+
*/
|
|
73
|
+
updateCache(data) {
|
|
74
|
+
this.cache = Object.assign(Object.assign({}, this.cache), data);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cleanup - remove event listeners and clear state.
|
|
78
|
+
*/
|
|
79
|
+
destroy() {
|
|
80
|
+
if (this.messageHandler) {
|
|
81
|
+
window.removeEventListener('message', this.messageHandler);
|
|
82
|
+
this.messageHandler = null;
|
|
83
|
+
}
|
|
84
|
+
if (this.resizeHandler) {
|
|
85
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
86
|
+
window.removeEventListener('orientationchange', this.resizeHandler);
|
|
87
|
+
this.resizeHandler = null;
|
|
88
|
+
}
|
|
89
|
+
this.uploads.clear();
|
|
90
|
+
this.iframe = null;
|
|
91
|
+
}
|
|
92
|
+
// ===========================================================================
|
|
93
|
+
// URL Resolution
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
async resolveAppUrl() {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
// Use explicit override if provided
|
|
98
|
+
if (this.options.appUrl) {
|
|
99
|
+
this.appUrl = this.options.appUrl;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Check pre-populated cache
|
|
103
|
+
const cachedApps = this.cache.apps;
|
|
104
|
+
if (cachedApps) {
|
|
105
|
+
const app = cachedApps.find(a => a.id === this.options.appId);
|
|
106
|
+
if (app) {
|
|
107
|
+
this.appUrl = this.getVersionUrl(app);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Fetch from API with caching
|
|
112
|
+
try {
|
|
113
|
+
const appsConfig = await cache.getOrFetch(`apps:${this.options.collectionId}`, () => collection.getAppsConfig(this.options.collectionId), { ttl: 5 * 60 * 1000, storage: 'session' });
|
|
114
|
+
const apps = appsConfig.apps;
|
|
115
|
+
const app = apps.find(a => a.id === this.options.appId);
|
|
116
|
+
if (!app) {
|
|
117
|
+
throw new Error(`App "${this.options.appId}" not found in collection "${this.options.collectionId}"`);
|
|
118
|
+
}
|
|
119
|
+
this.appUrl = this.getVersionUrl(app);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
(_b = (_a = this.options).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
getVersionUrl(app) {
|
|
127
|
+
// Use publicIframeUrl from AppConfig
|
|
128
|
+
if (!app.publicIframeUrl) {
|
|
129
|
+
throw new Error(`App "${app.id}" does not have a publicIframeUrl configured`);
|
|
130
|
+
}
|
|
131
|
+
return app.publicIframeUrl;
|
|
132
|
+
}
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// URL Building
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
buildIframeSrc() {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
if (!this.appUrl) {
|
|
139
|
+
throw new Error('App URL not resolved');
|
|
140
|
+
}
|
|
141
|
+
const params = new URLSearchParams();
|
|
142
|
+
// Required context
|
|
143
|
+
params.set('collectionId', this.options.collectionId);
|
|
144
|
+
params.set('appId', this.options.appId);
|
|
145
|
+
// Optional context
|
|
146
|
+
if (this.options.productId)
|
|
147
|
+
params.set('productId', this.options.productId);
|
|
148
|
+
if (this.options.proofId)
|
|
149
|
+
params.set('proofId', this.options.proofId);
|
|
150
|
+
if (this.options.isAdmin)
|
|
151
|
+
params.set('isAdmin', 'true');
|
|
152
|
+
// Dark mode from collection
|
|
153
|
+
const isDark = (_b = (_a = this.cache.collection) === null || _a === void 0 ? void 0 : _a.dark) !== null && _b !== void 0 ? _b : false;
|
|
154
|
+
params.set('dark', isDark ? '1' : '0');
|
|
155
|
+
// Parent URL for redirects
|
|
156
|
+
try {
|
|
157
|
+
params.set('parentUrl', window.location.href);
|
|
158
|
+
}
|
|
159
|
+
catch (_c) {
|
|
160
|
+
// Ignore if can't access location
|
|
161
|
+
}
|
|
162
|
+
// Encode theme from collection
|
|
163
|
+
if (this.cache.collection) {
|
|
164
|
+
try {
|
|
165
|
+
const themeData = {
|
|
166
|
+
p: this.cache.collection.primaryColor,
|
|
167
|
+
s: this.cache.collection.secondaryColor,
|
|
168
|
+
m: this.cache.collection.dark ? 'd' : 'l',
|
|
169
|
+
};
|
|
170
|
+
if (themeData.p || themeData.s) {
|
|
171
|
+
params.set('theme', btoa(JSON.stringify(themeData)));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (_d) {
|
|
175
|
+
// Ignore encoding errors
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Build URL
|
|
179
|
+
let base = this.appUrl.replace(/#\/?$/, '');
|
|
180
|
+
if (base.endsWith('/')) {
|
|
181
|
+
base = base.slice(0, -1);
|
|
182
|
+
}
|
|
183
|
+
// Build hash path
|
|
184
|
+
let hashPath = this.options.initialPath || '';
|
|
185
|
+
if (hashPath && !hashPath.startsWith('/')) {
|
|
186
|
+
hashPath = '/' + hashPath;
|
|
187
|
+
}
|
|
188
|
+
if (hashPath === '/') {
|
|
189
|
+
hashPath = '';
|
|
190
|
+
}
|
|
191
|
+
return `${base}/#/${hashPath}?${params.toString()}`.replace('/#//', '/#/');
|
|
192
|
+
}
|
|
193
|
+
// ===========================================================================
|
|
194
|
+
// Viewport Resize Calculation
|
|
195
|
+
// ===========================================================================
|
|
196
|
+
calculateViewportHeight() {
|
|
197
|
+
var _a, _b;
|
|
198
|
+
if (!this.iframe)
|
|
199
|
+
return;
|
|
200
|
+
const container = this.iframe.parentElement;
|
|
201
|
+
if (!container)
|
|
202
|
+
return;
|
|
203
|
+
const rect = container.getBoundingClientRect();
|
|
204
|
+
const viewportHeight = window.innerHeight;
|
|
205
|
+
const calculatedHeight = Math.max(0, viewportHeight - rect.top);
|
|
206
|
+
(_b = (_a = this.options).onResize) === null || _b === void 0 ? void 0 : _b.call(_a, calculatedHeight);
|
|
207
|
+
}
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// Message Handling
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
async handleMessage(event) {
|
|
212
|
+
// Validate source is our iframe
|
|
213
|
+
if (!this.iframe || event.source !== this.iframe.contentWindow) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const data = event.data;
|
|
217
|
+
if (!data || typeof data !== 'object')
|
|
218
|
+
return;
|
|
219
|
+
// Route changes (deep linking)
|
|
220
|
+
if (data.type === 'smartlinks-route-change') {
|
|
221
|
+
this.handleRouteChange(data);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Standardized iframe messages
|
|
225
|
+
if (data._smartlinksIframeMessage) {
|
|
226
|
+
await this.handleStandardMessage(data, event);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// File upload proxy
|
|
230
|
+
if (data._smartlinksProxyUpload) {
|
|
231
|
+
await this.handleUpload(data, event);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// API proxy requests
|
|
235
|
+
if (data._smartlinksProxyRequest) {
|
|
236
|
+
await this.handleProxyRequest(data, event);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ===========================================================================
|
|
241
|
+
// Route Changes (Deep Linking)
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
handleRouteChange(data) {
|
|
244
|
+
var _a, _b;
|
|
245
|
+
// Skip initial load to prevent duplicating path
|
|
246
|
+
if (this.isInitialLoad) {
|
|
247
|
+
this.isInitialLoad = false;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { context = {}, state = {}, path = '' } = data;
|
|
251
|
+
// Filter out known iframe params, keep only app-specific state
|
|
252
|
+
const KNOWN_PARAMS = new Set([
|
|
253
|
+
'collectionId', 'appId', 'productId', 'proofId',
|
|
254
|
+
'isAdmin', 'dark', 'parentUrl', 'theme', 'lang',
|
|
255
|
+
]);
|
|
256
|
+
const filteredState = {};
|
|
257
|
+
Object.entries(context).forEach(([key, value]) => {
|
|
258
|
+
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
259
|
+
filteredState[key] = value;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
Object.entries(state).forEach(([key, value]) => {
|
|
263
|
+
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
264
|
+
filteredState[key] = value;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
(_b = (_a = this.options).onRouteChange) === null || _b === void 0 ? void 0 : _b.call(_a, path, filteredState);
|
|
268
|
+
}
|
|
269
|
+
// ===========================================================================
|
|
270
|
+
// Standard Iframe Messages
|
|
271
|
+
// ===========================================================================
|
|
272
|
+
async handleStandardMessage(data, event) {
|
|
273
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
274
|
+
switch (data.type) {
|
|
275
|
+
case 'smartlinks:resize': {
|
|
276
|
+
const contentHeight = (_a = data.payload) === null || _a === void 0 ? void 0 : _a.height;
|
|
277
|
+
if (typeof contentHeight === 'number' && contentHeight > 0) {
|
|
278
|
+
(_c = (_b = this.options).onResize) === null || _c === void 0 ? void 0 : _c.call(_b, contentHeight);
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case 'smartlinks:redirect': {
|
|
283
|
+
const url = (_d = data.payload) === null || _d === void 0 ? void 0 : _d.url;
|
|
284
|
+
if (url && typeof url === 'string') {
|
|
285
|
+
window.location.href = url;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 'smartlinks:authkit:login': {
|
|
290
|
+
const { token, user, accountData, messageId } = data.payload || {};
|
|
291
|
+
if (!this.options.onAuthLogin) {
|
|
292
|
+
this.sendResponse(event, {
|
|
293
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
294
|
+
messageId,
|
|
295
|
+
success: false,
|
|
296
|
+
error: 'No auth handler available',
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
// TODO: Validate token using SDK auth utilities when available
|
|
302
|
+
// await auth.verifyToken(token);
|
|
303
|
+
await this.options.onAuthLogin(token, user, accountData);
|
|
304
|
+
// Update cache with new user
|
|
305
|
+
this.cache.user = user;
|
|
306
|
+
this.sendResponse(event, {
|
|
307
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
308
|
+
messageId,
|
|
309
|
+
success: true,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
this.sendResponse(event, {
|
|
314
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
315
|
+
messageId,
|
|
316
|
+
success: false,
|
|
317
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || 'Auth failed',
|
|
318
|
+
});
|
|
319
|
+
(_f = (_e = this.options).onError) === null || _f === void 0 ? void 0 : _f.call(_e, err);
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case 'smartlinks:authkit:logout': {
|
|
324
|
+
if (this.options.onAuthLogout) {
|
|
325
|
+
await this.options.onAuthLogout();
|
|
326
|
+
this.cache.user = null;
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'smartlinks:authkit:redirect': {
|
|
331
|
+
const url = (_g = data.payload) === null || _g === void 0 ? void 0 : _g.url;
|
|
332
|
+
if (url && typeof url === 'string') {
|
|
333
|
+
window.location.href = url;
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
// API Proxy
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
async handleProxyRequest(data, event) {
|
|
343
|
+
var _a, _b, _c;
|
|
344
|
+
const response = {
|
|
345
|
+
_smartlinksProxyResponse: true,
|
|
346
|
+
id: data.id,
|
|
347
|
+
};
|
|
348
|
+
// Handle custom proxy requests (redirects, etc.)
|
|
349
|
+
if ('_smartlinksCustomProxyRequest' in data && data._smartlinksCustomProxyRequest) {
|
|
350
|
+
if (data.request === 'REDIRECT') {
|
|
351
|
+
const url = (_a = data.params) === null || _a === void 0 ? void 0 : _a.url;
|
|
352
|
+
if (url) {
|
|
353
|
+
window.location.href = url;
|
|
354
|
+
}
|
|
355
|
+
response.data = { success: true };
|
|
356
|
+
this.sendResponse(event, response);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const proxyData = data;
|
|
361
|
+
try {
|
|
362
|
+
const path = proxyData.path.startsWith('/') ? proxyData.path.slice(1) : proxyData.path;
|
|
363
|
+
// Check for cached data matches on GET requests
|
|
364
|
+
if (proxyData.method === 'GET') {
|
|
365
|
+
const cachedResponse = this.getCachedResponse(path);
|
|
366
|
+
if (cachedResponse !== null) {
|
|
367
|
+
response.data = cachedResponse;
|
|
368
|
+
this.sendResponse(event, response);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Forward to actual API using SDK's http utilities
|
|
373
|
+
// Build full URL and use fetch for now (can be replaced with SDK request when needed)
|
|
374
|
+
const baseUrl = '/api/v1';
|
|
375
|
+
const fetchOptions = {
|
|
376
|
+
method: proxyData.method,
|
|
377
|
+
headers: proxyData.headers,
|
|
378
|
+
};
|
|
379
|
+
if (proxyData.body && proxyData.method !== 'GET') {
|
|
380
|
+
fetchOptions.body = JSON.stringify(proxyData.body);
|
|
381
|
+
fetchOptions.headers = Object.assign(Object.assign({}, fetchOptions.headers), { 'Content-Type': 'application/json' });
|
|
382
|
+
}
|
|
383
|
+
const fetchResponse = await fetch(`${baseUrl}/${path}`, fetchOptions);
|
|
384
|
+
response.data = await fetchResponse.json();
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
response.error = (err === null || err === void 0 ? void 0 : err.message) || 'Unknown error';
|
|
388
|
+
(_c = (_b = this.options).onError) === null || _c === void 0 ? void 0 : _c.call(_b, err);
|
|
389
|
+
}
|
|
390
|
+
this.sendResponse(event, response);
|
|
391
|
+
}
|
|
392
|
+
getCachedResponse(path) {
|
|
393
|
+
// Collection request
|
|
394
|
+
if (path.includes('/collection/') && this.cache.collection) {
|
|
395
|
+
const match = path.match(/collection\/([^/]+)/);
|
|
396
|
+
if (match && match[1] === this.options.collectionId) {
|
|
397
|
+
return JSON.parse(JSON.stringify(this.cache.collection));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Product request
|
|
401
|
+
if (path.includes('/product/') && this.cache.product && this.options.productId) {
|
|
402
|
+
const match = path.match(/product\/([^/]+)/);
|
|
403
|
+
if (match && match[1] === this.options.productId) {
|
|
404
|
+
return JSON.parse(JSON.stringify(this.cache.product));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Proof request
|
|
408
|
+
if (path.includes('/proof/') && this.cache.proof && this.options.proofId) {
|
|
409
|
+
const match = path.match(/proof\/([^/]+)/);
|
|
410
|
+
if (match && match[1] === this.options.proofId) {
|
|
411
|
+
return JSON.parse(JSON.stringify(this.cache.proof));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Account request
|
|
415
|
+
if (path.includes('/account') && this.cache.user) {
|
|
416
|
+
return JSON.parse(JSON.stringify(Object.assign(Object.assign({}, this.cache.user.accountData), { uid: this.cache.user.uid, email: this.cache.user.email, displayName: this.cache.user.displayName })));
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
// ===========================================================================
|
|
421
|
+
// Chunked File Uploads
|
|
422
|
+
// ===========================================================================
|
|
423
|
+
async handleUpload(data, event) {
|
|
424
|
+
var _a, _b;
|
|
425
|
+
switch (data.phase) {
|
|
426
|
+
case 'start': {
|
|
427
|
+
const startData = data;
|
|
428
|
+
this.uploads.set(startData.id, {
|
|
429
|
+
chunks: [],
|
|
430
|
+
fields: startData.fields,
|
|
431
|
+
fileInfo: startData.fileInfo,
|
|
432
|
+
path: startData.path,
|
|
433
|
+
});
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'chunk': {
|
|
437
|
+
const chunkData = data;
|
|
438
|
+
const upload = this.uploads.get(chunkData.id);
|
|
439
|
+
if (upload) {
|
|
440
|
+
const uint8Array = new Uint8Array(chunkData.chunk);
|
|
441
|
+
upload.chunks.push(uint8Array);
|
|
442
|
+
this.sendResponse(event, {
|
|
443
|
+
_smartlinksProxyUpload: true,
|
|
444
|
+
phase: 'ack',
|
|
445
|
+
id: chunkData.id,
|
|
446
|
+
seq: chunkData.seq,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case 'end': {
|
|
452
|
+
const endData = data;
|
|
453
|
+
const upload = this.uploads.get(endData.id);
|
|
454
|
+
if (!upload)
|
|
455
|
+
break;
|
|
456
|
+
try {
|
|
457
|
+
const blobParts = upload.chunks.map(chunk => chunk.buffer.slice(0));
|
|
458
|
+
const blob = new Blob(blobParts, {
|
|
459
|
+
type: upload.fileInfo.type || 'application/octet-stream'
|
|
460
|
+
});
|
|
461
|
+
const formData = new FormData();
|
|
462
|
+
upload.fields.forEach(([key, value]) => formData.append(key, value));
|
|
463
|
+
formData.append(upload.fileInfo.key || 'file', blob, upload.fileInfo.name || 'upload.bin');
|
|
464
|
+
const path = upload.path.startsWith('/') ? upload.path.slice(1) : upload.path;
|
|
465
|
+
const baseUrl = '/api/v1';
|
|
466
|
+
const response = await fetch(`${baseUrl}/${path}`, {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
body: formData,
|
|
469
|
+
});
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
472
|
+
}
|
|
473
|
+
const result = await response.json();
|
|
474
|
+
this.sendResponse(event, {
|
|
475
|
+
_smartlinksProxyUpload: true,
|
|
476
|
+
phase: 'done',
|
|
477
|
+
id: endData.id,
|
|
478
|
+
ok: true,
|
|
479
|
+
data: result,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
this.sendResponse(event, {
|
|
484
|
+
_smartlinksProxyUpload: true,
|
|
485
|
+
phase: 'done',
|
|
486
|
+
id: endData.id,
|
|
487
|
+
ok: false,
|
|
488
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || 'Upload failed',
|
|
489
|
+
});
|
|
490
|
+
(_b = (_a = this.options).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
491
|
+
}
|
|
492
|
+
this.uploads.delete(endData.id);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// ===========================================================================
|
|
498
|
+
// Utilities
|
|
499
|
+
// ===========================================================================
|
|
500
|
+
sendResponse(event, message) {
|
|
501
|
+
if (event.source && 'postMessage' in event.source) {
|
|
502
|
+
event.source.postMessage(message, event.origin);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// =============================================================================
|
|
507
|
+
// Helper Functions
|
|
508
|
+
// =============================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Determine admin status from collection/proof roles.
|
|
511
|
+
*/
|
|
512
|
+
export function isAdminFromRoles(user, collection, proof) {
|
|
513
|
+
var _a, _b;
|
|
514
|
+
if (!(user === null || user === void 0 ? void 0 : user.uid))
|
|
515
|
+
return false;
|
|
516
|
+
return !!(((_a = collection === null || collection === void 0 ? void 0 : collection.roles) === null || _a === void 0 ? void 0 : _a[user.uid]) || ((_b = proof === null || proof === void 0 ? void 0 : proof.roles) === null || _b === void 0 ? void 0 : _b[user.uid]));
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Build iframe src URL with all context params.
|
|
520
|
+
* Standalone helper for cases where you don't need the full IframeResponder.
|
|
521
|
+
*/
|
|
522
|
+
export function buildIframeSrc(options) {
|
|
523
|
+
var _a, _b;
|
|
524
|
+
const params = new URLSearchParams();
|
|
525
|
+
params.set('collectionId', options.collectionId);
|
|
526
|
+
params.set('appId', options.appId);
|
|
527
|
+
if (options.productId)
|
|
528
|
+
params.set('productId', options.productId);
|
|
529
|
+
if (options.proofId)
|
|
530
|
+
params.set('proofId', options.proofId);
|
|
531
|
+
if (options.isAdmin)
|
|
532
|
+
params.set('isAdmin', 'true');
|
|
533
|
+
params.set('dark', options.dark ? '1' : '0');
|
|
534
|
+
if (((_a = options.theme) === null || _a === void 0 ? void 0 : _a.primary) || ((_b = options.theme) === null || _b === void 0 ? void 0 : _b.secondary)) {
|
|
535
|
+
const themeData = { p: options.theme.primary, s: options.theme.secondary };
|
|
536
|
+
params.set('theme', btoa(JSON.stringify(themeData)));
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
params.set('parentUrl', window.location.href);
|
|
540
|
+
}
|
|
541
|
+
catch (_c) {
|
|
542
|
+
// Ignore
|
|
543
|
+
}
|
|
544
|
+
let base = options.appUrl.replace(/#\/?$/, '').replace(/\/$/, '');
|
|
545
|
+
let path = options.initialPath || '';
|
|
546
|
+
if (path && !path.startsWith('/'))
|
|
547
|
+
path = '/' + path;
|
|
548
|
+
return `${base}/#${path}?${params.toString()}`.replace('/#/?', '/#?').replace('/#/', '/#/');
|
|
549
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { initializeApi, request, sendCustomProxyMessage } from "./http";
|
|
|
2
2
|
export * from "./api";
|
|
3
3
|
export * from "./types";
|
|
4
4
|
export { iframe } from "./iframe";
|
|
5
|
+
export * as cache from './cache';
|
|
6
|
+
export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
|
|
5
7
|
export type { LoginResponse, VerifyTokenResponse, AccountInfoResponse, } from "./api/auth";
|
|
6
8
|
export type { UserAccountRegistrationRequest, } from "./types/auth";
|
|
7
9
|
export type { CommunicationEvent, CommsQueryByUser, CommsRecipientIdsQuery, CommsRecipientsWithoutActionQuery, CommsRecipientsWithActionQuery, RecipientId, RecipientWithOutcome, LogCommunicationEventBody, LogBulkCommunicationEventsBody, AppendResult, AppendBulkResult, CommsSettings, TopicConfig, CommsSettingsGetResponse, CommsSettingsPatchBody, CommsPublicTopicsResponse, UnsubscribeQuery, UnsubscribeResponse, CommsConsentUpsertRequest, CommsPreferencesUpsertRequest, CommsSubscribeRequest, CommsSubscribeResponse, CommsSubscriptionCheckQuery, CommsSubscriptionCheckResponse, CommsListMethodsQuery, CommsListMethodsResponse, RegisterEmailMethodRequest, RegisterSmsMethodRequest, RegisterMethodResponse, SubscriptionsResolveRequest, SubscriptionsResolveResponse, } from "./types/comms";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,7 @@ export * from "./api";
|
|
|
5
5
|
export * from "./types";
|
|
6
6
|
// Iframe namespace
|
|
7
7
|
export { iframe } from "./iframe";
|
|
8
|
+
import * as cache_1 from './cache';
|
|
9
|
+
export { cache_1 as cache };
|
|
10
|
+
// IframeResponder (also exported via iframe namespace)
|
|
11
|
+
export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type { AuthKitUser } from './authKit';
|
|
2
|
+
export type { AppConfig as CollectionApp } from './collection';
|
|
3
|
+
import type { AppConfig } from './collection';
|
|
4
|
+
export interface CachedData {
|
|
5
|
+
collection?: Record<string, any>;
|
|
6
|
+
product?: Record<string, any>;
|
|
7
|
+
proof?: Record<string, any>;
|
|
8
|
+
user?: {
|
|
9
|
+
uid: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
accountData?: Record<string, any>;
|
|
13
|
+
} | null;
|
|
14
|
+
apps?: AppConfig[];
|
|
15
|
+
}
|
|
16
|
+
export interface IframeResponderOptions {
|
|
17
|
+
collectionId: string;
|
|
18
|
+
appId: string;
|
|
19
|
+
productId?: string;
|
|
20
|
+
proofId?: string;
|
|
21
|
+
/** Version to load: 'stable' | 'development' | specific version */
|
|
22
|
+
version?: string;
|
|
23
|
+
/** Override auto-resolved URL (for local development) */
|
|
24
|
+
appUrl?: string;
|
|
25
|
+
/** Initial hash path (e.g., '/settings') */
|
|
26
|
+
initialPath?: string;
|
|
27
|
+
/** Is user an admin of this collection */
|
|
28
|
+
isAdmin?: boolean;
|
|
29
|
+
cache?: CachedData;
|
|
30
|
+
onAuthLogin?: (token: string, user: any, accountData?: Record<string, any>) => Promise<void>;
|
|
31
|
+
onAuthLogout?: () => Promise<void>;
|
|
32
|
+
onRouteChange?: (path: string, state: Record<string, string>) => void;
|
|
33
|
+
onResize?: (height: number) => void;
|
|
34
|
+
onError?: (error: Error) => void;
|
|
35
|
+
onReady?: () => void;
|
|
36
|
+
}
|
|
37
|
+
export interface RouteChangeMessage {
|
|
38
|
+
type: 'smartlinks-route-change';
|
|
39
|
+
path: string;
|
|
40
|
+
context: Record<string, string>;
|
|
41
|
+
state: Record<string, string>;
|
|
42
|
+
appId?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SmartlinksIframeMessage {
|
|
45
|
+
_smartlinksIframeMessage: true;
|
|
46
|
+
type: 'smartlinks:resize' | 'smartlinks:redirect' | 'smartlinks:authkit:login' | 'smartlinks:authkit:logout' | 'smartlinks:authkit:redirect';
|
|
47
|
+
payload: Record<string, any>;
|
|
48
|
+
messageId?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ProxyRequest {
|
|
51
|
+
_smartlinksProxyRequest: true;
|
|
52
|
+
id: string;
|
|
53
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
54
|
+
path: string;
|
|
55
|
+
body?: any;
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
export interface CustomProxyRequest {
|
|
59
|
+
_smartlinksProxyRequest: true;
|
|
60
|
+
_smartlinksCustomProxyRequest: true;
|
|
61
|
+
id: string;
|
|
62
|
+
request: 'REDIRECT' | string;
|
|
63
|
+
params: Record<string, any>;
|
|
64
|
+
}
|
|
65
|
+
export interface ProxyResponse {
|
|
66
|
+
_smartlinksProxyResponse: true;
|
|
67
|
+
id: string;
|
|
68
|
+
data?: any;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface UploadStartMessage {
|
|
72
|
+
_smartlinksProxyUpload: true;
|
|
73
|
+
phase: 'start';
|
|
74
|
+
id: string;
|
|
75
|
+
fields: [string, string][];
|
|
76
|
+
fileInfo: {
|
|
77
|
+
type?: string;
|
|
78
|
+
name?: string;
|
|
79
|
+
key?: string;
|
|
80
|
+
};
|
|
81
|
+
path: string;
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
}
|
|
84
|
+
export interface UploadChunkMessage {
|
|
85
|
+
_smartlinksProxyUpload: true;
|
|
86
|
+
phase: 'chunk';
|
|
87
|
+
id: string;
|
|
88
|
+
seq: number;
|
|
89
|
+
chunk: ArrayBuffer;
|
|
90
|
+
}
|
|
91
|
+
export interface UploadEndMessage {
|
|
92
|
+
_smartlinksProxyUpload: true;
|
|
93
|
+
phase: 'end';
|
|
94
|
+
id: string;
|
|
95
|
+
}
|
|
96
|
+
export interface UploadAckMessage {
|
|
97
|
+
_smartlinksProxyUpload: true;
|
|
98
|
+
phase: 'ack';
|
|
99
|
+
id: string;
|
|
100
|
+
seq: number;
|
|
101
|
+
}
|
|
102
|
+
export interface UploadDoneMessage {
|
|
103
|
+
_smartlinksProxyUpload: true;
|
|
104
|
+
phase: 'done';
|
|
105
|
+
id: string;
|
|
106
|
+
ok: boolean;
|
|
107
|
+
data?: any;
|
|
108
|
+
error?: string;
|
|
109
|
+
}
|
|
110
|
+
export type UploadMessage = UploadStartMessage | UploadChunkMessage | UploadEndMessage | UploadAckMessage | UploadDoneMessage;
|
|
111
|
+
/** Reserved iframe context parameters (not app state) */
|
|
112
|
+
export declare const KNOWN_IFRAME_PARAMS: Set<string>;
|