@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.
- package/dist/cli.js +127 -28
- 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
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
const
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1337
|
+
log2.info({ channel: this.channelName }, "attaching channel");
|
|
1239
1338
|
try {
|
|
1240
1339
|
await this.channel.attach();
|
|
1241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1404
|
+
log2.info({ data, channelState: this.channel.state }, "entering presence");
|
|
1306
1405
|
try {
|
|
1307
1406
|
await this.channel.presence.enter(data);
|
|
1308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1460
|
+
log2.info({ count: messages.length }, "history fetched");
|
|
1362
1461
|
return messages;
|
|
1363
1462
|
}
|
|
1364
1463
|
};
|