@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.d.ts CHANGED
@@ -406,6 +406,104 @@ declare class EventEmitter {
406
406
  protected emit<K extends keyof GeonicDBEventMap>(event: K, data?: GeonicDBEventMap[K]): void;
407
407
  protected emit(event: string, data?: unknown): void;
408
408
  }
409
+ /**
410
+ * Copyright (c) 2026 Geolonia Inc.
411
+ * Released under the MIT License. See src/sdk/LICENSE for details.
412
+ */
413
+ /**
414
+ * Web Push (VAPID) notification helpers (#3014 / #3060).
415
+ *
416
+ * Pure conversion functions between the browser Push API and the
417
+ * NGSIv2 `notification.webpush` / NGSI-LD `endpoint.webpush` shapes
418
+ * documented in docs/SUBSCRIPTIONS.md. These do not touch the network —
419
+ * see `GeonicDB.getWebPushVapidKey()` / `subscribeWebPush()` /
420
+ * `unsubscribeWebPush()` in `./index` for the browser-facing API.
421
+ */
422
+ /** `PushSubscription.toJSON().keys` shape (RFC 8291). */
423
+ export interface WebPushKeys {
424
+ p256dh: string;
425
+ auth: string;
426
+ }
427
+ /** RFC 8030 delivery options, shared by both NGSI protocol shapes. */
428
+ export interface WebPushDeliveryOptions {
429
+ /** Push message TTL in seconds. */
430
+ ttl?: number;
431
+ urgency?: "very-low" | "low" | "normal" | "high";
432
+ topic?: string;
433
+ }
434
+ /** Options for `GeonicDB.subscribeWebPush()`. */
435
+ export interface WebPushSubscribeOptions {
436
+ /**
437
+ * VAPID public key (base64url). Auto-fetched from
438
+ * `GET /.well-known/webpush-vapid-key` when omitted.
439
+ */
440
+ applicationServerKey?: string;
441
+ /**
442
+ * Service worker registration to subscribe through. Defaults to
443
+ * `navigator.serviceWorker.ready` — the caller must have already
444
+ * registered a service worker (e.g. `navigator.serviceWorker.register('/sw.js')`).
445
+ */
446
+ serviceWorkerRegistration?: ServiceWorkerRegistration;
447
+ }
448
+ /**
449
+ * Accepted by all conversion functions: either a `PushSubscription.toJSON()`
450
+ * plain-object shape (`{ endpoint, keys: { p256dh, auth } }`), or a native
451
+ * `PushSubscription` instance, which has no `keys` property and exposes the
452
+ * raw key material via `getKey()` instead (RFC 8291).
453
+ */
454
+ export interface WebPushSubscriptionLike {
455
+ endpoint?: string;
456
+ keys?: Partial<WebPushKeys>;
457
+ getKey?: (name: "p256dh" | "auth") => ArrayBuffer | null;
458
+ }
459
+ /**
460
+ * Convert a browser `PushSubscription` (or its `.toJSON()` output) into the
461
+ * NGSIv2 `notification.webpush` shape (#3014).
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * const subscription = await db.subscribeWebPush();
466
+ * const webpush = toNgsiv2WebPushNotification(subscription, { ttl: 86400 });
467
+ * await db.request('POST', '/v2/subscriptions', {
468
+ * description: 'Web Push notification',
469
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
470
+ * notification: { webpush },
471
+ * });
472
+ * ```
473
+ */
474
+ export declare function toNgsiv2WebPushNotification(subscription: WebPushSubscriptionLike, options?: WebPushDeliveryOptions): {
475
+ url: string;
476
+ keys: WebPushKeys;
477
+ } & WebPushDeliveryOptions;
478
+ /**
479
+ * Convert a browser `PushSubscription` (or its `.toJSON()` output) into the
480
+ * NGSI-LD `endpoint.webpush` shape (#3014, GeonicDB extension). The
481
+ * returned object is meant to be spread into `notification.endpoint`
482
+ * alongside `uri`.
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * const subscription = await db.subscribeWebPush();
487
+ * const { uri, webpush } = toNgsiLdWebPushEndpoint(subscription);
488
+ * await db.request('POST', '/ngsi-ld/v1/subscriptions', {
489
+ * type: 'Subscription',
490
+ * entities: [{ type: 'Sensor' }],
491
+ * notification: { endpoint: { uri, accept: 'application/json', webpush } },
492
+ * });
493
+ * ```
494
+ */
495
+ export declare function toNgsiLdWebPushEndpoint(subscription: WebPushSubscriptionLike, options?: WebPushDeliveryOptions): {
496
+ uri: string;
497
+ webpush: {
498
+ keys: WebPushKeys;
499
+ } & WebPushDeliveryOptions;
500
+ };
501
+ /**
502
+ * Convert a base64url-encoded VAPID public key (as returned by
503
+ * `GET /.well-known/webpush-vapid-key`) into the `Uint8Array` expected by
504
+ * `PushManager.subscribe({ applicationServerKey })`.
505
+ */
506
+ export declare function urlBase64ToUint8Array(base64Url: string): Uint8Array<ArrayBuffer>;
409
507
  /**
410
508
  * Copyright (c) 2026 Geolonia Inc.
411
509
  * Released under the MIT License. See src/sdk/LICENSE for details.
@@ -695,6 +793,50 @@ export declare class GeonicDB extends EventEmitter {
695
793
  isConnected(): boolean;
696
794
  /** Subscribe to entity events (can be called before or after connect). */
