@mutmutco/installer-launcher 1.3.1 → 1.4.1

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/README.md CHANGED
@@ -57,6 +57,50 @@ Only `login` (and the login half of `install`) opens a browser:
57
57
  The product dir defaults to `~/.<product>/` on mac/linux and `%LOCALAPPDATA%\<product>` on
58
58
  Windows, and holds `tokens.json` (0600), `state.json`, and `payload/`.
59
59
 
60
+ ## Noninteractive session API
61
+
62
+ `@mutmutco/installer-launcher/session` is a plain ESM entry with TypeScript definitions;
63
+ importing it does not start the CLI or load a baked product config.
64
+
65
+ ```ts
66
+ import { getAccessToken, LoginRequiredError } from '@mutmutco/installer-launcher/session';
67
+
68
+ try {
69
+ const accessToken = await getAccessToken({
70
+ product: 'example-product',
71
+ host: 'https://gate.example.com',
72
+ loginKind: 'github', // explicit; use 'google' for a Google product gate
73
+ signal: AbortSignal.timeout(40_000), // shared with the caller's entire attempt
74
+ });
75
+ // Use accessToken for authenticated requests; never log it.
76
+ } catch (error) {
77
+ if (error instanceof LoginRequiredError) {
78
+ // Ask the caller to sign in using its normal product login flow.
79
+ } else {
80
+ throw error; // transport, timeout, cancellation, storage or lock failure
81
+ }
82
+ }
83
+ ```
84
+
85
+ All three identity options are required; no host/product/protocol is inferred. The API
86
+ reuses the machine's gate session and returns only its access token. Tokens within 60 seconds
87
+ of expiry are refreshed and persisted using the launcher's private store. It never opens a
88
+ browser, invokes `gh`, starts device authentication, or exposes the token-file format.
89
+ Missing/unusable sessions and permanent refresh rejection (GitHub gate 403; Google OAuth 400/401/403, e.g. expired or revoked grants) throw `LoginRequiredError`; a release
90
+ resource's `ForbiddenError` instead means revoked release access and is not this error.
91
+ Network and non-403 HTTP failures propagate without erasing the session.
92
+
93
+ Optional `dir` overrides `LAUNCHER_DIR`, then the usual platform product directory applies.
94
+ Optional `fetch` injects a transport for tests and must honor the supplied AbortSignal.
95
+ `timeoutMs` defaults to 10,000 and must be a positive integer at most 40,000. Its signal is
96
+ composed with `signal` and any transport input signal; cancellation is checked before
97
+ persisting or clearing refreshed tokens. For a 40-second attempt / 120-second overall staging
98
+ budget, pass the caller's attempt signal and keep the auth timeout inside that budget.
99
+
100
+ Calls take the existing installation lock and fail rather than queue behind another owner.
101
+ This protects callers using that lock, not all concurrent token writers: existing login paths
102
+ can release it before authentication. Do not assume cross-process refresh coalescing.
103
+
60
104
  ## Config
61
105
 
62
106
  `config/product.template.json` per product:
package/dist/launcher.js CHANGED
@@ -1295,6 +1295,7 @@ function createInstallerRun(value, options = {}) {
1295
1295
  };
1296
1296
  let started = false;
1297
1297
  let finished = false;
1298
+ let currentSurfaces = 0;
1298
1299
  let operation = options.operation;
