@camstack/sdk 1.2.127 → 1.2.129

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
@@ -666,6 +666,9 @@ SuperJSON.allowErrorProps;
666
666
  * ui-library Provider can seed React Query with the same WS connection
667
667
  * — a future phase will narrow the surface further.
668
668
  */
669
+ function refreshFlag(options) {
670
+ return options.refreshable === true ? { refreshable: true } : {};
671
+ }
669
672
  var WS_KEEP_ALIVE_INTERVAL_MS = 1e4;
670
673
  var WS_KEEP_ALIVE_PONG_TIMEOUT_MS = 6e3;
671
674
  var WS_RECONNECT_RETRY_DELAY_MS = 2e3;
@@ -945,10 +948,11 @@ var System = class {
945
948
  this.mirror = null;
946
949
  this.mirrorInit = null;
947
950
  }
948
- async login(username, password) {
951
+ async login(username, password, options = {}) {
949
952
  const result = await this._trpcClient.auth.login.mutate({
950
953
  username,
951
- password
954
+ password,
955
+ ...refreshFlag(options)
952
956
  });
953
957
  if (typeof result === "object" && result !== null && "token" in result) {
954
958
  const r = result;
@@ -965,10 +969,11 @@ var System = class {
965
969
  * server mints the real session JWT, which we set as the active
966
970
  * token on this client.
967
971
  */
968
- async loginVerifyTotp(challengeToken, code) {
972
+ async loginVerifyTotp(challengeToken, code, options = {}) {
969
973
  const result = await this._trpcClient.auth.loginVerifyTotp.mutate({
970
974
  challengeToken,
971
- code
975
+ code,
976
+ ...refreshFlag(options)
972
977
  });
973
978
  if (typeof result === "object" && result !== null && "token" in result) {
974
979
  const token = result.token;
@@ -989,10 +994,11 @@ var System = class {
989
994
  * success the server mints the real session JWT, which we set as the
990
995
  * active token on this client.
991
996
  */
992
- async loginVerifyPasskey(challengeToken, response) {
997
+ async loginVerifyPasskey(challengeToken, response, options = {}) {
993
998
  const result = await this._trpcClient.auth.loginVerifyPasskey.mutate({
994
999
  challengeToken,
995
- response
1000
+ response,
1001
+ ...refreshFlag(options)
996
1002
  });
997
1003
  if (typeof result === "object" && result !== null && "token" in result) {
998
1004
  const token = result.token;
@@ -1021,8 +1027,11 @@ var System = class {
1021
1027
  * `secondFactors`. That token is NOT persisted as the active session
1022
1028
  * — the caller completes the remaining factor via `loginVerifyTotp`.
1023
1029
  */
1024
- async passkeyLoginFinish(response) {
1025
- const result = await this._trpcClient.auth.passkeyLoginFinish.mutate({ response });
1030
+ async passkeyLoginFinish(response, options = {}) {
1031
+ const result = await this._trpcClient.auth.passkeyLoginFinish.mutate({
1032
+ response,
1033
+ ...refreshFlag(options)
1034
+ });
1026
1035
  if (typeof result === "object" && result !== null && "token" in result) {
1027
1036
  const r = result;
1028
1037
  const requiresSecondFactor = r.requiresTotp === true || Array.isArray(r.secondFactors) && r.secondFactors.length > 0;
@@ -1030,8 +1039,22 @@ var System = class {
1030
1039
  }
1031
1040
  return result;
1032
1041
  }
1033
- async logout() {
1034
- await this._trpcClient.auth.logout.mutate();
1042
+ /**
1043
+ * Swap a refresh token for a new access token and a ROTATED refresh token
1044
+ * (the old one stops working). Sets the new access token on this client.
1045
+ * Throws a tRPC UNAUTHORIZED when the session is over — sign in again.
1046
+ */
1047
+ async refreshSession(refreshToken) {
1048
+ const result = await this._trpcClient.auth.refreshSession.mutate({ refreshToken });
1049
+ this.setToken(result.token);
1050
+ return result;
1051
+ }
1052
+ /**
1053
+ * End this session on the hub (its tokens stop working at once);
1054
+ * `everywhere: true` ends every session of the user.
1055
+ */
1056
+ async logout(options = {}) {
1057
+ await this._trpcClient.auth.logout.mutate(options.everywhere === true ? { everywhere: true } : void 0);
1035
1058
  }
1036
1059
  async getMe() {
1037
1060
  return await this._trpcClient.auth.me.query();
@@ -1045,8 +1068,25 @@ var System = class {
1045
1068
  async confirmOwnTotp(input) {
1046
1069
  return await this._trpcClient.auth.confirmOwnTotp.mutate(input);
1047
1070
  }
1048
- async disableOwnTotp() {
1049
- return await this._trpcClient.auth.disableOwnTotp.mutate();
1071
+ /** Needs a fresh `reauthToken` from {@link reauthenticate}. */
1072
+ async disableOwnTotp(input) {
1073
+ return await this._trpcClient.auth.disableOwnTotp.mutate(input);
1074
+ }
1075
+ /**
1076
+ * Re-authentication, passkey path step 1: assertion options for the
1077
+ * caller's own passkeys. Feed `optionsJSON` to `startAuthentication`, then
1078
+ * pass the result to {@link reauthenticate} as `passkeyResponse`.
1079
+ */
1080
+ async reauthBeginPasskey() {
1081
+ return await this._trpcClient.auth.reauthBeginPasskey.mutate();
1082
+ }
1083
+ /**
1084
+ * Prove it is you — the current password or a passkey assertion — for a
1085
+ * 5-minute `reauthToken`, required by `disableOwnTotp`, `removeOwnPasskey`
1086
+ * and `auth.beginExternalLink` (valid only in this session).
1087
+ */
1088
+ async reauthenticate(input) {
1089
+ return await this._trpcClient.auth.reauthenticate.mutate(input);
1050
1090
  }
1051
1091
  async getOwnTotpStatus() {
1052
1092
  return await this._trpcClient.auth.getOwnTotpStatus.query();
@@ -1060,6 +1100,7 @@ var System = class {
1060
1100
  async listOwnPasskeys() {
1061
1101
  return await this._trpcClient.auth.listOwnPasskeys.query();
1062
1102
  }
1103
+ /** Needs a fresh `reauthToken` from {@link reauthenticate}. */
1063
1104
  async removeOwnPasskey(input) {
1064
1105
  return await this._trpcClient.auth.removeOwnPasskey.mutate(input);
1065
1106
  }
@@ -1409,6 +1450,119 @@ async function raceFastestEndpoint(candidates, timeoutMs) {
1409
1450
  }
1410
1451
  }
1411
1452
  //#endregion
1453
+ //#region src/session-keeper.ts
1454
+ var DEFAULT_MARGIN_MS = 5 * 6e4;
1455
+ var RETRY_MS = 3e4;
1456
+ var MIN_DELAY_MS = 1e3;
1457
+ /** Decode a JWT's `exp` (seconds) to epoch ms, or `null`. Never throws. */
1458
+ function jwtExpiresAt(token) {
1459
+ const part = token.split(".")[1];
1460
+ if (part === void 0) return null;
1461
+ try {
1462
+ const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
1463
+ const json = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("utf8");
1464
+ const payload = JSON.parse(json);
1465
+ if (payload === null || typeof payload !== "object") return null;
1466
+ const exp = Reflect.get(payload, "exp");
1467
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : null;
1468
+ } catch {
1469
+ return null;
1470
+ }
1471
+ }
1472
+ /** Is `error` the hub saying the session is over (as opposed to a transport failure)? */
1473
+ function isSessionRefusal(error) {
1474
+ if (error === null || typeof error !== "object") return false;
1475
+ const data = Reflect.get(error, "data");
1476
+ const code = data !== null && typeof data === "object" ? Reflect.get(data, "code") : void 0;
1477
+ return code === "UNAUTHORIZED" || code === "FORBIDDEN";
1478
+ }
1479
+ var SessionKeeper = class {
1480
+ options;
1481
+ current = null;
1482
+ cancel = null;
1483
+ inflight = null;
1484
+ now;
1485
+ setTimer;
1486
+ constructor(options) {
1487
+ this.options = options;
1488
+ this.now = options.now ?? Date.now;
1489
+ this.setTimer = options.setTimer ?? ((fn, ms) => {
1490
+ const handle = setTimeout(fn, ms);
1491
+ return () => clearTimeout(handle);
1492
+ });
1493
+ }
1494
+ /** Take over `tokens` (a login, a restore from storage) and schedule its refresh. */
1495
+ adopt(tokens) {
1496
+ this.current = tokens;
1497
+ this.schedule();
1498
+ }
1499
+ /** The tokens the keeper holds now. */
1500
+ tokens() {
1501
+ return this.current;
1502
+ }
1503
+ /**
1504
+ * Refresh now if the access token expires within `marginMs` (default: the
1505
+ * keeper's margin) — call it before reconnecting after the app was in the
1506
+ * background. Resolves when done; concurrent callers share one refresh.
1507
+ */
1508
+ async ensureFresh(marginMs = this.margin()) {
1509
+ const current = this.current;
1510
+ if (current?.refreshToken === void 0) return;
1511
+ const expiresAt = this.expiresAtOf(current);
1512
+ if (expiresAt !== null && expiresAt - this.now() > marginMs) return;
1513
+ await this.refreshNow();
1514
+ }
1515
+ /** Refresh immediately. `null` when there is nothing to refresh or it failed. */
1516
+ refreshNow() {
1517
+ if (this.inflight !== null) return this.inflight;
1518
+ const refreshToken = this.current?.refreshToken;
1519
+ if (refreshToken === void 0) return Promise.resolve(null);
1520
+ this.inflight = this.options.refresh(refreshToken).then((next) => {
1521
+ this.current = next;
1522
+ this.options.onTokens(next);
1523
+ this.schedule();
1524
+ return next;
1525
+ }).catch((error) => {
1526
+ if (isSessionRefusal(error)) {
1527
+ this.stop();
1528
+ this.current = null;
1529
+ this.options.onExpired();
1530
+ } else this.arm(RETRY_MS);
1531
+ return null;
1532
+ }).finally(() => {
1533
+ this.inflight = null;
1534
+ });
1535
+ return this.inflight;
1536
+ }
1537
+ stop() {
1538
+ this.cancel?.();
1539
+ this.cancel = null;
1540
+ }
1541
+ margin() {
1542
+ return this.options.marginMs ?? DEFAULT_MARGIN_MS;
1543
+ }
1544
+ expiresAtOf(tokens) {
1545
+ return tokens.expiresAt ?? jwtExpiresAt(tokens.token);
1546
+ }
1547
+ schedule() {
1548
+ this.stop();
1549
+ const current = this.current;
1550
+ if (current?.refreshToken === void 0) return;
1551
+ const expiresAt = this.expiresAtOf(current);
1552
+ if (expiresAt === null) return;
1553
+ const remaining = expiresAt - this.now();
1554
+ const margin = Math.min(this.margin(), Math.max(0, remaining / 2));
1555
+ this.arm(Math.max(MIN_DELAY_MS, remaining - margin));
1556
+ }
1557
+ arm(ms) {
1558
+ this.stop();
1559
+ this.cancel = this.setTimer(() => {
1560
+ this.cancel = null;
1561
+ this.refreshNow();
1562
+ }, ms);
1563
+ }
1564
+ };
1565
+ //#endregion
1412
1566
  //#region src/detection.ts
1413
1567
  /**
1414
1568
  * Detection classes — shared between CamStack app and proxy.
@@ -1622,6 +1776,9 @@ var sensorLabelClasses = [
1622
1776
  "flooded",
1623
1777
  "entry_open"
1624
1778
  ];
1779
+ function listIncludes(list, value) {
1780
+ return list.includes(value);
1781
+ }
1625
1782
  var detectionClassesDefaultMap = {
1626
1783
  ...animalClasses.reduce((tot, curr) => ({
1627
1784
  ...tot,
@@ -1668,16 +1825,16 @@ var detectionClassesDefaultMap = {
1668
1825
  [curr]: "sensor"
1669
1826
  }), {})
1670
1827
  };
1671
- var isFaceClassname = (c) => faceClasses.includes(c);
1672
- var isPlateClassname = (c) => licensePlateClasses.includes(c);
1673
- var isAnimalClassname = (c) => animalClasses.includes(c);
1674
- var isPersonClassname = (c) => personClasses.includes(c);
1675
- var isVehicleClassname = (c) => vehicleClasses.includes(c);
1676
- var isMotionClassname = (c) => motionClasses.includes(c);
1677
- var isDoorbellClassname = (c) => doorbellClasses.includes(c);
1678
- var isPackageClassname = (c) => packageClasses.includes(c);
1679
- var isAudioClassname = (c) => audioClasses.includes(c) || audioLabelClasses.includes(c);
1680
- var isSensorLabelClassname = (c) => sensorLabelClasses.includes(c);
1828
+ var isFaceClassname = (c) => listIncludes(faceClasses, c);
1829
+ var isPlateClassname = (c) => listIncludes(licensePlateClasses, c);
1830
+ var isAnimalClassname = (c) => listIncludes(animalClasses, c);
1831
+ var isPersonClassname = (c) => listIncludes(personClasses, c);
1832
+ var isVehicleClassname = (c) => listIncludes(vehicleClasses, c);
1833
+ var isMotionClassname = (c) => listIncludes(motionClasses, c);
1834
+ var isDoorbellClassname = (c) => listIncludes(doorbellClasses, c);
1835
+ var isPackageClassname = (c) => listIncludes(packageClasses, c);
1836
+ var isAudioClassname = (c) => listIncludes(audioClasses, c) || listIncludes(audioLabelClasses, c);
1837
+ var isSensorLabelClassname = (c) => listIncludes(sensorLabelClasses, c);
1681
1838
  var isLabelDetection = (c) => isFaceClassname(c) || isPlateClassname(c);
1682
1839
  var getParentClass = (className) => detectionClassesDefaultMap[className];
1683
1840
  var getParentDetectionClass = (det) => {
@@ -1721,4 +1878,4 @@ function getClassesForTimelinePreset(preset, customClasses) {
1721
1878
  }
1722
1879
  }
1723
1880
  //#endregion
1724
- export { DEFAULT_ENABLED_CLASSES, DetectionClass, System, TIMELINE_PRESET_ALL, TIMELINE_PRESET_CRITICAL, TIMELINE_PRESET_IMPORTANT, animalClasses, audioClasses, audioLabelClasses, createSystem, defaultDetectionClasses, detectionClassesDefaultMap, doorbellClasses, faceClasses, getClassesForTimelinePreset, getParentClass, getParentDetectionClass, isAnimalClassname, isAudioClassname, isDoorbellClassname, isFaceClassname, isLabelDetection, isMotionClassname, isPackageClassname, isPersonClassname, isPlateClassname, isSensorLabelClassname, isVehicleClassname, licensePlateClasses, motionClasses, packageClasses, personClasses, raceFastestEndpoint, sensorLabelClasses, vehicleClasses };
1881
+ export { DEFAULT_ENABLED_CLASSES, DetectionClass, SessionKeeper, System, TIMELINE_PRESET_ALL, TIMELINE_PRESET_CRITICAL, TIMELINE_PRESET_IMPORTANT, animalClasses, audioClasses, audioLabelClasses, createSystem, defaultDetectionClasses, detectionClassesDefaultMap, doorbellClasses, faceClasses, getClassesForTimelinePreset, getParentClass, getParentDetectionClass, isAnimalClassname, isAudioClassname, isDoorbellClassname, isFaceClassname, isLabelDetection, isMotionClassname, isPackageClassname, isPersonClassname, isPlateClassname, isSensorLabelClassname, isSessionRefusal, isVehicleClassname, jwtExpiresAt, licensePlateClasses, motionClasses, packageClasses, personClasses, raceFastestEndpoint, sensorLabelClasses, vehicleClasses };