697
795
  subscribe(options?: SubscribeOptions): void;
796
+ /**
797
+ * Fetch the deployment's VAPID public key for Web Push subscriptions.
798
+ *
799
+ * Hits the unauthenticated discovery endpoint
800
+ * `GET /.well-known/webpush-vapid-key` directly (not via `request()`) —
801
+ * it needs no login/API key and predates any session setup. Throws
802
+ * `GeonicDBError` with `statusCode: 503` if the deployment has no VAPID
803
+ * keys configured (503 has no dedicated error class — see
804
+ * `createErrorFromResponse()` in `./errors`).
805
+ */
806
+ getWebPushVapidKey(): Promise<string>;
807
+ /**
808
+ * Subscribe the current browser to Web Push notifications (#3014).
809
+ *
810
+ * Reuses an existing subscription on the service worker registration if
811
+ * one is already active *and* still matches the resolved VAPID public key
812
+ * — otherwise (e.g. after the deployment rotated its VAPID keys) it
813
+ * unsubscribes the stale one and re-subscribes with the current key.
814
+ * Fetches the deployment's VAPID public key (unless `applicationServerKey`
815
+ * is given) and calls `PushManager.subscribe()`. Convert the result with
816
+ * `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()` and create
817
+ * the NGSI subscription with `request('POST', ...)`.
818
+ *
819
+ * Browser-only — throws in environments without Service Worker + Push API
820
+ * support (e.g. Node.js).
821
+ *
822
+ * @example
823
+ * ```ts
824
+ * await navigator.serviceWorker.register('/sw.js');
825
+ * const subscription = await db.subscribeWebPush();
826
+ * await db.request('POST', '/v2/subscriptions', {
827
+ * description: 'Web Push notification',
828
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
829
+ * notification: { webpush: toNgsiv2WebPushNotification(subscription) },
830
+ * });
831
+ * ```
832
+ */
833
+ subscribeWebPush(options?: WebPushSubscribeOptions): Promise<PushSubscription>;
834
+ /**
835
+ * Unsubscribe the current browser from Web Push notifications, if
836
+ * subscribed. Returns `false` (no-op) when there is no active
837
+ * subscription or the environment has no Service Worker support.
838
+ */
839
+ unsubscribeWebPush(registration?: ServiceWorkerRegistration): Promise<boolean>;
698
840
  }