1299
1300
  const start = () => {
1300
1301
  if (started || finished) return;
@@ -1363,6 +1364,7 @@ function createInstallerRun(value, options = {}) {
1363
1364
  if (!status) throw new Error("installer run: unknown surface state");
1364
1365
  const separator = tty ? "\xB7" : "-";
1365
1366
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
1367
+ if (facts.state === "current" && facts.id !== LAUNCHER_SURFACE) currentSurfaces++;
1366
1368
  const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
1367
1369
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
1368
1370
  if (facts.detail) run2.relay(facts.detail);
@@ -1465,7 +1467,7 @@ function createInstallerRun(value, options = {}) {
1465
1467
  spinner.stop();
1466
1468
  finished = true;
1467
1469
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1468
- const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
1470
+ const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : facts.updated === 0 && currentSurfaces > 0 ? `${currentSurfaces >= facts.total ? "All" : `${currentSurfaces} of`} ${facts.total} surfaces already at ${facts.version}.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
1469
1471
  const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
1470
1472
  const deferred = Boolean(facts.deferred) && facts.failed === 0 && !facts.operationFailed;
1471
1473
  const body = [
@@ -2011,7 +2013,8 @@ async function refreshGoogleToken(host, clientId, refreshToken, fetchImpl = fetc
2011
2013
  client_id: clientId
2012
2014
  }).toString()
2013
2015
  });
2014
- if (!response.ok) return null;
2016
+ if ([400, 401, 403].includes(response.status)) return null;
2017
+ if (!response.ok) throw new Error(`POST ${server}/oauth/token \u2192 ${response.status}: refresh failed`);
2015
2018
  const tokens = await response.json();
2016
2019
  if (typeof tokens.access_token !== "string" || typeof tokens.expires_in !== "number") return null;
2017
2020
  return {
@@ -2026,13 +2029,15 @@ function page(title, body) {
2026
2029
  }
2027
2030
 
2028
2031
  // src/tokens.ts
2029
- async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2032
+ async function ensureFreshToken(config, dir, fetchImpl = fetch, signal) {
2033
+ signal?.throwIfAborted();
2030
2034
  const stored = readTokens(dir);
2031
2035
  if (!stored) return null;
2032
2036
  if (stored.expiresAt - Date.now() > 6e4) return stored.accessToken;
2033
2037
  if (!stored.refreshToken) return null;
2034
2038
  if (config.loginKind === "github") {
2035
2039
  const refreshed2 = await refreshAccessToken(config, stored.refreshToken, fetchImpl);
2040
+ signal?.throwIfAborted();
2036
2041
  if (!refreshed2) {
2037
2042
  clearTokens(dir);
2038
2043
  return null;
@@ -2049,6 +2054,7 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2049
2054
  return null;
2050
2055
  }
2051
2056
  const refreshed = await refreshGoogleToken(config.host, stored.clientId, stored.refreshToken, fetchImpl);
2057
+ signal?.throwIfAborted();
2052
2058
  if (!refreshed) {
2053
2059
  clearTokens(dir);
2054
2060
  return null;
@@ -2259,7 +2265,7 @@ async function refreshLauncher(target, options = {}) {
2259
2265
  }
2260
2266
 
2261
2267
  // src/index.ts
2262
- var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
2268
+ var LAUNCHER_VERSION = true ? "1.4.1" : readVersionFromPackage();
2263
2269
  function defaultPrint(message) {
2264
2270
  process.stdout.write(`${message}
2265
2271
  `);
@@ -1328,6 +1328,7 @@ function createInstallerRun(value, options = {}) {
1328
1328
  };
1329
1329
  let started = false;
1330
1330
  let finished = false;
1331
+ let currentSurfaces = 0;
1331
1332
  let operation = options.operation;
1332
1333
  const start = () => {
1333
1334
  if (started || finished) return;
@@ -1396,6 +1397,7 @@ function createInstallerRun(value, options = {}) {
1396
1397
  if (!status) throw new Error("installer run: unknown surface state");
1397
1398
  const separator = tty ? "\xB7" : "-";
1398
1399
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
1400
+ if (facts.state === "current" && facts.id !== LAUNCHER_SURFACE) currentSurfaces++;
1399
1401
  const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
1400
1402
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
1401
1403
  if (facts.detail) run2.relay(facts.detail);
@@ -1498,7 +1500,7 @@ function createInstallerRun(value, options = {}) {
1498
1500
  spinner.stop();
1499
1501
  finished = true;
1500
1502
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1501
- const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
1503
+ const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : facts.updated === 0 && currentSurfaces > 0 ? `${currentSurfaces >= facts.total ? "All" : `${currentSurfaces} of`} ${facts.total} surfaces already at ${facts.version}.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
1502
1504
  const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
1503
1505
  const deferred = Boolean(facts.deferred) && facts.failed === 0 && !facts.operationFailed;
1504
1506
  const body = [
@@ -2044,7 +2046,8 @@ async function refreshGoogleToken(host, clientId, refreshToken, fetchImpl = fetc
2044
2046
  client_id: clientId
2045
2047
  }).toString()
2046
2048
  });
2047
- if (!response.ok) return null;
2049
+ if ([400, 401, 403].includes(response.status)) return null;
2050
+ if (!response.ok) throw new Error(`POST ${server}/oauth/token \u2192 ${response.status}: refresh failed`);
2048
2051
  const tokens = await response.json();
2049
2052
  if (typeof tokens.access_token !== "string" || typeof tokens.expires_in !== "number") return null;
2050
2053
  return {
@@ -2059,13 +2062,15 @@ function page(title, body) {
2059
2062
  }
2060
2063
 
2061
2064
  // src/tokens.ts
2062
- async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2065
+ async function ensureFreshToken(config, dir, fetchImpl = fetch, signal) {
2066
+ signal?.throwIfAborted();
2063
2067
  const stored = readTokens(dir);
2064
2068
  if (!stored) return null;
2065
2069
  if (stored.expiresAt - Date.now() > 6e4) return stored.accessToken;
2066
2070
  if (!stored.refreshToken) return null;
2067
2071
  if (config.loginKind === "github") {
2068
2072
  const refreshed2 = await refreshAccessToken(config, stored.refreshToken, fetchImpl);
2073
+ signal?.throwIfAborted();
2069
2074
  if (!refreshed2) {
2070
2075
  clearTokens(dir);
2071
2076
  return null;
@@ -2082,6 +2087,7 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2082
2087
  return null;
2083
2088
  }
2084
2089
  const refreshed = await refreshGoogleToken(config.host, stored.clientId, stored.refreshToken, fetchImpl);
2090
+ signal?.throwIfAborted();
2085
2091
  if (!refreshed) {
2086
2092
  clearTokens(dir);
2087
2093
  return null;
@@ -2292,7 +2298,7 @@ async function refreshLauncher(target, options = {}) {
2292
2298
  }
2293
2299
 
2294
2300
  // src/index.ts
2295
- var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
2301
+ var LAUNCHER_VERSION = true ? "1.4.1" : readVersionFromPackage();
2296
2302
  function defaultPrint(message) {
2297
2303
  process.stdout.write(`${message}
2298
2304
  `);
@@ -0,0 +1,19 @@
1
+ export interface SessionOptions {
2
+ product: string;
3
+ host: string;
4
+ loginKind: 'github' | 'google';
5
+ /** Overrides LAUNCHER_DIR and the platform's default product directory. */
6
+ dir?: string;
7
+ /** Optional transport injection; defaults to the global fetch. */
8
+ fetch?: typeof fetch;
9
+ /** Cancellation for the caller's overall attempt. */
10
+ signal?: AbortSignal;
11
+ /** Auth budget in milliseconds: defaults to 10,000; positive and at most 40,000. */
12
+ timeoutMs?: number;
13
+ }
14
+ /** The caller must arrange sign-in; this API never starts authentication. */
15
+ export declare class LoginRequiredError extends Error {
16
+ constructor();
17
+ }
18
+ /** Returns an existing gate access token, refreshing and persisting it when necessary. */
19
+ export declare function getAccessToken(options: SessionOptions): Promise<string>;
@@ -0,0 +1,236 @@
1
+ // src/acquisition.ts
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync, readdirSync, lstatSync, rmSync as rmSync2 } from "node:fs";
4
+ import { join as join2, resolve, relative, isAbsolute, dirname } from "node:path";
5
+
6
+ // src/store.ts
7
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, renameSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ function defaultProductDir(product) {
11
+ if (process.platform === "win32") {
12
+ const base = process.env.LOCALAPPDATA ?? join(tmpdir(), "launcher-fallback");
13
+ return join(base, product);
14
+ }
15
+ const home = process.env.HOME ?? tmpdir();
16
+ return join(home, `.${product}`);
17
+ }
18
+ function resolveProductDir(product, explicit) {
19
+ return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
20
+ }
21
+ function tokensPath(dir) {
22
+ return join(dir, "tokens.json");
23
+ }
24
+ function readTokens(dir) {
25
+ try {
26
+ const data = JSON.parse(readFileSync(tokensPath(dir), "utf8"));
27
+ if (typeof data.accessToken !== "string" || !data.accessToken) return null;
28
+ if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
29
+ const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
30
+ if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
31
+ if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
32
+ return tokens;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function writeTokens(dir, tokens) {
38
+ mkdirSync(dir, { recursive: true });
39
+ try {
40
+ writeFileSync(tokensPath(dir), `${JSON.stringify(tokens)}
41
+ `, { mode: 384 });
42
+ } catch {
43
+ writeFileSync(tokensPath(dir), `${JSON.stringify(tokens)}
44
+ `);
45
+ }
46
+ }
47
+ function clearTokens(dir) {
48
+ rmSync(tokensPath(dir), { force: true });
49
+ }
50
+
51
+ // src/config.ts
52
+ import { getAsset } from "node:sea";
53
+
54
+ // src/acquisition.ts
55
+ function lockInstallation(dir) {
56
+ mkdirSync2(dir, { recursive: true });
57
+ const path = join2(dir, "installation.lock");
58
+ const owner = `${process.pid}:${randomUUID()}`;
59
+ try {
60
+ writeFileSync2(path, owner, { flag: "wx", mode: 384 });
61
+ } catch (error) {
62
+ if (error.code !== "EEXIST") throw error;
63
+ const recovery = `${path}.recovery`;
64
+ try {
65
+ writeFileSync2(recovery, owner, { flag: "wx", mode: 384, flush: true });
66
+ } catch {
67
+ throw new Error("installation lock recovery is already pending");
68
+ }
69
+ try {
70
+ const previous = readFileSync2(path, "utf8");
71
+ const pid = Number(previous.split(":")[0]);
72
+ if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
73
+ try {
74
+ process.kill(pid, 0);
75
+ throw new Error("another installer is running");
76
+ } catch (probe) {
77
+ if (probe.code !== "ESRCH") throw probe;
78
+ }
79
+ unlinkSync(path);
80
+ writeFileSync2(path, owner, { flag: "wx", mode: 384, flush: true });
81
+ } finally {
82
+ unlinkSync(recovery);
83
+ }
84
+ }
85
+ let released = false;
86
+ return () => {
87
+ if (!released && readFileSync2(path, "utf8") === owner) unlinkSync(path);
88
+ released = true;
89
+ };
90
+ }
91
+
92
+ // src/login-github.ts
93
+ async function postJson(url, body, fetchImpl) {
94
+ return fetchImpl(url, {
95
+ method: "POST",
96
+ headers: { "content-type": "application/json" },
97
+ body: JSON.stringify(body)
98
+ });
99
+ }
100
+ async function readBody(response) {
101
+ try {
102
+ return await response.text();
103
+ } catch {
104
+ return "";
105
+ }
106
+ }
107
+ function gateRefusal(method, url, status, body) {
108
+ const head = body.replace(/\s+/g, " ").trim().slice(0, 200);
109
+ return `the sign-in server refused the request \u2014 ${method} ${url} \u2192 ${status}${head ? `: ${head}` : ""}`;
110
+ }
111
+ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
112
+ const url = `${config.host}/gate/refresh`;
113
+ const response = await postJson(url, { refresh_token: refreshToken }, fetchImpl);
114
+ if (response.status === 403) return null;
115
+ if (!response.ok) {
116
+ throw new Error(gateRefusal("POST", url, response.status, await readBody(response)));
117
+ }
118
+ const data = await response.json();
119
+ if (typeof data.access_token !== "string" || typeof data.expires_in !== "number") {
120
+ throw new Error("the sign-in server returned a malformed refresh");
121
+ }
122
+ return {
123
+ accessToken: data.access_token,
124
+ expiresIn: data.expires_in,
125
+ ...typeof data.refresh_token === "string" ? { refreshToken: data.refresh_token } : {}
126
+ };
127
+ }
128
+
129
+ // src/login-google.ts
130
+ async function refreshGoogleToken(host, clientId, refreshToken, fetchImpl = fetch) {
131
+ const server = host.replace(/\/+$/, "");
132
+ const response = await fetchImpl(`${server}/oauth/token`, {
133
+ method: "POST",
134
+ headers: { "content-type": "application/x-www-form-urlencoded" },
135
+ body: new URLSearchParams({
136
+ grant_type: "refresh_token",
137
+ refresh_token: refreshToken,
138
+ client_id: clientId
139
+ }).toString()
140
+ });
141
+ if ([400, 401, 403].includes(response.status)) return null;
142
+ if (!response.ok) throw new Error(`POST ${server}/oauth/token \u2192 ${response.status}: refresh failed`);
143
+ const tokens = await response.json();
144
+ if (typeof tokens.access_token !== "string" || typeof tokens.expires_in !== "number") return null;
145
+ return {
146
+ accessToken: tokens.access_token,
147
+ refreshToken: typeof tokens.refresh_token === "string" ? tokens.refresh_token : refreshToken,
148
+ expiresIn: tokens.expires_in
149
+ };
150
+ }
151
+
152
+ // src/tokens.ts
153
+ async function ensureFreshToken(config, dir, fetchImpl = fetch, signal) {
154
+ signal?.throwIfAborted();
155
+ const stored = readTokens(dir);
156
+ if (!stored) return null;
157
+ if (stored.expiresAt - Date.now() > 6e4) return stored.accessToken;
158
+ if (!stored.refreshToken) return null;
159
+ if (config.loginKind === "github") {
160
+ const refreshed2 = await refreshAccessToken(config, stored.refreshToken, fetchImpl);
161
+ signal?.throwIfAborted();
162
+ if (!refreshed2) {
163
+ clearTokens(dir);
164
+ return null;
165
+ }
166
+ writeTokens(dir, {
167
+ accessToken: refreshed2.accessToken,
168
+ refreshToken: refreshed2.refreshToken ?? stored.refreshToken,
169
+ expiresAt: Date.now() + refreshed2.expiresIn * 1e3
170
+ });
171
+ return refreshed2.accessToken;
172
+ }
173
+ if (!stored.clientId) {
174
+ clearTokens(dir);
175
+ return null;
176
+ }
177
+ const refreshed = await refreshGoogleToken(config.host, stored.clientId, stored.refreshToken, fetchImpl);
178
+ signal?.throwIfAborted();
179
+ if (!refreshed) {
180
+ clearTokens(dir);
181
+ return null;
182
+ }
183
+ writeTokens(dir, {
184
+ accessToken: refreshed.accessToken,
185
+ refreshToken: refreshed.refreshToken,
186
+ expiresAt: Date.now() + refreshed.expiresIn * 1e3,
187
+ clientId: stored.clientId
188
+ });
189
+ return refreshed.accessToken;
190
+ }
191
+
192
+ // src/session.ts
193
+ var LoginRequiredError = class extends Error {
194
+ constructor() {
195
+ super("Product sign-in is required.");
196
+ this.name = "LoginRequiredError";
197
+ }
198
+ };
199
+ async function getAccessToken(options) {
200
+ if (!options.product || options.loginKind !== "github" && options.loginKind !== "google") {
201
+ throw new Error("Session options require product and loginKind (github or google).");
202
+ }
203
+ const host = new URL(options.host);
204
+ if (host.protocol !== "https:" && host.protocol !== "http:") {
205
+ throw new Error("Session host must be an http(s) URL.");
206
+ }
207
+ const timeoutMs = options.timeoutMs ?? 1e4;
208
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 4e4) {
209
+ throw new Error("Session timeoutMs must be a positive integer at most 40000.");
210
+ }
211
+ const signal = AbortSignal.any([AbortSignal.timeout(timeoutMs), ...options.signal ? [options.signal] : []]);
212
+ signal.throwIfAborted();
213
+ const transport = options.fetch ?? fetch;
214
+ const fetchImpl = (input, init) => {
215
+ const inherited = init?.signal ?? (input instanceof Request ? input.signal : void 0);
216
+ return transport(input, { ...init, signal: inherited ? AbortSignal.any([signal, inherited]) : signal });
217
+ };
218
+ const dir = resolveProductDir(options.product, options.dir);
219
+ const unlock = lockInstallation(dir);
220
+ try {
221
+ const token = await ensureFreshToken(
222
+ { host: options.host.replace(/\/+$/, ""), loginKind: options.loginKind },
223
+ dir,
224
+ fetchImpl,
225
+ signal
226
+ );
227
+ if (!token) throw new LoginRequiredError();
228
+ return token;
229
+ } finally {
230
+ unlock();
231
+ }
232
+ }
233
+ export {
234
+ LoginRequiredError,
235
+ getAccessToken
236
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-launcher",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Single-executable (SEA) launcher: sign-in, gated payload fetch, self-update. One copy ships per product.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -14,7 +14,11 @@
14
14
  "exports": {
15
15
  ".": "./dist/launcher.js",
16
16
  "./acquisition-required": "./dist/acquisition-required.mjs",
17
- "./refresh": "./dist/refresh.js"
17
+ "./refresh": "./dist/refresh.js",
18
+ "./session": {
19
+ "types": "./dist/session.d.ts",
20
+ "import": "./dist/session.js"
21
+ }
18
22
  },
19
23
  "files": [
20
24
  "dist",