@hipnation-truth/sdk 0.5.0 → 0.7.0

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.mjs CHANGED
@@ -572,6 +572,188 @@ var NotesResource = class {
572
572
  }
573
573
  };
574
574
 
575
+ // src/resources/notifications.ts
576
+ var NotificationsError = class extends Error {
577
+ constructor(operation, status, message) {
578
+ super(message != null ? message : `Notifications ${operation} failed (HTTP ${status})`);
579
+ this.name = "NotificationsError";
580
+ this.status = status;
581
+ }
582
+ };
583
+ var NotificationsResource = class {
584
+ constructor(apiBaseUrl, apiKey) {
585
+ this.baseUrl = apiBaseUrl;
586
+ this.apiKey = apiKey;
587
+ }
588
+ post(path, body) {
589
+ return __async(this, null, function* () {
590
+ const res = yield fetch(`${this.baseUrl}/api${path}`, {
591
+ method: "POST",
592
+ headers: {
593
+ "Content-Type": "application/json",
594
+ Accept: "application/json",
595
+ "X-API-Key": this.apiKey
596
+ },
597
+ body: JSON.stringify(body)
598
+ });
599
+ if (!res.ok) {
600
+ const text = yield res.text().catch(() => "");
601
+ throw new NotificationsError(path, res.status, text.slice(0, 200));
602
+ }
603
+ return yield res.json();
604
+ });
605
+ }
606
+ get(path, params) {
607
+ return __async(this, null, function* () {
608
+ const url = new URL(`${this.baseUrl}/api${path}`);
609
+ for (const [k, v] of Object.entries(params)) {
610
+ url.searchParams.set(k, v);
611
+ }
612
+ const res = yield fetch(url.toString(), {
613
+ method: "GET",
614
+ headers: {
615
+ Accept: "application/json",
616
+ "X-API-Key": this.apiKey
617
+ }
618
+ });
619
+ if (!res.ok) {
620
+ const text = yield res.text().catch(() => "");
621
+ throw new NotificationsError(path, res.status, text.slice(0, 200));
622
+ }
623
+ return yield res.json();
624
+ });
625
+ }
626
+ delete(path) {
627
+ return __async(this, null, function* () {
628
+ const res = yield fetch(`${this.baseUrl}/api${path}`, {
629
+ method: "DELETE",
630
+ headers: {
631
+ Accept: "application/json",
632
+ "X-API-Key": this.apiKey
633
+ }
634
+ });
635
+ if (!res.ok) {
636
+ const text = yield res.text().catch(() => "");
637
+ throw new NotificationsError(path, res.status, text.slice(0, 200));
638
+ }
639
+ return yield res.json();
640
+ });
641
+ }
642
+ /**
643
+ * Register a device (or refresh its metadata) for push delivery.
644
+ * Safe to call repeatedly — the server dedupes by native token.
645
+ */
646
+ registerDevice(input) {
647
+ return __async(this, null, function* () {
648
+ return this.post("/notifications/devices/register", input);
649
+ });
650
+ }
651
+ /** Revoke a device — on sign-out or when the OS reports an invalid token. */
652
+ unregisterDevice(input) {
653
+ return __async(this, null, function* () {
654
+ return this.post("/notifications/devices/unregister", input);
655
+ });
656
+ }
657
+ /**
658
+ * Send a push notification to every active device belonging to
659
+ * `userId`. Honors the user's notificationPreferences (quiet hours,
660
+ * DND, channel off) before publishing.
661
+ */
662
+ send(input) {
663
+ return __async(this, null, function* () {
664
+ return this.post("/notifications/send", input);
665
+ });
666
+ }
667
+ /** Read a user's notification preferences. Returns defaults when no row exists. */
668
+ getPreferences(userId) {
669
+ return __async(this, null, function* () {
670
+ return this.get("/notifications/preferences", { userId });
671
+ });
672
+ }
673
+ updatePreferences(input) {
674
+ return __async(this, null, function* () {
675
+ return this.post("/notifications/preferences", input);
676
+ });
677
+ }
678
+ /**
679
+ * Schedule a future push notification. Convex's native scheduler
680
+ * fires the send at `scheduledAt` and runs the same delivery
681
+ * pipeline as `send()` (preferences, devices, history audit).
682
+ *
683
+ * Throws `NotificationsError` with status 400 if `scheduledAt` is
684
+ * not strictly in the future.
685
+ */
686
+ schedule(input) {
687
+ return __async(this, null, function* () {
688
+ return this.post("/notifications/schedule", input);
689
+ });
690
+ }
691
+ /**
692
+ * Cancel a pending scheduled notification. Returns `cancelled: false`
693
+ * (no error) if the job has already executed, was previously
694
+ * cancelled, or no longer exists — `reason` describes which case.
695
+ */
696
+ cancelScheduled(jobId) {
697
+ return __async(this, null, function* () {
698
+ return this.delete(`/notifications/schedule/${encodeURIComponent(jobId)}`);
699
+ });
700
+ }
701
+ /**
702
+ * List scheduled notifications for a user — pending, executed,
703
+ * cancelled, or failed. Most-recent first. Default limit 100.
704
+ */
705
+ listScheduled(userId, options) {
706
+ return __async(this, null, function* () {
707
+ const params = { userId };
708
+ if ((options == null ? void 0 : options.limit) !== void 0) {
709
+ params.limit = String(options.limit);
710
+ }
711
+ return this.get("/notifications/schedule", params);
712
+ });
713
+ }
714
+ getVapidKey() {
715
+ return __async(this, null, function* () {
716
+ try {
717
+ const result = yield this.get(
718
+ "/notifications/vapid-key",
719
+ {}
720
+ );
721
+ return result.vapidPublicKey;
722
+ } catch (e) {
723
+ return null;
724
+ }
725
+ });
726
+ }
727
+ onPushReceived(callback) {
728
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
729
+ return () => {
730
+ };
731
+ }
732
+ const handler = (event) => {
733
+ var _a;
734
+ if (((_a = event.data) == null ? void 0 : _a.type) === "TRUTH_PUSH_RECEIVED") {
735
+ callback(event.data.payload);
736
+ }
737
+ };
738
+ navigator.serviceWorker.addEventListener("message", handler);
739
+ return () => navigator.serviceWorker.removeEventListener("message", handler);
740
+ }
741
+ onPushTapped(callback) {
742
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
743
+ return () => {
744
+ };
745
+ }
746
+ const handler = (event) => {
747
+ var _a;
748
+ if (((_a = event.data) == null ? void 0 : _a.type) === "TRUTH_PUSH_TAPPED") {
749
+ callback(event.data.payload);
750
+ }
751
+ };
752
+ navigator.serviceWorker.addEventListener("message", handler);
753
+ return () => navigator.serviceWorker.removeEventListener("message", handler);
754
+ }
755
+ };
756
+
575
757
  // src/resources/patient-details.ts
