@urateam/cli 0.1.41 → 0.1.43

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 (50) hide show
  1. package/dist/__tests__/env-file.test.d.ts +2 -0
  2. package/dist/__tests__/env-file.test.d.ts.map +1 -0
  3. package/dist/__tests__/env-file.test.js +89 -0
  4. package/dist/__tests__/env-file.test.js.map +1 -0
  5. package/dist/__tests__/linear-oauth.test.d.ts +2 -0
  6. package/dist/__tests__/linear-oauth.test.d.ts.map +1 -0
  7. package/dist/__tests__/linear-oauth.test.js +162 -0
  8. package/dist/__tests__/linear-oauth.test.js.map +1 -0
  9. package/dist/__tests__/oauth-state.test.d.ts +2 -0
  10. package/dist/__tests__/oauth-state.test.d.ts.map +1 -0
  11. package/dist/__tests__/oauth-state.test.js +40 -0
  12. package/dist/__tests__/oauth-state.test.js.map +1 -0
  13. package/dist/__tests__/self-auth-linear.test.d.ts +2 -0
  14. package/dist/__tests__/self-auth-linear.test.d.ts.map +1 -0
  15. package/dist/__tests__/self-auth-linear.test.js +92 -0
  16. package/dist/__tests__/self-auth-linear.test.js.map +1 -0
  17. package/dist/__tests__/tunnel.test.d.ts +2 -0
  18. package/dist/__tests__/tunnel.test.d.ts.map +1 -0
  19. package/dist/__tests__/tunnel.test.js +256 -0
  20. package/dist/__tests__/tunnel.test.js.map +1 -0
  21. package/dist/commands/self-auth-linear.d.ts +22 -0
  22. package/dist/commands/self-auth-linear.d.ts.map +1 -0
  23. package/dist/commands/self-auth-linear.js +100 -0
  24. package/dist/commands/self-auth-linear.js.map +1 -0
  25. package/dist/commands/start.d.ts.map +1 -1
  26. package/dist/commands/start.js +84 -1
  27. package/dist/commands/start.js.map +1 -1
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/env-file.d.ts +3 -0
  31. package/dist/lib/env-file.d.ts.map +1 -0
  32. package/dist/lib/env-file.js +69 -0
  33. package/dist/lib/env-file.js.map +1 -0
  34. package/dist/lib/linear-oauth-deps.d.ts +14 -0
  35. package/dist/lib/linear-oauth-deps.d.ts.map +1 -0
  36. package/dist/lib/linear-oauth-deps.js +54 -0
  37. package/dist/lib/linear-oauth-deps.js.map +1 -0
  38. package/dist/lib/linear-oauth.d.ts +46 -0
  39. package/dist/lib/linear-oauth.d.ts.map +1 -0
  40. package/dist/lib/linear-oauth.js +154 -0
  41. package/dist/lib/linear-oauth.js.map +1 -0
  42. package/dist/lib/oauth-state.d.ts +9 -0
  43. package/dist/lib/oauth-state.d.ts.map +1 -0
  44. package/dist/lib/oauth-state.js +43 -0
  45. package/dist/lib/oauth-state.js.map +1 -0
  46. package/dist/lib/tunnel.d.ts +105 -0
  47. package/dist/lib/tunnel.d.ts.map +1 -0
  48. package/dist/lib/tunnel.js +245 -0
  49. package/dist/lib/tunnel.js.map +1 -0
  50. package/package.json +3 -3
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Minimal `.env` read / upsert preserving unrelated keys, comments, and blank
3
+ * lines. We don't need the full `dotenv` spec — just replace-or-append.
4
+ *
5
+ * `upsertEnvFile` writes atomically by writing a sibling `<path>.tmp` first
6
+ * and then renaming. Same-FS rename is atomic on POSIX and on Windows in
7
+ * Node 22, which is the only target.
8
+ */
9
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ export function readEnvFile(path) {
12
+ if (!existsSync(path))
13
+ return {};
14
+ const out = {};
15
+ for (const raw of readFileSync(path, "utf8").split("\n")) {
16
+ const line = raw.trim();
17
+ if (!line || line.startsWith("#"))
18
+ continue;
19
+ const eq = line.indexOf("=");
20
+ if (eq < 1)
21
+ continue;
22
+ const key = line.slice(0, eq).trim();
23
+ const value = line.slice(eq + 1).trim();
24
+ out[key] = value;
25
+ }
26
+ return out;
27
+ }
28
+ export function upsertEnvFile(path, updates) {
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ const updateKeys = new Set(Object.keys(updates));
31
+ const seen = new Set();
32
+ let lines = [];
33
+ if (existsSync(path)) {
34
+ lines = readFileSync(path, "utf8").split("\n");
35
+ }
36
+ const out = [];
37
+ for (const raw of lines) {
38
+ const trimmed = raw.trim();
39
+ if (!trimmed || trimmed.startsWith("#")) {
40
+ out.push(raw);
41
+ continue;
42
+ }
43
+ const eq = trimmed.indexOf("=");
44
+ if (eq < 1) {
45
+ out.push(raw);
46
+ continue;
47
+ }
48
+ const key = trimmed.slice(0, eq).trim();
49
+ if (updateKeys.has(key)) {
50
+ out.push(`${key}=${updates[key]}`);
51
+ seen.add(key);
52
+ }
53
+ else {
54
+ out.push(raw);
55
+ }
56
+ }
57
+ while (out.length > 0 && out[out.length - 1] === "")
58
+ out.pop();
59
+ for (const key of updateKeys) {
60
+ if (!seen.has(key)) {
61
+ out.push(`${key}=${updates[key]}`);
62
+ }
63
+ }
64
+ out.push("");
65
+ const tmp = `${path}.tmp`;
66
+ writeFileSync(tmp, out.join("\n"));
67
+ renameSync(tmp, path);
68
+ }
69
+ //# sourceMappingURL=env-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/lib/env-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { LinearTokenResponse } from "./linear-oauth.js";
2
+ export declare function defaultBrowserOpen(url: string): Promise<void>;
3
+ export declare function defaultFetchTokenEndpoint(body: {
4
+ code: string;
5
+ client_id: string;
6
+ client_secret: string;
7
+ redirect_uri: string;
8
+ grant_type: "authorization_code";
9
+ }): Promise<LinearTokenResponse>;
10
+ export declare function defaultFetchViewer(accessToken: string): Promise<{
11
+ workspaceId: string;
12
+ workspaceName?: string;
13
+ }>;
14
+ //# sourceMappingURL=linear-oauth-deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth-deps.d.ts","sourceRoot":"","sources":["../../src/lib/linear-oauth-deps.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnE;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,oBAAoB,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/B;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB1D"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Default real-world implementations of the LinearOAuthDeps callbacks.
3
+ * Split into its own module so tests can mock the entire shim without
4
+ * pulling in node:child_process and friends.
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execFileP = promisify(execFile);
9
+ export async function defaultBrowserOpen(url) {
10
+ // macOS: `open`, Linux: `xdg-open`. If neither resolves, print the URL.
11
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
12
+ try {
13
+ await execFileP(cmd, [url]);
14
+ }
15
+ catch {
16
+ console.log("Open this URL in your browser:");
17
+ console.log(` ${url}`);
18
+ }
19
+ }
20
+ export async function defaultFetchTokenEndpoint(body) {
21
+ const params = new URLSearchParams();
22
+ for (const [k, v] of Object.entries(body))
23
+ params.set(k, v);
24
+ const res = await fetch("https://api.linear.app/oauth/token", {
25
+ method: "POST",
26
+ headers: { "content-type": "application/x-www-form-urlencoded" },
27
+ body: params.toString(),
28
+ });
29
+ if (!res.ok) {
30
+ const errText = await res.text().catch(() => res.statusText);
31
+ throw new Error(`${res.status}: ${errText.slice(0, 200)}`);
32
+ }
33
+ return (await res.json());
34
+ }
35
+ export async function defaultFetchViewer(accessToken) {
36
+ const query = "query { organization { id name } }";
37
+ const res = await fetch("https://api.linear.app/graphql", {
38
+ method: "POST",
39
+ headers: {
40
+ "content-type": "application/json",
41
+ authorization: `Bearer ${accessToken}`,
42
+ },
43
+ body: JSON.stringify({ query }),
44
+ });
45
+ if (!res.ok) {
46
+ throw new Error(`Failed to fetch workspace metadata: ${res.status} ${res.statusText}`);
47
+ }
48
+ const json = (await res.json());
49
+ const org = json?.data?.organization;
50
+ if (!org?.id)
51
+ throw new Error("Linear returned no organization id");
52
+ return { workspaceId: org.id, workspaceName: org.name };
53
+ }
54
+ //# sourceMappingURL=linear-oauth-deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth-deps.js","sourceRoot":"","sources":["../../src/lib/linear-oauth-deps.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,wEAAwE;IACxE,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;IAChE,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,IAM/C;IACC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,oCAAoC,EAAE;QAC5D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,WAAmB;IAEnB,MAAM,KAAK,GAAG,oCAAoC,CAAC;IACnD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gCAAgC,EAAE;QACxD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,uCAAuC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAE7B,CAAC;IACF,MAAM,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC;IACrC,IAAI,CAAC,GAAG,EAAE,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpE,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,46 @@
1
+ export interface LinearOAuthDeps {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ scope: string;
5
+ /** Total time to wait for the operator to authorize, in milliseconds. */
6
+ timeoutMs: number;
7
+ /**
8
+ * Port to bind the local callback server to. Fixed because Linear requires
9
+ * the redirect URI registered in the OAuth app settings to match the URI
10
+ * sent in the authorize request EXACTLY (host + port + path), so a
11
+ * random port would force the operator to re-register every time.
12
+ * Pass `0` to bind a random free port (test-only — production code must
13
+ * pass a stable value).
14
+ */
15
+ port: number;
16
+ /** Opens the authorize URL in the operator's browser. Pure-test override. */
17
+ openBrowser: (url: string) => Promise<void>;
18
+ /** Exchanges the code for an access token. */
19
+ fetchTokenEndpoint: (body: {
20
+ code: string;
21
+ client_id: string;
22
+ client_secret: string;
23
+ redirect_uri: string;
24
+ grant_type: "authorization_code";
25
+ }) => Promise<LinearTokenResponse>;
26
+ /** Fetches workspace metadata for the audit event. */
27
+ fetchViewer: (accessToken: string) => Promise<{
28
+ workspaceId: string;
29
+ workspaceName?: string;
30
+ }>;
31
+ }
32
+ export interface LinearTokenResponse {
33
+ access_token: string;
34
+ token_type: string;
35
+ expires_in: number;
36
+ scope: string;
37
+ }
38
+ export interface LinearOAuthResult {
39
+ accessToken: string;
40
+ workspaceId: string;
41
+ workspaceName?: string;
42
+ scope: string;
43
+ expiresInSeconds: number;
44
+ }
45
+ export declare function runLinearOAuth(deps: LinearOAuthDeps): Promise<LinearOAuthResult>;
46
+ //# sourceMappingURL=linear-oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth.d.ts","sourceRoot":"","sources":["../../src/lib/linear-oauth.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,8CAA8C;IAC9C,kBAAkB,EAAE,CAAC,IAAI,EAAE;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,oBAAoB,CAAC;KAClC,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnC,sDAAsD;IACtD,WAAW,EAAE,CACX,WAAW,EAAE,MAAM,KAChB,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAkBD,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,iBAAiB,CAAC,CA6J5B"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Linear OAuth 2.0 authorization-code flow, runnable headlessly with
3
+ * dependency-injected browser-open and HTTP transport for tests.
4
+ *
5
+ * High-level:
6
+ * 1. Bind an ephemeral 127.0.0.1 server (random free port).
7
+ * 2. Redirect the operator's browser to Linear with the loopback callback.
8
+ * 3. Verify the HMAC-signed state on the callback.
9
+ * 4. Exchange the code for an access token.
10
+ * 5. Look up workspace metadata (for the audit event).
11
+ * 6. Shut the server down.
12
+ *
13
+ * Token handling: the access token never crosses console.log, never lands
14
+ * in the success-page HTML, and never appears in the audit event payload.
15
+ */
16
+ import { createServer } from "node:http";
17
+ import { randomBytes } from "node:crypto";
18
+ import { newNonce, signState, verifyState } from "./oauth-state.js";
19
+ const SUCCESS_HTML = `<!doctype html>
20
+ <html><head><meta charset="utf-8"><title>urateam OAuth</title></head>
21
+ <body style="font-family: -apple-system, sans-serif; max-width: 520px; margin: 80px auto; line-height: 1.5;">
22
+ <h1>Authorized</h1>
23
+ <p>You can close this tab. Return to your terminal to continue.</p>
24
+ </body></html>`;
25
+ const ERROR_HTML = (msg) => `<!doctype html>
26
+ <html><head><meta charset="utf-8"><title>urateam OAuth</title></head>
27
+ <body style="font-family: -apple-system, sans-serif; max-width: 520px; margin: 80px auto; line-height: 1.5;">
28
+ <h1>Authorization failed</h1>
29
+ <p>${msg}</p>
30
+ </body></html>`;
31
+ const AUTHORIZE_URL = "https://linear.app/oauth/authorize";
32
+ export async function runLinearOAuth(deps) {
33
+ const stateSecret = randomBytes(32).toString("hex");
34
+ const nonce = newNonce();
35
+ const state = signState(stateSecret, nonce);
36
+ return await new Promise((resolve, reject) => {
37
+ let resolved = false;
38
+ let server = null;
39
+ const timeout = setTimeout(() => {
40
+ if (resolved)
41
+ return;
42
+ resolved = true;
43
+ server?.close();
44
+ reject(new Error(`ura self-auth-linear: timed out waiting for the OAuth callback (${deps.timeoutMs}ms)`));
45
+ }, deps.timeoutMs);
46
+ const finalize = (ok, err, e) => {
47
+ if (resolved)
48
+ return;
49
+ resolved = true;
50
+ clearTimeout(timeout);
51
+ server?.close();
52
+ if (e && err)
53
+ err(e);
54
+ else
55
+ ok();
56
+ };
57
+ server = createServer(async (req, res) => {
58
+ try {
59
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
60
+ if (url.pathname !== "/callback") {
61
+ res.writeHead(404).end();
62
+ return;
63
+ }
64
+ const code = url.searchParams.get("code");
65
+ const incomingState = url.searchParams.get("state") ?? "";
66
+ const verified = verifyState(stateSecret, incomingState);
67
+ if (!verified || verified !== nonce) {
68
+ res.writeHead(400, { "content-type": "text/html" });
69
+ res.end(ERROR_HTML("State mismatch — possible CSRF; abort."));
70
+ finalize(() => { }, (e) => reject(e), new Error("ura self-auth-linear: state mismatch — aborting"));
71
+ return;
72
+ }
73
+ if (!code) {
74
+ res.writeHead(400, { "content-type": "text/html" });
75
+ res.end(ERROR_HTML("Missing 'code' parameter from Linear."));
76
+ finalize(() => { }, (e) => reject(e), new Error("ura self-auth-linear: missing code in callback"));
77
+ return;
78
+ }
79
+ const port = server.address().port;
80
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
81
+ const token = await deps.fetchTokenEndpoint({
82
+ code,
83
+ client_id: deps.clientId,
84
+ client_secret: deps.clientSecret,
85
+ redirect_uri: redirectUri,
86
+ grant_type: "authorization_code",
87
+ });
88
+ // Render the success page IMMEDIATELY after token exchange succeeds.
89
+ // The operator's view of "Authorized" must not depend on the
90
+ // subsequent viewer fetch, which is only used for the audit event's
91
+ // display name. If `fetchViewer` fails (Linear GraphQL hiccup), we
92
+ // still resolve with the token — falling back to an "unknown"
93
+ // workspace placeholder.
94
+ res.writeHead(200, { "content-type": "text/html" });
95
+ res.end(SUCCESS_HTML);
96
+ let viewer;
97
+ try {
98
+ viewer = await deps.fetchViewer(token.access_token);
99
+ }
100
+ catch {
101
+ viewer = { workspaceId: "unknown", workspaceName: undefined };
102
+ }
103
+ if (!resolved) {
104
+ resolved = true;
105
+ clearTimeout(timeout);
106
+ server?.close();
107
+ resolve({
108
+ accessToken: token.access_token,
109
+ workspaceId: viewer.workspaceId,
110
+ workspaceName: viewer.workspaceName,
111
+ scope: token.scope,
112
+ expiresInSeconds: token.expires_in,
113
+ });
114
+ }
115
+ }
116
+ catch (err) {
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ try {
119
+ res.writeHead(500, { "content-type": "text/html" });
120
+ res.end(ERROR_HTML("Token exchange failed; check the terminal."));
121
+ }
122
+ catch {
123
+ // response may already be sent
124
+ }
125
+ finalize(() => { }, (e) => reject(e), new Error(`ura self-auth-linear: ${message}`));
126
+ }
127
+ });
128
+ server.on("error", (err) => {
129
+ if (err.code === "EADDRINUSE") {
130
+ finalize(() => { }, (e) => reject(e), new Error(`ura self-auth-linear: port ${deps.port} is already in use. ` +
131
+ `Pass --port <other-port> and register the matching redirect URI in your Linear OAuth app.`));
132
+ return;
133
+ }
134
+ finalize(() => { }, (e) => reject(e), err instanceof Error ? err : new Error(String(err)));
135
+ });
136
+ server.listen(deps.port, "127.0.0.1", async () => {
137
+ try {
138
+ const port = server.address().port;
139
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
140
+ const authUrl = new URL(AUTHORIZE_URL);
141
+ authUrl.searchParams.set("client_id", deps.clientId);
142
+ authUrl.searchParams.set("redirect_uri", redirectUri);
143
+ authUrl.searchParams.set("response_type", "code");
144
+ authUrl.searchParams.set("scope", deps.scope);
145
+ authUrl.searchParams.set("state", state);
146
+ await deps.openBrowser(authUrl.toString());
147
+ }
148
+ catch (err) {
149
+ finalize(() => { }, (e) => reject(e), err instanceof Error ? err : new Error(String(err)));
150
+ }
151
+ });
152
+ });
153
+ }
154
+ //# sourceMappingURL=linear-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth.js","sourceRoot":"","sources":["../../src/lib/linear-oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare function newNonce(): string;
2
+ export declare function signState(secret: string, nonce: string): string;
3
+ /**
4
+ * Returns the verified nonce when `state` is valid; `null` otherwise.
5
+ * Constant-time signature comparison so attacker timing observations don't
6
+ * leak the expected signature byte-by-byte.
7
+ */
8
+ export declare function verifyState(secret: string, state: string): string | null;
9
+ //# sourceMappingURL=oauth-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-state.d.ts","sourceRoot":"","sources":["../../src/lib/oauth-state.ts"],"names":[],"mappings":"AAUA,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAexE"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * HMAC-signed OAuth `state` parameter helpers.
3
+ *
4
+ * The OAuth provider echoes `state` back on the callback; verifying the HMAC
5
+ * before trusting the callback's `code` defends against open-redirect / CSRF
6
+ * attacks. Format: `<nonce>.<hmac-sha256-hex>`. The secret is per-invocation
7
+ * and never persisted — sign + verify happen in the same process.
8
+ */
9
+ import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";
10
+ export function newNonce() {
11
+ return randomBytes(16).toString("hex");
12
+ }
13
+ export function signState(secret, nonce) {
14
+ const sig = createHmac("sha256", secret).update(nonce).digest("hex");
15
+ return `${nonce}.${sig}`;
16
+ }
17
+ /**
18
+ * Returns the verified nonce when `state` is valid; `null` otherwise.
19
+ * Constant-time signature comparison so attacker timing observations don't
20
+ * leak the expected signature byte-by-byte.
21
+ */
22
+ export function verifyState(secret, state) {
23
+ if (!state)
24
+ return null;
25
+ const parts = state.split(".");
26
+ if (parts.length !== 2)
27
+ return null;
28
+ const [nonce, providedSig] = parts;
29
+ if (!nonce || !providedSig)
30
+ return null;
31
+ const expectedSig = createHmac("sha256", secret).update(nonce).digest("hex");
32
+ const a = Buffer.from(providedSig, "hex");
33
+ const b = Buffer.from(expectedSig, "hex");
34
+ if (a.length !== b.length)
35
+ return null;
36
+ try {
37
+ return timingSafeEqual(a, b) ? nonce : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ //# sourceMappingURL=oauth-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-state.js","sourceRoot":"","sources":["../../src/lib/oauth-state.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,UAAU,QAAQ;IACtB,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,KAAa;IACrD,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrE,OAAO,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,KAAa;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC;IACnC,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7E,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `ura start --tunnel <mode>` runtime: wraps `cloudflared` as a child process.
3
+ *
4
+ * Two modes:
5
+ * - "cloudflare-quick": `cloudflared tunnel --url http://localhost:<port>`.
6
+ * Cloudflare assigns a random `*.trycloudflare.com` URL, which is parsed
7
+ * out of cloudflared's stderr.
8
+ * - "cloudflare-token": `cloudflared tunnel --token <token> run`. Operator
9
+ * has already configured a named tunnel in Cloudflare; the public URL
10
+ * is known ahead of time and must be passed via `opts.publicUrl` /
11
+ * `URATEAM_PUBLIC_URL`.
12
+ *
13
+ * Restart-on-exit: the child is supervised with exponential-backoff retries
14
+ * (default 1s → 2s → 4s ... capped at 30s, max 10 attempts). On `stop()`
15
+ * the supervisor cancels any pending retry timer and SIGTERMs the child.
16
+ *
17
+ * All I/O is injectable for tests; the real `spawn` is only invoked when the
18
+ * caller doesn't override `opts.spawn`.
19
+ */
20
+ import { spawn as realSpawn } from "node:child_process";
21
+ import { EventEmitter } from "node:events";
22
+ export type TunnelMode = "cloudflare-quick" | "cloudflare-token";
23
+ export interface TunnelManagerOpts {
24
+ mode: TunnelMode;
25
+ /** Required for "cloudflare-token" — typically from `CLOUDFLARE_TUNNEL_TOKEN`. */
26
+ token?: string;
27
+ /** Static public URL — required for "cloudflare-token", ignored for quick. */
28
+ publicUrl?: string;
29
+ /** Local port the daemon listens on. For "cloudflare-quick". */
30
+ localPort: number;
31
+ /**
32
+ * Override for real `spawn` — tests pass a stub returning a controllable
33
+ * EventEmitter with `stdout`, `stderr`, `kill`, and an `on("exit", ...)`
34
+ * surface.
35
+ */
36
+ spawn?: typeof realSpawn;
37
+ /** Initial restart delay (ms). Default 1000. */
38
+ initialRestartDelayMs?: number;
39
+ /** Max restart delay (ms). Default 30000. */
40
+ maxRestartDelayMs?: number;
41
+ /** Max restart attempts before giving up. Default 10. */
42
+ maxRestartAttempts?: number;
43
+ /** Time to wait for the public URL to appear in stderr (quick mode). Default 30s. */
44
+ urlDetectTimeoutMs?: number;
45
+ /** Logger (defaults to console.log). */
46
+ log?: (msg: string) => void;
47
+ }
48
+ export interface TunnelStartResult {
49
+ publicUrl: string;
50
+ restartCount: number;
51
+ }
52
+ /**
53
+ * Thrown when the `cloudflared` binary isn't installed (ENOENT on spawn).
54
+ * Carries an install hint so the start command can render an actionable
55
+ * error.
56
+ */
57
+ export declare class CloudflaredMissingError extends Error {
58
+ constructor();
59
+ }
60
+ export declare class TunnelManager extends EventEmitter {
61
+ private readonly opts;
62
+ private child;
63
+ private restartCount;
64
+ private shuttingDown;
65
+ private restartTimer;
66
+ private publicUrl;
67
+ private readonly spawnFn;
68
+ private readonly initialRestartDelayMs;
69
+ private readonly maxRestartDelayMs;
70
+ private readonly maxRestartAttempts;
71
+ private readonly urlDetectTimeoutMs;
72
+ private readonly log;
73
+ constructor(opts: TunnelManagerOpts);
74
+ /**
75
+ * Start the tunnel. Resolves with the detected (quick) or pre-configured
76
+ * (token) public URL. Throws `CloudflaredMissingError` on ENOENT.
77
+ */
78
+ start(): Promise<TunnelStartResult>;
79
+ /**
80
+ * Stop the tunnel. Cancels any pending restart, SIGTERMs the child, and
81
+ * awaits the exit. Subsequent restarts (e.g. an exit fired during
82
+ * shutdown) are skipped.
83
+ */
84
+ stop(): Promise<void>;
85
+ private buildArgs;
86
+ private spawnChild;
87
+ private spawnAndDetectUrl;
88
+ /**
89
+ * Wire the supervisor: on unexpected exit, schedule a restart with
90
+ * exponential backoff. Caps at `maxRestartAttempts` then emits "error".
91
+ */
92
+ private wireSupervisor;
93
+ private restart;
94
+ /** Currently-known public URL, or null before `start()`. */
95
+ getPublicUrl(): string | null;
96
+ /**
97
+ * Total restart attempts made since `start()`. Never resets within the
98
+ * lifetime of a single TunnelManager — so even a long stable period
99
+ * followed by a single new failure still incurs the cap-tier delay.
100
+ * Operators who care can construct a fresh manager. Used by the
101
+ * `tunnel.stopped` audit event to attribute flap loops correctly.
102
+ */
103
+ getRestartCount(): number;
104
+ }
105
+ //# sourceMappingURL=tunnel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/lib/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,KAAK,IAAI,SAAS,EAGnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,gDAAgD;IAChD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,6CAA6C;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wCAAwC;IACxC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAKD;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;;CAUjD;AAED,qBAAa,aAAc,SAAQ,YAAY;IAajC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZjC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;gBAEf,IAAI,EAAE,iBAAiB;IAUpD;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAoBzC;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAe3B,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;YAYJ,iBAAiB;IAoD/B;;;OAGG;IACH,OAAO,CAAC,cAAc;YAiCR,OAAO;IA6BrB,4DAA4D;IAC5D,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B;;;;;;OAMG;IACH,eAAe,IAAI,MAAM;CAG1B"}