@remixhq/core 0.1.32 → 0.1.34
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/api.d.ts +2 -1
- package/dist/api.js +1 -1
- package/dist/auth.d.ts +40 -3
- package/dist/auth.js +11 -1
- package/dist/{chunk-DOMO3LNV.js → chunk-KURN7YZ5.js} +1 -0
- package/dist/{chunk-MAM4CGDF.js → chunk-OMJ4JDME.js} +151 -15
- package/dist/collab.js +166 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -2
- package/dist/tokenProvider-B9so5Pm3.d.ts +123 -0
- package/package.json +1 -1
- package/dist/tokenProvider-BWTusyj4.d.ts +0 -63
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CoreConfig } from './config.js';
|
|
2
|
-
import { T as TokenProvider } from './tokenProvider-
|
|
2
|
+
import { T as TokenProvider } from './tokenProvider-B9so5Pm3.js';
|
|
3
3
|
import { T as TurnUsage } from './contracts-WCYiMqwB.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
@@ -790,6 +790,7 @@ type ApiClient = {
|
|
|
790
790
|
}): Promise<Json>;
|
|
791
791
|
getAppContext(appId: string): Promise<Json>;
|
|
792
792
|
getAppOverview(appId: string): Promise<Json>;
|
|
793
|
+
getAppMergePreview(appId: string): Promise<Json>;
|
|
793
794
|
listAppTimeline(appId: string, params?: {
|
|
794
795
|
limit?: number;
|
|
795
796
|
cursor?: string;
|
package/dist/api.js
CHANGED
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SessionStore, a as StoredSession } from './tokenProvider-
|
|
2
|
-
export { R as RefreshStoredSession,
|
|
1
|
+
import { W as WithRefreshLock, S as SessionStore, a as StoredSession } from './tokenProvider-B9so5Pm3.js';
|
|
2
|
+
export { R as RefreshLockOptions, e as RefreshStoredSession, f as ResolvedAuthToken, T as TokenProvider, c as createRefreshLock, b as createStoredSessionTokenProvider, s as shouldRefreshSoon, d as storedSessionSchema } from './tokenProvider-B9so5Pm3.js';
|
|
3
3
|
import { CoreConfig } from './config.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
@@ -8,6 +8,43 @@ declare function createLocalSessionStore(params?: {
|
|
|
8
8
|
account?: string;
|
|
9
9
|
filePath?: string;
|
|
10
10
|
}): SessionStore;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the on-disk session file path that `createLocalSessionStore`
|
|
13
|
+
* uses by default. Exposed so callers can wire the matching refresh
|
|
14
|
+
* lock without duplicating the xdg lookup.
|
|
15
|
+
*/
|
|
16
|
+
declare function defaultSessionFilePath(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Build a `WithRefreshLock` bound to the session file's `.lock`
|
|
19
|
+
* sibling. Every process that touches a given session file should use
|
|
20
|
+
* the same lock so concurrent refreshes serialize correctly across
|
|
21
|
+
* the CLI, the desktop helper subprocess, and the MCP plugin host.
|
|
22
|
+
*/
|
|
23
|
+
declare function createDefaultRefreshLock(filePath?: string): WithRefreshLock;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classification helpers for refresh-token failures.
|
|
27
|
+
*
|
|
28
|
+
* The token provider needs to react very differently depending on
|
|
29
|
+
* WHY a refresh failed:
|
|
30
|
+
*
|
|
31
|
+
* - `isNetworkError`: transient — DNS / TCP / TLS / undici fetch
|
|
32
|
+
* dropped mid-flight. Keep the existing session (it may already
|
|
33
|
+
* be unusable but the *refresh token* is most likely still alive
|
|
34
|
+
* server-side, especially within Supabase's reuse interval). The
|
|
35
|
+
* next attempt will retry.
|
|
36
|
+
*
|
|
37
|
+
* - `isInvalidGrantError`: terminal — Supabase explicitly said the
|
|
38
|
+
* refresh token is dead (revoked, reused outside the grace
|
|
39
|
+
* window, user signed out elsewhere). No amount of retrying will
|
|
40
|
+
* fix it; the user must re-authenticate.
|
|
41
|
+
*
|
|
42
|
+
* Anything else (5xx, unexpected) is treated like a network error by
|
|
43
|
+
* default — better to keep the session and retry than to log the
|
|
44
|
+
* user out on a transient backend hiccup.
|
|
45
|
+
*/
|
|
46
|
+
declare function isNetworkError(err: unknown): boolean;
|
|
47
|
+
declare function isInvalidGrantError(err: unknown): boolean;
|
|
11
48
|
|
|
12
49
|
type SupabaseAuthHelpers = {
|
|
13
50
|
startGoogleLogin(params: {
|
|
@@ -24,4 +61,4 @@ type SupabaseAuthHelpers = {
|
|
|
24
61
|
};
|
|
25
62
|
declare function createSupabaseAuthHelpers(config: CoreConfig): SupabaseAuthHelpers;
|
|
26
63
|
|
|
27
|
-
export { SessionStore, StoredSession, type SupabaseAuthHelpers, createLocalSessionStore, createSupabaseAuthHelpers };
|
|
64
|
+
export { SessionStore, StoredSession, type SupabaseAuthHelpers, WithRefreshLock, createDefaultRefreshLock, createLocalSessionStore, createSupabaseAuthHelpers, defaultSessionFilePath, isInvalidGrantError, isNetworkError };
|
package/dist/auth.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createDefaultRefreshLock,
|
|
2
3
|
createLocalSessionStore,
|
|
4
|
+
createRefreshLock,
|
|
3
5
|
createStoredSessionTokenProvider,
|
|
4
6
|
createSupabaseAuthHelpers,
|
|
7
|
+
defaultSessionFilePath,
|
|
8
|
+
isInvalidGrantError,
|
|
9
|
+
isNetworkError,
|
|
5
10
|
shouldRefreshSoon,
|
|
6
11
|
storedSessionSchema
|
|
7
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-OMJ4JDME.js";
|
|
8
13
|
import "./chunk-7XJGOKEO.js";
|
|
9
14
|
export {
|
|
15
|
+
createDefaultRefreshLock,
|
|
10
16
|
createLocalSessionStore,
|
|
17
|
+
createRefreshLock,
|
|
11
18
|
createStoredSessionTokenProvider,
|
|
12
19
|
createSupabaseAuthHelpers,
|
|
20
|
+
defaultSessionFilePath,
|
|
21
|
+
isInvalidGrantError,
|
|
22
|
+
isNetworkError,
|
|
13
23
|
shouldRefreshSoon,
|
|
14
24
|
storedSessionSchema
|
|
15
25
|
};
|
|
@@ -366,6 +366,7 @@ function createApiClient(config, opts) {
|
|
|
366
366
|
openApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/open`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
|
|
367
367
|
getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
|
|
368
368
|
getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
|
|
369
|
+
getAppMergePreview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/merge-preview`, { method: "GET" }),
|
|
369
370
|
listAppTimeline: (appId, params) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline${buildAppTimelineQuery(params)}`, { method: "GET" }),
|
|
370
371
|
getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
|
|
371
372
|
listAppEditQueue: (appId, params) => request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${buildAppEditQueueQuery(params)}`, { method: "GET" }),
|
|
@@ -16,14 +16,82 @@ var storedSessionSchema = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
// src/auth/localSessionStore.ts
|
|
19
|
+
import fs2 from "fs/promises";
|
|
20
|
+
import os2 from "os";
|
|
21
|
+
import path2 from "path";
|
|
22
|
+
|
|
23
|
+
// src/auth/refreshLock.ts
|
|
19
24
|
import fs from "fs/promises";
|
|
20
25
|
import os from "os";
|
|
21
26
|
import path from "path";
|
|
27
|
+
var DEFAULT_STALE_MS = 3e4;
|
|
28
|
+
var DEFAULT_MAX_WAIT_MS = 15e3;
|
|
29
|
+
var DEFAULT_POLL_MS = 50;
|
|
30
|
+
function createRefreshLock(opts) {
|
|
31
|
+
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
32
|
+
const maxWaitMs = opts.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
33
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
34
|
+
let inProcess = null;
|
|
35
|
+
return async function withRefreshLock(fn) {
|
|
36
|
+
if (inProcess) {
|
|
37
|
+
await inProcess.catch(() => {
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const job = (async () => {
|
|
41
|
+
const start = Date.now();
|
|
42
|
+
while (true) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.mkdir(path.dirname(opts.lockPath), { recursive: true });
|
|
45
|
+
const fh = await fs.open(opts.lockPath, "wx");
|
|
46
|
+
try {
|
|
47
|
+
await fh.writeFile(`${process.pid}
|
|
48
|
+
${Date.now()}
|
|
49
|
+
${os.hostname()}
|
|
50
|
+
`);
|
|
51
|
+
} finally {
|
|
52
|
+
await fh.close().catch(() => {
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return await fn();
|
|
57
|
+
} finally {
|
|
58
|
+
await fs.unlink(opts.lockPath).catch(() => {
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const code = err?.code;
|
|
63
|
+
if (code !== "EEXIST") throw err;
|
|
64
|
+
const stat = await fs.stat(opts.lockPath).catch(() => null);
|
|
65
|
+
if (stat && Date.now() - stat.mtimeMs > staleMs) {
|
|
66
|
+
await fs.unlink(opts.lockPath).catch(() => {
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (Date.now() - start > maxWaitMs) {
|
|
71
|
+
return await fn();
|
|
72
|
+
}
|
|
73
|
+
await sleep(pollMs);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
inProcess = job;
|
|
78
|
+
try {
|
|
79
|
+
return await job;
|
|
80
|
+
} finally {
|
|
81
|
+
if (inProcess === job) inProcess = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function sleep(ms) {
|
|
86
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/auth/localSessionStore.ts
|
|
22
90
|
var loadKeytarModule = () => new Function("return import('keytar')")();
|
|
23
91
|
function xdgConfigHome() {
|
|
24
92
|
const value = process.env.XDG_CONFIG_HOME;
|
|
25
93
|
if (typeof value === "string" && value.trim()) return value;
|
|
26
|
-
return
|
|
94
|
+
return path2.join(os2.homedir(), ".config");
|
|
27
95
|
}
|
|
28
96
|
async function maybeLoadKeytar() {
|
|
29
97
|
try {
|
|
@@ -41,22 +109,22 @@ async function maybeLoadKeytar() {
|
|
|
41
109
|
return null;
|
|
42
110
|
}
|
|
43
111
|
async function ensurePathPermissions(filePath) {
|
|
44
|
-
const dir =
|
|
45
|
-
await
|
|
112
|
+
const dir = path2.dirname(filePath);
|
|
113
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
46
114
|
try {
|
|
47
|
-
await
|
|
115
|
+
await fs2.chmod(dir, 448);
|
|
48
116
|
} catch {
|
|
49
117
|
}
|
|
50
118
|
try {
|
|
51
|
-
await
|
|
119
|
+
await fs2.chmod(filePath, 384);
|
|
52
120
|
} catch {
|
|
53
121
|
}
|
|
54
122
|
}
|
|
55
123
|
async function writeJsonAtomic(filePath, value) {
|
|
56
|
-
await
|
|
124
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
57
125
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
58
|
-
await
|
|
59
|
-
await
|
|
126
|
+
await fs2.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
127
|
+
await fs2.rename(tmpPath, filePath);
|
|
60
128
|
}
|
|
61
129
|
async function writeSessionFileFallback(filePath, session) {
|
|
62
130
|
await writeJsonAtomic(filePath, session);
|
|
@@ -65,7 +133,7 @@ async function writeSessionFileFallback(filePath, session) {
|
|
|
65
133
|
function createLocalSessionStore(params) {
|
|
66
134
|
const service = params?.service?.trim() || "remix-cli";
|
|
67
135
|
const account = params?.account?.trim() || "default";
|
|
68
|
-
const filePath = params?.filePath?.trim() ||
|
|
136
|
+
const filePath = params?.filePath?.trim() || path2.join(xdgConfigHome(), "remix", "session.json");
|
|
69
137
|
async function readKeytar() {
|
|
70
138
|
const keytar = await maybeLoadKeytar();
|
|
71
139
|
if (!keytar) return null;
|
|
@@ -79,7 +147,7 @@ function createLocalSessionStore(params) {
|
|
|
79
147
|
}
|
|
80
148
|
}
|
|
81
149
|
async function readFile() {
|
|
82
|
-
const raw = await
|
|
150
|
+
const raw = await fs2.readFile(filePath, "utf8").catch(() => null);
|
|
83
151
|
if (!raw) return null;
|
|
84
152
|
try {
|
|
85
153
|
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
@@ -116,6 +184,42 @@ function createLocalSessionStore(params) {
|
|
|
116
184
|
}
|
|
117
185
|
};
|
|
118
186
|
}
|
|
187
|
+
function defaultSessionFilePath() {
|
|
188
|
+
return path2.join(xdgConfigHome(), "remix", "session.json");
|
|
189
|
+
}
|
|
190
|
+
function createDefaultRefreshLock(filePath) {
|
|
191
|
+
const target = filePath?.trim() || defaultSessionFilePath();
|
|
192
|
+
return createRefreshLock({ lockPath: `${target}.lock` });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/auth/refreshErrors.ts
|
|
196
|
+
var NETWORK_CODE_PATTERN = /^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|EHOSTUNREACH|EADDRNOTAVAIL|UND_ERR_)/;
|
|
197
|
+
var NETWORK_MESSAGE_PATTERN = /fetch failed|failed to fetch|network request failed|socket hang up|network error|aborted/i;
|
|
198
|
+
function isNetworkError(err) {
|
|
199
|
+
if (!err || typeof err !== "object") return false;
|
|
200
|
+
const e = err;
|
|
201
|
+
const code = typeof e.code === "string" ? e.code : void 0;
|
|
202
|
+
const errno = typeof e.errno === "string" ? e.errno : void 0;
|
|
203
|
+
const msg = typeof e.message === "string" ? e.message : "";
|
|
204
|
+
if (code && NETWORK_CODE_PATTERN.test(code)) return true;
|
|
205
|
+
if (errno && NETWORK_CODE_PATTERN.test(errno)) return true;
|
|
206
|
+
if (e.name === "TypeError" && NETWORK_MESSAGE_PATTERN.test(msg)) return true;
|
|
207
|
+
if (e.name === "AbortError") return true;
|
|
208
|
+
if (NETWORK_MESSAGE_PATTERN.test(msg)) return true;
|
|
209
|
+
if (e.cause) return isNetworkError(e.cause);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
var INVALID_GRANT_MESSAGE_PATTERN = /invalid[_ ]grant|invalid refresh token|refresh[_ ]token[_ ]not[_ ]found|token has expired or (been )?revoked|already used|user from sub claim in jwt does not exist/i;
|
|
213
|
+
function isInvalidGrantError(err) {
|
|
214
|
+
if (!err || typeof err !== "object") return false;
|
|
215
|
+
const e = err;
|
|
216
|
+
const code = typeof e.code === "string" ? e.code : "";
|
|
217
|
+
const msg = typeof e.message === "string" ? e.message : "";
|
|
218
|
+
if (code === "invalid_grant" || code === "refresh_token_not_found") return true;
|
|
219
|
+
if (INVALID_GRANT_MESSAGE_PATTERN.test(msg)) return true;
|
|
220
|
+
if (e.cause) return isInvalidGrantError(e.cause);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
119
223
|
|
|
120
224
|
// src/auth/tokenProvider.ts
|
|
121
225
|
function shouldRefreshSoon(session, skewSeconds = 60) {
|
|
@@ -123,6 +227,7 @@ function shouldRefreshSoon(session, skewSeconds = 60) {
|
|
|
123
227
|
return session.expires_at <= nowSec + skewSeconds;
|
|
124
228
|
}
|
|
125
229
|
function createStoredSessionTokenProvider(params) {
|
|
230
|
+
const withLock = params.withRefreshLock ?? (async (fn) => fn());
|
|
126
231
|
return async (opts) => {
|
|
127
232
|
const forceRefresh = Boolean(opts?.forceRefresh);
|
|
128
233
|
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
@@ -137,11 +242,38 @@ function createStoredSessionTokenProvider(params) {
|
|
|
137
242
|
});
|
|
138
243
|
}
|
|
139
244
|
if (forceRefresh || shouldRefreshSoon(session)) {
|
|
245
|
+
const sessionAtEntry = session;
|
|
140
246
|
try {
|
|
141
|
-
session = await
|
|
142
|
-
|
|
247
|
+
session = await withLock(async () => {
|
|
248
|
+
const latest = await params.sessionStore.getSession();
|
|
249
|
+
const base = latest ?? sessionAtEntry;
|
|
250
|
+
if (!base) {
|
|
251
|
+
throw new RemixError("Not signed in.", {
|
|
252
|
+
exitCode: 2,
|
|
253
|
+
hint: "Run `remix login`, or set COMERGE_ACCESS_TOKEN for CI."
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const siblingRefreshed = latest != null && latest.access_token !== sessionAtEntry.access_token;
|
|
257
|
+
if (siblingRefreshed && !forceRefresh && !shouldRefreshSoon(base, 5)) {
|
|
258
|
+
return base;
|
|
259
|
+
}
|
|
260
|
+
const refreshed = await params.refreshStoredSession({
|
|
261
|
+
config: params.config,
|
|
262
|
+
session: base
|
|
263
|
+
});
|
|
264
|
+
await params.sessionStore.setSession(refreshed);
|
|
265
|
+
return refreshed;
|
|
266
|
+
});
|
|
143
267
|
} catch (err) {
|
|
144
|
-
|
|
268
|
+
if (isInvalidGrantError(err)) {
|
|
269
|
+
throw new RemixError("Session expired. Please sign in again.", {
|
|
270
|
+
exitCode: 2,
|
|
271
|
+
hint: "Run `remix login` to re-authenticate."
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (isNetworkError(err)) {
|
|
275
|
+
} else {
|
|
276
|
+
}
|
|
145
277
|
}
|
|
146
278
|
}
|
|
147
279
|
return { token: session.access_token, session, fromEnv: false };
|
|
@@ -217,8 +349,7 @@ function createSupabaseAuthHelpers(config) {
|
|
|
217
349
|
return toStoredSession(data.session);
|
|
218
350
|
},
|
|
219
351
|
async refreshWithStoredSession(params) {
|
|
220
|
-
const { data, error } = await supabase.auth.
|
|
221
|
-
access_token: params.session.access_token,
|
|
352
|
+
const { data, error } = await supabase.auth.refreshSession({
|
|
222
353
|
refresh_token: params.session.refresh_token
|
|
223
354
|
});
|
|
224
355
|
if (error) throw error;
|
|
@@ -230,7 +361,12 @@ function createSupabaseAuthHelpers(config) {
|
|
|
230
361
|
|
|
231
362
|
export {
|
|
232
363
|
storedSessionSchema,
|
|
364
|
+
createRefreshLock,
|
|
233
365
|
createLocalSessionStore,
|
|
366
|
+
defaultSessionFilePath,
|
|
367
|
+
createDefaultRefreshLock,
|
|
368
|
+
isNetworkError,
|
|
369
|
+
isInvalidGrantError,
|
|
234
370
|
shouldRefreshSoon,
|
|
235
371
|
createStoredSessionTokenProvider,
|
|
236
372
|
createSupabaseAuthHelpers
|
package/dist/collab.js
CHANGED
|
@@ -509,6 +509,13 @@ import path3 from "path";
|
|
|
509
509
|
function getBaselinePath(params) {
|
|
510
510
|
return path3.join(getBaselinesRoot(), `${buildLaneStateKey(params)}.json`);
|
|
511
511
|
}
|
|
512
|
+
function normalizeNullableString(value) {
|
|
513
|
+
const normalized = String(value ?? "").trim();
|
|
514
|
+
return normalized || null;
|
|
515
|
+
}
|
|
516
|
+
function isSameBaselineIdentity(left, right) {
|
|
517
|
+
return left.repoRoot === right.repoRoot && (left.repoFingerprint ?? null) === (right.repoFingerprint ?? null) && (left.laneId ?? null) === (right.laneId ?? null) && left.currentAppId === right.currentAppId && (left.branchName ?? null) === (right.branchName ?? null);
|
|
518
|
+
}
|
|
512
519
|
async function readLocalBaseline(params) {
|
|
513
520
|
try {
|
|
514
521
|
const raw = await fs2.readFile(getBaselinePath(params), "utf8");
|
|
@@ -558,6 +565,105 @@ async function writeLocalBaseline(baseline) {
|
|
|
558
565
|
await writeJsonAtomic(getBaselinePath(baseline), normalized);
|
|
559
566
|
return normalized;
|
|
560
567
|
}
|
|
568
|
+
async function readAllLocalBaselines() {
|
|
569
|
+
let entries;
|
|
570
|
+
try {
|
|
571
|
+
entries = await fs2.readdir(getBaselinesRoot());
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (error?.code === "ENOENT") return [];
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
const baselines = [];
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
if (!entry.endsWith(".json")) continue;
|
|
579
|
+
try {
|
|
580
|
+
const raw = await fs2.readFile(path3.join(getBaselinesRoot(), entry), "utf8");
|
|
581
|
+
const parsed = JSON.parse(raw);
|
|
582
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
583
|
+
if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
baselines.push({
|
|
587
|
+
schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
|
|
588
|
+
key: parsed.key,
|
|
589
|
+
repoRoot: parsed.repoRoot,
|
|
590
|
+
repoFingerprint: parsed.repoFingerprint ?? null,
|
|
591
|
+
laneId: parsed.laneId ?? null,
|
|
592
|
+
currentAppId: String(parsed.currentAppId ?? ""),
|
|
593
|
+
branchName: parsed.branchName ?? null,
|
|
594
|
+
lastSnapshotId: parsed.lastSnapshotId ?? null,
|
|
595
|
+
lastSnapshotHash: parsed.lastSnapshotHash ?? null,
|
|
596
|
+
lastServerRevisionId: parsed.lastServerRevisionId ?? null,
|
|
597
|
+
lastServerTreeHash: parsed.lastServerTreeHash ?? null,
|
|
598
|
+
lastServerHeadHash: parsed.lastServerHeadHash ?? null,
|
|
599
|
+
lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
|
|
600
|
+
updatedAt: String(parsed.updatedAt ?? "")
|
|
601
|
+
});
|
|
602
|
+
} catch {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return baselines;
|
|
607
|
+
}
|
|
608
|
+
async function migrateLocalBaselineForLaneChange(params) {
|
|
609
|
+
if ((params.fromLaneId ?? null) === (params.toLaneId ?? null)) {
|
|
610
|
+
return { status: "same_lane" };
|
|
611
|
+
}
|
|
612
|
+
const target = await readLocalBaseline({
|
|
613
|
+
repoRoot: params.repoRoot,
|
|
614
|
+
repoFingerprint: params.repoFingerprint,
|
|
615
|
+
laneId: params.toLaneId
|
|
616
|
+
});
|
|
617
|
+
if (target) {
|
|
618
|
+
return { status: "target_exists" };
|
|
619
|
+
}
|
|
620
|
+
const source = await readLocalBaseline({
|
|
621
|
+
repoRoot: params.repoRoot,
|
|
622
|
+
repoFingerprint: params.repoFingerprint,
|
|
623
|
+
laneId: params.fromLaneId
|
|
624
|
+
});
|
|
625
|
+
if (!source) {
|
|
626
|
+
return { status: "source_missing" };
|
|
627
|
+
}
|
|
628
|
+
const expectedSource = {
|
|
629
|
+
repoRoot: params.repoRoot,
|
|
630
|
+
repoFingerprint: params.repoFingerprint,
|
|
631
|
+
laneId: params.fromLaneId,
|
|
632
|
+
currentAppId: params.currentAppId,
|
|
633
|
+
branchName: params.branchName
|
|
634
|
+
};
|
|
635
|
+
const actualSource = {
|
|
636
|
+
repoRoot: source.repoRoot,
|
|
637
|
+
repoFingerprint: source.repoFingerprint,
|
|
638
|
+
laneId: source.laneId,
|
|
639
|
+
currentAppId: source.currentAppId,
|
|
640
|
+
branchName: source.branchName
|
|
641
|
+
};
|
|
642
|
+
if (!isSameBaselineIdentity(actualSource, expectedSource)) {
|
|
643
|
+
return { status: "source_missing" };
|
|
644
|
+
}
|
|
645
|
+
const baseline = await writeLocalBaseline({
|
|
646
|
+
...source,
|
|
647
|
+
laneId: params.toLaneId
|
|
648
|
+
});
|
|
649
|
+
return { status: "migrated", baseline };
|
|
650
|
+
}
|
|
651
|
+
async function findCompatibleLocalBaseline(params) {
|
|
652
|
+
const targetKey = buildLaneStateKey(params);
|
|
653
|
+
const branchName = normalizeNullableString(params.branchName);
|
|
654
|
+
const candidates = (await readAllLocalBaselines()).filter((baseline) => {
|
|
655
|
+
if (baseline.key === targetKey) return false;
|
|
656
|
+
if (baseline.repoRoot !== params.repoRoot) return false;
|
|
657
|
+
if ((baseline.repoFingerprint ?? null) !== (params.repoFingerprint ?? null)) return false;
|
|
658
|
+
if (baseline.currentAppId !== params.currentAppId) return false;
|
|
659
|
+
return true;
|
|
660
|
+
});
|
|
661
|
+
const branchMatches = candidates.filter((baseline) => normalizeNullableString(baseline.branchName) === branchName);
|
|
662
|
+
const compatible = branchMatches.length > 0 ? branchMatches : candidates.filter((baseline) => normalizeNullableString(baseline.branchName) === null);
|
|
663
|
+
if (compatible.length === 0) return { status: "none", candidateCount: 0 };
|
|
664
|
+
if (compatible.length > 1) return { status: "ambiguous", candidateCount: compatible.length };
|
|
665
|
+
return { status: "found", baseline: compatible[0] };
|
|
666
|
+
}
|
|
561
667
|
async function clearLocalBaseline(params) {
|
|
562
668
|
try {
|
|
563
669
|
await fs2.unlink(getBaselinePath(params));
|
|
@@ -1500,6 +1606,7 @@ function resolveLaneLookupProjectId(params) {
|
|
|
1500
1606
|
return params.explicitRootProjectId ?? (params.requireRemoteLane ? void 0 : localProjectId ?? params.fallbackProjectId ?? void 0);
|
|
1501
1607
|
}
|
|
1502
1608
|
async function persistResolvedLane(repoRoot, binding) {
|
|
1609
|
+
const previousBinding = await readCollabBinding(repoRoot);
|
|
1503
1610
|
await writeCollabBinding(repoRoot, {
|
|
1504
1611
|
projectId: binding.projectId,
|
|
1505
1612
|
currentAppId: binding.currentAppId,
|
|
@@ -1512,6 +1619,16 @@ async function persistResolvedLane(repoRoot, binding) {
|
|
|
1512
1619
|
branchName: binding.branchName,
|
|
1513
1620
|
bindingMode: binding.bindingMode
|
|
1514
1621
|
});
|
|
1622
|
+
if (previousBinding && (previousBinding.laneId ?? null) !== (binding.laneId ?? null)) {
|
|
1623
|
+
await migrateLocalBaselineForLaneChange({
|
|
1624
|
+
repoRoot,
|
|
1625
|
+
repoFingerprint: binding.repoFingerprint,
|
|
1626
|
+
currentAppId: binding.currentAppId,
|
|
1627
|
+
branchName: binding.branchName,
|
|
1628
|
+
fromLaneId: previousBinding.laneId,
|
|
1629
|
+
toLaneId: binding.laneId
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1515
1632
|
return readCollabBinding(repoRoot);
|
|
1516
1633
|
}
|
|
1517
1634
|
function buildAmbiguousResolution(params) {
|
|
@@ -1911,11 +2028,36 @@ async function collabDetectRepoState(params) {
|
|
|
1911
2028
|
return detected;
|
|
1912
2029
|
}
|
|
1913
2030
|
try {
|
|
1914
|
-
|
|
2031
|
+
let baseline = await readLocalBaseline({
|
|
1915
2032
|
repoFingerprint: binding.repoFingerprint,
|
|
1916
2033
|
laneId: binding.laneId,
|
|
1917
2034
|
repoRoot
|
|
1918
2035
|
});
|
|
2036
|
+
let ambiguousBaselineCandidateCount = null;
|
|
2037
|
+
if (!baseline) {
|
|
2038
|
+
const compatibleBaseline = await findCompatibleLocalBaseline({
|
|
2039
|
+
repoRoot,
|
|
2040
|
+
repoFingerprint: binding.repoFingerprint,
|
|
2041
|
+
laneId: binding.laneId,
|
|
2042
|
+
currentAppId: binding.currentAppId,
|
|
2043
|
+
branchName: binding.branchName
|
|
2044
|
+
});
|
|
2045
|
+
if (compatibleBaseline.status === "found") {
|
|
2046
|
+
baseline = await writeLocalBaseline({
|
|
2047
|
+
...compatibleBaseline.baseline,
|
|
2048
|
+
repoRoot,
|
|
2049
|
+
repoFingerprint: binding.repoFingerprint,
|
|
2050
|
+
laneId: binding.laneId,
|
|
2051
|
+
currentAppId: binding.currentAppId,
|
|
2052
|
+
branchName: binding.branchName
|
|
2053
|
+
});
|
|
2054
|
+
detected.warnings.push(
|
|
2055
|
+
`Recovered local Remix baseline after lane id changed from ${compatibleBaseline.baseline.laneId ?? "null"} to ${binding.laneId ?? "null"} for app ${binding.currentAppId} on branch ${binding.branchName ?? "(unknown)"}.`
|
|
2056
|
+
);
|
|
2057
|
+
} else if (compatibleBaseline.status === "ambiguous") {
|
|
2058
|
+
ambiguousBaselineCandidateCount = compatibleBaseline.candidateCount;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1919
2061
|
const hasFullBaseline = Boolean(baseline?.lastSnapshotHash && baseline?.lastServerHeadHash);
|
|
1920
2062
|
const metadataIdentityPromise = hasFullBaseline ? getAppDeltaCached(params.api, binding.currentAppId, {
|
|
1921
2063
|
baseHeadHash: baseline.lastServerHeadHash,
|
|
@@ -1988,7 +2130,7 @@ async function collabDetectRepoState(params) {
|
|
|
1988
2130
|
}
|
|
1989
2131
|
}
|
|
1990
2132
|
detected.repoState = "external_local_base_changed";
|
|
1991
|
-
detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
|
|
2133
|
+
detected.hint = ambiguousBaselineCandidateCount !== null ? `No local Remix revision baseline exists for this lane, and multiple compatible local baselines were found (${ambiguousBaselineCandidateCount}). Run \`remix collab status\` or sync this lane to seed an unambiguous baseline.` : "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
|
|
1992
2134
|
return detected;
|
|
1993
2135
|
}
|
|
1994
2136
|
const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
|
|
@@ -2506,6 +2648,7 @@ function buildWorkspaceMetadata(params) {
|
|
|
2506
2648
|
|
|
2507
2649
|
// src/application/collab/finalizeDecision.ts
|
|
2508
2650
|
var FINALIZE_AUTO_TERMINAL_THRESHOLD = 5;
|
|
2651
|
+
var FINALIZE_AUTO_TERMINAL_TOTAL_RETRY_THRESHOLD = 8;
|
|
2509
2652
|
function classifyFinalizeError(error) {
|
|
2510
2653
|
const tagged = error;
|
|
2511
2654
|
return {
|
|
@@ -2518,12 +2661,20 @@ function computeFailureEscalation(job, reason) {
|
|
|
2518
2661
|
const previousReason = job.metadata.consecutiveFailureReason;
|
|
2519
2662
|
const previousCountRaw = job.metadata.consecutiveFailures;
|
|
2520
2663
|
const previousCount = typeof previousCountRaw === "number" && Number.isFinite(previousCountRaw) && previousCountRaw > 0 ? Math.floor(previousCountRaw) : 0;
|
|
2664
|
+
const previousTotalRaw = job.metadata.totalFailures;
|
|
2665
|
+
const previousTotal = typeof previousTotalRaw === "number" && Number.isFinite(previousTotalRaw) && previousTotalRaw > 0 ? Math.floor(previousTotalRaw) : 0;
|
|
2521
2666
|
const sameReason = previousReason === reason;
|
|
2522
2667
|
const next = sameReason ? previousCount + 1 : 1;
|
|
2668
|
+
const totalFailures = previousTotal + 1;
|
|
2669
|
+
const consecutiveTripped = next >= FINALIZE_AUTO_TERMINAL_THRESHOLD;
|
|
2670
|
+
const totalTripped = totalFailures >= FINALIZE_AUTO_TERMINAL_TOTAL_RETRY_THRESHOLD;
|
|
2671
|
+
const escalationTrigger = consecutiveTripped ? "consecutive_same_reason" : totalTripped ? "total_retries" : "none";
|
|
2523
2672
|
return {
|
|
2524
2673
|
consecutiveFailures: next,
|
|
2525
2674
|
consecutiveFailureReason: reason,
|
|
2526
|
-
|
|
2675
|
+
totalFailures,
|
|
2676
|
+
shouldEscalateToTerminal: consecutiveTripped || totalTripped,
|
|
2677
|
+
escalationTrigger
|
|
2527
2678
|
};
|
|
2528
2679
|
}
|
|
2529
2680
|
function hasFinalizeBaselineDrifted(params) {
|
|
@@ -2703,15 +2854,25 @@ async function processClaimedPendingFinalizeJob(params) {
|
|
|
2703
2854
|
const classified = classifyFinalizeError(error);
|
|
2704
2855
|
const escalation = computeFailureEscalation(job, classified.reason);
|
|
2705
2856
|
const finalDisposition = classified.disposition === "terminal" || escalation.shouldEscalateToTerminal ? "terminal" : "retryable";
|
|
2857
|
+
const autoEscalated = finalDisposition === "terminal" && escalation.shouldEscalateToTerminal && classified.disposition !== "terminal";
|
|
2858
|
+
let escalationSuffix = "";
|
|
2859
|
+
if (autoEscalated) {
|
|
2860
|
+
if (escalation.escalationTrigger === "consecutive_same_reason") {
|
|
2861
|
+
escalationSuffix = ` (auto-escalated to terminal after ${escalation.consecutiveFailures} consecutive ${classified.reason} failures)`;
|
|
2862
|
+
} else if (escalation.escalationTrigger === "total_retries") {
|
|
2863
|
+
escalationSuffix = ` (auto-escalated to terminal after ${escalation.totalFailures} total retries with mixed failure reasons; latest=${classified.reason})`;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2706
2866
|
await updatePendingFinalizeJob(job.id, {
|
|
2707
2867
|
status: finalDisposition === "terminal" ? "failed" : "queued",
|
|
2708
|
-
error:
|
|
2868
|
+
error: `${classified.message}${escalationSuffix}`,
|
|
2709
2869
|
nextRetryAt: finalDisposition === "terminal" ? null : buildNextRetryAt(job.retryCount),
|
|
2710
2870
|
metadata: {
|
|
2711
2871
|
failureDisposition: finalDisposition,
|
|
2712
2872
|
failureReason: classified.reason,
|
|
2713
2873
|
consecutiveFailures: escalation.consecutiveFailures,
|
|
2714
|
-
consecutiveFailureReason: escalation.consecutiveFailureReason
|
|
2874
|
+
consecutiveFailureReason: escalation.consecutiveFailureReason,
|
|
2875
|
+
totalFailures: escalation.totalFailures
|
|
2715
2876
|
}
|
|
2716
2877
|
});
|
|
2717
2878
|
throw error;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { CliError, REMIX_ERROR_CODES, CliError as RemixError, RemixErrorCode } from './errors.js';
|
|
2
2
|
export { CoreConfig, ResolveConfigOptions, configSchema, resolveConfig } from './config.js';
|
|
3
|
-
export { S as SessionStore, a as StoredSession, c as createStoredSessionTokenProvider, s as shouldRefreshSoon,
|
|
4
|
-
export { createLocalSessionStore, createSupabaseAuthHelpers } from './auth.js';
|
|
3
|
+
export { S as SessionStore, a as StoredSession, W as WithRefreshLock, c as createRefreshLock, b as createStoredSessionTokenProvider, s as shouldRefreshSoon, d as storedSessionSchema } from './tokenProvider-B9so5Pm3.js';
|
|
4
|
+
export { createDefaultRefreshLock, createLocalSessionStore, createSupabaseAuthHelpers, defaultSessionFilePath, isInvalidGrantError, isNetworkError } from './auth.js';
|
|
5
5
|
export { AgentMemoryItem, AgentMemoryKind, AgentMemorySearchItem, AgentMemorySearchResponse, AgentMemorySummary, AgentMemoryTimelineResponse, ApiClient, AppContext, AppContextAccessPath, AppPreviewExpectedKind, AppPreviewProcessStatus, AppPreviewQuery, AppPreviewResponse, AppReconcileResponse, AppSandboxCommandRun, Bundle, BundlePlatform, BundleStatus, ChangeStepDiffResponse, DetectProjectRuntimeTargetsPayload, ImportProjectRuntimeEnvPayload, InitiateBundleRequest, InvitationRecord, MergeRequest, MergeRequestReview, MergeRequestStatus, ProjectRuntimeEnvMetadata, ProjectRuntimeEnvScope, ProjectRuntimeTargetMetadata, ProjectRuntimeTargetScope, ProjectTrigger, ProjectTriggerPayload, ProjectTriggerScope, ProjectTriggerStep, ProjectTriggerStepPayload, ReconcilePreflightResponse, ResolveProjectRuntimeEnvForLocalPullResponse, RunAppRuntimeTargetPayload, RunAppSandboxCommandPayload, RunAppTriggerEventPayload, RunAppTriggerPayload, RuntimeCommandKind, RuntimeTargetKind, SetProjectRuntimeEnvPayload, SetProjectRuntimeTargetPayload, SyncLocalResponse, SyncUpstreamResponse, TriggerEventMetadata, TriggerEventSource, TriggerIntegrationStatus, TriggerMode, TriggerRun, TriggerStepRun, TriggerStepType, UpdateProjectTriggerPayload, createApiClient } from './api.js';
|
|
6
6
|
import 'zod';
|
|
7
7
|
import './contracts-WCYiMqwB.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApiClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KURN7YZ5.js";
|
|
4
4
|
import {
|
|
5
|
+
createDefaultRefreshLock,
|
|
5
6
|
createLocalSessionStore,
|
|
7
|
+
createRefreshLock,
|
|
6
8
|
createStoredSessionTokenProvider,
|
|
7
9
|
createSupabaseAuthHelpers,
|
|
10
|
+
defaultSessionFilePath,
|
|
11
|
+
isInvalidGrantError,
|
|
12
|
+
isNetworkError,
|
|
8
13
|
shouldRefreshSoon,
|
|
9
14
|
storedSessionSchema
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-OMJ4JDME.js";
|
|
11
16
|
import {
|
|
12
17
|
configSchema,
|
|
13
18
|
resolveConfig
|
|
@@ -25,9 +30,14 @@ export {
|
|
|
25
30
|
RemixError,
|
|
26
31
|
configSchema,
|
|
27
32
|
createApiClient,
|
|
33
|
+
createDefaultRefreshLock,
|
|
28
34
|
createLocalSessionStore,
|
|
35
|
+
createRefreshLock,
|
|
29
36
|
createStoredSessionTokenProvider,
|
|
30
37
|
createSupabaseAuthHelpers,
|
|
38
|
+
defaultSessionFilePath,
|
|
39
|
+
isInvalidGrantError,
|
|
40
|
+
isNetworkError,
|
|
31
41
|
resolveConfig,
|
|
32
42
|
shouldRefreshSoon,
|
|
33
43
|
storedSessionSchema
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CoreConfig } from './config.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
type RefreshLockOptions = {
|
|
5
|
+
/** Lock file path. Conventionally `<sessionFile>.lock`. */
|
|
6
|
+
lockPath: string;
|
|
7
|
+
/** Treat an existing lock as stale when older than this. */
|
|
8
|
+
staleMs?: number;
|
|
9
|
+
/** Give up waiting and proceed without the lock after this. */
|
|
10
|
+
maxWaitMs?: number;
|
|
11
|
+
/** Sleep between EEXIST retries while waiting. */
|
|
12
|
+
pollMs?: number;
|
|
13
|
+
};
|
|
14
|
+
type WithRefreshLock = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
15
|
+
declare function createRefreshLock(opts: RefreshLockOptions): WithRefreshLock;
|
|
16
|
+
|
|
17
|
+
declare const storedSessionSchema: z.ZodObject<{
|
|
18
|
+
access_token: z.ZodString;
|
|
19
|
+
refresh_token: z.ZodString;
|
|
20
|
+
expires_at: z.ZodNumber;
|
|
21
|
+
token_type: z.ZodOptional<z.ZodString>;
|
|
22
|
+
user: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
id: z.ZodString;
|
|
24
|
+
email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
id: string;
|
|
27
|
+
email?: string | null | undefined;
|
|
28
|
+
}, {
|
|
29
|
+
id: string;
|
|
30
|
+
email?: string | null | undefined;
|
|
31
|
+
}>>;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
access_token: string;
|
|
34
|
+
refresh_token: string;
|
|
35
|
+
expires_at: number;
|
|
36
|
+
token_type?: string | undefined;
|
|
37
|
+
user?: {
|
|
38
|
+
id: string;
|
|
39
|
+
email?: string | null | undefined;
|
|
40
|
+
} | undefined;
|
|
41
|
+
}, {
|
|
42
|
+
access_token: string;
|
|
43
|
+
refresh_token: string;
|
|
44
|
+
expires_at: number;
|
|
45
|
+
token_type?: string | undefined;
|
|
46
|
+
user?: {
|
|
47
|
+
id: string;
|
|
48
|
+
email?: string | null | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
}>;
|
|
51
|
+
type StoredSession = z.infer<typeof storedSessionSchema>;
|
|
52
|
+
type SessionStore = {
|
|
53
|
+
getSession(): Promise<StoredSession | null>;
|
|
54
|
+
setSession(session: StoredSession): Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type RefreshStoredSession = (params: {
|
|
58
|
+
config: CoreConfig;
|
|
59
|
+
session: StoredSession;
|
|
60
|
+
}) => Promise<StoredSession>;
|
|
61
|
+
type ResolvedAuthToken = {
|
|
62
|
+
token: string;
|
|
63
|
+
session: StoredSession | null;
|
|
64
|
+
fromEnv: boolean;
|
|
65
|
+
};
|
|
66
|
+
type TokenProvider = (opts?: {
|
|
67
|
+
forceRefresh?: boolean;
|
|
68
|
+
}) => Promise<ResolvedAuthToken>;
|
|
69
|
+
declare function shouldRefreshSoon(session: StoredSession, skewSeconds?: number): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Token provider that resolves a fresh access token from a stored
|
|
72
|
+
* Supabase session, refreshing it through `refreshStoredSession` when
|
|
73
|
+
* the cached token is at/near expiry.
|
|
74
|
+
*
|
|
75
|
+
* # Why this code is more careful than it looks
|
|
76
|
+
*
|
|
77
|
+
* Supabase rotates refresh tokens: every successful refresh server-
|
|
78
|
+
* side invalidates the old refresh token and issues a new one. If we
|
|
79
|
+
* lose the new token before persisting it (network drop after the
|
|
80
|
+
* server processed our request; multiple processes refreshing the
|
|
81
|
+
* same session in parallel; a `getUser` call after refresh that
|
|
82
|
+
* fails) the session is permanently dead even though the user did
|
|
83
|
+
* nothing wrong. Real-world incident: a laptop coming out of sleep
|
|
84
|
+
* during the 60-second drainer interval lost the rotated token and
|
|
85
|
+
* appeared as a "silent logout" to the user.
|
|
86
|
+
*
|
|
87
|
+
* Three properties protect against this:
|
|
88
|
+
*
|
|
89
|
+
* 1. **Single-flight via `withRefreshLock`** — at most one refresh
|
|
90
|
+
* runs at a time across the whole machine (lock file shared
|
|
91
|
+
* between CLI, desktop, MCP). Inside the lock we re-read the
|
|
92
|
+
* session so we don't double-refresh if another process beat us
|
|
93
|
+
* to it.
|
|
94
|
+
*
|
|
95
|
+
* 2. **Persist-before-return** — `refreshStoredSession` returns the
|
|
96
|
+
* new pair, we write it to disk via `setSession`, and only
|
|
97
|
+
* THEN return to the caller. If the write fails we throw and
|
|
98
|
+
* the caller will retry rather than acting on a token we
|
|
99
|
+
* couldn't durably store.
|
|
100
|
+
*
|
|
101
|
+
* 3. **Network-vs-auth error classification** — `fetch failed`
|
|
102
|
+
* keeps the existing session (the refresh may or may not have
|
|
103
|
+
* landed server-side; we'll find out on the next attempt, ideally
|
|
104
|
+
* within Supabase's reuse interval). Only `invalid_grant` /
|
|
105
|
+
* "refresh token not found" clears state and surfaces a clean
|
|
106
|
+
* sign-in prompt — by then the token is definitively dead and
|
|
107
|
+
* retrying won't help.
|
|
108
|
+
*/
|
|
109
|
+
declare function createStoredSessionTokenProvider(params: {
|
|
110
|
+
config: CoreConfig;
|
|
111
|
+
sessionStore: SessionStore;
|
|
112
|
+
refreshStoredSession: RefreshStoredSession;
|
|
113
|
+
/**
|
|
114
|
+
* Optional cross-process lock around the refresh-and-persist step.
|
|
115
|
+
* Omitting this is safe for tests and single-process scenarios; in
|
|
116
|
+
* production every caller should provide one bound to the session
|
|
117
|
+
* file's `.lock` sibling so concurrent CLI / desktop / plugin
|
|
118
|
+
* processes serialize correctly.
|
|
119
|
+
*/
|
|
120
|
+
withRefreshLock?: WithRefreshLock;
|
|
121
|
+
}): TokenProvider;
|
|
122
|
+
|
|
123
|
+
export { type RefreshLockOptions as R, type SessionStore as S, type TokenProvider as T, type WithRefreshLock as W, type StoredSession as a, createStoredSessionTokenProvider as b, createRefreshLock as c, storedSessionSchema as d, type RefreshStoredSession as e, type ResolvedAuthToken as f, shouldRefreshSoon as s };
|
package/package.json
CHANGED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { CoreConfig } from './config.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
declare const storedSessionSchema: z.ZodObject<{
|
|
5
|
-
access_token: z.ZodString;
|
|
6
|
-
refresh_token: z.ZodString;
|
|
7
|
-
expires_at: z.ZodNumber;
|
|
8
|
-
token_type: z.ZodOptional<z.ZodString>;
|
|
9
|
-
user: z.ZodOptional<z.ZodObject<{
|
|
10
|
-
id: z.ZodString;
|
|
11
|
-
email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
12
|
-
}, "strip", z.ZodTypeAny, {
|
|
13
|
-
id: string;
|
|
14
|
-
email?: string | null | undefined;
|
|
15
|
-
}, {
|
|
16
|
-
id: string;
|
|
17
|
-
email?: string | null | undefined;
|
|
18
|
-
}>>;
|
|
19
|
-
}, "strip", z.ZodTypeAny, {
|
|
20
|
-
access_token: string;
|
|
21
|
-
refresh_token: string;
|
|
22
|
-
expires_at: number;
|
|
23
|
-
token_type?: string | undefined;
|
|
24
|
-
user?: {
|
|
25
|
-
id: string;
|
|
26
|
-
email?: string | null | undefined;
|
|
27
|
-
} | undefined;
|
|
28
|
-
}, {
|
|
29
|
-
access_token: string;
|
|
30
|
-
refresh_token: string;
|
|
31
|
-
expires_at: number;
|
|
32
|
-
token_type?: string | undefined;
|
|
33
|
-
user?: {
|
|
34
|
-
id: string;
|
|
35
|
-
email?: string | null | undefined;
|
|
36
|
-
} | undefined;
|
|
37
|
-
}>;
|
|
38
|
-
type StoredSession = z.infer<typeof storedSessionSchema>;
|
|
39
|
-
type SessionStore = {
|
|
40
|
-
getSession(): Promise<StoredSession | null>;
|
|
41
|
-
setSession(session: StoredSession): Promise<void>;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
type RefreshStoredSession = (params: {
|
|
45
|
-
config: CoreConfig;
|
|
46
|
-
session: StoredSession;
|
|
47
|
-
}) => Promise<StoredSession>;
|
|
48
|
-
type ResolvedAuthToken = {
|
|
49
|
-
token: string;
|
|
50
|
-
session: StoredSession | null;
|
|
51
|
-
fromEnv: boolean;
|
|
52
|
-
};
|
|
53
|
-
type TokenProvider = (opts?: {
|
|
54
|
-
forceRefresh?: boolean;
|
|
55
|
-
}) => Promise<ResolvedAuthToken>;
|
|
56
|
-
declare function shouldRefreshSoon(session: StoredSession, skewSeconds?: number): boolean;
|
|
57
|
-
declare function createStoredSessionTokenProvider(params: {
|
|
58
|
-
config: CoreConfig;
|
|
59
|
-
sessionStore: SessionStore;
|
|
60
|
-
refreshStoredSession: RefreshStoredSession;
|
|
61
|
-
}): TokenProvider;
|
|
62
|
-
|
|
63
|
-
export { type RefreshStoredSession as R, type SessionStore as S, type TokenProvider as T, type StoredSession as a, storedSessionSchema as b, createStoredSessionTokenProvider as c, type ResolvedAuthToken as d, shouldRefreshSoon as s };
|