@cloudsignal/pwa-sdk 2.1.0 → 2.1.3

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,5 +1,5 @@
1
- import { isValidUUID } from './chunk-IQHSODT4.js';
2
- export { generateAuthHeaders, generateHMACSignature, isValidUUID, makeAuthenticatedRequest } from './chunk-IQHSODT4.js';
1
+ import { isValidUUID } from './chunk-NSF32IGO.js';
2
+ export { generateAuthHeaders, generateHMACSignature, isValidUUID, makeAuthenticatedRequest } from './chunk-NSF32IGO.js';
3
3
 
4
4
  /**
5
5
  * CloudSignal PWA SDK v1.0.0
@@ -960,7 +960,7 @@ async function makeAuthenticatedRequestWithContext(authContext, method, url, bod
960
960
  body
961
961
  );
962
962
  }
963
- const { makeAuthenticatedRequest: makeAuthenticatedRequest4 } = await import('./hmac-WITZIX2O.js');
963
+ const { makeAuthenticatedRequest: makeAuthenticatedRequest4 } = await import('./hmac-YQQ5XQWC.js');
964
964
  if (!authContext.organizationPublishableKey) {
965
965
  throw new Error("organizationPublishableKey required for HMAC auth mode");
966
966
  }
@@ -1038,6 +1038,14 @@ function markInstallationRegistered(organizationId, serviceId, registrationId) {
1038
1038
  setStorageItem(`install_id_${organizationId}_${serviceId}`, registrationId);
1039
1039
  return setStorageItem(key, true);
1040
1040
  }
1041
+ function getLastRegisteredIdentity(organizationId, serviceId) {
1042
+ const key = `last_identity_${organizationId}_${serviceId}`;
1043
+ return getStorageItem(key);
1044
+ }
1045
+ function setLastRegisteredIdentity(organizationId, serviceId, identity) {
1046
+ const key = `last_identity_${organizationId}_${serviceId}`;
1047
+ return setStorageItem(key, identity);
1048
+ }
1041
1049
  var IndexedDBStorage = class {
1042
1050
  constructor(dbName = "CloudSignalPWA", dbVersion = 1) {
1043
1051
  this.db = null;
@@ -1308,6 +1316,27 @@ var PushNotificationManager = class {
1308
1316
  this.onPermissionDenied?.();
1309
1317
  return null;
1310
1318
  }
1319
+ const incomingEmail = options.userEmail ?? null;
1320
+ const incomingUserId = options.userId ?? null;
1321
+ const stored = getLastRegisteredIdentity(this.organizationId, this.serviceId);
1322
+ if (stored) {
1323
+ const emailChanged = stored.email !== null && incomingEmail !== null && stored.email !== incomingEmail;
1324
+ const userIdChanged = stored.userId !== null && incomingUserId !== null && stored.userId !== incomingUserId;
1325
+ if (emailChanged || userIdChanged) {
1326
+ this.log(
1327
+ `Identity changed on this device (was ${stored.email || stored.userId}, now ${incomingEmail || incomingUserId}); dropping stale push subscription`
1328
+ );
1329
+ try {
1330
+ const stale = await this.serviceWorkerRegistration.pushManager.getSubscription();
1331
+ if (stale) {
1332
+ await stale.unsubscribe();
1333
+ this.pushSubscription = null;
1334
+ }
1335
+ } catch (err) {
1336
+ this.log(`Stale subscription unsubscribe failed: ${err}`, "error");
1337
+ }
1338
+ }
1339
+ }
1311
1340
  const subscription = await this.subscribeToPush();
1312
1341
  if (!subscription) {
1313
1342
  throw new Error("Failed to subscribe to push notifications");
@@ -1348,12 +1377,23 @@ var PushNotificationManager = class {
1348
1377
  this.onTokenExpired
1349
1378
  );
1350
1379
  if (!response.ok) {
1380
+ if (response.status === 422) {
1381
+ const body = await response.clone().json().catch(() => null);
1382
+ if (body?.detail?.code === "user_identity_required") {
1383
+ this.log("Skipping push registration: service requires user identity");
1384
+ return null;
1385
+ }
1386
+ }
1351
1387
  const errorText = await response.text();
1352
1388
  throw new Error(`Registration failed: ${response.status} - ${errorText}`);
1353
1389
  }
1354
1390
  const result = await response.json();
1355
1391
  this.registrationId = result.registration_id;
1356
1392
  setRegistrationId(this.organizationId, this.serviceId, this.registrationId);
1393
+ setLastRegisteredIdentity(this.organizationId, this.serviceId, {
1394
+ email: incomingEmail,
1395
+ userId: incomingUserId
1396
+ });
1357
1397
  const registration = {
1358
1398
  registrationId: result.registration_id,
1359
1399
  status: result.status || "active",
@@ -1414,6 +1454,13 @@ var PushNotificationManager = class {
1414
1454
  this.onTokenExpired
1415
1455
  );
1416
1456
  if (!response.ok) {
1457
+ if (response.status === 422) {
1458
+ const body = await response.clone().json().catch(() => null);
1459
+ if (body?.detail?.code === "user_identity_required") {
1460
+ this.log("Skipping install-only registration: service requires user identity");
1461
+ return null;
1462
+ }
1463
+ }
1417
1464
  const errorText = await response.text();
1418
1465
  throw new Error(`Installation registration failed: ${response.status} - ${errorText}`);
1419
1466
  }
@@ -4099,5 +4146,3 @@ var NotificationPermissionPrompt = class {
4099
4146
  var VERSION = "1.2.0";
4100
4147
 
4101
4148
  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 };
4102
- //# sourceMappingURL=index.js.map
4103
- //# sourceMappingURL=index.js.map
@@ -452,5 +452,3 @@
452
452
  console.log(`[CloudSignal SW] Service worker loaded ${CACHE_VERSION}`);
453
453
 
454
454
  })();
455
- //# sourceMappingURL=service-worker.js.map
456
- //# sourceMappingURL=service-worker.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudsignal/pwa-sdk",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "CloudSignal PWA SDK - Progressive Web App features with push notifications, JWT/HMAC authentication, installation management, device tracking, offline queue, wake lock, and notification analytics",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "build:watch": "tsup --watch",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "clean": "rm -rf dist",
34
- "prepublishOnly": "npm run clean && npm run build",
34
+ "prepublishOnly": "npm run clean && npm run build && bash ../scripts/check-publish-safe.sh .",
35
35
  "test": "vitest run",
36
36
  "test:watch": "vitest",
37
37
  "test:coverage": "vitest run --coverage"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/hmac.ts"],"names":[],"mappings":"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","file":"chunk-IQHSODT4.js","sourcesContent":["/**\n * HMAC Authentication Utilities\n * Implements CloudSignal's HMAC signature scheme for API authentication\n */\n\n/**\n * Convert ArrayBuffer to hex string\n */\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map(b => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Convert ArrayBuffer to base64 string\n */\nfunction toBase64(buffer: ArrayBuffer): string {\n return btoa(String.fromCharCode(...new Uint8Array(buffer)))\n}\n\n/**\n * Generate HMAC signature for CloudSignal API requests\n *\n * @param secret - Signing key (organization publishable key, pk_*)\n * @param organizationId - Organization UUID\n * @param timestamp - Unix timestamp string\n * @param method - HTTP method (GET, POST, etc.)\n * @param url - Full URL or path\n * @param body - Request body (optional)\n * @returns Base64-encoded HMAC signature\n */\nexport async function generateHMACSignature(\n secret: string,\n organizationId: string,\n timestamp: string,\n method: string,\n url: string,\n body: string = ''\n): Promise<string> {\n const encoder = new TextEncoder()\n\n // Parse URL to extract path and query\n let path: string\n let query: string\n\n try {\n const urlObj = url.startsWith('http')\n ? new URL(url)\n : new URL(url, 'https://pwa.cloudsignal.app')\n path = urlObj.pathname\n query = urlObj.search ? urlObj.search.slice(1) : ''\n } catch {\n // Fallback for simple paths\n const queryIndex = url.indexOf('?')\n if (queryIndex > -1) {\n path = url.slice(0, queryIndex)\n query = url.slice(queryIndex + 1)\n } else {\n path = url\n query = ''\n }\n }\n\n // Build canonical string parts\n const canonicalParts = [\n method.toUpperCase(),\n path,\n query,\n organizationId,\n timestamp,\n ]\n\n // Add body hash if body is present\n if (body && body.length > 0) {\n const bodyHashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(body))\n const bodyHashHex = toHex(bodyHashBuffer)\n canonicalParts.push(bodyHashHex)\n }\n\n // Join parts with newlines\n const canonicalString = canonicalParts.join('\\n')\n\n // Import secret as HMAC key\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n\n // Sign the canonical string\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(canonicalString))\n\n return toBase64(signature)\n}\n\n/**\n * Generate authentication headers for CloudSignal API requests\n *\n * @param organizationId - Organization UUID\n * @param organizationPublishableKey - Organization publishable key (pk_*)\n * @param method - HTTP method\n * @param url - Request URL\n * @param body - Request body (optional)\n * @returns Headers object with authentication headers\n */\nexport async function generateAuthHeaders(\n organizationId: string,\n organizationPublishableKey: string,\n method: string,\n url: string,\n body?: string\n): Promise<Record<string, string>> {\n const timestamp = Math.floor(Date.now() / 1000).toString()\n\n const signature = await generateHMACSignature(\n organizationPublishableKey,\n organizationId,\n timestamp,\n method,\n url,\n body || ''\n )\n\n return {\n 'X-CloudSignal-Organization-ID': organizationId,\n 'X-CloudSignal-Timestamp': timestamp,\n 'X-CloudSignal-Signature': signature,\n 'Content-Type': 'application/json',\n }\n}\n\n/**\n * Make an authenticated request to CloudSignal API\n *\n * @param organizationId - Organization UUID\n * @param organizationPublishableKey - Organization publishable key (pk_*)\n * @param method - HTTP method\n * @param url - Request URL\n * @param body - Request body (optional)\n * @returns Fetch Response\n */\nexport async function makeAuthenticatedRequest(\n organizationId: string,\n organizationPublishableKey: string,\n method: string,\n url: string,\n body?: Record<string, any>\n): Promise<Response> {\n const bodyStr = body ? JSON.stringify(body) : undefined\n\n const headers = await generateAuthHeaders(\n organizationId,\n organizationPublishableKey,\n method,\n url,\n bodyStr\n )\n\n const options: RequestInit = {\n method,\n headers,\n }\n\n if (bodyStr) {\n options.body = bodyStr\n }\n\n return fetch(url, options)\n}\n\n/**\n * Validate UUID format\n * \n * @param value - String to validate\n * @returns Whether the string is a valid UUID\n */\nexport function isValidUUID(value: string | null | undefined): boolean {\n if (!value || typeof value !== 'string') return false\n 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)\n}\n"]}
@@ -1,3 +0,0 @@
1
- export { generateAuthHeaders, generateHMACSignature, isValidUUID, makeAuthenticatedRequest } from './chunk-IQHSODT4.js';
2
- //# sourceMappingURL=hmac-WITZIX2O.js.map
3
- //# sourceMappingURL=hmac-WITZIX2O.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"hmac-WITZIX2O.js"}