@cloudsignal/pwa-sdk 1.1.1 → 1.2.1

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/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { isValidUUID } from './chunk-IMM7VF4N.js';
2
+ export { generateAuthHeaders, generateHMACSignature, isValidUUID, makeAuthenticatedRequest } from './chunk-IMM7VF4N.js';
3
+
1
4
  /**
2
5
  * CloudSignal PWA SDK v1.0.0
3
6
  * https://cloudsignal.io
@@ -889,80 +892,17 @@ var InstallationManager = class {
889
892
  }
890
893
  };
891
894
 
892
- // src/utils/hmac.ts
893
- function toHex(buffer) {
894
- return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
895
- }
896
- function toBase64(buffer) {
897
- return btoa(String.fromCharCode(...new Uint8Array(buffer)));
898
- }
899
- async function generateHMACSignature(secret, organizationId, timestamp, method, url, body = "") {
900
- const encoder = new TextEncoder();
901
- let path;
902
- let query;
903
- try {
904
- const urlObj = url.startsWith("http") ? new URL(url) : new URL(url, "https://pwa.cloudsignal.app");
905
- path = urlObj.pathname;
906
- query = urlObj.search ? urlObj.search.slice(1) : "";
907
- } catch {
908
- const queryIndex = url.indexOf("?");
909
- if (queryIndex > -1) {
910
- path = url.slice(0, queryIndex);
911
- query = url.slice(queryIndex + 1);
912
- } else {
913
- path = url;
914
- query = "";
915
- }
916
- }
917
- const canonicalParts = [
918
- method.toUpperCase(),
919
- path,
920
- query,
921
- organizationId,
922
- timestamp
923
- ];
924
- if (body && body.length > 0) {
925
- const bodyHashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(body));
926
- const bodyHashHex = toHex(bodyHashBuffer);
927
- canonicalParts.push(bodyHashHex);
928
- }
929
- const canonicalString = canonicalParts.join("\n");
930
- const key = await crypto.subtle.importKey(
931
- "raw",
932
- encoder.encode(secret),
933
- { name: "HMAC", hash: "SHA-256" },
934
- false,
935
- ["sign"]
936
- );
937
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(canonicalString));
938
- return toBase64(signature);
939
- }
940
- async function generateAuthHeaders(organizationId, organizationSecret, method, url, body) {
941
- const timestamp = Math.floor(Date.now() / 1e3).toString();
942
- const signature = await generateHMACSignature(
943
- organizationSecret,
944
- organizationId,
945
- timestamp,
946
- method,
947
- url,
948
- body || ""
949
- );
895
+ // src/utils/jwt-auth.ts
896
+ function generateJWTHeaders(token, organizationId) {
950
897
  return {
898
+ "Authorization": `Bearer ${token}`,
951
899
  "X-CloudSignal-Organization-ID": organizationId,
952
- "X-CloudSignal-Timestamp": timestamp,
953
- "X-CloudSignal-Signature": signature,
954
900
  "Content-Type": "application/json"
955
901
  };
956
902
  }
957
- async function makeAuthenticatedRequest(organizationId, organizationSecret, method, url, body) {
903
+ async function makeJWTAuthenticatedRequest(config, method, url, body) {
958
904
  const bodyStr = body ? JSON.stringify(body) : void 0;
959
- const headers = await generateAuthHeaders(
960
- organizationId,
961
- organizationSecret,
962
- method,
963
- url,
964
- bodyStr
965
- );
905
+ let headers = generateJWTHeaders(config.token, config.organizationId);
966
906
  const options = {
967
907
  method,
968
908
  headers
@@ -970,11 +910,81 @@ async function makeAuthenticatedRequest(organizationId, organizationSecret, meth
970
910
  if (bodyStr) {
971
911
  options.body = bodyStr;
972
912
  }
973
- return fetch(url, options);
913
+ let response = await fetch(url, options);
914
+ if (response.status === 401 && config.onTokenExpired) {
915
+ try {
916
+ const newToken = await config.onTokenExpired();
917
+ if (newToken) {
918
+ headers = generateJWTHeaders(newToken, config.organizationId);
919
+ options.headers = headers;
920
+ response = await fetch(url, options);
921
+ }
922
+ } catch (refreshError) {
923
+ console.warn("[CloudSignal PWA] Token refresh failed:", refreshError);
924
+ }
925
+ }
926
+ return response;
974
927
  }
975
- function isValidUUID(value) {
976
- if (!value || typeof value !== "string") return false;
977
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
928
+ function createAuthContext(config) {
929
+ if (config.userToken) {
930
+ return {
931
+ mode: "jwt",
932
+ organizationId: config.organizationId,
933
+ userToken: config.userToken,
934
+ onTokenExpired: config.onTokenExpired
935
+ };
936
+ }
937
+ if (!config.organizationSecret) {
938
+ throw new Error("Either userToken or organizationSecret is required");
939
+ }
940
+ return {
941
+ mode: "hmac",
942
+ organizationId: config.organizationId,
943
+ organizationSecret: config.organizationSecret
944
+ };
945
+ }
946
+ async function makeAuthenticatedRequestWithContext(authContext, method, url, body, onTokenExpired) {
947
+ if (authContext.mode === "jwt") {
948
+ if (!authContext.userToken) {
949
+ throw new Error("userToken required for JWT auth mode");
950
+ }
951
+ return makeJWTAuthenticatedRequest(
952
+ {
953
+ token: authContext.userToken,
954
+ organizationId: authContext.organizationId,
955
+ // Allow override of token expired callback
956
+ onTokenExpired: onTokenExpired || authContext.onTokenExpired
957
+ },
958
+ method,
959
+ url,
960
+ body
961
+ );
962
+ }
963
+ const { makeAuthenticatedRequest: makeAuthenticatedRequest4 } = await import('./hmac-LWLR6F7Z.js');
964
+ if (!authContext.organizationSecret) {
965
+ throw new Error("organizationSecret required for HMAC auth mode");
966
+ }
967
+ return makeAuthenticatedRequest4(
968
+ authContext.organizationId,
969
+ authContext.organizationSecret,
970
+ method,
971
+ url,
972
+ body
973
+ );
974
+ }
975
+ function updateAuthToken(authContext, newToken) {
976
+ if (authContext.mode !== "jwt") {
977
+ return {
978
+ mode: "jwt",
979
+ organizationId: authContext.organizationId,
980
+ userToken: newToken,
981
+ onTokenExpired: authContext.onTokenExpired
982
+ };
983
+ }
984
+ return {
985
+ ...authContext,
986
+ userToken: newToken
987
+ };
978
988
  }
979
989
 
980
990
  // src/utils/storage.ts
@@ -1220,14 +1230,19 @@ var PushNotificationManager = class {
1220
1230
  this.vapidPublicKey = null;
1221
1231
  this.serviceUrl = options.serviceUrl;
1222
1232
  this.organizationId = options.organizationId;
1223
- this.organizationSecret = options.organizationSecret;
1224
1233
  this.serviceId = options.serviceId;
1225
1234
  this.debug = options.debug ?? false;
1226
1235
  this.deviceDetector = new DeviceDetector();
1236
+ this.authContext = createAuthContext({
1237
+ organizationId: options.organizationId,
1238
+ organizationSecret: options.organizationSecret,
1239
+ userToken: options.userToken
1240
+ });
1227
1241
  this.onRegistered = options.onRegistered;
1228
1242
  this.onUnregistered = options.onUnregistered;
1229
1243
  this.onError = options.onError;
1230
1244
  this.onPermissionDenied = options.onPermissionDenied;
1245
+ this.onTokenExpired = options.onTokenExpired;
1231
1246
  this.registrationId = getRegistrationId(this.organizationId, this.serviceId);
1232
1247
  }
1233
1248
  /**
@@ -1254,6 +1269,19 @@ var PushNotificationManager = class {
1254
1269
  isRegistered() {
1255
1270
  return this.registrationId !== null;
1256
1271
  }
1272
+ /**
1273
+ * Get current authentication mode
1274
+ */
1275
+ getAuthMode() {
1276
+ return this.authContext.mode;
1277
+ }
1278
+ /**
1279
+ * Update JWT token (for token refresh or upgrading from HMAC to JWT)
1280
+ */
1281
+ updateToken(token) {
1282
+ this.authContext = updateAuthToken(this.authContext, token);
1283
+ this.log(`Auth mode: ${this.authContext.mode}`);
1284
+ }
1257
1285
  /**
1258
1286
  * Register for push notifications
1259
1287
  */