699
841
  /**
700
842
  * 汎用クエリ文字列ビルダ。`undefined` / `null` は省略、`boolean` は
package/geonicdb.iife.js CHANGED
@@ -35,7 +35,10 @@ var GeonicDBModule = (() => {
35
35
  TemporaryPasswordExpiredError: () => TemporaryPasswordExpiredError,
36
36
  ValidationError: () => ValidationError,
37
37
  buildPathWithParams: () => buildPathWithParams,
38
- default: () => index_default
38
+ default: () => index_default,
39
+ toNgsiLdWebPushEndpoint: () => toNgsiLdWebPushEndpoint,
40
+ toNgsiv2WebPushNotification: () => toNgsiv2WebPushNotification,
41
+ urlBase64ToUint8Array: () => urlBase64ToUint8Array
39
42
  });
40
43
 
41
44
  // src/sdk/events.ts
@@ -91,6 +94,7 @@ var GeonicDBModule = (() => {
91
94
  var SDK_CACHE_IDB_NAME = "geonicdb-sdk-cache";
92
95
  var SDK_CACHE_IDB_STORE = "responses";
93
96
  var SDK_CACHE_IDB_VERSION = 1;
97
+ var WEBPUSH_VAPID_KEY_PATH = "/.well-known/webpush-vapid-key";
94
98
 
95
99
  // src/sdk/dpop.ts
96
100
  var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
@@ -640,6 +644,10 @@ var GeonicDBModule = (() => {
640
644
  });
641
645
  }
642
646
  }
647
+ /** The base URL requests are sent against (trailing slash already stripped). */
648
+ get baseUrl() {
649
+ return this._baseUrl;
650
+ }
643
651
  /**
644
652
  * True when running unauthenticated.
645
653
  *
@@ -1728,6 +1736,61 @@ var GeonicDBModule = (() => {
1728
1736
  return cachePersistenceSupported ? new IdbCachePersistence() : null;
1729
1737
  }
1730
1738
 
1739
+ // src/sdk/webpush.ts
1740
+ function arrayBufferToBase64Url(buf) {
1741
+ const bytes = new Uint8Array(buf);
1742
+ let binary = "";
1743
+ for (let i = 0; i < bytes.length; i++) {
1744
+ binary += String.fromCharCode(bytes[i]);
1745
+ }
1746
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1747
+ }
1748
+ function extractKeys(subscription) {
1749
+ const endpoint = subscription.endpoint;
1750
+ let p256dh = subscription.keys?.p256dh;
1751
+ let auth = subscription.keys?.auth;
1752
+ if ((!p256dh || !auth) && typeof subscription.getKey === "function") {
1753
+ const p256dhBuf = subscription.getKey("p256dh");
1754
+ const authBuf = subscription.getKey("auth");
1755
+ if (!p256dh && p256dhBuf) p256dh = arrayBufferToBase64Url(p256dhBuf);
1756
+ if (!auth && authBuf) auth = arrayBufferToBase64Url(authBuf);
1757
+ }
1758
+ if (!endpoint || !p256dh || !auth) {
1759
+ throw new Error(
1760
+ "Invalid PushSubscription: endpoint, keys.p256dh, and keys.auth are all required"
1761
+ );
1762
+ }
1763
+ return { endpoint, keys: { p256dh, auth } };
1764
+ }
1765
+ function toNgsiv2WebPushNotification(subscription, options) {
1766
+ const { endpoint, keys } = extractKeys(subscription);
1767
+ return { url: endpoint, keys, ...options };
1768
+ }
1769
+ function toNgsiLdWebPushEndpoint(subscription, options) {
1770
+ const { endpoint, keys } = extractKeys(subscription);
1771
+ return { uri: endpoint, webpush: { keys, ...options } };
1772
+ }
1773
+ function urlBase64ToUint8Array(base64Url) {
1774
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
1775
+ const base64 = (base64Url + padding).replace(/-/g, "+").replace(/_/g, "/");
1776
+ const raw = atob(base64);
1777
+ const output = new Uint8Array(raw.length);
1778
+ for (let i = 0; i < raw.length; i++) {
1779
+ output[i] = raw.charCodeAt(i);
1780
+ }
1781
+ return output;
1782
+ }
1783
+ function matchesApplicationServerKey(subscription, desiredKey) {
1784
+ const existingKey = subscription.options?.applicationServerKey;
1785
+ if (!existingKey) return true;
1786
+ const existingBytes = new Uint8Array(existingKey);
1787
+ if (existingBytes.length !== desiredKey.length) return false;
1788
+ for (let i = 0; i < existingBytes.length; i++) {
1789
+ if (existingBytes[i] !== desiredKey[i]) return false;
1790
+ }
1791
+ return true;
1792
+ }
1793
+
1731
1794
  // src/sdk/index.ts
1732
1795
  var GeonicDB = class extends EventEmitter {
1733
1796
  constructor(options) {
@@ -2174,6 +2237,84 @@ var GeonicDBModule = (() => {
2174
2237
  subscribe(options) {
2175
2238
  this._ws.subscribe(options);
2176
2239
  }
2240
+ // --- Web Push (#3014 / #3060) ---
2241
+ /**
2242
+ * Fetch the deployment's VAPID public key for Web Push subscriptions.
2243
+ *
2244
+ * Hits the unauthenticated discovery endpoint
2245
+ * `GET /.well-known/webpush-vapid-key` directly (not via `request()`) —
2246
+ * it needs no login/API key and predates any session setup. Throws
2247
+ * `GeonicDBError` with `statusCode: 503` if the deployment has no VAPID
2248
+ * keys configured (503 has no dedicated error class — see
2249
+ * `createErrorFromResponse()` in `./errors`).
2250
+ */
2251
+ async getWebPushVapidKey() {
2252
+ const res = await fetch(this._auth.baseUrl + WEBPUSH_VAPID_KEY_PATH);
2253
+ if (!res.ok) {
2254
+ const e = await res.json().catch(() => ({}));
2255
+ throw createErrorFromResponse(
2256
+ res.status,
2257
+ e,
2258
+ "Failed to fetch Web Push VAPID key: " + res.status
2259
+ );
2260
+ }
2261
+ const body = await res.json();
2262
+ return body.publicKey;
2263
+ }
2264
+ /**
2265
+ * Subscribe the current browser to Web Push notifications (#3014).
2266
+ *
2267
+ * Reuses an existing subscription on the service worker registration if
2268
+ * one is already active *and* still matches the resolved VAPID public key
2269
+ * — otherwise (e.g. after the deployment rotated its VAPID keys) it
2270
+ * unsubscribes the stale one and re-subscribes with the current key.
2271
+ * Fetches the deployment's VAPID public key (unless `applicationServerKey`
2272
+ * is given) and calls `PushManager.subscribe()`. Convert the result with
2273
+ * `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()` and create
2274
+ * the NGSI subscription with `request('POST', ...)`.
2275
+ *
2276
+ * Browser-only — throws in environments without Service Worker + Push API
2277
+ * support (e.g. Node.js).
2278
+ *
2279
+ * @example
2280
+ * ```ts
2281
+ * await navigator.serviceWorker.register('/sw.js');
2282
+ * const subscription = await db.subscribeWebPush();
2283
+ * await db.request('POST', '/v2/subscriptions', {
2284
+ * description: 'Web Push notification',
2285
+ * subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
2286
+ * notification: { webpush: toNgsiv2WebPushNotification(subscription) },
2287
+ * });
2288
+ * ```
2289
+ */
2290
+ async subscribeWebPush(options) {
2291
+ if (typeof navigator === "undefined" || !navigator.serviceWorker || typeof window === "undefined" || !window.PushManager) {
2292
+ throw new Error(
2293
+ "subscribeWebPush() requires a browser with Service Worker + Push API support"
2294
+ );
2295
+ }
2296
+ const registration = options?.serviceWorkerRegistration ?? await navigator.serviceWorker.ready;
2297
+ const publicKey = options?.applicationServerKey ?? await this.getWebPushVapidKey();
2298
+ const applicationServerKey = urlBase64ToUint8Array(publicKey);
2299
+ const existing = await registration.pushManager.getSubscription();
2300
+ if (existing) {
2301
+ if (matchesApplicationServerKey(existing, applicationServerKey)) return existing;
2302
+ await existing.unsubscribe();
2303
+ }
2304
+ return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
2305
+ }
2306
+ /**
2307
+ * Unsubscribe the current browser from Web Push notifications, if
2308
+ * subscribed. Returns `false` (no-op) when there is no active
2309
+ * subscription or the environment has no Service Worker support.
2310
+ */
2311
+ async unsubscribeWebPush(registration) {
2312
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return false;
2313
+ const reg = registration ?? await navigator.serviceWorker.ready;
2314
+ const existing = await reg.pushManager.getSubscription();
2315
+ if (!existing) return false;
2316
+ return existing.unsubscribe();
2317
+ }
2177
2318
  };
2178
2319
  var index_default = GeonicDB;
2179
2320
  if (typeof window !== "undefined") {