@implicit-ai/relay 0.0.4 → 0.0.6

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.
Files changed (2) hide show
  1. package/dist/cli.js +127 -28
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -997,10 +997,13 @@ function truncateString(value, maxChars) {
997
997
 
998
998
  // src/lib/ably-transport.ts
999
999
  import Ably from "ably";
1000
- import pino from "pino";
1000
+ import pino2 from "pino";
1001
1001
 
1002
1002
  // src/lib/auth-session.ts
1003
+ import { createHash } from "crypto";
1003
1004
  import { createClient, isAuthRetryableFetchError } from "@supabase/supabase-js";
1005
+ import pino from "pino";
1006
+ var log = pino({ name: "auth-session" });
1004
1007
  var SUPABASE_CONFIGS = {
1005
1008
  production: {
1006
1009
  url: "https://fqvwludchxxqfpvjzktf.supabase.co",
@@ -1012,6 +1015,10 @@ var SUPABASE_CONFIGS = {
1012
1015
  }
1013
1016
  };
1014
1017
  var REFRESH_SAFETY_MARGIN_MS = 6e4;
1018
+ function tokenFingerprint(token) {
1019
+ if (!token) return "none";
1020
+ return createHash("sha256").update(token).digest("hex").slice(0, 12);
1021
+ }
1015
1022
  var NeedsRePairingError = class extends Error {
1016
1023
  constructor() {
1017
1024
  super("Your session has expired. Run `implicit pair` to re-authenticate.");
@@ -1024,16 +1031,50 @@ var TransientAuthError = class extends Error {
1024
1031
  this.name = "TransientAuthError";
1025
1032
  }
1026
1033
  };
1034
+ var inFlightRefresh = null;
1027
1035
  async function getFreshAccessToken() {
1036
+ if (inFlightRefresh) {
1037
+ log.debug("getFreshAccessToken: joining in-flight refresh");
1038
+ return inFlightRefresh;
1039
+ }
1040
+ inFlightRefresh = doGetFreshAccessToken().finally(() => {
1041
+ inFlightRefresh = null;
1042
+ });
1043
+ return inFlightRefresh;
1044
+ }
1045
+ async function doGetFreshAccessToken() {
1028
1046
  const auth = loadAuth();
1029
1047
  if (!auth || !auth.refreshToken) {
1048
+ log.warn(
1049
+ { hasAuth: !!auth, hasRefreshToken: !!auth?.refreshToken },
1050
+ "getFreshAccessToken: no stored auth or missing refresh token \u2014 re-pair required"
1051
+ );
1030
1052
  throw new NeedsRePairingError();
1031
1053
  }
1032
- if (Date.now() < auth.expiresAt - REFRESH_SAFETY_MARGIN_MS) {
1054
+ const now = Date.now();
1055
+ const env = auth.env ?? "production";
1056
+ const rtFp = tokenFingerprint(auth.refreshToken);
1057
+ const expiresInMs = auth.expiresAt - now;
1058
+ const baseCtx = {
1059
+ env,
1060
+ userId: auth.userId,
1061
+ refreshTokenFp: rtFp,
1062
+ accessTokenExpiresAt: new Date(auth.expiresAt).toISOString(),
1063
+ accessTokenExpiresInMs: expiresInMs
1064
+ };
1065
+ if (now < auth.expiresAt - REFRESH_SAFETY_MARGIN_MS) {
1066
+ log.debug(baseCtx, "getFreshAccessToken: returning cached access token");
1033
1067
  return auth.accessToken;
1034
1068
  }
1035
- const { url, anonKey } = SUPABASE_CONFIGS[auth.env ?? "production"];
1036
- const supabase = createClient(url, anonKey);
1069
+ log.info(baseCtx, "getFreshAccessToken: token within safety margin \u2014 calling supabase.refreshSession");
1070
+ const { url, anonKey } = SUPABASE_CONFIGS[env];
1071
+ const supabase = createClient(url, anonKey, {
1072
+ auth: {
1073
+ autoRefreshToken: false,
1074
+ persistSession: false,
1075
+ detectSessionInUrl: false
1076
+ }
1077
+ });
1037
1078
  let result;
1038
1079
  try {
1039
1080
  result = await supabase.auth.refreshSession({
@@ -1041,31 +1082,72 @@ async function getFreshAccessToken() {
1041
1082
  });
1042
1083
  } catch (err) {
1043
1084
  if (isAuthRetryableFetchError(err) || isNetworkLikeError(err)) {
1085
+ log.warn(
1086
+ { ...baseCtx, errMessage: err instanceof Error ? err.message : String(err) },
1087
+ "refreshSession threw transient error \u2014 preserving auth"
1088
+ );
1044
1089
  throw new TransientAuthError(
1045
1090
  `Network error refreshing session: ${err instanceof Error ? err.message : String(err)}`
1046
1091
  );
1047
1092
  }
1093
+ log.error(
1094
+ {
1095
+ ...baseCtx,
1096
+ errName: err instanceof Error ? err.name : "?",
1097
+ errMessage: err instanceof Error ? err.message : String(err),
1098
+ errStatus: err?.status ?? "?",
1099
+ errCode: err?.code ?? "?"
1100
+ },
1101
+ "refreshSession threw non-retryable error \u2014 re-throwing (will NOT clear auth)"
1102
+ );
1048
1103
  throw err;
1049
1104
  }
1050
1105
  const { data, error } = result;
1051
1106
  if (error) {
1052
1107
  if (isAuthRetryableFetchError(error) || isNetworkLikeError(error)) {
1108
+ log.warn(
1109
+ { ...baseCtx, errMessage: error.message },
1110
+ "refreshSession returned transient error \u2014 preserving auth"
1111
+ );
1053
1112
  throw new TransientAuthError(`Network error refreshing session: ${error.message}`);
1054
1113
  }
1114
+ log.error(
1115
+ {
1116
+ ...baseCtx,
1117
+ errStatus: error.status ?? "?",
1118
+ errCode: error.code ?? "?",
1119
+ errName: error.name ?? "?",
1120
+ errMessage: error.message ?? "?"
1121
+ },
1122
+ "refreshSession rejected refresh_token \u2014 clearing auth, re-pair required"
1123
+ );
1055
1124
  clearAuth();
1056
1125
  throw new NeedsRePairingError();
1057
1126
  }
1058
1127
  if (!data.session) {
1128
+ log.error(baseCtx, "refreshSession returned no session \u2014 clearing auth, re-pair required");
1059
1129
  clearAuth();
1060
1130
  throw new NeedsRePairingError();
1061
1131
  }
1132
+ const newExpiresAt = (data.session.expires_at ?? Math.floor(Date.now() / 1e3) + 3600) * 1e3;
1062
1133
  const newAuth = {
1063
1134
  ...auth,
1064
1135
  accessToken: data.session.access_token,
1065
1136
  refreshToken: data.session.refresh_token,
1066
- expiresAt: (data.session.expires_at ?? Math.floor(Date.now() / 1e3) + 3600) * 1e3
1137
+ expiresAt: newExpiresAt
1067
1138
  };
1068
1139
  saveAuth(newAuth);
1140
+ log.info(
1141
+ {
1142
+ env,
1143
+ userId: auth.userId,
1144
+ oldRefreshTokenFp: rtFp,
1145
+ newRefreshTokenFp: tokenFingerprint(data.session.refresh_token),
1146
+ rotated: rtFp !== tokenFingerprint(data.session.refresh_token),
1147
+ newAccessTokenExpiresAt: new Date(newExpiresAt).toISOString()
1148
+ },
1149
+ "refreshSession succeeded \u2014 auth saved"
1150
+ );
1069
1151
  return data.session.access_token;
1070
1152
  }
1071
1153
  function isNetworkLikeError(err) {
@@ -1108,7 +1190,7 @@ async function fetchAblyToken(opts) {
1108
1190
  }
1109
1191
 
1110
1192
  // src/lib/ably-transport.ts
1111
- var log = pino({ name: "ably-transport" });
1193
+ var log2 = pino2({ name: "ably-transport" });
1112
1194
  var AblyTransport = class {
1113
1195
  railwayUrl;
1114
1196
  userId;
@@ -1141,13 +1223,30 @@ var AblyTransport = class {
1141
1223
  this.realtime = null;
1142
1224
  this.channel = null;
1143
1225
  }
1226
+ let authCallbackInvocations = 0;
1144
1227
  this.realtime = new Ably.Realtime({
1145
1228
  authCallback: async (_tokenParams, callback) => {
1229
+ const invocation = ++authCallbackInvocations;
1230
+ log2.info({ invocation, userId: this.userId }, "Ably authCallback fired");
1146
1231
  try {
1147
1232
  const tokenDetails = await fetchAblyToken({ railwayUrl, getAccessToken: this.getAccessToken });
1233
+ log2.info(
1234
+ { invocation, ablyTokenExpires: tokenDetails.expires ? new Date(tokenDetails.expires).toISOString() : "?" },
1235
+ "Ably authCallback succeeded"
1236
+ );
1148
1237
  callback(null, tokenDetails);
1149
1238
  } catch (err) {
1150
- if (err instanceof NeedsRePairingError) {
1239
+ const isRepair = err instanceof NeedsRePairingError;
1240
+ log2.error(
1241
+ {
1242
+ invocation,
1243
+ isRepair,
1244
+ errName: err instanceof Error ? err.name : "?",
1245
+ errMessage: err instanceof Error ? err.message : String(err)
1246
+ },
1247
+ "Ably authCallback failed"
1248
+ );
1249
+ if (isRepair) {
1151
1250
  for (const h of this.errorHandlers) h(err.message);
1152
1251
  }
1153
1252
  callback(err instanceof Error ? err.message : String(err), null);
@@ -1157,36 +1256,36 @@ var AblyTransport = class {
1157
1256
  if (!this.connectionListenersWired) {
1158
1257
  this.connectionListenersWired = true;
1159
1258
  this.realtime.connection.on("connected", () => {
1160
- log.info({ connectionId: this.realtime?.connection.id, connectionKey: this.realtime?.connection.key }, "connection state: connected");
1259
+ log2.info({ connectionId: this.realtime?.connection.id, connectionKey: this.realtime?.connection.key }, "connection state: connected");
1161
1260
  this._isConnected = true;
1162
1261
  if (this.firstConnectDone) {
1163
- log.info({ handlers: this.reconnectHandlers.length }, "reconnected \u2014 firing handlers");
1262
+ log2.info({ handlers: this.reconnectHandlers.length }, "reconnected \u2014 firing handlers");
1164
1263
  for (const h of this.reconnectHandlers) h();
1165
1264
  }
1166
1265
  this.firstConnectDone = true;
1167
1266
  for (const h of this.connectionStateHandlers) h("connected");
1168
1267
  });
1169
1268
  this.realtime.connection.on("disconnected", (stateChange) => {
1170
- log.warn({ reason: stateChange?.reason?.message }, "connection state: disconnected");
1269
+ log2.warn({ reason: stateChange?.reason?.message }, "connection state: disconnected");
1171
1270
  this._isConnected = false;
1172
1271
  for (const h of this.connectionStateHandlers) h("disconnected");
1173
1272
  });
1174
1273
  this.realtime.connection.on("failed", (stateChange) => {
1175
1274
  const msg = stateChange?.reason?.message ?? "Ably connection failed";
1176
- log.error({ reason: msg }, "connection state: failed");
1275
+ log2.error({ reason: msg }, "connection state: failed");
1177
1276
  this._isConnected = false;
1178
1277
  for (const h of this.errorHandlers) h(msg);
1179
1278
  for (const h of this.connectionStateHandlers) h("failed");
1180
1279
  });
1181
1280
  this.realtime.connection.on("suspended", (stateChange) => {
1182
1281
  const msg = stateChange?.reason?.message ?? "Ably connection suspended";
1183
- log.warn({ reason: msg }, "connection state: suspended");
1282
+ log2.warn({ reason: msg }, "connection state: suspended");
1184
1283
  this._isConnected = false;
1185
1284
  for (const h of this.errorHandlers) h(msg);
1186
1285
  for (const h of this.connectionStateHandlers) h("suspended");
1187
1286
  });
1188
1287
  this.realtime.connection.on("closed", () => {
1189
- log.info("connection state: closed");
1288
+ log2.info("connection state: closed");
1190
1289
  this._isConnected = false;
1191
1290
  for (const h of this.connectionStateHandlers) h("closed");
1192
1291
  });
@@ -1196,7 +1295,7 @@ var AblyTransport = class {
1196
1295
  this.channelListenersWired = true;
1197
1296
  const ch = this.channel;
1198
1297
  ch.on("attached", (stateChange) => {
1199
- log.info(
1298
+ log2.info(
1200
1299
  {
1201
1300
  channel: this.channelName,
1202
1301
  resumed: stateChange?.resumed,
@@ -1206,25 +1305,25 @@ var AblyTransport = class {
1206
1305
  );
1207
1306
  });
1208
1307
  ch.on("failed", (stateChange) => {
1209
- log.error(
1308
+ log2.error(
1210
1309
  { channel: this.channelName, reason: stateChange?.reason?.message, code: stateChange?.reason?.code },
1211
1310
  "channel state: failed"
1212
1311
  );
1213
1312
  });
1214
1313
  ch.on("detached", (stateChange) => {
1215
- log.warn(
1314
+ log2.warn(
1216
1315
  { channel: this.channelName, reason: stateChange?.reason?.message, code: stateChange?.reason?.code },
1217
1316
  "channel state: detached"
1218
1317
  );
1219
1318
  });
1220
1319
  ch.on("suspended", (stateChange) => {
1221
- log.warn(
1320
+ log2.warn(
1222
1321
  { channel: this.channelName, reason: stateChange?.reason?.message, code: stateChange?.reason?.code },
1223
1322
  "channel state: suspended"
1224
1323
  );
1225
1324
  });
1226
1325
  ch.on("update", (stateChange) => {
1227
- log.info(
1326
+ log2.info(
1228
1327
  {
1229
1328
  channel: this.channelName,
1230
1329
  current: stateChange?.current,
@@ -1235,13 +1334,13 @@ var AblyTransport = class {
1235
1334
  );
1236
1335
  });
1237
1336
  }
1238
- log.info({ channel: this.channelName }, "attaching channel");
1337
+ log2.info({ channel: this.channelName }, "attaching channel");
1239
1338
  try {
1240
1339
  await this.channel.attach();
1241
- log.info({ channel: this.channelName, state: this.channel.state }, "attach() resolved");
1340
+ log2.info({ channel: this.channelName, state: this.channel.state }, "attach() resolved");
1242
1341
  } catch (err) {
1243
1342
  const message = err instanceof Error ? err.message : String(err);
1244
- log.error({ channel: this.channelName, err: message }, "attach() rejected");
1343
+ log2.error({ channel: this.channelName, err: message }, "attach() rejected");
1245
1344
  throw err;
1246
1345
  }
1247
1346
  for (const { event, handler } of this.pendingSubscriptions) {
@@ -1302,13 +1401,13 @@ var AblyTransport = class {
1302
1401
  // --- Presence ---
1303
1402
  async presenceEnter(data) {
1304
1403
  if (!this.channel) throw new Error("AblyTransport: not connected \u2014 call connect() first");
1305
- log.info({ data, channelState: this.channel.state }, "entering presence");
1404
+ log2.info({ data, channelState: this.channel.state }, "entering presence");
1306
1405
  try {
1307
1406
  await this.channel.presence.enter(data);
1308
- log.info({ channel: this.channelName }, "presence.enter() resolved");
1407
+ log2.info({ channel: this.channelName }, "presence.enter() resolved");
1309
1408
  } catch (err) {
1310
1409
  const message = err instanceof Error ? err.message : String(err);
1311
- log.error({ channel: this.channelName, err: message }, "presence.enter() rejected");
1410
+ log2.error({ channel: this.channelName, err: message }, "presence.enter() rejected");
1312
1411
  throw err;
1313
1412
  }
1314
1413
  }
@@ -1333,7 +1432,7 @@ var AblyTransport = class {
1333
1432
  }
1334
1433
  this.channel.presence.subscribe("enter", (msg) => {
1335
1434
  const clientId = msg.clientId ?? "unknown";
1336
- log.info({ clientId }, "presence enter");
1435
+ log2.info({ clientId }, "presence enter");
1337
1436
  handler({ clientId, data: msg.data ?? {} });
1338
1437
  });
1339
1438
  }
@@ -1344,21 +1443,21 @@ var AblyTransport = class {
1344
1443
  }
1345
1444
  this.channel.presence.subscribe("leave", (msg) => {
1346
1445
  const clientId = msg.clientId ?? "unknown";
1347
- log.info({ clientId }, "presence leave");
1446
+ log2.info({ clientId }, "presence leave");
1348
1447
  handler({ clientId, data: msg.data ?? {} });
1349
1448
  });
1350
1449
  }
1351
1450
  // --- History ---
1352
1451
  async fetchHistory(limit) {
1353
1452
  if (!this.channel) throw new Error("AblyTransport: not connected");
1354
- log.info({ limit }, "fetching channel history");
1453
+ log2.info({ limit }, "fetching channel history");
1355
1454
  const result = await this.channel.history({ limit, direction: "backwards" });
1356
1455
  const messages = (result.items ?? []).map((msg) => ({
1357
1456
  event: msg.name ?? "",
1358
1457
  data: msg.data ?? {},
1359
1458
  timestamp: msg.timestamp ?? 0
1360
1459
  }));
1361
- log.info({ count: messages.length }, "history fetched");
1460
+ log2.info({ count: messages.length }, "history fetched");
1362
1461
  return messages;
1363
1462
  }
1364
1463
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@implicit-ai/relay",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Control Claude Code from your phone with Implicit",
5
5
  "type": "module",
6
6
  "bin": {