@@ -1303,12 +1331,12 @@ var PushNotificationManager = class {
1303
1331
  language: options.language || navigator.language || "en-US"
1304
1332
  };
1305
1333
  const url = `${this.serviceUrl}/api/v1/registration/register`;
1306
- const response = await makeAuthenticatedRequest(
1307
- this.organizationId,
1308
- this.organizationSecret,
1334
+ const response = await makeAuthenticatedRequestWithContext(
1335
+ this.authContext,
1309
1336
  "POST",
1310
1337
  url,
1311
- registrationData
1338
+ registrationData,
1339
+ this.onTokenExpired
1312
1340
  );
1313
1341
  if (!response.ok) {
1314
1342
  const errorText = await response.text();
@@ -1347,12 +1375,12 @@ var PushNotificationManager = class {
1347
1375
  this.pushSubscription = null;
1348
1376
  }
1349
1377
  const url = `${this.serviceUrl}/api/v1/registration/unregister`;
1350
- const response = await makeAuthenticatedRequest(
1351
- this.organizationId,
1352
- this.organizationSecret,
1378
+ const response = await makeAuthenticatedRequestWithContext(
1379
+ this.authContext,
1353
1380
  "POST",
1354
1381
  url,
1355
- { registration_id: this.registrationId }
1382
+ { registration_id: this.registrationId },
1383
+ this.onTokenExpired
1356
1384
  );
1357
1385
  if (!response.ok) {
1358
1386
  this.log(`Backend unregistration failed: ${response.status}`, "warn");
@@ -1378,9 +1406,8 @@ var PushNotificationManager = class {
1378
1406
  throw new Error("Not registered for push notifications");
1379
1407
  }
1380
1408
  const url = `${this.serviceUrl}/api/v1/registration/update`;
1381
- const response = await makeAuthenticatedRequest(
1382
- this.organizationId,
1383
- this.organizationSecret,
1409
+ const response = await makeAuthenticatedRequestWithContext(
1410
+ this.authContext,
1384
1411
  "POST",
1385
1412
  url,
1386
1413
  {
@@ -1389,7 +1416,8 @@ var PushNotificationManager = class {
1389
1416
  timezone: preferences.timezone,
1390
1417
  language: preferences.language,
1391
1418
  is_active: preferences.isActive
1392
- }
1419
+ },
1420
+ this.onTokenExpired
1393
1421
  );
1394
1422
  if (!response.ok) {
1395
1423
  throw new Error(`Update failed: ${response.status}`);
@@ -1412,11 +1440,12 @@ var PushNotificationManager = class {
1412
1440
  return null;
1413
1441
  }
1414
1442
  const url = `${this.serviceUrl}/api/v1/registration/status/${this.registrationId}`;
1415
- const response = await makeAuthenticatedRequest(
1416
- this.organizationId,
1417
- this.organizationSecret,
1443
+ const response = await makeAuthenticatedRequestWithContext(
1444
+ this.authContext,
1418
1445
  "GET",
1419
- url
1446
+ url,
1447
+ void 0,
1448
+ this.onTokenExpired
1420
1449
  );
1421
1450
  if (!response.ok) {
1422
1451
  if (response.status === 404) {
@@ -1529,13 +1558,18 @@ var HeartbeatManager = class {
1529
1558
  this.batteryManager = null;
1530
1559
  this.serviceUrl = options.serviceUrl;
1531
1560
  this.organizationId = options.organizationId;
1532
- this.organizationSecret = options.organizationSecret;
1533
1561
  this.debug = options.debug ?? false;
1534
1562
  this.onHeartbeatSent = options.onHeartbeatSent;
1535
1563
  this.onHeartbeatError = options.onHeartbeatError;
1536
1564
  this.onIntervalChanged = options.onIntervalChanged;
1537
1565
  this.onPausedForBattery = options.onPausedForBattery;
1538
1566
  this.onResumedFromBattery = options.onResumedFromBattery;
1567
+ this.onTokenExpired = options.onTokenExpired;
1568
+ this.authContext = createAuthContext({
1569
+ organizationId: options.organizationId,
1570
+ organizationSecret: options.organizationSecret,
1571
+ userToken: options.userToken
1572
+ });
1539
1573
  this.config = {
1540
1574
  enabled: options.config?.enabled ?? true,
1541
1575
  interval: options.config?.interval ?? 3e4,
@@ -1626,6 +1660,19 @@ var HeartbeatManager = class {
1626
1660
  isHeartbeatRunning() {
1627
1661
  return this.isRunning;
1628
1662
  }
1663
+ /**
1664
+ * Get current authentication mode
1665
+ */
1666
+ getAuthMode() {
1667
+ return this.authContext.mode;
1668
+ }
1669
+ /**
1670
+ * Update JWT token (for token refresh or upgrading from HMAC to JWT)
1671
+ */
1672
+ updateToken(token) {
1673
+ this.authContext = updateAuthToken(this.authContext, token);
1674
+ this.log(`Auth mode: ${this.authContext.mode}`);
1675
+ }
1629
1676
  /**
1630
1677
  * Send a single heartbeat
1631
1678
  */
@@ -1636,11 +1683,12 @@ var HeartbeatManager = class {
1636
1683
  }
1637
1684
  try {
1638
1685
  const url = `${this.serviceUrl}/api/v1/registration/heartbeat/${this.registrationId}`;
1639
- const response = await makeAuthenticatedRequest(
1640
- this.organizationId,
1641
- this.organizationSecret,
1686
+ const response = await makeAuthenticatedRequestWithContext(
1687
+ this.authContext,
1642
1688
  "POST",
1643
- url
1689
+ url,
1690
+ void 0,
1691
+ this.onTokenExpired
1644
1692
  );
1645
1693
  if (!response.ok) {
1646
1694
  throw new Error(`Heartbeat failed: ${response.status}`);
@@ -2956,7 +3004,7 @@ var IOSInstallBanner = class {
2956
3004
 
2957
3005
  // src/CloudSignalPWA.ts
2958
3006
  var DEFAULT_SERVICE_URL = "https://pwa.cloudsignal.app";
2959
- var SDK_VERSION = "1.1.0";
3007
+ var SDK_VERSION = "1.2.0";
2960
3008
  var CloudSignalPWA = class {
2961
3009
  constructor(config) {
2962
3010
  this.initialized = false;
@@ -2970,6 +3018,14 @@ var CloudSignalPWA = class {
2970
3018
  this.config = config;
2971
3019
  this.serviceUrl = config.serviceUrl || DEFAULT_SERVICE_URL;
2972
3020
  this.debug = config.debug ?? false;
3021
+ if (!config.organizationSecret && !config.userToken) {
3022
+ throw new Error("Either organizationSecret or userToken must be provided");
3023
+ }
3024
+ this.authContext = createAuthContext({
3025
+ organizationId: config.organizationId,
3026
+ organizationSecret: config.organizationSecret,
3027
+ userToken: config.userToken
3028
+ });
2973
3029
  this.deviceDetector = new DeviceDetector();
2974
3030
  this.serviceWorkerManager = new ServiceWorkerManager({
2975
3031
  config: config.serviceWorker,
@@ -2989,6 +3045,8 @@ var CloudSignalPWA = class {
2989
3045
  serviceUrl: this.serviceUrl,
2990
3046
  organizationId: config.organizationId,
2991
3047
  organizationSecret: config.organizationSecret,
3048
+ userToken: config.userToken,
3049
+ onTokenExpired: config.onTokenExpired,
2992
3050
  serviceId: config.serviceId,
2993
3051
  debug: this.debug,
2994
3052
  onRegistered: (reg) => {
@@ -3006,6 +3064,8 @@ var CloudSignalPWA = class {
3006
3064
  serviceUrl: this.serviceUrl,
3007
3065
  organizationId: config.organizationId,
3008
3066
  organizationSecret: config.organizationSecret,
3067
+ userToken: config.userToken,
3068
+ onTokenExpired: config.onTokenExpired,
3009
3069
  config: config.heartbeat,
3010
3070
  debug: this.debug,
3011
3071
  onHeartbeatSent: () => this.emit("heartbeat:sent", { timestamp: Date.now() }),
@@ -3114,12 +3174,12 @@ var CloudSignalPWA = class {
3114
3174
  async downloadConfig() {
3115
3175
  try {
3116
3176
  const url = `${this.serviceUrl}/api/v1/config/download`;
3117
- const response = await makeAuthenticatedRequest(
3118
- this.config.organizationId,
3119
- this.config.organizationSecret,
3177
+ const response = await makeAuthenticatedRequestWithContext(
3178
+ this.authContext,
3120
3179
  "POST",
3121
3180
  url,
3122
- { organization_id: this.config.organizationId }
3181
+ { organization_id: this.config.organizationId },
3182
+ this.config.onTokenExpired
3123
3183
  );
3124
3184
  if (!response.ok) {
3125
3185
  throw new Error(`Config download failed: ${response.status}`);
@@ -3221,6 +3281,27 @@ var CloudSignalPWA = class {
3221
3281
  return this.pushNotificationManager.requestPermission();
3222
3282
  }
3223
3283
  // ============================================
3284
+ // Authentication
3285
+ // ============================================
3286
+ /**
3287
+ * Get current authentication mode
3288
+ */
3289
+ getAuthMode() {
3290
+ return this.authContext.mode;
3291
+ }
3292
+ /**
3293
+ * Set JWT user token (for upgrading anonymous to authenticated, or token refresh)
3294
+ * After calling this, you should call registerForPush() again to re-register with user context
3295
+ * @param token - JWT token from identity provider
3296
+ */
3297
+ setUserToken(token) {
3298
+ this.authContext = updateAuthToken(this.authContext, token);
3299
+ this.pushNotificationManager.updateToken(token);
3300
+ this.heartbeatManager.updateToken(token);
3301
+ this.emit("auth:tokenUpdated", { mode: this.authContext.mode });
3302
+ this.log(`Auth mode updated to: ${this.authContext.mode}`);
3303
+ }
3304
+ // ============================================
3224
3305
  // Device Information
3225
3306
  // ============================================
3226
3307
  /**
@@ -3940,8 +4021,8 @@ var NotificationPermissionPrompt = class {
3940
4021
  };
3941
4022
 
3942
4023
  // src/index.ts
3943
- var VERSION = "1.1.0";
4024
+ var VERSION = "1.2.0";
3944
4025
 
3945
- export { CloudSignalPWA, DeviceDetector, HeartbeatManager, IOSInstallBanner, IOS_BANNER_TRANSLATIONS, IndexedDBStorage, InstallationManager, NOTIFICATION_PROMPT_TRANSLATIONS, NotificationPermissionPrompt, OfflineQueueManager, PushNotificationManager, ServiceWorkerManager, VERSION, WakeLockManager, CloudSignalPWA_default as default, detectBrowserLanguage, deviceDetector, generateAuthHeaders, generateBrowserFingerprint, generateHMACSignature, generateTrackingId, getRegistrationId, getStorageItem, isValidUUID, makeAuthenticatedRequest, removeRegistrationId, removeStorageItem, setRegistrationId, setStorageItem };
4026
+ export { CloudSignalPWA, DeviceDetector, HeartbeatManager, IOSInstallBanner, IOS_BANNER_TRANSLATIONS, IndexedDBStorage, InstallationManager, NOTIFICATION_PROMPT_TRANSLATIONS, NotificationPermissionPrompt, OfflineQueueManager, PushNotificationManager, ServiceWorkerManager, VERSION, WakeLockManager, createAuthContext, CloudSignalPWA_default as default, detectBrowserLanguage, deviceDetector, generateBrowserFingerprint, generateJWTHeaders, generateTrackingId, getRegistrationId, getStorageItem, makeAuthenticatedRequestWithContext, makeJWTAuthenticatedRequest, removeRegistrationId, removeStorageItem, setRegistrationId, setStorageItem, updateAuthToken };
3946
4027
  //# sourceMappingURL=index.js.map
3947
4028
  //# sourceMappingURL=index.js.map