@yoooclaw/phone-notifications 1.7.2 → 1.7.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/dist/index.js CHANGED
@@ -5856,11 +5856,96 @@ function buildDeviceAuthPayload(params) {
5856
5856
  params.nonce
5857
5857
  ].join("|");
5858
5858
  }
5859
- function resolveStateDir2() {
5860
- return resolveStateDir();
5859
+ function resolveClientStateDir(stateDir) {
5860
+ return stateDir ?? resolveStateDir();
5861
5861
  }
5862
- function loadOrCreateDeviceIdentity() {
5863
- const filePath = path.join(resolveStateDir2(), "identity", "device.json");
5862
+ function ensureDir(filePath) {
5863
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5864
+ }
5865
+ function resolveIdentityPath(stateDir) {
5866
+ return path.join(stateDir, "identity", "device.json");
5867
+ }
5868
+ function normalizeDeviceAuthRole(role) {
5869
+ return role.trim();
5870
+ }
5871
+ function normalizeDeviceAuthScopes(scopes) {
5872
+ const out = /* @__PURE__ */ new Set();
5873
+ for (const scope of scopes) {
5874
+ const trimmed = scope.trim();
5875
+ if (trimmed) {
5876
+ out.add(trimmed);
5877
+ }
5878
+ }
5879
+ return [...out].sort();
5880
+ }
5881
+ function resolveDeviceAuthPath(stateDir) {
5882
+ return path.join(stateDir, "identity", "device-auth.json");
5883
+ }
5884
+ function readDeviceAuthStore(filePath) {
5885
+ try {
5886
+ if (!fs.existsSync(filePath)) return null;
5887
+ const raw = fs.readFileSync(filePath, "utf8");
5888
+ const parsed = JSON.parse(raw);
5889
+ if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") return null;
5890
+ if (!parsed.tokens || typeof parsed.tokens !== "object") return null;
5891
+ return parsed;
5892
+ } catch {
5893
+ return null;
5894
+ }
5895
+ }
5896
+ function writeDeviceAuthStore(filePath, store) {
5897
+ ensureDir(filePath);
5898
+ fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}
5899
+ `, {
5900
+ mode: 384
5901
+ });
5902
+ try {
5903
+ fs.chmodSync(filePath, 384);
5904
+ } catch {
5905
+ }
5906
+ }
5907
+ function loadDeviceAuthToken(params) {
5908
+ const store = readDeviceAuthStore(resolveDeviceAuthPath(params.stateDir));
5909
+ if (!store || store.deviceId !== params.deviceId) return null;
5910
+ const entry = store.tokens[normalizeDeviceAuthRole(params.role)];
5911
+ if (!entry || typeof entry.token !== "string") return null;
5912
+ return entry;
5913
+ }
5914
+ function storeDeviceAuthToken(params) {
5915
+ const filePath = resolveDeviceAuthPath(params.stateDir);
5916
+ const existing = readDeviceAuthStore(filePath);
5917
+ const role = normalizeDeviceAuthRole(params.role);
5918
+ const next = {
5919
+ version: 1,
5920
+ deviceId: params.deviceId,
5921
+ tokens: existing && existing.deviceId === params.deviceId && existing.tokens ? { ...existing.tokens } : {}
5922
+ };
5923
+ const entry = {
5924
+ token: params.token,
5925
+ role,
5926
+ scopes: normalizeDeviceAuthScopes(params.scopes),
5927
+ updatedAtMs: Date.now()
5928
+ };
5929
+ next.tokens[role] = entry;
5930
+ writeDeviceAuthStore(filePath, next);
5931
+ return entry;
5932
+ }
5933
+ function clearDeviceAuthToken(params) {
5934
+ const filePath = resolveDeviceAuthPath(params.stateDir);
5935
+ const store = readDeviceAuthStore(filePath);
5936
+ if (!store || store.deviceId !== params.deviceId) return;
5937
+ const role = normalizeDeviceAuthRole(params.role);
5938
+ if (!store.tokens[role]) return;
5939
+ const next = {
5940
+ version: 1,
5941
+ deviceId: store.deviceId,
5942
+ tokens: { ...store.tokens }
5943
+ };
5944
+ delete next.tokens[role];
5945
+ writeDeviceAuthStore(filePath, next);
5946
+ }
5947
+ function loadOrCreateDeviceIdentity(stateDir) {
5948
+ const filePath = resolveIdentityPath(stateDir);
5864
5949
  try {
5865
5950
  if (fs.existsSync(filePath)) {
5866
5951
  const raw = fs.readFileSync(filePath, "utf8");
@@ -5890,6 +5975,7 @@ function loadOrCreateDeviceIdentity() {
5890
5975
  ...identity,
5891
5976
  createdAtMs: Date.now()
5892
5977
  };
5978
+ ensureDir(filePath);
5893
5979
  fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
5894
5980
  `, {
5895
5981
  mode: 384
@@ -5899,7 +5985,8 @@ function loadOrCreateDeviceIdentity() {
5899
5985
  var TunnelProxy = class _TunnelProxy {
5900
5986
  constructor(opts) {
5901
5987
  this.opts = opts;
5902
- this.deviceIdentity = loadOrCreateDeviceIdentity();
5988
+ this.stateDir = resolveClientStateDir(opts.stateDir);
5989
+ this.deviceIdentity = loadOrCreateDeviceIdentity(this.stateDir);
5903
5990
  opts.logger.info(
5904
5991
  `TunnelProxy: loaded device identity (deviceId=${this.deviceIdentity.deviceId})`
5905
5992
  );
@@ -5915,6 +6002,7 @@ var TunnelProxy = class _TunnelProxy {
5915
6002
  gatewayWsPending = [];
5916
6003
  /** 设备身份,用于 Gateway connect 握手 */
5917
6004
  deviceIdentity;
6005
+ stateDir;
5918
6006
  pushGatewayPending(payload, reason) {
5919
6007
  this.gatewayWsPending.push(payload);
5920
6008
  this.opts.logger.info(
@@ -5932,6 +6020,65 @@ var TunnelProxy = class _TunnelProxy {
5932
6020
  password
5933
6021
  };
5934
6022
  }
6023
+ loadStoredDeviceToken(role) {
6024
+ return loadDeviceAuthToken({
6025
+ stateDir: this.stateDir,
6026
+ deviceId: this.deviceIdentity.deviceId,
6027
+ role
6028
+ })?.token ?? void 0;
6029
+ }
6030
+ buildGatewayConnectAuth(role) {
6031
+ const explicitAuth = this.resolveGatewayConnectAuth();
6032
+ const authPassword = explicitAuth?.password?.trim() || void 0;
6033
+ const explicitGatewayToken = explicitAuth?.token?.trim() || void 0;
6034
+ const deviceToken = explicitGatewayToken ? void 0 : this.loadStoredDeviceToken(role);
6035
+ const authToken = explicitGatewayToken ?? deviceToken;
6036
+ const auth = authToken || authPassword || deviceToken ? {
6037
+ token: authToken,
6038
+ deviceToken,
6039
+ password: authPassword
6040
+ } : void 0;
6041
+ return {
6042
+ auth,
6043
+ authToken,
6044
+ authPassword,
6045
+ deviceToken
6046
+ };
6047
+ }
6048
+ storeIssuedDeviceToken(params) {
6049
+ const token = params.authInfo?.deviceToken;
6050
+ if (typeof token !== "string" || !token.trim()) {
6051
+ return;
6052
+ }
6053
+ const role = typeof params.authInfo?.role === "string" && params.authInfo.role.trim() ? params.authInfo.role.trim() : params.fallbackRole;
6054
+ const scopes = Array.isArray(params.authInfo?.scopes) ? params.authInfo.scopes.filter(
6055
+ (scope) => typeof scope === "string" && !!scope.trim()
6056
+ ) : params.fallbackScopes;
6057
+ storeDeviceAuthToken({
6058
+ stateDir: this.stateDir,
6059
+ deviceId: this.deviceIdentity.deviceId,
6060
+ role,
6061
+ token: token.trim(),
6062
+ scopes
6063
+ });
6064
+ }
6065
+ maybeClearStoredDeviceTokenOnMismatch(code, reason) {
6066
+ const explicitAuth = this.resolveGatewayConnectAuth();
6067
+ if (explicitAuth?.token || explicitAuth?.password) {
6068
+ return;
6069
+ }
6070
+ if (code !== 1008 || !reason.toLowerCase().includes("device token mismatch")) {
6071
+ return;
6072
+ }
6073
+ clearDeviceAuthToken({
6074
+ stateDir: this.stateDir,
6075
+ deviceId: this.deviceIdentity.deviceId,
6076
+ role: "operator"
6077
+ });
6078
+ this.opts.logger.warn(
6079
+ `TunnelProxy: cleared stale stored device token after gateway mismatch (deviceId=${this.deviceIdentity.deviceId})`
6080
+ );
6081
+ }
5935
6082
  buildLocalGatewayAuthAttempts(baseHeaders) {
5936
6083
  const auth = this.resolveGatewayConnectAuth();
5937
6084
  const attempts = [];
@@ -6101,12 +6248,12 @@ var TunnelProxy = class _TunnelProxy {
6101
6248
  if (frame.type === "event" && frame.event === "connect.challenge") {
6102
6249
  const challengeNonce = frame.payload?.nonce ?? "";
6103
6250
  const connectRequestId = `tunnel-connect-${randomUUID2()}`;
6104
- const gatewayAuth = this.resolveGatewayConnectAuth();
6105
- this.opts.logger.info(
6106
- `TunnelProxy: received connect.challenge (nonce=${challengeNonce}, connectReqId=${connectRequestId}, hasToken=${!!gatewayAuth?.token}, hasPassword=${!!gatewayAuth?.password}), sending connect request with device identity`
6107
- );
6108
6251
  const role = "operator";
6109
6252
  const scopes = ["operator.admin"];
6253
+ const gatewayConnectAuth = this.buildGatewayConnectAuth(role);
6254
+ this.opts.logger.info(
6255
+ `TunnelProxy: received connect.challenge (nonce=${challengeNonce}, connectReqId=${connectRequestId}, hasToken=${!!gatewayConnectAuth.authToken}, hasPassword=${!!gatewayConnectAuth.authPassword}, hasDeviceToken=${!!gatewayConnectAuth.deviceToken}), sending connect request with device identity`
6256
+ );
6110
6257
  const signedAtMs = Date.now();
6111
6258
  const clientId = "gateway-client";
6112
6259
  const clientMode = "backend";
@@ -6117,7 +6264,7 @@ var TunnelProxy = class _TunnelProxy {
6117
6264
  role,
6118
6265
  scopes,
6119
6266
  signedAtMs,
6120
- token: gatewayAuth?.token ?? null,
6267
+ token: gatewayConnectAuth.authToken ?? null,
6121
6268
  nonce: challengeNonce
6122
6269
  });
6123
6270
  const signature = signDevicePayload(
@@ -6139,7 +6286,7 @@ var TunnelProxy = class _TunnelProxy {
6139
6286
  },
6140
6287
  role,
6141
6288
  scopes,
6142
- ...gatewayAuth ? { auth: gatewayAuth } : {},
6289
+ ...gatewayConnectAuth.auth ? { auth: gatewayConnectAuth.auth } : {},
6143
6290
  device: {
6144
6291
  id: this.deviceIdentity.deviceId,
6145
6292
  publicKey: publicKeyRawBase64UrlFromPem(
@@ -6155,6 +6302,11 @@ var TunnelProxy = class _TunnelProxy {
6155
6302
  return;
6156
6303
  }
6157
6304
  if (frame.type === "res" && frame.ok === true && frame.payload?.type === "hello-ok") {
6305
+ this.storeIssuedDeviceToken({
6306
+ fallbackRole: "operator",
6307
+ fallbackScopes: ["operator.admin"],
6308
+ authInfo: frame.payload?.auth
6309
+ });
6158
6310
  this.gatewayWsReady = true;
6159
6311
  this.gatewayWsConnecting = false;
6160
6312
  this.opts.logger.info(
@@ -6178,14 +6330,16 @@ var TunnelProxy = class _TunnelProxy {
6178
6330
  ws.on("close", (code, reason) => {
6179
6331
  const wasReady = this.gatewayWsReady;
6180
6332
  const pendingCount = this.gatewayWsPending.length;
6333
+ const reasonText = reason.toString();
6181
6334
  this.opts.logger.info(
6182
- `TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reason.toString()}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsConnections.size})`
6335
+ `TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reasonText}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsConnections.size})`
6183
6336
  );
6184
6337
  if (this.gatewayWs === ws) {
6185
6338
  this.gatewayWs = null;
6186
6339
  this.gatewayWsReady = false;
6187
6340
  }
6188
6341
  this.gatewayWsConnecting = false;
6342
+ this.maybeClearStoredDeviceTokenOnMismatch(code, reasonText);
6189
6343
  });
6190
6344
  ws.on("error", (err) => {
6191
6345
  this.opts.logger.warn(
@@ -6549,6 +6703,7 @@ function createTunnelService(opts) {
6549
6703
  logger
6550
6704
  });
6551
6705
  proxy = new TunnelProxy({
6706
+ stateDir: baseStateDir,
6552
6707
  gatewayBaseUrl: opts.gatewayBaseUrl,
6553
6708
  gatewayAuthMode: opts.gatewayAuthMode,
6554
6709
  gatewayToken: opts.gatewayToken,