dome-embedded-app-sdk 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # The Official Dome SDK
2
+
3
+ Use this SDK to build plugins for Dome. There are two plugins supported:
4
+
5
+ 1. Cards : Extend the functionality of Dome by adding custom cards.
6
+
7
+ 2. Document Viewer : Add support for editing & viewing any document in Dome.
8
+
9
+
10
+ ## 1. Cards
11
+
12
+ A "card" in Dome enables you to extend the functionality of a dome. Each "card" is like a mini website (webapp) that you can build as per your needs. Each dome is made up of cards. By adding your custom card to your dome, you can extend it's functionality.
13
+
14
+ Use this SDK to create your custom card that can do whatever you like: a simple todo list, snake & ladder game, to a complex project management tool, a 3D AR visualization, to anything else you can imagine! Anything you can build in React or Angular can be created into a card!
15
+
16
+ As of Dec 2024, we support Angular and React cards. In future, we will add support for more frameworks.
17
+
18
+
19
+ ### Getting Started with your first Card
20
+
21
+ #### Register your card
22
+ Register your card at https://dev.dome.so
23
+
24
+ #### Code
25
+ Import the SDK
26
+ ```
27
+ import { CardSdk } from "dome-embedded-app-sdk";
28
+ ```
29
+
30
+ Call init to get instance of the SDK. It takes a secret and event handler as input.
31
+ ```
32
+ CardSdk.init(my_dev_card_secret, {
33
+ onInit: (data: any) => {
34
+ this.user = data?.user;
35
+ this.api_token = data?.api_token;
36
+ },
37
+ onError: (error_code: string | number, message: string, data: any) => {
38
+ console.error("Some Error", message + "(" + error_code + ")");
39
+ },
40
+ onRefreshRequest: (data: any) => {
41
+ console.debug("Refresh requested", data);
42
+ }
43
+ })
44
+ ```
45
+ Note: the `secret` is given to you when you register your card (step 1)
46
+
47
+ ### Deploy
48
+ Deploy your code at https://dev.dome.so
49
+
50
+
51
+ ## 2. Document Viewer
52
+
53
+ Add view / edit capability for any document in Dome. For example, you can come up with your own spreadsheet and make it instantly available to all users of Dome. Alternatively, you can create your own viewer that views & (optionally) edits existing documents such as Excel files. The options a limitless!
54
+
55
+ As of Dec 2024, we support Angular and React document viewers. In future, we will add support for more frameworks.
56
+
57
+
58
+ ### Getting Started with your first Document Viewer
59
+
60
+
61
+
62
+ ## Help
63
+
64
+ Join our developer community on Dome here: <url>. Hang out, get latest updates, ask questions, experience Dome, and much more!
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "dome-embedded-app-sdk",
3
+ "version": "0.1.2",
4
+ "source": "src/index.ts",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build:tsc": "tsc --emitDeclarationOnly",
15
+ "build:webpack": "webpack --config webpack.config.ts",
16
+ "build": "npm run build:tsc && npm run build:webpack",
17
+ "release": "npm run build && npm publish",
18
+ "watch": "webpack --watch"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "keywords": [
24
+ "dome",
25
+ "dome-sdk"
26
+ ],
27
+ "author": "",
28
+ "license": "ISC",
29
+ "description": "",
30
+ "devDependencies": {
31
+ "@types/node": "^22.13.10",
32
+ "@types/webpack": "^5.28.5",
33
+ "ts-loader": "^9.5.1",
34
+ "ts-node": "^10.9.2",
35
+ "typescript": "^5.8.2",
36
+ "webpack-cli": "^5.1.4"
37
+ }
38
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,133 @@
1
+
2
+ // Provide enc / dec using Algorithm01
3
+ export class CryptoA01 {
4
+ private subtleCrypto: SubtleCrypto;
5
+
6
+ constructor() {
7
+ // Initialize subtleCrypto once
8
+ this.subtleCrypto = window.crypto?.subtle;
9
+ if (!this.subtleCrypto) {
10
+ throw new Error('SubtleCrypto API is not available in this environment.');
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Perform decryption using AES based V1 algorithm.
16
+ *
17
+ * string: the encrypted string (base64 encoded)
18
+ * password: the password used for encryption
19
+ * salt: the base64 encoded salt used
20
+ */
21
+ public async decrypt(token: string, password: string, salt: string): Promise<string> {
22
+ try {
23
+
24
+ if (!token) {
25
+ throw new Error("Invalid token");
26
+ }
27
+
28
+ const tokenBytes = this.base64UrlDecode(token);
29
+
30
+ // Extract token components
31
+ const version = tokenBytes[0];
32
+ if (version !== 0x80) {
33
+ // console.log("Incorrect Version: ", version);
34
+ throw new Error('Invalid version');
35
+ }
36
+
37
+ const timestamp = tokenBytes.slice(1, 9);
38
+ const iv = tokenBytes.slice(9, 25);
39
+ const ciphertext = tokenBytes.slice(25, -32);
40
+ const hmacFromToken = tokenBytes.slice(-32);
41
+
42
+ // Derive the key and split it into HMAC and AES keys
43
+ const fullKey = await this.deriveKey(password, salt);
44
+ const { hmacKey, aesKey } = await this.splitKey(fullKey);
45
+
46
+ // Compute HMAC over version + timestamp + IV + ciphertext
47
+ const hmacInput = tokenBytes.slice(0, -32);
48
+ const computedHmac = new Uint8Array(await this.subtleCrypto.sign('HMAC', hmacKey, hmacInput));
49
+
50
+ // Validate HMAC
51
+ if (!computedHmac.every((byte, i) => byte === hmacFromToken[i])) {
52
+ throw new Error('Invalid HMAC. Token has been tampered with!');
53
+ }
54
+
55
+ // Decrypt the ciphertext
56
+ const decrypted = await this.subtleCrypto.decrypt(
57
+ {
58
+ name: 'AES-CBC',
59
+ iv: iv,
60
+ },
61
+ aesKey,
62
+ ciphertext
63
+ );
64
+
65
+ // Convert decrypted data to UTF-8 string
66
+ const decoder = new TextDecoder();
67
+ return decoder.decode(decrypted);
68
+ } catch (err) {
69
+ console.log("Error in decrypt:", err);
70
+ throw err;
71
+ }
72
+ }
73
+
74
+
75
+ private async deriveKey(password: string, salt: string, iterations: number=10000): Promise<CryptoKey> {
76
+ const encoder = new TextEncoder();
77
+ const keyMaterial = await this.subtleCrypto.importKey(
78
+ "raw",
79
+ encoder.encode(password),
80
+ "PBKDF2",
81
+ false,
82
+ ["deriveKey"]
83
+ );
84
+
85
+ return this.subtleCrypto.deriveKey(
86
+ {
87
+ name: "PBKDF2",
88
+ hash: "SHA-256",
89
+ salt: encoder.encode(salt),
90
+ iterations: iterations,
91
+ },
92
+ keyMaterial,
93
+ { name: "AES-CBC", length: 256 },
94
+ true, // Allow export of the derived key (req for splitting)
95
+ ["encrypt", "decrypt"]
96
+ );
97
+ }
98
+
99
+
100
+ // Split the full key into HMAC and AES keys
101
+ private async splitKey(fullKey: CryptoKey): Promise<{ hmacKey: CryptoKey; aesKey: CryptoKey }> {
102
+ const rawKey = new Uint8Array(await this.subtleCrypto.exportKey('raw', fullKey));
103
+
104
+ // Split the key into HMAC (first 16 bytes) and AES (last 16 bytes)
105
+ const hmacKey = await this.subtleCrypto.importKey(
106
+ 'raw',
107
+ rawKey.slice(0, 16),
108
+ { name: 'HMAC', hash: 'SHA-256' },
109
+ false,
110
+ ['sign', 'verify']
111
+ );
112
+
113
+ const aesKey = await this.subtleCrypto.importKey(
114
+ 'raw',
115
+ rawKey.slice(16),
116
+ { name: 'AES-CBC' },
117
+ false,
118
+ ['encrypt', 'decrypt']
119
+ );
120
+
121
+ return { hmacKey, aesKey };
122
+ }
123
+
124
+
125
+ // Decode Base64 URL-safe strings
126
+ private base64UrlDecode(base64: string): Uint8Array {
127
+ // assumes URL safe encoding that has + in place of - and _ in place of /
128
+ const base64String = base64.replace(/-/g, '+').replace(/_/g, '/');
129
+ const decodedString = atob(base64String);
130
+ return new Uint8Array([...decodedString].map(c => c.charCodeAt(0)));
131
+ }
132
+
133
+ }
@@ -0,0 +1,778 @@
1
+ import pkg from "../package.json";
2
+
3
+ import { generateUUID, getNameString } from './utils'
4
+ import { CryptoA01 } from './crypto'
5
+
6
+ // Enum defining message types sent from the viewer to the parent application
7
+ export enum ViewerMessageType {
8
+ CONNECTION_SUCCESS = "CONNECTION_SUCCESS",
9
+ INIT = "INIT", // Indicates the app is initialized
10
+ REQUEST_SAVE = "REQUEST_SAVE", // Request to save data in the parent
11
+ REQUEST_CLOSE = "REQUEST_CLOSE", // Request to save data in the parent
12
+ REQUEST_INITIAL_DATA = "REQUEST_INITIAL_DATA", // Request to load initial data from the parent
13
+ SET_DIRTY = "SET_DIRTY", // Indicates the app has unsaved changes
14
+ SEND_CLOSE = "SEND_CLOSE", // Signals the app is ready to close
15
+ SEND_EXCEPTION = "SEND_EXCEPTION" // Sends an exception event to parent
16
+ }
17
+
18
+ // Enum defining message types sent from the parent application to the embedded app
19
+ enum ClientMessageType {
20
+ CONNECT = "CONNECT",
21
+ REQUEST_CLOSE = "REQUEST_CLOSE", // Requests the app to close
22
+ REQUEST_SAVE = "REQUEST_SAVE", // Requests the app for data to be saved
23
+ SAVE_ERROR = "SAVE_ERROR", // Sends status of the save event
24
+ SAVE_SUCCESS = "SAVE_SUCCESS", // Sends status of the save event
25
+ DATA_CHANGE = "DATA_CHANGE",
26
+ INIT_ACK = "INIT_ACK",
27
+ ERROR = "ERROR",
28
+ REFRESH = "REFRESH",
29
+ }
30
+
31
+
32
+ // Interface defining the structure of a message from the parent
33
+ interface ClientMessageEvent {
34
+ type: ClientMessageType; // Message type
35
+ data: any; // Payload data for the message
36
+ }
37
+
38
+ interface SaveStatusData {
39
+ status: 'success' | 'error';
40
+ message: string;
41
+ }
42
+
43
+ // Interface defining handlers for different message types received from the parent
44
+ export interface ViewerEventHandler {
45
+ onInitialData: (data: { doc: Document, ui: UiProps, isNewFile: boolean, perms: any, config?: Record<string, any> }) => void;
46
+ onDataChange?: (data: { doc: Document, perms: any, userConsent: "override" | null }) => void;
47
+ onCloseRequest: () => void;
48
+ onSaveRequest: () => void;
49
+ };
50
+
51
+ // Interface defining the structure for the doc object
52
+ export interface Document {
53
+ data: any;
54
+ name: string;
55
+ type: string;
56
+ }
57
+
58
+ // Interface defining the UI properties
59
+ export interface UiProps {
60
+ theme: string;
61
+ }
62
+
63
+ const ALLOWED_ORIGINS = new Set(getAllowedOrigins());
64
+
65
+
66
+ function getAllowedOrigins(): string[] {
67
+ if (typeof window === 'undefined') return [];
68
+
69
+ return [
70
+ window.location.origin,
71
+ 'https://dome.so',
72
+ 'https://spaces.intouchapp.com/',
73
+ 'http://localhost:4200',
74
+ 'http://localhost:4201',
75
+ 'null',
76
+ ];
77
+ }
78
+
79
+ /**
80
+ * DomeEmbeddedAppSdk:
81
+ * Base SDK class providing methods to send messages to the parent application.
82
+ */
83
+ class DomeEmbeddedAppSdk {
84
+ private readonly targetOrigin: string = "*";
85
+ protected isAppReady: boolean = false;
86
+ protected port2: MessagePort | null = null;
87
+ private platform: "android" | "ios" | "web" | "unknown" = "unknown"; // Store detected platform
88
+
89
+ constructor() {
90
+ this.detectPlatform();
91
+ }
92
+
93
+
94
+ /**
95
+ * Detects the platform (iOS, Android, or Web) and saves it.
96
+ */
97
+ private detectPlatform(): void {
98
+ if (typeof window.AndroidBridge !== "undefined") {
99
+ this.platform = "android";
100
+ } else if (typeof window.webkit !== "undefined") {
101
+ this.platform = "ios";
102
+ } else {
103
+ this.platform = "web";
104
+ }
105
+
106
+ console.debug(`Detected platform: ${this.platform}`);
107
+ }
108
+
109
+
110
+ /**
111
+ * Method to send messages to the parent application.
112
+ * Ensures the parent window exists and sends a structured message with type and data.
113
+ * @param type - The type of message being sent
114
+ * @param data - (Optional) payload data for the message
115
+ */
116
+ protected sendMessage(type: ViewerMessageType | CardMessageType, data?: any): void {
117
+ const message = { type, data: data ?? null };
118
+
119
+ switch (this.platform) {
120
+ case "android":
121
+ window.AndroidBridge?.sendMessage(JSON.stringify(message));
122
+ break;
123
+
124
+ case "ios":
125
+ if (window?.webkit?.messageHandlers) {
126
+ window.webkit?.messageHandlers.appHandler.postMessage(JSON.stringify(message));
127
+ } else {
128
+ console.error("webkit.messageHandlers not found")
129
+ }
130
+ break;
131
+
132
+ case "web":
133
+ if (this.port2) {
134
+ this.port2.postMessage(message);
135
+ } else {
136
+ console.error("Web connection is not established.");
137
+ }
138
+ break;
139
+
140
+ default:
141
+ console.error("Unsupported platform, cannot send message.");
142
+ break;
143
+ }
144
+
145
+ console.debug(`Sent message to ${this.platform}:`, message);
146
+ }
147
+
148
+ /**
149
+ * Notifies the parent application that the app is ready, if it hasn’t already.
150
+ */
151
+ protected sendAppInit(): void {
152
+ if (!this.isAppReady) {
153
+ this.isAppReady = true;
154
+ this.sendMessage(ViewerMessageType.INIT, { sdk: { ver: pkg.version } })
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Safely invokes a function from the handler object if it exists.
160
+ * and logs a warning if the handler is not provided for the given message type.
161
+ *
162
+ * @param eventName - Name of the event method to be invoked from the handler.
163
+ * @param handlerObj - The handler object that contains the message handling methods.
164
+ * @param data - (Optional) The data to be passed to the handler function if invoked.
165
+ */
166
+ protected safeInvoke<T extends ViewerEventHandler | CardEventHandler>(eventName: keyof T, handlerObj: T, data?: any): void {
167
+ const handler = handlerObj[eventName];
168
+
169
+ if (typeof handler === 'function') {
170
+ handler(data);
171
+ } else {
172
+ console.warn(`Handler for '${String(eventName)}' is not defined.`);
173
+ }
174
+ }
175
+
176
+
177
+ // Sets up connection with iframe parent using message channel
178
+ // Call this once only in the lifetime. Should be called
179
+ // _before_ iFrame's onLoad is called (otherwise messaging will
180
+ // not be setup)
181
+ protected setupParentConnection(): Promise<void> {
182
+ return new Promise((resolve, reject) => {
183
+ switch (this.platform) {
184
+ case "android":
185
+ window.receiveFromAndroid = (message: { type: string; data: any }) => {
186
+ console.debug("Message received from Android:", message);
187
+ this.handleMessage(message.type, message.data);
188
+ };
189
+ resolve();
190
+ break;
191
+
192
+ case "ios":
193
+ window.receiveFromIOS = (message: { type: string; data: any }) => {
194
+ console.debug("Message received from iOS:", message);
195
+ this.handleMessage(message.type, message.data);
196
+ };
197
+ resolve();
198
+ break;
199
+
200
+ case "web":
201
+ if (this.port2) {
202
+ console.warn("Connection already established. Skipping reinitialization.");
203
+ resolve();
204
+ }
205
+
206
+ const handleMessage = (event: MessageEvent) => {
207
+ const { type } = event.data || {};
208
+
209
+ if (type !== ClientMessageType.CONNECT) return;
210
+
211
+ if (event.ports && event.ports.length > 0) {
212
+ this.port2 = event.ports[0];
213
+ this.port2.onmessage = (e) => this.handlePortMessage(e);
214
+ window.removeEventListener("message", handleMessage); // Cleanup
215
+ this.notifyConnectionSuccess();
216
+ resolve();
217
+ }
218
+ };
219
+
220
+ // Listen for browser-based `message` events
221
+ window.addEventListener("message", handleMessage);
222
+ break;
223
+
224
+ default:
225
+ console.error("Unknown platform.");
226
+ reject("Unknown platform");
227
+ }
228
+ });
229
+ }
230
+
231
+
232
+ // Send CONNECTION_SUCCESS message to parent
233
+ private notifyConnectionSuccess(): void {
234
+ this.sendMessage(ViewerMessageType.CONNECTION_SUCCESS);
235
+ }
236
+
237
+ // Handle messages coming over message channel port
238
+ private handlePortMessage(event: MessageEvent) {
239
+ const { type, data } = event.data || {};
240
+ if (!type) return;
241
+
242
+ // Delegate to subclass-specific message handler
243
+ this.handleMessage(type, data);
244
+ }
245
+
246
+ // Common method for handling messages to be implemented by sub-classes
247
+ protected handleMessage(type: string, data: any): void {
248
+ throw new Error("Subclasses must implement handleMessage.");
249
+ }
250
+
251
+ }
252
+
253
+ /**
254
+ * ViewerSdk:
255
+ * A subclass of DomeEmbeddedAppSdk specifically for document viewer applications.
256
+ * It includes additional methods and properties to manage app interactions.
257
+ */
258
+ export class ViewerSdk extends DomeEmbeddedAppSdk {
259
+
260
+ private static instance: ViewerSdk; // Singleton instance of ViewerSdk
261
+ private static initialized = false;
262
+ private handler: ViewerEventHandler | null = null; // Handler instance for client messages
263
+ private pendingRequests: Map<string, (status: SaveStatusData) => void> = new Map();
264
+ private pendingInitAck: unknown | null = null;
265
+
266
+
267
+ private constructor() {
268
+ super();
269
+ }
270
+
271
+
272
+ /**
273
+ * Static initialization method to get or create the singleton instance of ViewerSdk.
274
+ * Allows setting the handler during initialization.
275
+ * @param handler - (Optional) Custom handler for different message types
276
+ * @returns The singleton ViewerSdk instance
277
+ */
278
+ static init(handler?: ViewerEventHandler): ViewerSdk {
279
+ console.debug("init called", handler && "with handler");
280
+ // Prevent reinitialization if already initialized
281
+ if (ViewerSdk.initialized) {
282
+ console.warn("ViewerSdk is already initialized. Skipping initialization.");
283
+ return ViewerSdk.instance;
284
+ }
285
+
286
+ if (!ViewerSdk.instance) {
287
+ ViewerSdk.instance = new ViewerSdk();
288
+
289
+ // Initialize parent communication - REQUIRED!
290
+ ViewerSdk.instance.setupParentConnection()
291
+ .then(() => {
292
+ try {
293
+ // Connection established with parent
294
+ ViewerSdk.instance.initializeViewerSdk();
295
+ } catch (err) {
296
+ console.error("Error in initializeViewerSdk:", err);
297
+ }
298
+ })
299
+ .catch((err: any) => {
300
+ console.error("init: Error setting up parent connection!", err);
301
+ console.trace("called from:")
302
+ });
303
+ }
304
+
305
+ if (handler) {
306
+ ViewerSdk.instance.setHandler(handler); // Set handler if provided during initialization
307
+ }
308
+
309
+ // Mark as initialized
310
+ ViewerSdk.initialized = true;
311
+
312
+ return ViewerSdk.instance;
313
+ }
314
+
315
+
316
+ /**
317
+ * Method to set or update the handler object.
318
+ * @param handler - Custom handler for different message types
319
+ */
320
+ setHandler(handler: ViewerEventHandler): void {
321
+ this.handler = handler;
322
+
323
+ // If INIT_ACK message was received and stored, process it now
324
+ if (this.pendingInitAck) {
325
+ console.debug("Processing pending INIT_ACK message after handler is set.");
326
+ this.safeInvoke("onInitialData", this.handler, this.pendingInitAck);
327
+ this.pendingInitAck = null; // Clear the stored message
328
+ }
329
+ }
330
+
331
+
332
+ /**
333
+ * Checks if the given permissions string allows reading.
334
+ * @param perms - The permissions string.
335
+ * @returns - True if the permission string includes read access.
336
+ */
337
+ canRead(perms?: string): boolean {
338
+ return !!perms?.includes('r');
339
+ }
340
+
341
+ /**
342
+ * Checks if the given permissions string allows writing.
343
+ * @param perms - The permissions string.
344
+ * @returns - True if the permission string includes write access.
345
+ */
346
+ canWrite(perms?: string): boolean {
347
+ return !!perms?.includes('w') || !!perms?.includes('*');
348
+ }
349
+
350
+
351
+ // Initializes the viewer SDK, setting up the message listener and sending an initial "ready" message.
352
+ initializeViewerSdk() {
353
+ console.debug("initializing viewer sdk");
354
+ this.sendAppInit();
355
+ }
356
+
357
+ /**
358
+ * Sends a request to the parent application to retrieve initial data.
359
+ */
360
+ requestInitialData(): void {
361
+ this.sendMessage(ViewerMessageType.REQUEST_INITIAL_DATA);
362
+ }
363
+
364
+
365
+ /**
366
+ * Sends a request to the parent application to save data.
367
+ * @param doc - payload data to be saved
368
+ * @param isDataDirty - Boolean indicating indicating modified data
369
+ */
370
+ requestSave(doc: Document, isDataDirty: boolean): Promise<SaveStatusData> {
371
+ const requestId = generateUUID();
372
+
373
+ // Send save request with the generated requestId
374
+ this.sendMessage(ViewerMessageType.REQUEST_SAVE, { doc, isDataDirty, requestId });
375
+
376
+ return new Promise((resolve, reject) => {
377
+
378
+ this.pendingRequests.set(requestId, resolve);
379
+ this.pendingRequests.set(requestId + '_reject', reject);
380
+
381
+ // Timeout if the parent fails to respond in time
382
+ setTimeout(() => {
383
+ if (this.pendingRequests.has(requestId)) {
384
+ this.pendingRequests.delete(requestId);
385
+ this.pendingRequests.delete(requestId + '_reject');
386
+ }
387
+ }, 30000);
388
+ });
389
+ }
390
+
391
+ private handleOnSave(data: { requestId: string, status: "error" | "success", message: string }) {
392
+ const { requestId, status, message } = data;
393
+
394
+ // Check if we have a pending request for this requestId
395
+ const resolve = this.pendingRequests.get(requestId);
396
+ const reject = this.pendingRequests.get(requestId + '_reject');
397
+
398
+ if (resolve) {
399
+ // If status is "error", reject the promise, otherwise resolve it
400
+ if (status === "error") {
401
+ reject?.({ status, message });
402
+ } else {
403
+ resolve({ status, message });
404
+ }
405
+
406
+ // Clean up
407
+ this.pendingRequests.delete(requestId);
408
+ this.pendingRequests.delete(requestId + '_reject');
409
+ }
410
+ }
411
+
412
+
413
+ /**
414
+ * Sets the viewer's "dirty" state, indicating modified data.
415
+ * @param isDirty - Boolean indicating whether the viewer has modified data
416
+ */
417
+ setDirty(isDirty: boolean) {
418
+ this.sendMessage(ViewerMessageType.SET_DIRTY, isDirty);
419
+ }
420
+
421
+ /**
422
+ * Sends a close request to the parent, with information on whether the data is dirty.
423
+ * @param doc - Latest document data
424
+ * @param isDataDirty - Boolean indicating indicating modified data
425
+ */
426
+ sendClose(doc: Document, isDataDirty: boolean): void {
427
+ this.sendMessage(ViewerMessageType.SEND_CLOSE, { doc, isDataDirty });
428
+ }
429
+
430
+ /**
431
+ * Sends an exception to parent.
432
+ * @param error - An error object with name and message or an error string
433
+ */
434
+ sendException(error: { name?: string, message: string } | string): void {
435
+ this.sendMessage(ViewerMessageType.SEND_EXCEPTION, error);
436
+ }
437
+
438
+
439
+ // Sets up the message listener for viewer to receive messages from the parent.
440
+ protected handleMessage(type: ClientMessageType, data: any): void {
441
+
442
+ console.debug("handleMessage called for:", type, "with data:", data);
443
+
444
+ if (!this.handler) {
445
+ if (type === ClientMessageType.INIT_ACK) {
446
+ console.warn("Handler not set. Storing INIT_ACK message for later processing.");
447
+ this.pendingInitAck = data; // Save INIT_ACK message
448
+ } else {
449
+ console.error("Message handler not found for type:", type);
450
+ }
451
+ return;
452
+ }
453
+
454
+ switch (type) {
455
+ case ClientMessageType.INIT_ACK:
456
+ this.safeInvoke("onInitialData", this.handler, data);
457
+ break;
458
+ case ClientMessageType.DATA_CHANGE:
459
+ this.safeInvoke("onDataChange", this.handler, data);
460
+ break;
461
+ case ClientMessageType.REQUEST_CLOSE:
462
+ this.safeInvoke("onCloseRequest", this.handler);
463
+ break;
464
+ case ClientMessageType.REQUEST_SAVE:
465
+ this.safeInvoke("onSaveRequest", this.handler);
466
+ break;
467
+ case ClientMessageType.SAVE_SUCCESS:
468
+ this.handleOnSave(data);
469
+ break;
470
+ case ClientMessageType.SAVE_ERROR:
471
+ this.handleOnSave(data);
472
+ break;
473
+ default:
474
+ console.warn(`No handler found for message type: ${type}`);
475
+ }
476
+ }
477
+
478
+ }
479
+
480
+ // Card SDK
481
+
482
+ // Enum defining message types sent from the card to the parent application
483
+ export enum CardMessageType {
484
+ APP_READY = "APP_READY", // Indicates the app is ready to be interacted with
485
+ INIT = "INIT" // Event to send the init key to the viewer
486
+ }
487
+
488
+ export interface CardEventHandler {
489
+ onInit: (data: any) => void;
490
+ onError: (data: { error_code: string | number, message: string, data: any }) => void;
491
+ onRefreshRequest: (data: any) => void;
492
+ };
493
+
494
+ /**
495
+ * Use CardSdk to create webapp cards
496
+ */
497
+ export class CardSdk extends DomeEmbeddedAppSdk {
498
+ private static instance: CardSdk; // Singleton instance of CardSdk
499
+ private handler: CardEventHandler | null = null; // Handler instance for client messages
500
+ private cryptoA01: any;
501
+
502
+ public dataStore: any;
503
+
504
+ private constructor() {
505
+ super();
506
+ this.cryptoA01 = new CryptoA01();
507
+ console.debug("CardSdk::constructor: done");
508
+ }
509
+
510
+ /**
511
+ * Static initialization method to get or create the singleton instance of CardSdk.
512
+ * @param secret - The card developer secret key
513
+ * @param handler - (Optional) Handler for different events emitted by the SDK
514
+ * @returns The singleton CardSdk instance
515
+ */
516
+ public static async init(secret: string, handler?: CardEventHandler, options?: { devMode?: boolean }): Promise<CardSdk> {
517
+ try {
518
+ console.debug("CardSdk::init");
519
+
520
+ if (!CardSdk.instance) {
521
+ CardSdk.instance = new CardSdk();
522
+ // Initialize parent communication - REQUIRED!
523
+ CardSdk.instance.setupParentConnection()
524
+ .then(() => {
525
+ // Connection established with parents..
526
+
527
+ // Initialize SDK in async
528
+ CardSdk.instance.initializeCardSdk(secret);
529
+ })
530
+ .catch((err: any) => {
531
+ console.error(err);
532
+ })
533
+ } else {
534
+ return CardSdk.instance;
535
+ }
536
+
537
+ // Setup handlers
538
+ if (handler) {
539
+ CardSdk.instance.setHandler(handler); // Set handler if provided during initialization
540
+ }
541
+
542
+ return CardSdk.instance;
543
+
544
+ } catch (err) {
545
+ console.error("CardSdk: Unrecoverable error in init", err);
546
+ throw err;
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Method to set or update the handler object.
552
+ * @param handler - Custom handler for different message types
553
+ */
554
+ public setHandler(handler: CardEventHandler): void {
555
+ this.handler = handler;
556
+ }
557
+
558
+ // Function to initialize SDK after instance is created
559
+ private async initializeCardSdk(secret: any) {
560
+ let TAG = "CardSdk::initializeCardSdk:";
561
+ try {
562
+ console.debug(TAG, "enter");
563
+
564
+ if (!secret) {
565
+ throw new Error("Invalid secret");
566
+ }
567
+
568
+ // Get data from HTML
569
+ const data_af1 = (window as any).IT_DATA_AF1;
570
+
571
+ if (!data_af1) {
572
+ console.error(TAG, "No data");
573
+ throw new Error('No data');
574
+ }
575
+
576
+ const url = window.location.href;
577
+ console.debug(TAG, "url:", url);
578
+ const urlObject = new URL(url)
579
+ const segments = urlObject.pathname.split('/').filter(segment => segment);
580
+ let url_part;
581
+ if (segments.length > 1) {
582
+ url_part = segments[segments.length - 2];
583
+ } else if (segments.length == 1) {
584
+ url_part = segments[0];
585
+ } else {
586
+ throw new Error('Invalid URL');
587
+ }
588
+ const ss = url_part.split('').reverse().join('').substring(4, 25);
589
+ console.debug(TAG, "ss:", ss);
590
+ if (!ss) {
591
+ throw new Error('Cannot decrypt (1)');
592
+ }
593
+
594
+ const decData = await this.cryptoA01.decrypt(data_af1, secret, ss)
595
+ try {
596
+ const dataFromServer = JSON.parse(decData);
597
+ console.debug("CardSdk: dataFromServer:", dataFromServer);
598
+ if (!dataFromServer.ite) {
599
+ throw new Error("Invalid data");
600
+ }
601
+
602
+ this.dataStore = {
603
+ 'denc': dataFromServer.d,
604
+ 'kw2': dataFromServer.kw2
605
+ };
606
+
607
+ CardSdk.instance.sendInit(dataFromServer.ite);
608
+
609
+ } catch (err) {
610
+ console.error("Initial Decryption failed (2):", err);
611
+ throw err;
612
+ }
613
+
614
+ } catch (err: any) {
615
+ console.error(TAG, "Init failed:", err);
616
+ this.sendEventError('init_failed', err.message);
617
+ }
618
+ }
619
+
620
+ private async sendInit(token: string) {
621
+ this.sendMessage(CardMessageType.INIT, { 'token': token, sdk: { ver: pkg.version } });
622
+ }
623
+
624
+ // Sets up the message listener for cards to receive messages from the parent.
625
+ protected handleMessage = (type: ClientMessageType, data: any) => {
626
+
627
+ if (!this.handler) {
628
+ throw new Error("Message handler not found!");
629
+ }
630
+
631
+ switch (type) {
632
+ case ClientMessageType.INIT_ACK:
633
+ // Parent sent INIT_ACK
634
+ console.debug("CardSdk: INIT_ACK received");
635
+
636
+ this.dataStore.kw1 = data.key_wa1;
637
+ this.dataStore.iuid = data.iuid;
638
+
639
+ try {
640
+ this.cryptoA01.decrypt(this.dataStore.denc, data.key_wa1 + this.dataStore.kw2, data.iuid)
641
+ .then((decData: any) => {
642
+ console.debug("CardSdk: INIT_ACK: decrypted data ", decData);
643
+ const decryptedData = JSON.parse(decData);
644
+ if (this.handler) this.safeInvoke("onInit", this.handler, { ...decryptedData, ui: data.ui });
645
+ // no need for orig enc data.. free to delete it
646
+ delete this.dataStore.denc;
647
+ })
648
+ .catch((err: any) => {
649
+ console.error("Final decrypt error", err);
650
+ throw err;
651
+ });
652
+ } catch (err: any) {
653
+ console.error("Decryption failed!", err);
654
+ this.sendEventError('dec2_failed', err.message);
655
+ }
656
+
657
+ break;
658
+
659
+ case ClientMessageType.ERROR:
660
+ // Parent sent an ERROR
661
+ this.safeInvoke("onError", this.handler, data);
662
+ break;
663
+
664
+ case ClientMessageType.REFRESH:
665
+ // Asking for UI refresh
666
+ this.safeInvoke("onRefreshRequest", this.handler, data);
667
+ break;
668
+
669
+ default:
670
+ console.warn(`No handler found for message type: ${type}`);
671
+ }
672
+ };
673
+
674
+ /**
675
+ * Converts a JavaScript object to a JSON file.
676
+ */
677
+ private async convertToJsonFile(object: any, type = 'json', fileName = 'file.json'): Promise<File> {
678
+ const jsonString = JSON.stringify(object, null, 2);
679
+ const jsonFileType = 'application/json';
680
+ const blob = new Blob([jsonString], { type: jsonFileType });
681
+
682
+ return new File([blob], fileName, { type: jsonFileType });
683
+ }
684
+
685
+ /**
686
+ * Converts data to a file.
687
+ */
688
+ private async convertToFile(data: any, fileName: string, fileType: 'json' | 'blob' = 'json', fileMimeType?: string): Promise<File> {
689
+ if (fileType === 'json') {
690
+ return this.convertToJsonFile(data, 'json', fileName);
691
+ } else {
692
+ const blob = new Blob([data], fileMimeType ? { type: fileMimeType } : undefined);
693
+ return new File([blob], fileName, { type: blob.type });
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Get document associated with the current card
699
+ */
700
+ public async getCardDocument(documentIuid?: string, cardIuid?: string): Promise<any> {
701
+ const fetchUrl = `/api/v1/${documentIuid}/`;
702
+ const createUrl = `/api/v1/documents/create_for/${cardIuid}`;
703
+
704
+ // Helper to handle response content
705
+ const parseResponse = async (response: Response) => {
706
+ const contentType = response.headers.get('Content-Type') || '';
707
+ if (contentType.includes('application/json')) return response.json();
708
+ if (contentType.includes('text/')) return response.text();
709
+ throw new Error(`Unsupported response type: ${contentType}`);
710
+ };
711
+
712
+ try {
713
+ // Try to fetch the document
714
+ const fetchResponse = await fetch(fetchUrl, { method: 'GET' });
715
+ if (fetchResponse.ok) return parseResponse(fetchResponse);
716
+
717
+ if (fetchResponse.status === 404) {
718
+ // Create the document if not found
719
+ const createResponse = await fetch(createUrl, { method: 'POST' });
720
+ if (!createResponse.ok) throw new Error(`Failed to create document: ${createResponse.status}`);
721
+ return parseResponse(createResponse);
722
+ }
723
+
724
+ throw new Error(`Failed to fetch document: ${fetchResponse.status}`);
725
+ } catch (error) {
726
+ console.error('Error handling document:', error);
727
+ throw error;
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Uploads a file to the specified document IUID endpoint.
733
+ */
734
+ public async saveDocument(fileObj: any, documentIuid: string): Promise<Response> {
735
+ // Convert the fileObj to a File
736
+ const { name, type } = fileObj;
737
+
738
+ const file = await this.convertToFile(fileObj, name, type);
739
+
740
+ // Construct the full URL
741
+ const apiUrl = `/api/v1/${documentIuid}/`;
742
+
743
+ // Create FormData and append the file
744
+ const formData = new FormData();
745
+ formData.append('file', file);
746
+
747
+ // Make the POST request
748
+ return fetch(apiUrl, {
749
+ method: 'PUT',
750
+ body: formData,
751
+ });
752
+ }
753
+
754
+
755
+ // Send event on error (clients will get "onError" event)
756
+ private sendEventError(error_code: string, message: string, data: any = undefined) {
757
+ let data_to_send = {
758
+ 'message': message,
759
+ 'error_code': error_code,
760
+ 'data': data
761
+ }
762
+ if (this.handler) this.safeInvoke("onError", this.handler, data_to_send);
763
+ }
764
+
765
+
766
+ // Get username string from user object received from parent
767
+ public getUsername(userObj: any): string {
768
+ const nameObj = userObj?.name;
769
+
770
+ if (nameObj && Object.values(nameObj).some(value => value)) {
771
+ return getNameString(nameObj);
772
+ }
773
+
774
+ return '';
775
+ }
776
+
777
+ }
778
+
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Class imports
2
+ export { ViewerSdk, CardSdk } from "./dome-sdk";
3
+ export { CryptoA01 } from "./crypto";
4
+
5
+ // Viewer Type imports
6
+ export type { ViewerMessageType, ViewerEventHandler, Document, UiProps } from "./dome-sdk";
7
+
8
+ // Card Type imports
9
+ export type { CardMessageType, CardEventHandler } from "./dome-sdk";
@@ -0,0 +1,20 @@
1
+ interface AndroidBridgeType {
2
+ postMessage(message: any): void;
3
+ sendMessage(message: any): void;
4
+ }
5
+
6
+ interface WebkitType {
7
+ messageHandlers: {
8
+ [handlerName: string]: {
9
+ postMessage(message: any): void;
10
+ };
11
+ };
12
+ }
13
+
14
+ interface Window {
15
+ AndroidBridge?: AndroidBridgeType;
16
+ webkit?: WebkitType;
17
+ app?: any;
18
+ receiveFromAndroid: (message: { type: string, data: any }) => void;
19
+ receiveFromIOS: (message: { type: string, data: any }) => void;
20
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Helper function to generate a unique requestId (UUID).
3
+ * This uses the browser's crypto API for random UUID generation.
4
+ */
5
+ export function generateUUID(): string {
6
+ // If in a browser environment with crypto support (modern browsers)
7
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
8
+ return window.crypto.randomUUID();
9
+ }
10
+
11
+ // Fallback for non-browser environments (e.g., Node.js)
12
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
13
+ return crypto.randomUUID();
14
+ }
15
+
16
+ // Fallback for environments without crypto support
17
+ throw new Error('UUID generation is not supported in this environment');
18
+ }
19
+
20
+ /**
21
+ * Helper function to get username from user object
22
+ * @param nameObj the user object
23
+ */
24
+ export function getNameString(nameObj: any): string {
25
+ const { prefix = '', given = '', middle = '', family = '', suffix = '' } = nameObj || {};
26
+ const fullname = [prefix, given, middle, family, suffix].filter(namePart => namePart).join(' ');
27
+
28
+ return fullname.trim();
29
+ }
30
+
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "strict": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "isolatedModules": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "outDir": "./dist",
14
+ "rootDir": "./src",
15
+ "resolveJsonModule": true,
16
+ "lib": ["es2020", "dom"],
17
+ },
18
+ "ts-node": {
19
+ "compilerOptions": {
20
+ "module": "CommonJS"
21
+ }
22
+ },
23
+ "include": ["./src/**/*"],
24
+ "exclude": ["dist", "node_modules"]
25
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'path';
2
+ import webpack from 'webpack';
3
+
4
+ const config: webpack.Configuration = {
5
+ mode: "production",
6
+ entry: "./src/index.ts",
7
+ output: {
8
+ path: path.resolve(__dirname, "dist"),
9
+ clean: true,
10
+ filename: "index.js",
11
+ library: 'domeSdk',
12
+ libraryTarget: 'umd',
13
+ globalObject: "this",
14
+ },
15
+ resolve: {
16
+ extensions: [".ts", ".js"],
17
+ },
18
+ module: {
19
+ rules: [
20
+ {
21
+ test: /\.ts$/,
22
+ use: "ts-loader",
23
+ exclude: /node_modules/,
24
+ },
25
+ ],
26
+ },
27
+ optimization: {
28
+ splitChunks: {
29
+ chunks: "all",
30
+ },
31
+ },
32
+ devtool: "source-map",
33
+ };
34
+
35
+ export default config;