@geolonia/geonicdb-sdk 0.18.1 → 0.19.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/geonicdb.mjs CHANGED
@@ -55,6 +55,7 @@ var DPOP_IDB_VERSION = 1;
55
55
  var SDK_CACHE_IDB_NAME = "geonicdb-sdk-cache";
56
56
  var SDK_CACHE_IDB_STORE = "responses";
57
57
  var SDK_CACHE_IDB_VERSION = 1;
58
+ var WEBPUSH_VAPID_KEY_PATH = "/.well-known/webpush-vapid-key";
58
59
 
59
60
  // src/sdk/dpop.ts
60
61
  var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
@@ -604,6 +605,10 @@ var _AuthManager = class _AuthManager {
604
605
  });
605
606
  }
606
607
  }
608
+ /** The base URL requests are sent against (trailing slash already stripped). */
609
+ get baseUrl() {
610
+ return this._baseUrl;
611
+ }
607
612
  /**
608
613
  * True when running unauthenticated.
609
614
  *
@@ -1692,6 +1697,61 @@ function createCachePersistence() {
1692
1697
  return cachePersistenceSupported ? new IdbCachePersistence() : null;
1693
1698
  }
1694
1699
 
1700
+ // src/sdk/webpush.ts
1701
+ function arrayBufferToBase64Url(buf) {
1702
+ const bytes = new Uint8Array(buf);
1703
+ let binary = "";
1704
+ for (let i = 0; i < bytes.length; i++) {
1705
+ binary += String.fromCharCode(bytes[i]);
1706
+ }
1707
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1708
+ }
1709
+ function extractKeys(subscription) {
1710
+ const endpoint = subscription.endpoint;
1711
+ let p256dh = subscription.keys?.p256dh;
1712
+ let auth = subscription.keys?.auth;
1713
+ if ((!p256dh || !auth) && typeof subscription.getKey === "function") {
1714
+ const p256dhBuf = subscription.getKey("p256dh");
1715
+ const authBuf = subscription.getKey("auth");
1716
+ if (!p256dh && p256dhBuf) p256dh = arrayBufferToBase64Url(p256dhBuf);
1717
+ if (!auth && authBuf) auth = arrayBufferToBase64Url(authBuf);
1718
+ }
1719
+ if (!endpoint || !p256dh || !auth) {
1720
+ throw new Error(
1721
+ "Invalid PushSubscription: endpoint, keys.p256dh, and keys.auth are all required"
1722
+ );
1723
+ }
1724
+ return { endpoint, keys: { p256dh, auth } };
1725
+ }
1726
+ function toNgsiv2WebPushNotification(subscription, options) {
1727
+ const { endpoint, keys } = extractKeys(subscription);
1728
+ return { url: endpoint, keys, ...options };
1729
+ }
1730
+ function toNgsiLdWebPushEndpoint(subscription, options) {
1731
+ const { endpoint, keys } = extractKeys(subscription);
1732
+ return { uri: endpoint, webpush: { keys, ...options } };
1733
+ }
1734
+ function urlBase64ToUint8Array(base64Url) {
1735
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
1736
+ const base64 = (base64Url + padding).replace(/-/g, "+").replace(/_/g, "/");
1737
+ const raw = atob(base64);
1738
+ const output = new Uint8Array(raw.length);
1739
+ for (let i = 0; i < raw.length; i++) {
1740
+ output[i] = raw.charCodeAt(i);
1741
+ }
1742
+ return output;
1743
+ }
1744
+ function matchesApplicationServerKey(subscription, desiredKey) {
1745
+ const existingKey = subscription.options?.applicationServerKey;
1746
+ if (!existingKey) return true;
1747
+ const existingBytes = new Uint8Array(existingKey);
1748
+ if (existingBytes.length !== desiredKey.length) return false;
1749
+ for (let i = 0; i < existingBytes.length; i++) {
1750
+ if (existingBytes[i] !== desiredKey[i]) return false;
1751
+ }
1752
+ return true;
1753
+ }
1754
+
1695
1755
  // src/sdk/index.ts
1696
1756
  var GeonicDB = class extends EventEmitter {
1697
1757
  constructor(options) {
@@ -2138,6 +2198,84 @@ var GeonicDB = class extends EventEmitter {
2138
2198
  subscribe(options) {
2139
2199
  this._ws.subscribe(options);
2140
2200
  }
2201
+ // --- Web Push (#3014 / #3060) ---
2202
+ /**
2203
+ * Fetch the deployment's VAPID public key for Web Push subscriptions.
2204
+ *
2205
+ * Hits the unauthenticated discovery endpoint
2206
+ * `GET /.well-known/webpush-vapid-key` directly (not via `request()`) —
2207
+ * it needs no login/API key and predates any session setup. Throws
2208
+ * `GeonicDBError` with `statusCode: 503` if the deployment has no VAPID
2209
+ * keys configured (503 has no dedicated error class — see
2210
+ * `createErrorFromResponse()` in `./errors`).
2211
+ */
2212
+ async getWebPushVapidKey() {
2213
+ const res = await fetch(this._auth.baseUrl + WEBPUSH_VAPID_KEY_PATH);
2214
+ if (!res.ok) {
2215
+ const e = await res.json().catch(() => ({}));
2216
+ throw createErrorFromResponse(
2217
+ res.status,
2218
+ e,
2219
+ "Failed to fetch Web Push VAPID key: " + res.status
2220
+ );
2221
+ }
2222
+ const body = await res.json();
2223
+ return body.publicKey;
2224
+ }
2225
+ /**
2226
+ * Subscribe the current browser to Web Push notifications (#3014).
2227
+ *
2228
+ * Reuses an existing subscription on the service worker registration if
2229
+ * one is already active *and* still matches the resolved VAPID public key
2230
+ * — otherwise (e.g. after the deployment rotated its VAPID keys) it
2231
+ * unsubscribes the stale one and re-subscribes with the current key.
2232
+ * Fetches the deployment's VAPID public key (unless `applicationServerKey`
2233
+ * is given) and calls `PushManager.subscribe()`. Convert the result with
2234
+ * `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()` and create
2235
+ * the NGSI subscription with `request('POST', ...)`.
2236
+ *
2237
+ * Browser-only — throws in environments without Service Worker + Push API
2238
+ * support (e.g. Node.js).
2239
+ *
2240
+ * @example
2241
+ * ```ts
2242
+ * await navigator.serviceWorker.register('/sw.js');
2243
+ * const subscription = await db.subscribeWebPush();
2244
+ * await db.request('POST', '/v2/subscriptions', {
2245
+ * description: 'Web Push notification',
2246
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
2247
+ * notification: { webpush: toNgsiv2WebPushNotification(subscription) },
2248
+ * });
2249
+ * ```
2250
+ */
2251
+ async subscribeWebPush(options) {
2252
+ if (typeof navigator === "undefined" || !navigator.serviceWorker || typeof window === "undefined" || !window.PushManager) {
2253
+ throw new Error(
2254
+ "subscribeWebPush() requires a browser with Service Worker + Push API support"
2255
+ );
2256
+ }
2257
+ const registration = options?.serviceWorkerRegistration ?? await navigator.serviceWorker.ready;
2258
+ const publicKey = options?.applicationServerKey ?? await this.getWebPushVapidKey();
2259
+ const applicationServerKey = urlBase64ToUint8Array(publicKey);
2260
+ const existing = await registration.pushManager.getSubscription();
2261
+ if (existing) {
2262
+ if (matchesApplicationServerKey(existing, applicationServerKey)) return existing;
2263
+ await existing.unsubscribe();
2264
+ }
2265
+ return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
2266
+ }
2267
+ /**
2268
+ * Unsubscribe the current browser from Web Push notifications, if
2269
+ * subscribed. Returns `false` (no-op) when there is no active
2270
+ * subscription or the environment has no Service Worker support.
2271
+ */
2272
+ async unsubscribeWebPush(registration) {
2273
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return false;
2274
+ const reg = registration ?? await navigator.serviceWorker.ready;
2275
+ const existing = await reg.pushManager.getSubscription();
2276
+ if (!existing) return false;
2277
+ return existing.unsubscribe();
2278
+ }
2141
2279
  };
2142
2280
  var index_default = GeonicDB;
2143
2281
  if (typeof window !== "undefined") {
@@ -2181,6 +2319,9 @@ export {
2181
2319
  TemporaryPasswordExpiredError,
2182
2320
  ValidationError,
2183
2321
  buildPathWithParams,
2184
- index_default as default
2322
+ index_default as default,
2323
+ toNgsiLdWebPushEndpoint,
2324
+ toNgsiv2WebPushNotification,
2325
+ urlBase64ToUint8Array
2185
2326
  };
2186
2327
  //# sourceMappingURL=geonicdb.mjs.map