576
758
  var PatientDetailsError = class extends Error {
577
759
  constructor(operation, status, message) {
@@ -1137,6 +1319,65 @@ function sleep(ms) {
1137
1319
  return new Promise((resolve) => setTimeout(resolve, ms));
1138
1320
  }
1139
1321
 
1322
+ // src/web-push.ts
1323
+ function isWebPushSupported() {
1324
+ return typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window;
1325
+ }
1326
+ function registerServiceWorker(path = "/truth-sw.js") {
1327
+ return __async(this, null, function* () {
1328
+ return navigator.serviceWorker.register(path);
1329
+ });
1330
+ }
1331
+ function subscribeToPush(registration, vapidPublicKey) {
1332
+ return __async(this, null, function* () {
1333
+ const existing = yield registration.pushManager.getSubscription();
1334
+ if (existing) {
1335
+ return existing;
1336
+ }
1337
+ return registration.pushManager.subscribe({
1338
+ userVisibleOnly: true,
1339
+ applicationServerKey: urlBase64ToUint8Array(
1340
+ vapidPublicKey
1341
+ )
1342
+ });
1343
+ });
1344
+ }
1345
+ function subscriptionToJSON(sub) {
1346
+ var _a, _b, _c, _d;
1347
+ const json = sub.toJSON();
1348
+ return {
1349
+ endpoint: sub.endpoint,
1350
+ keys: {
1351
+ p256dh: (_b = (_a = json.keys) == null ? void 0 : _a.p256dh) != null ? _b : "",
1352
+ auth: (_d = (_c = json.keys) == null ? void 0 : _c.auth) != null ? _d : ""
1353
+ }
1354
+ };
1355
+ }
1356
+ function urlBase64ToUint8Array(base64String) {
1357
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
1358
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
1359
+ const rawData = atob(base64);
1360
+ const outputArray = new Uint8Array(rawData.length);
1361
+ for (let i = 0; i < rawData.length; ++i) {
1362
+ outputArray[i] = rawData.charCodeAt(i);
1363
+ }
1364
+ return outputArray;
1365
+ }
1366
+ function onServiceWorkerMessage(type, callback) {
1367
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
1368
+ return () => {
1369
+ };
1370
+ }
1371
+ const handler = (event) => {
1372
+ var _a;
1373
+ if (((_a = event.data) == null ? void 0 : _a.type) === type) {
1374
+ callback(event.data.payload);
1375
+ }
1376
+ };
1377
+ navigator.serviceWorker.addEventListener("message", handler);
1378
+ return () => navigator.serviceWorker.removeEventListener("message", handler);
1379
+ }
1380
+
1140
1381
  // src/client.ts
1141
1382
  var CONVEX_URLS = {
1142
1383
  local: "https://courteous-duck-623.convex.cloud",
@@ -1148,7 +1389,9 @@ var CONVEX_URLS = {
1148
1389
  };
1149
1390
  var TruthClient = class {
1150
1391
  constructor(config) {
1151
- var _a, _b, _c, _d, _e, _f, _g;
1392
+ this._vapidPublicKey = null;
1393
+ this._webPushReady = null;
1394
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1152
1395
  const convexUrl = (_b = (_a = config.convexUrl) != null ? _a : CONVEX_URLS[config.environment]) != null ? _b : CONVEX_URLS.local;
1153
1396
  this.convex = new ConvexHttpClient(convexUrl);
1154
1397
  this.tracker = new Tracker({
@@ -1177,6 +1420,40 @@ var TruthClient = class {
1177
1420
  );
1178
1421
  this.notes = new NotesResource(apiUrl, config.apiKey);
1179
1422
  this.physicians = new PhysiciansResource(this.convex);
1423
+ this.notifications = new NotificationsResource(apiUrl, config.apiKey);
1424
+ this._serviceWorkerPath = (_h = config.serviceWorkerPath) != null ? _h : "/truth-sw.js";
1425
+ if (typeof window !== "undefined" && isWebPushSupported() && config.autoInitServiceWorker !== false) {
1426
+ this._webPushReady = this.initWebPush();
1427
+ }
1428
+ }
1429
+ get vapidPublicKey() {
1430
+ return this._vapidPublicKey;
1431
+ }
1432
+ get webPushReady() {
1433
+ return this._webPushReady;
1434
+ }
1435
+ initWebPush() {
1436
+ return __async(this, null, function* () {
1437
+ try {
1438
+ const key = yield this.notifications.getVapidKey();
1439
+ if (!key) {
1440
+ return;
1441
+ }
1442
+ this._vapidPublicKey = key;
1443
+ const registration = yield registerServiceWorker(this._serviceWorkerPath);
1444
+ yield navigator.serviceWorker.ready;
1445
+ const subscription = yield subscribeToPush(registration, key);
1446
+ const subJSON = subscriptionToJSON(subscription);
1447
+ yield this.notifications.registerDevice({
1448
+ userId: "__pending__",
1449
+ platform: "web",
1450
+ webPushSubscription: subJSON,
1451
+ locale: navigator.language,
1452
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1453
+ });
1454
+ } catch (e) {
1455
+ }
1456
+ });
1180
1457
  }
1181
1458
  /**
1182
1459
  * The resolved Truth API base URL for this environment.
@@ -1333,6 +1610,8 @@ export {
1333
1610
  NOTIFICATION_EVENTS,
1334
1611
  NotesError,
1335
1612
  NotesResource,
1613
+ NotificationsError,
1614
+ NotificationsResource,
1336
1615
  PROVIDER_EVENTS,
1337
1616
  PatientDetailsError,
1338
1617
  PatientDetailsResource,
@@ -1348,6 +1627,11 @@ export {
1348
1627
  TranslationError,
1349
1628
  TranslationResource,
1350
1629
  TruthClient,
1351
- generateUuidV7
1630
+ generateUuidV7,
1631
+ isWebPushSupported,
1632
+ onServiceWorkerMessage,
1633
+ registerServiceWorker,
1634
+ subscribeToPush,
1635
+ subscriptionToJSON
1352
1636
  };
1353
1637
  //# sourceMappingURL=index.mjs.map