@geolonia/geonicdb-sdk 0.19.2 → 0.19.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/README.md +36 -20
- package/geonicdb.cjs +264 -7
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.d.ts +131 -16
- package/geonicdb.iife.js +264 -7
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +264 -7
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
- package/react/index.d.ts +109 -7
package/geonicdb.mjs
CHANGED
|
@@ -56,6 +56,7 @@ 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
58
|
var WEBPUSH_VAPID_KEY_PATH = "/.well-known/webpush-vapid-key";
|
|
59
|
+
var WEBPUSH_SUBSCRIPTION_ID_STORAGE_PREFIX = "geonicdb:webpush:subscriptionId";
|
|
59
60
|
|
|
60
61
|
// src/sdk/dpop.ts
|
|
61
62
|
var dpopSupported = typeof crypto !== "undefined" && !!crypto.subtle && !!crypto.subtle.generateKey;
|
|
@@ -323,6 +324,12 @@ var RateLimitError = class extends GeonicDBError {
|
|
|
323
324
|
this.retryAfter = retryAfter;
|
|
324
325
|
}
|
|
325
326
|
};
|
|
327
|
+
var QuotaExceededError = class extends GeonicDBError {
|
|
328
|
+
constructor(message = "Quota exceeded") {
|
|
329
|
+
super(message, 507);
|
|
330
|
+
this.name = "QuotaExceededError";
|
|
331
|
+
}
|
|
332
|
+
};
|
|
326
333
|
var NetworkError = class extends GeonicDBError {
|
|
327
334
|
constructor(message = "Network error") {
|
|
328
335
|
super(message, 0);
|
|
@@ -344,6 +351,8 @@ function createErrorFromResponse(status, body, fallbackMessage) {
|
|
|
344
351
|
return new ValidationError(message);
|
|
345
352
|
case 429:
|
|
346
353
|
return new RateLimitError(message);
|
|
354
|
+
case 507:
|
|
355
|
+
return new QuotaExceededError(message);
|
|
347
356
|
default:
|
|
348
357
|
return new GeonicDBError(message, status);
|
|
349
358
|
}
|
|
@@ -609,6 +618,10 @@ var _AuthManager = class _AuthManager {
|
|
|
609
618
|
get baseUrl() {
|
|
610
619
|
return this._baseUrl;
|
|
611
620
|
}
|
|
621
|
+
/** Fiware-Service / NGSILD-Tenant value configured at construction time. */
|
|
622
|
+
get tenant() {
|
|
623
|
+
return this._tenant;
|
|
624
|
+
}
|
|
612
625
|
/**
|
|
613
626
|
* True when running unauthenticated.
|
|
614
627
|
*
|
|
@@ -1698,6 +1711,9 @@ function createCachePersistence() {
|
|
|
1698
1711
|
}
|
|
1699
1712
|
|
|
1700
1713
|
// src/sdk/webpush.ts
|
|
1714
|
+
function buildWebPushSubscriptionStorageKey(baseUrl, tenant, protocol) {
|
|
1715
|
+
return WEBPUSH_SUBSCRIPTION_ID_STORAGE_PREFIX + ":" + encodeURIComponent(baseUrl) + ":" + encodeURIComponent(tenant) + ":" + protocol;
|
|
1716
|
+
}
|
|
1701
1717
|
function arrayBufferToBase64Url(buf) {
|
|
1702
1718
|
const bytes = new Uint8Array(buf);
|
|
1703
1719
|
let binary = "";
|
|
@@ -1758,6 +1774,12 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1758
1774
|
super();
|
|
1759
1775
|
__publicField(this, "_auth");
|
|
1760
1776
|
__publicField(this, "_ws");
|
|
1777
|
+
/**
|
|
1778
|
+
* In-flight `registerWebPushSubscription()` calls keyed by storageKey (#3092).
|
|
1779
|
+
* Concurrent callers with the same key share one Promise so a race cannot
|
|
1780
|
+
* POST two subscriptions before the first id is persisted.
|
|
1781
|
+
*/
|
|
1782
|
+
__publicField(this, "_webPushRegisterInflight", /* @__PURE__ */ new Map());
|
|
1761
1783
|
/**
|
|
1762
1784
|
* Legacy callback for token refresh events.
|
|
1763
1785
|
* Prefer `db.on('tokenRefresh', callback)` for new code.
|
|
@@ -2230,9 +2252,13 @@ var GeonicDB = class extends EventEmitter {
|
|
|
2230
2252
|
* — otherwise (e.g. after the deployment rotated its VAPID keys) it
|
|
2231
2253
|
* unsubscribes the stale one and re-subscribes with the current key.
|
|
2232
2254
|
* Fetches the deployment's VAPID public key (unless `applicationServerKey`
|
|
2233
|
-
* is given) and calls `PushManager.subscribe()`.
|
|
2234
|
-
*
|
|
2235
|
-
* the
|
|
2255
|
+
* is given) and calls `PushManager.subscribe()`.
|
|
2256
|
+
*
|
|
2257
|
+
* This method only covers the browser Push API. To also create (or reuse)
|
|
2258
|
+
* the GeonicDB subscription idempotently, prefer
|
|
2259
|
+
* `registerWebPushSubscription()` (#3092). For a manual flow, convert the
|
|
2260
|
+
* result with `toNgsiv2WebPushNotification()` / `toNgsiLdWebPushEndpoint()`
|
|
2261
|
+
* and `request('POST', ...)`.
|
|
2236
2262
|
*
|
|
2237
2263
|
* Browser-only — throws in environments without Service Worker + Push API
|
|
2238
2264
|
* support (e.g. Node.js).
|
|
@@ -2240,11 +2266,9 @@ var GeonicDB = class extends EventEmitter {
|
|
|
2240
2266
|
* @example
|
|
2241
2267
|
* ```ts
|
|
2242
2268
|
* await navigator.serviceWorker.register('/sw.js');
|
|
2243
|
-
* const
|
|
2244
|
-
*
|
|
2245
|
-
* description: 'Web Push notification',
|
|
2269
|
+
* const { subscriptionId, created } = await db.registerWebPushSubscription({
|
|
2270
|
+
* protocol: 'ngsiv2',
|
|
2246
2271
|
* subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
|
|
2247
|
-
* notification: { webpush: toNgsiv2WebPushNotification(subscription) },
|
|
2248
2272
|
* });
|
|
2249
2273
|
* ```
|
|
2250
2274
|
*/
|
|
@@ -2264,10 +2288,121 @@ var GeonicDB = class extends EventEmitter {
|
|
|
2264
2288
|
}
|
|
2265
2289
|
return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
|
|
2266
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Idempotently register a GeonicDB subscription that delivers via Web Push (#3092).
|
|
2293
|
+
*
|
|
2294
|
+
* 1. Calls `subscribeWebPush()` for the browser PushSubscription.
|
|
2295
|
+
* 2. Looks up a previously saved subscription id in `localStorage`.
|
|
2296
|
+
* 3. If that id still exists on the broker (`GET` → 200), returns it without
|
|
2297
|
+
* `POST`ing. When the push endpoint changed (e.g. VAPID key rotation),
|
|
2298
|
+
* `PATCH`es only `notification.webpush` / `endpoint.webpush` so a new
|
|
2299
|
+
* subscription is not created (avoids burning the subscription quota).
|
|
2300
|
+
* 4. Otherwise `POST`s a new subscription and persists its id.
|
|
2301
|
+
*
|
|
2302
|
+
* `protocol` is required — there is no default. A 507 from the broker is
|
|
2303
|
+
* thrown as `QuotaExceededError`.
|
|
2304
|
+
*
|
|
2305
|
+
* @example
|
|
2306
|
+
* ```ts
|
|
2307
|
+
* import { QuotaExceededError } from '@geolonia/geonicdb-sdk';
|
|
2308
|
+
*
|
|
2309
|
+
* try {
|
|
2310
|
+
* const { subscriptionId, created } = await db.registerWebPushSubscription({
|
|
2311
|
+
* protocol: 'ngsiv2',
|
|
2312
|
+
* subject: { entities: [{ type: 'Sensor' }], condition: { attrs: ['value'] } },
|
|
2313
|
+
* });
|
|
2314
|
+
* } catch (err) {
|
|
2315
|
+
* if (err instanceof QuotaExceededError) {
|
|
2316
|
+
* // plan grace ceiling — delete unused subscriptions or change plan, then retry
|
|
2317
|
+
* }
|
|
2318
|
+
* throw err;
|
|
2319
|
+
* }
|
|
2320
|
+
* ```
|
|
2321
|
+
*/
|
|
2322
|
+
async registerWebPushSubscription(options) {
|
|
2323
|
+
if (!options || options.protocol !== "ngsiv2" && options.protocol !== "ngsi-ld") {
|
|
2324
|
+
throw new GeonicDBError(
|
|
2325
|
+
"registerWebPushSubscription() requires protocol: 'ngsiv2' | 'ngsi-ld'"
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
if (options.protocol === "ngsiv2" && !options.subject?.entities?.length) {
|
|
2329
|
+
throw new GeonicDBError(
|
|
2330
|
+
"registerWebPushSubscription({ protocol: 'ngsiv2' }) requires subject.entities"
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
if (options.protocol === "ngsi-ld" && !options.entities?.length) {
|
|
2334
|
+
throw new GeonicDBError(
|
|
2335
|
+
"registerWebPushSubscription({ protocol: 'ngsi-ld' }) requires entities"
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
const storageKey = options.storageKey ?? buildWebPushSubscriptionStorageKey(
|
|
2339
|
+
this._auth.baseUrl,
|
|
2340
|
+
this._auth.tenant,
|
|
2341
|
+
options.protocol
|
|
2342
|
+
);
|
|
2343
|
+
assertWebPushSubscriptionStorageWritable(storageKey);
|
|
2344
|
+
const inflight = this._webPushRegisterInflight.get(storageKey);
|
|
2345
|
+
if (inflight) return inflight;
|
|
2346
|
+
const run = this._registerWebPushSubscriptionBody(options, storageKey).finally(() => {
|
|
2347
|
+
if (this._webPushRegisterInflight.get(storageKey) === run) {
|
|
2348
|
+
this._webPushRegisterInflight.delete(storageKey);
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
this._webPushRegisterInflight.set(storageKey, run);
|
|
2352
|
+
return run;
|
|
2353
|
+
}
|
|
2354
|
+
async _registerWebPushSubscriptionBody(options, storageKey) {
|
|
2355
|
+
const pushSubscription = await this.subscribeWebPush({
|
|
2356
|
+
applicationServerKey: options.applicationServerKey,
|
|
2357
|
+
serviceWorkerRegistration: options.serviceWorkerRegistration
|
|
2358
|
+
});
|
|
2359
|
+
const delivery = {
|
|
2360
|
+
...options.ttl !== void 0 && { ttl: options.ttl },
|
|
2361
|
+
...options.urgency !== void 0 && { urgency: options.urgency },
|
|
2362
|
+
...options.topic !== void 0 && { topic: options.topic }
|
|
2363
|
+
};
|
|
2364
|
+
const savedId = readWebPushSubscriptionId(storageKey);
|
|
2365
|
+
if (savedId) {
|
|
2366
|
+
const collectionPath = webPushSubscriptionCollectionPath(options.protocol);
|
|
2367
|
+
const itemPath = `${collectionPath}/${encodeURIComponent(savedId)}`;
|
|
2368
|
+
try {
|
|
2369
|
+
const existing = await this.request("GET", itemPath);
|
|
2370
|
+
const storedEndpoint = readStoredWebPushEndpoint(options.protocol, existing);
|
|
2371
|
+
if (storedEndpoint && storedEndpoint !== pushSubscription.endpoint) {
|
|
2372
|
+
await this.request(
|
|
2373
|
+
"PATCH",
|
|
2374
|
+
itemPath,
|
|
2375
|
+
buildWebPushPatchBody(options.protocol, pushSubscription, delivery)
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
return { subscriptionId: savedId, created: false };
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
if (!(err instanceof NotFoundError)) throw err;
|
|
2381
|
+
clearWebPushSubscriptionId(storageKey);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
const createPath = webPushSubscriptionCollectionPath(options.protocol);
|
|
2385
|
+
const body = buildWebPushCreateBody(options, pushSubscription, delivery);
|
|
2386
|
+
const res = await this.requestRaw("POST", createPath, body);
|
|
2387
|
+
if (!res.ok) {
|
|
2388
|
+
const e = await res.json().catch(() => ({}));
|
|
2389
|
+
throw createErrorFromResponse(res.status, e, "Request failed: " + res.status);
|
|
2390
|
+
}
|
|
2391
|
+
const subscriptionId = subscriptionIdFromLocation(
|
|
2392
|
+
res.headers.get("Location"),
|
|
2393
|
+
options.protocol
|
|
2394
|
+
);
|
|
2395
|
+
writeWebPushSubscriptionId(storageKey, subscriptionId);
|
|
2396
|
+
return { subscriptionId, created: true };
|
|
2397
|
+
}
|
|
2267
2398
|
/**
|
|
2268
2399
|
* Unsubscribe the current browser from Web Push notifications, if
|
|
2269
2400
|
* subscribed. Returns `false` (no-op) when there is no active
|
|
2270
2401
|
* subscription or the environment has no Service Worker support.
|
|
2402
|
+
*
|
|
2403
|
+
* Does not delete the GeonicDB subscription document — clear the
|
|
2404
|
+
* persisted id (see `buildWebPushSubscriptionStorageKey`) and
|
|
2405
|
+
* `DELETE` the subscription separately if needed.
|
|
2271
2406
|
*/
|
|
2272
2407
|
async unsubscribeWebPush(registration) {
|
|
2273
2408
|
if (typeof navigator === "undefined" || !navigator.serviceWorker) return false;
|
|
@@ -2306,6 +2441,126 @@ function buildPathWithParams(path, params) {
|
|
|
2306
2441
|
const qs = sp.toString();
|
|
2307
2442
|
return qs ? `${path}?${qs}` : path;
|
|
2308
2443
|
}
|
|
2444
|
+
function webPushSubscriptionCollectionPath(protocol) {
|
|
2445
|
+
return protocol === "ngsiv2" ? "/v2/subscriptions" : "/ngsi-ld/v1/subscriptions";
|
|
2446
|
+
}
|
|
2447
|
+
function subscriptionIdFromLocation(location, protocol) {
|
|
2448
|
+
if (!location) {
|
|
2449
|
+
throw new GeonicDBError("Missing Location header after Web Push subscription create");
|
|
2450
|
+
}
|
|
2451
|
+
const marker = protocol === "ngsiv2" ? "/v2/subscriptions/" : "/ngsi-ld/v1/subscriptions/";
|
|
2452
|
+
const idx = location.indexOf(marker);
|
|
2453
|
+
const raw = idx >= 0 ? location.slice(idx + marker.length) : location.split("/").pop() || "";
|
|
2454
|
+
const id = decodeURIComponent(raw.split("?")[0].split("#")[0]);
|
|
2455
|
+
if (!id) {
|
|
2456
|
+
throw new GeonicDBError("Could not parse subscription id from Location: " + location);
|
|
2457
|
+
}
|
|
2458
|
+
return id;
|
|
2459
|
+
}
|
|
2460
|
+
function readStoredWebPushEndpoint(protocol, body) {
|
|
2461
|
+
const notification = body.notification;
|
|
2462
|
+
if (!notification) return void 0;
|
|
2463
|
+
if (protocol === "ngsiv2") {
|
|
2464
|
+
const webpush = notification.webpush;
|
|
2465
|
+
return webpush?.url;
|
|
2466
|
+
}
|
|
2467
|
+
const endpoint = notification.endpoint;
|
|
2468
|
+
return endpoint?.uri;
|
|
2469
|
+
}
|
|
2470
|
+
function buildWebPushCreateBody(options, pushSubscription, delivery) {
|
|
2471
|
+
const description = options.description ?? "Web Push notification";
|
|
2472
|
+
if (options.protocol === "ngsiv2") {
|
|
2473
|
+
return {
|
|
2474
|
+
description,
|
|
2475
|
+
subject: options.subject,
|
|
2476
|
+
notification: {
|
|
2477
|
+
webpush: toNgsiv2WebPushNotification(pushSubscription, delivery)
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
const { uri, webpush } = toNgsiLdWebPushEndpoint(pushSubscription, delivery);
|
|
2482
|
+
return {
|
|
2483
|
+
type: "Subscription",
|
|
2484
|
+
description,
|
|
2485
|
+
entities: options.entities,
|
|
2486
|
+
...options.watchedAttributes && { watchedAttributes: options.watchedAttributes },
|
|
2487
|
+
...options.q && { q: options.q },
|
|
2488
|
+
notification: {
|
|
2489
|
+
endpoint: { uri, accept: "application/json", protocol: "webpush", webpush }
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function buildWebPushPatchBody(protocol, pushSubscription, delivery) {
|
|
2494
|
+
if (protocol === "ngsiv2") {
|
|
2495
|
+
return {
|
|
2496
|
+
notification: {
|
|
2497
|
+
webpush: toNgsiv2WebPushNotification(pushSubscription, delivery)
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
const { uri, webpush } = toNgsiLdWebPushEndpoint(pushSubscription, delivery);
|
|
2502
|
+
return {
|
|
2503
|
+
notification: {
|
|
2504
|
+
endpoint: { uri, accept: "application/json", protocol: "webpush", webpush }
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
function readWebPushSubscriptionId(storageKey) {
|
|
2509
|
+
try {
|
|
2510
|
+
if (typeof localStorage === "undefined") return null;
|
|
2511
|
+
return localStorage.getItem(storageKey);
|
|
2512
|
+
} catch {
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function writeWebPushSubscriptionId(storageKey, subscriptionId) {
|
|
2517
|
+
try {
|
|
2518
|
+
if (typeof localStorage === "undefined") {
|
|
2519
|
+
throw new GeonicDBError(
|
|
2520
|
+
"registerWebPushSubscription() requires localStorage for idempotent registration"
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
localStorage.setItem(storageKey, subscriptionId);
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
if (err instanceof GeonicDBError) throw err;
|
|
2526
|
+
throw new GeonicDBError(
|
|
2527
|
+
"registerWebPushSubscription() could not persist subscription id to localStorage"
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
function assertWebPushSubscriptionStorageWritable(storageKey) {
|
|
2532
|
+
const probeKey = storageKey + "::__writable__";
|
|
2533
|
+
try {
|
|
2534
|
+
if (typeof localStorage === "undefined") {
|
|
2535
|
+
throw new GeonicDBError(
|
|
2536
|
+
"registerWebPushSubscription() requires localStorage for idempotent registration"
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
localStorage.setItem(probeKey, "1");
|
|
2540
|
+
if (localStorage.getItem(probeKey) !== "1") {
|
|
2541
|
+
throw new GeonicDBError(
|
|
2542
|
+
"registerWebPushSubscription() could not persist subscription id to localStorage"
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
localStorage.removeItem(probeKey);
|
|
2546
|
+
} catch (err) {
|
|
2547
|
+
try {
|
|
2548
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(probeKey);
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
if (err instanceof GeonicDBError) throw err;
|
|
2552
|
+
throw new GeonicDBError(
|
|
2553
|
+
"registerWebPushSubscription() could not persist subscription id to localStorage"
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function clearWebPushSubscriptionId(storageKey) {
|
|
2558
|
+
try {
|
|
2559
|
+
if (typeof localStorage === "undefined") return;
|
|
2560
|
+
localStorage.removeItem(storageKey);
|
|
2561
|
+
} catch {
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2309
2564
|
export {
|
|
2310
2565
|
AuthenticationError,
|
|
2311
2566
|
AuthorizationError,
|
|
@@ -2315,10 +2570,12 @@ export {
|
|
|
2315
2570
|
NetworkError,
|
|
2316
2571
|
NotFoundError,
|
|
2317
2572
|
PasswordResetRequiredError,
|
|
2573
|
+
QuotaExceededError,
|
|
2318
2574
|
RateLimitError,
|
|
2319
2575
|
TemporaryPasswordExpiredError,
|
|
2320
2576
|
ValidationError,
|
|
2321
2577
|
buildPathWithParams,
|
|
2578
|
+
buildWebPushSubscriptionStorageKey,
|
|
2322
2579
|
index_default as default,
|
|
2323
2580
|
toNgsiLdWebPushEndpoint,
|
|
2324
2581
|
toNgsiv2WebPushNotification,
|