@ory/argus 0.1.0

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 (80) hide show
  1. package/README.md +134 -0
  2. package/assets/commands/local-down.md +19 -0
  3. package/assets/commands/local-up.md +27 -0
  4. package/assets/skills/auth-setup/SKILL.md +279 -0
  5. package/assets/skills/local-dev/SKILL.md +206 -0
  6. package/assets/skills/login-flow/SKILL.md +383 -0
  7. package/assets/skills/social-login/SKILL.md +312 -0
  8. package/dist/agent-auth.d.ts +204 -0
  9. package/dist/agent-auth.js +553 -0
  10. package/dist/auth-gate.d.ts +71 -0
  11. package/dist/auth-gate.js +308 -0
  12. package/dist/auth-store.d.ts +75 -0
  13. package/dist/auth-store.js +261 -0
  14. package/dist/auth.d.ts +93 -0
  15. package/dist/auth.js +323 -0
  16. package/dist/cli.d.ts +73 -0
  17. package/dist/cli.js +484 -0
  18. package/dist/client.d.ts +158 -0
  19. package/dist/client.js +679 -0
  20. package/dist/config.d.ts +135 -0
  21. package/dist/config.js +344 -0
  22. package/dist/denial.d.ts +79 -0
  23. package/dist/denial.js +103 -0
  24. package/dist/dev.d.ts +95 -0
  25. package/dist/dev.js +514 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +137 -0
  28. package/dist/local/cli.d.ts +12 -0
  29. package/dist/local/cli.js +95 -0
  30. package/dist/local/configs.d.ts +89 -0
  31. package/dist/local/configs.js +634 -0
  32. package/dist/local/health.d.ts +32 -0
  33. package/dist/local/health.js +65 -0
  34. package/dist/local/index.d.ts +6 -0
  35. package/dist/local/index.js +38 -0
  36. package/dist/local/jaeger-main.d.ts +13 -0
  37. package/dist/local/jaeger-main.js +85 -0
  38. package/dist/local/jaeger.d.ts +50 -0
  39. package/dist/local/jaeger.js +162 -0
  40. package/dist/local/main.d.ts +7 -0
  41. package/dist/local/main.js +14 -0
  42. package/dist/local/manager.d.ts +45 -0
  43. package/dist/local/manager.js +676 -0
  44. package/dist/local/seed.d.ts +71 -0
  45. package/dist/local/seed.js +237 -0
  46. package/dist/logger.d.ts +29 -0
  47. package/dist/logger.js +139 -0
  48. package/dist/mcp.d.ts +76 -0
  49. package/dist/mcp.js +122 -0
  50. package/dist/otel/exporter.d.ts +17 -0
  51. package/dist/otel/exporter.js +12 -0
  52. package/dist/otel/index.d.ts +2 -0
  53. package/dist/otel/index.js +8 -0
  54. package/dist/otel/otlp-http.d.ts +116 -0
  55. package/dist/otel/otlp-http.js +322 -0
  56. package/dist/registry/cli.d.ts +12 -0
  57. package/dist/registry/cli.js +76 -0
  58. package/dist/registry/config.d.ts +23 -0
  59. package/dist/registry/config.js +80 -0
  60. package/dist/registry/index.d.ts +3 -0
  61. package/dist/registry/index.js +21 -0
  62. package/dist/registry/main.d.ts +7 -0
  63. package/dist/registry/main.js +14 -0
  64. package/dist/registry/manager.d.ts +38 -0
  65. package/dist/registry/manager.js +674 -0
  66. package/dist/setup.d.ts +118 -0
  67. package/dist/setup.js +398 -0
  68. package/dist/skills.d.ts +78 -0
  69. package/dist/skills.js +264 -0
  70. package/dist/subject.d.ts +43 -0
  71. package/dist/subject.js +55 -0
  72. package/dist/tool-metadata.d.ts +41 -0
  73. package/dist/tool-metadata.js +127 -0
  74. package/dist/tracer.d.ts +172 -0
  75. package/dist/tracer.js +452 -0
  76. package/dist/types.d.ts +57 -0
  77. package/dist/types.js +3 -0
  78. package/dist/watch-sandbox.d.ts +9 -0
  79. package/dist/watch-sandbox.js +81 -0
  80. package/package.json +79 -0
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * User authentication gate. Orchestrates the "first interaction must
4
+ * authenticate the human user" requirement across all plugins:
5
+ *
6
+ * 1. Resolve config (env + ~/.config/ory-agent-plugins/config.json).
7
+ * 2. If no project URL, prompt for one when a TTY is available;
8
+ * otherwise skip with an audit span.
9
+ * 3. If a session/oauth2 token is already in env, short-circuit ok.
10
+ * 4. If persisted tokens are valid, return ok.
11
+ * 5. If they are expired with a refresh_token, refresh.
12
+ * 6. Otherwise launch the PKCE browser login. The flow prints the
13
+ * authorize URL to stderr and waits on a loopback callback, so a
14
+ * missing TTY is fine — users without an interactive terminal can
15
+ * paste the URL into any browser that can reach 127.0.0.1. Only
16
+ * truly unattended (CI=true) environments short-circuit early.
17
+ *
18
+ * Every terminal path emits exactly one `user.auth` trace span so that
19
+ * the audit trail is complete regardless of outcome.
20
+ *
21
+ * The whole gate is a no-op (mode `disabled`) unless the
22
+ * `ORY_AUTH_GATE` env var is set to `1`/`true` — phased rollout.
23
+ *
24
+ * This gate authenticates the *user* (the human at the keyboard). The
25
+ * separate agent identity (the AI process making the calls) is resolved
26
+ * non-interactively via env-configured machine credentials and is not
27
+ * handled here — see `ensureAgentIdentity` (forthcoming).
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.ensureAuthenticated = void 0;
31
+ exports.ensureUserAuthenticated = ensureUserAuthenticated;
32
+ const config_js_1 = require("./config.js");
33
+ const cli_js_1 = require("./cli.js");
34
+ const auth_store_js_1 = require("./auth-store.js");
35
+ const auth_js_1 = require("./auth.js");
36
+ const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
37
+ function isGateEnabled() {
38
+ const v = process.env.ORY_AUTH_GATE?.toLowerCase();
39
+ return !!v && ENABLED_VALUES.has(v);
40
+ }
41
+ function clientId() {
42
+ return process.env.ORY_OAUTH2_CLIENT_ID;
43
+ }
44
+ function recordAuthSpan(client, decision) {
45
+ const status = decision.mode === "ok" || decision.mode === "refreshed" || decision.mode === "env_token"
46
+ ? "ok"
47
+ : decision.mode === "declined"
48
+ ? "denied"
49
+ : decision.mode === "error"
50
+ ? "error"
51
+ : "skipped";
52
+ client.tracer.record("user.auth", status, {
53
+ attributes: {
54
+ mode: decision.mode,
55
+ reason: decision.reason,
56
+ ...(decision.subject ? { subject: decision.subject } : {}),
57
+ proceed: decision.proceed,
58
+ },
59
+ });
60
+ }
61
+ /**
62
+ * The entry point that every plugin's session-start handler should call
63
+ * to authenticate the human user. Always returns a decision; never throws.
64
+ */
65
+ async function ensureUserAuthenticated(client, options) {
66
+ if (!isGateEnabled()) {
67
+ const decision = {
68
+ proceed: true,
69
+ mode: "disabled",
70
+ reason: "ORY_AUTH_GATE is not enabled",
71
+ };
72
+ recordAuthSpan(client, decision);
73
+ return decision;
74
+ }
75
+ try {
76
+ const decision = await runGate(client, options);
77
+ attachUserPrincipal(client, decision);
78
+ recordAuthSpan(client, decision);
79
+ return decision;
80
+ }
81
+ catch (err) {
82
+ const decision = {
83
+ proceed: !options.allowBlock,
84
+ mode: "error",
85
+ reason: err instanceof Error ? err.message : String(err),
86
+ };
87
+ recordAuthSpan(client, decision);
88
+ return decision;
89
+ }
90
+ }
91
+ /**
92
+ * Attach the resolved user identity to the client so downstream
93
+ * permission checks and trace spans can reference it. Only fires for
94
+ * modes that actually authenticated a user — declined/skipped/error
95
+ * leave the principal alone.
96
+ */
97
+ function attachUserPrincipal(client, decision) {
98
+ if (decision.mode === "ok" || decision.mode === "refreshed") {
99
+ // Tokens were just persisted (or are still valid); pull them so
100
+ // downstream code (e.g. the agent gate's DCR flow) can use the
101
+ // user's bearer as an initial access token.
102
+ const tokens = (0, auth_store_js_1.loadTokens)();
103
+ const subject = decision.subject ?? tokens?.subject;
104
+ const principal = {};
105
+ if (subject)
106
+ principal.subject = subject;
107
+ if (tokens?.accessToken)
108
+ principal.token = tokens.accessToken;
109
+ if (principal.subject || principal.token) {
110
+ client.setUserPrincipal(principal);
111
+ }
112
+ return;
113
+ }
114
+ if (decision.mode === "env_token") {
115
+ const subject = process.env.ORY_USER_SUBJECT_ID;
116
+ const token = process.env.ORY_USER_SESSION_TOKEN ??
117
+ process.env.ORY_USER_OAUTH2_TOKEN ??
118
+ process.env.ORY_SESSION_TOKEN ??
119
+ process.env.ORY_OAUTH2_TOKEN;
120
+ client.setUserPrincipal({
121
+ ...(subject ? { subject } : {}),
122
+ ...(token ? { token } : {}),
123
+ });
124
+ }
125
+ }
126
+ async function runGate(client, options) {
127
+ const ttyCheck = options.isTtyAvailableFn ?? cli_js_1.isTtyAvailable;
128
+ const tty = ttyCheck();
129
+ let resolved = (0, config_js_1.resolveConfig)();
130
+ if (resolved.auditOnly) {
131
+ return {
132
+ proceed: true,
133
+ mode: "audit_only",
134
+ reason: "Configured for audit-only mode; auth gate is a no-op",
135
+ };
136
+ }
137
+ // 1. Project URL.
138
+ if (!resolved.projectUrl) {
139
+ if (!tty) {
140
+ return {
141
+ proceed: !options.allowBlock,
142
+ mode: "skipped",
143
+ reason: "No ORY_PROJECT_URL configured and no TTY to prompt",
144
+ };
145
+ }
146
+ const promptFn = options.promptForProjectUrlFn ?? cli_js_1.promptForProjectUrl;
147
+ const url = await promptFn(options.binName);
148
+ if (!url) {
149
+ return {
150
+ proceed: !options.allowBlock,
151
+ mode: "declined",
152
+ reason: "User declined to enter an Ory project URL",
153
+ };
154
+ }
155
+ resolved = (0, config_js_1.resolveConfig)();
156
+ if (!resolved.projectUrl) {
157
+ return {
158
+ proceed: !options.allowBlock,
159
+ mode: "error",
160
+ reason: "Project URL was entered but config did not persist",
161
+ };
162
+ }
163
+ }
164
+ // 2. Pre-supplied env tokens short-circuit the browser flow (CI, scripted runs).
165
+ if (process.env.ORY_SESSION_TOKEN || process.env.ORY_OAUTH2_TOKEN) {
166
+ return {
167
+ proceed: true,
168
+ mode: "env_token",
169
+ reason: "Session/OAuth2 token supplied via environment",
170
+ };
171
+ }
172
+ // 3. Persisted tokens.
173
+ const existing = (0, auth_store_js_1.loadTokens)();
174
+ if (existing && !(0, auth_store_js_1.isExpired)(existing)) {
175
+ return {
176
+ proceed: true,
177
+ mode: "ok",
178
+ reason: "Re-using persisted access token",
179
+ subject: existing.subject,
180
+ };
181
+ }
182
+ if (existing && existing.refreshToken) {
183
+ const cid = clientId() ?? existing.clientId;
184
+ if (cid) {
185
+ try {
186
+ const refreshed = await (0, auth_store_js_1.refreshAndSave)({
187
+ projectUrl: resolved.projectUrl,
188
+ clientId: cid,
189
+ current: existing,
190
+ });
191
+ return {
192
+ proceed: true,
193
+ mode: "refreshed",
194
+ reason: "Refreshed access token via refresh_token",
195
+ subject: refreshed.subject,
196
+ };
197
+ }
198
+ catch (err) {
199
+ // Fall through to browser login.
200
+ client.logger.warn("auth.refresh_failed", {
201
+ message: err instanceof Error ? err.message : String(err),
202
+ });
203
+ }
204
+ }
205
+ }
206
+ // 4. Browser flow. Requires a registered OAuth2 client id.
207
+ const cid = clientId();
208
+ if (!cid) {
209
+ return {
210
+ proceed: !options.allowBlock,
211
+ mode: "skipped",
212
+ reason: "ORY_OAUTH2_CLIENT_ID is not set; cannot start browser login (run `npx " +
213
+ options.binName +
214
+ " configure` and register an OAuth2 client with the loopback redirect URIs)",
215
+ };
216
+ }
217
+ // No TTY check here on purpose: PKCE only needs a browser that can reach
218
+ // the loopback callback port, not stdin. `pkceLogin` prints the
219
+ // authorize URL to stderr so users in non-TTY environments (SSH without
220
+ // `-t`, `docker exec` without `-it`, IDE-integrated terminals) can still
221
+ // complete sign-in by pasting the URL into any browser that can reach
222
+ // 127.0.0.1. Truly unattended runs (CI=true) are short-circuited by
223
+ // `pkceLogin` itself via `detectHeadless`, and operators can bypass the
224
+ // gate entirely with `ORY_USER_SESSION_TOKEN` or `ORY_AUTH_GATE=0`.
225
+ // PKCE-flight lock so concurrent processes share a single browser flow.
226
+ const lock = (0, auth_store_js_1.tryAcquirePkceFlightLock)();
227
+ if (!lock) {
228
+ const peer = await (0, auth_store_js_1.waitForPeerTokens)({ timeoutMs: 90_000 });
229
+ if (peer) {
230
+ (0, auth_store_js_1.saveTokens)(peer); // refresh on-disk timestamps
231
+ return {
232
+ proceed: true,
233
+ mode: "ok",
234
+ reason: "Reused tokens from concurrent agent process",
235
+ subject: peer.subject,
236
+ };
237
+ }
238
+ return {
239
+ proceed: !options.allowBlock,
240
+ mode: "skipped",
241
+ reason: "Another agent process is mid-login; gave up waiting",
242
+ };
243
+ }
244
+ try {
245
+ const loginFn = options.loginFn ?? auth_js_1.pkceLogin;
246
+ const outcome = await loginFn({
247
+ projectUrl: resolved.projectUrl,
248
+ clientId: cid,
249
+ });
250
+ if (outcome.kind === "ok") {
251
+ (0, auth_store_js_1.saveTokens)(outcome.tokens);
252
+ return {
253
+ proceed: true,
254
+ mode: "ok",
255
+ reason: "Completed PKCE browser login",
256
+ subject: outcome.tokens.subject,
257
+ };
258
+ }
259
+ return {
260
+ proceed: !options.allowBlock,
261
+ mode: declineToMode(outcome.reason),
262
+ reason: declineMessage(outcome.reason),
263
+ };
264
+ }
265
+ finally {
266
+ lock.release();
267
+ }
268
+ }
269
+ function declineToMode(reason) {
270
+ switch (reason) {
271
+ case "user_denied":
272
+ case "timeout":
273
+ case "aborted":
274
+ return "declined";
275
+ case "headless":
276
+ case "no_port":
277
+ case "browser_launch_failed":
278
+ return "skipped";
279
+ case "state_mismatch":
280
+ case "token_exchange_failed":
281
+ return "error";
282
+ }
283
+ }
284
+ function declineMessage(reason) {
285
+ switch (reason) {
286
+ case "headless":
287
+ return "Browser login skipped: environment is headless (CI/SSH/no DISPLAY)";
288
+ case "timeout":
289
+ return "Browser login timed out before the user completed sign-in";
290
+ case "user_denied":
291
+ return "User declined the OAuth2 consent screen";
292
+ case "state_mismatch":
293
+ return "OAuth2 callback state did not match — possible CSRF; aborted";
294
+ case "no_port":
295
+ return "Could not bind any of the registered loopback redirect ports";
296
+ case "browser_launch_failed":
297
+ return "Failed to launch the system browser";
298
+ case "aborted":
299
+ return "Login was aborted by the caller";
300
+ case "token_exchange_failed":
301
+ return "Authorization code exchange failed at /oauth2/token";
302
+ }
303
+ }
304
+ /**
305
+ * @deprecated Renamed to `ensureUserAuthenticated`. The old name will be
306
+ * removed in a future release. The behaviour is unchanged.
307
+ */
308
+ exports.ensureAuthenticated = ensureUserAuthenticated;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Persistence layer for the human user's OAuth2 tokens obtained via PKCE
3
+ * login.
4
+ *
5
+ * Tokens live in the `user.oauth2` block of the shared config file at
6
+ * ~/.config/ory-agent-plugins/config.json. Reads use loadConfig(); writes
7
+ * go through mutateConfig() so the existing lockfile and atomic-rename
8
+ * machinery covers concurrent processes.
9
+ *
10
+ * In addition, this module provides a pkce-flight lock so that when two
11
+ * agent processes start simultaneously and neither has tokens yet, only
12
+ * one opens a browser and the others poll for the result.
13
+ */
14
+ import { type OryOAuth2Tokens } from "./config.js";
15
+ /** Load the user's OAuth2 tokens from the persisted config file, if any. */
16
+ export declare function loadTokens(): OryOAuth2Tokens | undefined;
17
+ /** Persist the user's OAuth2 tokens. Existing tokens are replaced atomically. */
18
+ export declare function saveTokens(tokens: OryOAuth2Tokens): void;
19
+ /** Remove the user's persisted OAuth2 tokens. */
20
+ export declare function clearTokens(): void;
21
+ /** Number of seconds before nominal expiry that we consider tokens stale. */
22
+ export declare const TOKEN_EXPIRY_SKEW_SEC = 60;
23
+ /** Returns true if the access token is missing or within the skew window of expiry. */
24
+ export declare function isExpired(tokens: OryOAuth2Tokens, now?: number): boolean;
25
+ /**
26
+ * Refresh the persisted tokens using their refresh_token. Saves the
27
+ * resulting tokens back to disk. Throws if no refresh token is available
28
+ * or if the refresh call fails.
29
+ */
30
+ export declare function refreshAndSave(args: {
31
+ projectUrl: string;
32
+ clientId: string;
33
+ current: OryOAuth2Tokens;
34
+ }): Promise<OryOAuth2Tokens>;
35
+ export interface PkceFlightLock {
36
+ release: () => void;
37
+ }
38
+ /**
39
+ * Try to acquire the pkce-flight lock. Returns the lock handle if this
40
+ * caller is the leader (and should run the browser flow), or null if
41
+ * another process already holds the lock.
42
+ *
43
+ * The lock is a sibling file containing the holder's PID. A lock is
44
+ * considered stale (and reclaimed automatically) when either of these is
45
+ * true:
46
+ *
47
+ * - the PID written in the file is no longer a live process, or
48
+ * - the file is older than 5 minutes
49
+ *
50
+ * PID-liveness is the primary signal — it catches crashed / killed
51
+ * processes immediately so the next launch isn't stuck waiting. The age
52
+ * check is a fallback for the cross-host case (PID is meaningful only on
53
+ * the host that wrote it) and for PID reuse edge cases.
54
+ */
55
+ export declare function tryAcquirePkceFlightLock(): PkceFlightLock | null;
56
+ /**
57
+ * Unconditionally remove the pkce-flight lock, if any. Use this from
58
+ * single-owner contexts (e.g. the dev launcher at startup) where you
59
+ * know no peer is mid-flow. Returns true when a lock was removed.
60
+ */
61
+ export declare function clearPkceFlightLock(): boolean;
62
+ /**
63
+ * Block until another process completes the PKCE flow and writes tokens,
64
+ * or until the timeout elapses. Returns the tokens if they appeared in
65
+ * time, or null otherwise.
66
+ */
67
+ export declare function waitForPeerTokens(opts?: {
68
+ timeoutMs?: number;
69
+ pollIntervalMs?: number;
70
+ }): Promise<OryOAuth2Tokens | null>;
71
+ /** Synchronous variant for callers that already block on the file system. */
72
+ export declare function waitForPeerTokensSync(opts?: {
73
+ timeoutMs?: number;
74
+ pollIntervalMs?: number;
75
+ }): OryOAuth2Tokens | null;
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ /**
3
+ * Persistence layer for the human user's OAuth2 tokens obtained via PKCE
4
+ * login.
5
+ *
6
+ * Tokens live in the `user.oauth2` block of the shared config file at
7
+ * ~/.config/ory-agent-plugins/config.json. Reads use loadConfig(); writes
8
+ * go through mutateConfig() so the existing lockfile and atomic-rename
9
+ * machinery covers concurrent processes.
10
+ *
11
+ * In addition, this module provides a pkce-flight lock so that when two
12
+ * agent processes start simultaneously and neither has tokens yet, only
13
+ * one opens a browser and the others poll for the result.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.TOKEN_EXPIRY_SKEW_SEC = void 0;
50
+ exports.loadTokens = loadTokens;
51
+ exports.saveTokens = saveTokens;
52
+ exports.clearTokens = clearTokens;
53
+ exports.isExpired = isExpired;
54
+ exports.refreshAndSave = refreshAndSave;
55
+ exports.tryAcquirePkceFlightLock = tryAcquirePkceFlightLock;
56
+ exports.clearPkceFlightLock = clearPkceFlightLock;
57
+ exports.waitForPeerTokens = waitForPeerTokens;
58
+ exports.waitForPeerTokensSync = waitForPeerTokensSync;
59
+ const fs = __importStar(require("node:fs"));
60
+ const path = __importStar(require("node:path"));
61
+ const config_js_1 = require("./config.js");
62
+ const auth_js_1 = require("./auth.js");
63
+ const SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
64
+ function sleepSync(ms) {
65
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
66
+ }
67
+ function pkceLockPath() {
68
+ return (0, config_js_1.getConfigPath)() + ".pkce.lock";
69
+ }
70
+ /** Load the user's OAuth2 tokens from the persisted config file, if any. */
71
+ function loadTokens() {
72
+ return (0, config_js_1.loadConfig)().user?.oauth2;
73
+ }
74
+ /** Persist the user's OAuth2 tokens. Existing tokens are replaced atomically. */
75
+ function saveTokens(tokens) {
76
+ (0, config_js_1.mutateConfig)((current) => ({
77
+ ...current,
78
+ user: { ...(current.user ?? {}), oauth2: tokens },
79
+ }));
80
+ }
81
+ /** Remove the user's persisted OAuth2 tokens. */
82
+ function clearTokens() {
83
+ (0, config_js_1.mutateConfig)((current) => {
84
+ const next = { ...current };
85
+ if (!next.user)
86
+ return next;
87
+ const user = { ...next.user };
88
+ delete user.oauth2;
89
+ if (Object.keys(user).length === 0)
90
+ delete next.user;
91
+ else
92
+ next.user = user;
93
+ return next;
94
+ });
95
+ }
96
+ /** Number of seconds before nominal expiry that we consider tokens stale. */
97
+ exports.TOKEN_EXPIRY_SKEW_SEC = 60;
98
+ /** Returns true if the access token is missing or within the skew window of expiry. */
99
+ function isExpired(tokens, now = Date.now()) {
100
+ if (!tokens.accessToken)
101
+ return true;
102
+ const nowSec = Math.floor(now / 1000);
103
+ return tokens.expiresAt - exports.TOKEN_EXPIRY_SKEW_SEC <= nowSec;
104
+ }
105
+ /**
106
+ * Refresh the persisted tokens using their refresh_token. Saves the
107
+ * resulting tokens back to disk. Throws if no refresh token is available
108
+ * or if the refresh call fails.
109
+ */
110
+ async function refreshAndSave(args) {
111
+ if (!args.current.refreshToken) {
112
+ throw new Error("No refresh_token available; a fresh login is required.");
113
+ }
114
+ const next = await (0, auth_js_1.refreshAccessToken)({
115
+ projectUrl: args.projectUrl,
116
+ clientId: args.clientId,
117
+ refreshToken: args.current.refreshToken,
118
+ });
119
+ saveTokens(next);
120
+ return next;
121
+ }
122
+ /**
123
+ * Try to acquire the pkce-flight lock. Returns the lock handle if this
124
+ * caller is the leader (and should run the browser flow), or null if
125
+ * another process already holds the lock.
126
+ *
127
+ * The lock is a sibling file containing the holder's PID. A lock is
128
+ * considered stale (and reclaimed automatically) when either of these is
129
+ * true:
130
+ *
131
+ * - the PID written in the file is no longer a live process, or
132
+ * - the file is older than 5 minutes
133
+ *
134
+ * PID-liveness is the primary signal — it catches crashed / killed
135
+ * processes immediately so the next launch isn't stuck waiting. The age
136
+ * check is a fallback for the cross-host case (PID is meaningful only on
137
+ * the host that wrote it) and for PID reuse edge cases.
138
+ */
139
+ function tryAcquirePkceFlightLock() {
140
+ ensureDir(path.dirname((0, config_js_1.getConfigPath)()));
141
+ const file = pkceLockPath();
142
+ try {
143
+ const fd = fs.openSync(file, "wx", 0o600);
144
+ fs.writeSync(fd, String(process.pid));
145
+ fs.closeSync(fd);
146
+ return { release: () => releasePkceFlightLock() };
147
+ }
148
+ catch (err) {
149
+ if (err.code !== "EEXIST")
150
+ throw err;
151
+ }
152
+ // Lock exists. Reclaim it if the holder is dead or the file is old.
153
+ if (isLockStale(file)) {
154
+ try {
155
+ fs.unlinkSync(file);
156
+ }
157
+ catch {
158
+ /* ignore — another process may have just cleaned it */
159
+ }
160
+ return tryAcquirePkceFlightLock();
161
+ }
162
+ return null;
163
+ }
164
+ function isLockStale(file) {
165
+ let stat;
166
+ try {
167
+ stat = fs.statSync(file);
168
+ }
169
+ catch {
170
+ // File vanished between EEXIST and stat — treat as stale so the
171
+ // caller retries cleanly.
172
+ return true;
173
+ }
174
+ // Age fallback: any lock older than 5 minutes is reclaimable.
175
+ if (Date.now() - stat.mtimeMs > 5 * 60_000)
176
+ return true;
177
+ // Primary check: is the PID still alive on this host?
178
+ let pid;
179
+ try {
180
+ pid = parseInt(fs.readFileSync(file, "utf8").trim(), 10);
181
+ }
182
+ catch {
183
+ return true;
184
+ }
185
+ if (!Number.isFinite(pid) || pid <= 0)
186
+ return true;
187
+ if (pid === process.pid)
188
+ return false; // we hold it ourselves
189
+ try {
190
+ // Signal 0 doesn't deliver a signal — it just checks whether the
191
+ // process exists and we have permission to signal it. ESRCH means
192
+ // no such process; EPERM means it exists but we can't signal it
193
+ // (still alive). Anything else: assume dead.
194
+ process.kill(pid, 0);
195
+ return false;
196
+ }
197
+ catch (err) {
198
+ const code = err.code;
199
+ if (code === "EPERM")
200
+ return false;
201
+ return true;
202
+ }
203
+ }
204
+ /**
205
+ * Unconditionally remove the pkce-flight lock, if any. Use this from
206
+ * single-owner contexts (e.g. the dev launcher at startup) where you
207
+ * know no peer is mid-flow. Returns true when a lock was removed.
208
+ */
209
+ function clearPkceFlightLock() {
210
+ try {
211
+ fs.unlinkSync(pkceLockPath());
212
+ return true;
213
+ }
214
+ catch (err) {
215
+ if (err.code === "ENOENT")
216
+ return false;
217
+ return false;
218
+ }
219
+ }
220
+ function releasePkceFlightLock() {
221
+ try {
222
+ fs.unlinkSync(pkceLockPath());
223
+ }
224
+ catch {
225
+ /* ignore */
226
+ }
227
+ }
228
+ function ensureDir(dir) {
229
+ if (!fs.existsSync(dir))
230
+ fs.mkdirSync(dir, { recursive: true });
231
+ }
232
+ /**
233
+ * Block until another process completes the PKCE flow and writes tokens,
234
+ * or until the timeout elapses. Returns the tokens if they appeared in
235
+ * time, or null otherwise.
236
+ */
237
+ async function waitForPeerTokens(opts) {
238
+ const timeout = opts?.timeoutMs ?? 60_000;
239
+ const interval = opts?.pollIntervalMs ?? 250;
240
+ const deadline = Date.now() + timeout;
241
+ while (Date.now() < deadline) {
242
+ const tokens = loadTokens();
243
+ if (tokens && !isExpired(tokens))
244
+ return tokens;
245
+ await new Promise((resolve) => setTimeout(resolve, interval));
246
+ }
247
+ return null;
248
+ }
249
+ /** Synchronous variant for callers that already block on the file system. */
250
+ function waitForPeerTokensSync(opts) {
251
+ const timeout = opts?.timeoutMs ?? 60_000;
252
+ const interval = opts?.pollIntervalMs ?? 250;
253
+ const deadline = Date.now() + timeout;
254
+ while (Date.now() < deadline) {
255
+ const tokens = loadTokens();
256
+ if (tokens && !isExpired(tokens))
257
+ return tokens;
258
+ sleepSync(interval);
259
+ }
260
+ return null;
261
+ }