@urateam/cli 0.1.41 → 0.1.42

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 (39) 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/commands/self-auth-linear.d.ts +22 -0
  18. package/dist/commands/self-auth-linear.d.ts.map +1 -0
  19. package/dist/commands/self-auth-linear.js +100 -0
  20. package/dist/commands/self-auth-linear.js.map +1 -0
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/env-file.d.ts +3 -0
  24. package/dist/lib/env-file.d.ts.map +1 -0
  25. package/dist/lib/env-file.js +69 -0
  26. package/dist/lib/env-file.js.map +1 -0
  27. package/dist/lib/linear-oauth-deps.d.ts +14 -0
  28. package/dist/lib/linear-oauth-deps.d.ts.map +1 -0
  29. package/dist/lib/linear-oauth-deps.js +54 -0
  30. package/dist/lib/linear-oauth-deps.js.map +1 -0
  31. package/dist/lib/linear-oauth.d.ts +46 -0
  32. package/dist/lib/linear-oauth.d.ts.map +1 -0
  33. package/dist/lib/linear-oauth.js +154 -0
  34. package/dist/lib/linear-oauth.js.map +1 -0
  35. package/dist/lib/oauth-state.d.ts +9 -0
  36. package/dist/lib/oauth-state.d.ts.map +1 -0
  37. package/dist/lib/oauth-state.js +43 -0
  38. package/dist/lib/oauth-state.js.map +1 -0
  39. package/package.json +3 -3
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=env-file.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/env-file.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { upsertEnvFile, readEnvFile } from "../lib/env-file.js";
6
+ describe("upsertEnvFile", () => {
7
+ let tmp;
8
+ let path;
9
+ beforeEach(() => {
10
+ tmp = mkdtempSync(join(tmpdir(), "ura-env-"));
11
+ path = join(tmp, ".env");
12
+ });
13
+ afterEach(() => {
14
+ rmSync(tmp, { recursive: true, force: true });
15
+ });
16
+ it("creates the file when it does not exist", () => {
17
+ upsertEnvFile(path, { LINEAR_API_KEY: "lin_oauth_abc" });
18
+ const raw = readFileSync(path, "utf8");
19
+ expect(raw).toContain("LINEAR_API_KEY=lin_oauth_abc");
20
+ });
21
+ it("merges into an existing file preserving unrelated keys", () => {
22
+ writeFileSync(path, [
23
+ "ANTHROPIC_API_KEY=sk-ant-xyz",
24
+ "DASHBOARD_USER=admin",
25
+ "LINEAR_API_KEY=lin_api_old",
26
+ "",
27
+ ].join("\n"));
28
+ upsertEnvFile(path, { LINEAR_API_KEY: "lin_oauth_new" });
29
+ const raw = readFileSync(path, "utf8");
30
+ expect(raw).toContain("ANTHROPIC_API_KEY=sk-ant-xyz");
31
+ expect(raw).toContain("DASHBOARD_USER=admin");
32
+ expect(raw).toContain("LINEAR_API_KEY=lin_oauth_new");
33
+ expect(raw).not.toContain("lin_api_old");
34
+ });
35
+ it("preserves comments and blank lines around modified keys", () => {
36
+ writeFileSync(path, [
37
+ "# Linear",
38
+ "",
39
+ "LINEAR_API_KEY=lin_api_old",
40
+ "# Anthropic",
41
+ "ANTHROPIC_API_KEY=sk-ant-xyz",
42
+ "",
43
+ ].join("\n"));
44
+ upsertEnvFile(path, { LINEAR_API_KEY: "lin_oauth_new" });
45
+ const raw = readFileSync(path, "utf8");
46
+ expect(raw).toMatch(/^# Linear/m);
47
+ expect(raw).toMatch(/^# Anthropic/m);
48
+ });
49
+ it("appends new keys at the end when not already present", () => {
50
+ writeFileSync(path, "ANTHROPIC_API_KEY=sk-ant-xyz\n");
51
+ upsertEnvFile(path, { LINEAR_API_KEY: "lin_oauth_new" });
52
+ const lines = readFileSync(path, "utf8").split("\n");
53
+ const idx = lines.findIndex((l) => l.startsWith("LINEAR_API_KEY="));
54
+ const anthropicIdx = lines.findIndex((l) => l.startsWith("ANTHROPIC_API_KEY="));
55
+ expect(idx).toBeGreaterThan(anthropicIdx);
56
+ });
57
+ it("can upsert multiple keys in one call", () => {
58
+ upsertEnvFile(path, {
59
+ LINEAR_API_KEY: "lin_oauth_new",
60
+ LINEAR_WORKSPACE_ID: "ws_abc",
61
+ });
62
+ const raw = readFileSync(path, "utf8");
63
+ expect(raw).toContain("LINEAR_API_KEY=lin_oauth_new");
64
+ expect(raw).toContain("LINEAR_WORKSPACE_ID=ws_abc");
65
+ });
66
+ it("leaves no .env.tmp behind after a successful upsert", () => {
67
+ upsertEnvFile(path, { LINEAR_API_KEY: "lin_oauth_new" });
68
+ expect(existsSync(`${path}.tmp`)).toBe(false);
69
+ });
70
+ });
71
+ describe("readEnvFile", () => {
72
+ let tmp;
73
+ let path;
74
+ beforeEach(() => {
75
+ tmp = mkdtempSync(join(tmpdir(), "ura-env-"));
76
+ path = join(tmp, ".env");
77
+ });
78
+ afterEach(() => {
79
+ rmSync(tmp, { recursive: true, force: true });
80
+ });
81
+ it("returns an empty object when the file is absent", () => {
82
+ expect(readEnvFile(path)).toEqual({});
83
+ });
84
+ it("parses KEY=value lines, ignoring comments and blanks", () => {
85
+ writeFileSync(path, ["# comment", "", "FOO=bar", "BAZ=qux quux"].join("\n"));
86
+ expect(readEnvFile(path)).toEqual({ FOO: "bar", BAZ: "qux quux" });
87
+ });
88
+ });
89
+ //# sourceMappingURL=env-file.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.test.js","sourceRoot":"","sources":["../../src/__tests__/env-file.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=linear-oauth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/linear-oauth.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { runLinearOAuth } from "../lib/linear-oauth.js";
3
+ function makeDeps(overrides = {}) {
4
+ return {
5
+ clientId: "linear-client-id",
6
+ clientSecret: "linear-client-secret",
7
+ scope: "read,write",
8
+ timeoutMs: 1000,
9
+ // 0 = bind a random free port. Tests inspect the resulting redirect_uri to
10
+ // discover the actual port — production callers pass a fixed port.
11
+ port: 0,
12
+ openBrowser: vi.fn(async (_url) => { }),
13
+ fetchTokenEndpoint: vi.fn(async (_body) => ({
14
+ access_token: "lin_oauth_token_123",
15
+ token_type: "Bearer",
16
+ expires_in: 31536000,
17
+ scope: "read,write",
18
+ })),
19
+ fetchViewer: vi.fn(async (_token) => ({
20
+ workspaceId: "ws_abc",
21
+ workspaceName: "Acme",
22
+ })),
23
+ ...overrides,
24
+ };
25
+ }
26
+ async function postCallback(port, params) {
27
+ const qs = new URLSearchParams(params).toString();
28
+ return fetch(`http://127.0.0.1:${port}/callback?${qs}`);
29
+ }
30
+ describe("runLinearOAuth", () => {
31
+ it("happy path: returns token + workspace metadata after a valid callback", async () => {
32
+ const deps = makeDeps();
33
+ let capturedPort = 0;
34
+ let capturedState = "";
35
+ deps.openBrowser = vi.fn(async (url) => {
36
+ const parsed = new URL(url);
37
+ capturedState = parsed.searchParams.get("state");
38
+ const redirectUri = parsed.searchParams.get("redirect_uri");
39
+ capturedPort = Number(new URL(redirectUri).port);
40
+ await postCallback(capturedPort, {
41
+ code: "test-code",
42
+ state: capturedState,
43
+ });
44
+ });
45
+ const result = await runLinearOAuth(deps);
46
+ expect(result.accessToken).toBe("lin_oauth_token_123");
47
+ expect(result.workspaceId).toBe("ws_abc");
48
+ expect(result.workspaceName).toBe("Acme");
49
+ expect(deps.fetchTokenEndpoint).toHaveBeenCalledWith(expect.objectContaining({
50
+ code: "test-code",
51
+ client_id: "linear-client-id",
52
+ client_secret: "linear-client-secret",
53
+ grant_type: "authorization_code",
54
+ }));
55
+ });
56
+ it("rejects state mismatch", async () => {
57
+ const deps = makeDeps();
58
+ deps.openBrowser = vi.fn(async (url) => {
59
+ const parsed = new URL(url);
60
+ const redirectUri = parsed.searchParams.get("redirect_uri");
61
+ const port = Number(new URL(redirectUri).port);
62
+ await postCallback(port, {
63
+ code: "test-code",
64
+ state: "ATTACKER-STATE",
65
+ });
66
+ });
67
+ await expect(runLinearOAuth(deps)).rejects.toThrow(/state mismatch/i);
68
+ });
69
+ it("times out when the callback never arrives", async () => {
70
+ const deps = makeDeps({
71
+ timeoutMs: 100,
72
+ openBrowser: vi.fn(async () => { }),
73
+ });
74
+ await expect(runLinearOAuth(deps)).rejects.toThrow(/timed out/i);
75
+ });
76
+ it("surfaces Linear API errors during token exchange", async () => {
77
+ const deps = makeDeps({
78
+ fetchTokenEndpoint: vi.fn(async () => {
79
+ throw new Error("400: invalid_grant");
80
+ }),
81
+ });
82
+ deps.openBrowser = vi.fn(async (url) => {
83
+ const parsed = new URL(url);
84
+ const state = parsed.searchParams.get("state");
85
+ const redirectUri = parsed.searchParams.get("redirect_uri");
86
+ const port = Number(new URL(redirectUri).port);
87
+ await postCallback(port, { code: "bad-code", state });
88
+ });
89
+ await expect(runLinearOAuth(deps)).rejects.toThrow(/invalid_grant/);
90
+ });
91
+ it("never returns the token via the openBrowser URL or the callback response body", async () => {
92
+ const deps = makeDeps();
93
+ let urlPassedToBrowser = "";
94
+ let callbackBody = "";
95
+ deps.openBrowser = vi.fn(async (url) => {
96
+ urlPassedToBrowser = url;
97
+ const parsed = new URL(url);
98
+ const state = parsed.searchParams.get("state");
99
+ const redirectUri = parsed.searchParams.get("redirect_uri");
100
+ const port = Number(new URL(redirectUri).port);
101
+ const res = await postCallback(port, { code: "test-code", state });
102
+ callbackBody = await res.text();
103
+ });
104
+ await runLinearOAuth(deps);
105
+ expect(urlPassedToBrowser).not.toContain("lin_oauth_token_123");
106
+ expect(callbackBody).not.toContain("lin_oauth_token_123");
107
+ });
108
+ it("still returns the token when fetchViewer fails (operator already authorized)", async () => {
109
+ // Critical contract: a Linear GraphQL hiccup after a successful token
110
+ // exchange must NOT discard the access token. The operator already
111
+ // clicked "Authorize"; making them redo the flow because the metadata
112
+ // fetch hiccuped is hostile UX. Implementation renders SUCCESS_HTML
113
+ // BEFORE the viewer fetch so the browser shows success regardless.
114
+ const deps = makeDeps({
115
+ fetchViewer: vi.fn(async () => {
116
+ throw new Error("Linear GraphQL hiccup");
117
+ }),
118
+ });
119
+ deps.openBrowser = vi.fn(async (url) => {
120
+ const parsed = new URL(url);
121
+ const state = parsed.searchParams.get("state");
122
+ const redirectUri = parsed.searchParams.get("redirect_uri");
123
+ const port = Number(new URL(redirectUri).port);
124
+ // Fire-and-forget — we don't need the response body in this test.
125
+ // The OAuth orchestrator's promise resolution is what we assert.
126
+ void postCallback(port, { code: "test-code", state }).catch(() => { });
127
+ });
128
+ const result = await runLinearOAuth(deps);
129
+ expect(result.accessToken).toBe("lin_oauth_token_123");
130
+ expect(result.workspaceId).toBe("unknown");
131
+ expect(result.workspaceName).toBeUndefined();
132
+ expect(deps.fetchViewer).toHaveBeenCalled();
133
+ });
134
+ it("authorize URL contains the matching redirect_uri (port consistency)", async () => {
135
+ const deps = makeDeps();
136
+ let urlSeen = "";
137
+ deps.openBrowser = vi.fn(async (url) => {
138
+ urlSeen = url;
139
+ const parsed = new URL(url);
140
+ const state = parsed.searchParams.get("state");
141
+ const redirectUri = parsed.searchParams.get("redirect_uri");
142
+ const port = Number(new URL(redirectUri).port);
143
+ await postCallback(port, { code: "test-code", state });
144
+ });
145
+ await runLinearOAuth(deps);
146
+ const parsed = new URL(urlSeen);
147
+ const redirectUri = parsed.searchParams.get("redirect_uri");
148
+ expect(deps.fetchTokenEndpoint).toHaveBeenCalledWith(expect.objectContaining({ redirect_uri: redirectUri }));
149
+ });
150
+ it("rejects callback without a code", async () => {
151
+ const deps = makeDeps();
152
+ deps.openBrowser = vi.fn(async (url) => {
153
+ const parsed = new URL(url);
154
+ const state = parsed.searchParams.get("state");
155
+ const redirectUri = parsed.searchParams.get("redirect_uri");
156
+ const port = Number(new URL(redirectUri).port);
157
+ await postCallback(port, { state });
158
+ });
159
+ await expect(runLinearOAuth(deps)).rejects.toThrow(/missing code/i);
160
+ });
161
+ });
162
+ //# sourceMappingURL=linear-oauth.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-oauth.test.js","sourceRoot":"","sources":["../../src/__tests__/linear-oauth.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=oauth-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-state.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/oauth-state.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { signState, verifyState, newNonce } from "../lib/oauth-state.js";
3
+ describe("OAuth state HMAC", () => {
4
+ it("round-trips a nonce", () => {
5
+ const secret = "fixed-secret-for-test";
6
+ const signed = signState(secret, "nonce-abc");
7
+ expect(verifyState(secret, signed)).toBe("nonce-abc");
8
+ });
9
+ it("rejects a tampered nonce", () => {
10
+ const secret = "fixed-secret-for-test";
11
+ const signed = signState(secret, "nonce-abc");
12
+ const [nonce, sig] = signed.split(".");
13
+ const tampered = `${nonce}-tampered.${sig}`;
14
+ expect(verifyState(secret, tampered)).toBeNull();
15
+ });
16
+ it("rejects a state signed with a different secret", () => {
17
+ const signed = signState("secret-a", "nonce-abc");
18
+ expect(verifyState("secret-b", signed)).toBeNull();
19
+ });
20
+ it("rejects malformed input", () => {
21
+ expect(verifyState("any-secret", "")).toBeNull();
22
+ expect(verifyState("any-secret", "no-dot")).toBeNull();
23
+ expect(verifyState("any-secret", "too.many.dots")).toBeNull();
24
+ });
25
+ it("rejects same-length-but-wrong sig without throwing", () => {
26
+ const signed = signState("secret", "nonce");
27
+ const [nonce] = signed.split(".");
28
+ const wrongButSameLen = `${nonce}.${"f".repeat(64)}`;
29
+ expect(() => verifyState("secret", wrongButSameLen)).not.toThrow();
30
+ expect(verifyState("secret", wrongButSameLen)).toBeNull();
31
+ });
32
+ it("newNonce produces unique 32-char hex values", () => {
33
+ const a = newNonce();
34
+ const b = newNonce();
35
+ expect(a).toMatch(/^[0-9a-f]{32}$/);
36
+ expect(b).toMatch(/^[0-9a-f]{32}$/);
37
+ expect(a).not.toBe(b);
38
+ });
39
+ });
40
+ //# sourceMappingURL=oauth-state.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-state.test.js","sourceRoot":"","sources":["../../src/__tests__/oauth-state.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEzE,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,uBAAuB,CAAC;QACvC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC9C,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,MAAM,GAAG,uBAAuB,CAAC;QACvC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAG,GAAG,KAAK,aAAa,GAAG,EAAE,CAAC;QAC5C,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QAClD,MAAM,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACjD,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvD,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,eAAe,GAAG,GAAG,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACrD,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnE,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;QACrB,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACpC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACpC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=self-auth-linear.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-auth-linear.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/self-auth-linear.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ // Drive the OAuth flow by hijacking `defaultBrowserOpen` to post the simulated
6
+ // provider callback at the server the real `runLinearOAuth` opens; mock the
7
+ // token + viewer fetches so no network is touched.
8
+ vi.mock("../lib/linear-oauth-deps.js", async () => {
9
+ return {
10
+ defaultBrowserOpen: async (url) => {
11
+ const parsed = new URL(url);
12
+ const state = parsed.searchParams.get("state");
13
+ const redirectUri = parsed.searchParams.get("redirect_uri");
14
+ const port = Number(new URL(redirectUri).port);
15
+ const qs = new URLSearchParams({ code: "fake-code", state }).toString();
16
+ await fetch(`http://127.0.0.1:${port}/callback?${qs}`);
17
+ },
18
+ defaultFetchTokenEndpoint: async () => ({
19
+ access_token: "lin_oauth_TOKEN",
20
+ token_type: "Bearer",
21
+ expires_in: 31536000,
22
+ scope: "read,write",
23
+ }),
24
+ defaultFetchViewer: async () => ({
25
+ workspaceId: "ws_test",
26
+ workspaceName: "Test Workspace",
27
+ }),
28
+ };
29
+ });
30
+ const { selfAuthLinearCommand } = await import("../commands/self-auth-linear.js");
31
+ describe("ura self-auth-linear", () => {
32
+ let tmp;
33
+ beforeEach(() => {
34
+ tmp = mkdtempSync(join(tmpdir(), "ura-oauth-"));
35
+ process.env.URATEAM_HOME = tmp;
36
+ mkdirSync(tmp, { recursive: true });
37
+ process.env.LINEAR_CLIENT_ID = "client-abc";
38
+ process.env.LINEAR_CLIENT_SECRET = "secret-xyz";
39
+ });
40
+ afterEach(() => {
41
+ delete process.env.URATEAM_HOME;
42
+ delete process.env.LINEAR_CLIENT_ID;
43
+ delete process.env.LINEAR_CLIENT_SECRET;
44
+ rmSync(tmp, { recursive: true, force: true });
45
+ });
46
+ it("writes LINEAR_API_KEY to ~/.urateam/.env on success", async () => {
47
+ await selfAuthLinearCommand.parseAsync(["--port", "0"], { from: "user" });
48
+ const raw = readFileSync(join(tmp, ".env"), "utf8");
49
+ expect(raw).toContain("LINEAR_API_KEY=lin_oauth_TOKEN");
50
+ });
51
+ it("preserves unrelated keys in an existing .env", async () => {
52
+ writeFileSync(join(tmp, ".env"), "ANTHROPIC_API_KEY=sk-ant-xyz\nLINEAR_API_KEY=lin_api_old\n");
53
+ await selfAuthLinearCommand.parseAsync(["--port", "0"], { from: "user" });
54
+ const raw = readFileSync(join(tmp, ".env"), "utf8");
55
+ expect(raw).toContain("ANTHROPIC_API_KEY=sk-ant-xyz");
56
+ expect(raw).toContain("LINEAR_API_KEY=lin_oauth_TOKEN");
57
+ expect(raw).not.toContain("lin_api_old");
58
+ });
59
+ it("never logs the token to console", async () => {
60
+ const spy = vi.spyOn(console, "log").mockImplementation(() => { });
61
+ await selfAuthLinearCommand.parseAsync(["--port", "0"], { from: "user" });
62
+ const out = spy.mock.calls.flat().join("\n");
63
+ expect(out).not.toContain("lin_oauth_TOKEN");
64
+ spy.mockRestore();
65
+ });
66
+ it("logs the workspace name on success (operator-friendly confirmation)", async () => {
67
+ const spy = vi.spyOn(console, "log").mockImplementation(() => { });
68
+ await selfAuthLinearCommand.parseAsync(["--port", "0"], { from: "user" });
69
+ const out = spy.mock.calls.flat().join("\n");
70
+ expect(out).toMatch(/Test Workspace/);
71
+ spy.mockRestore();
72
+ });
73
+ it("fails when LINEAR_CLIENT_ID is missing", async () => {
74
+ delete process.env.LINEAR_CLIENT_ID;
75
+ await expect(selfAuthLinearCommand.parseAsync([], { from: "user" })).rejects.toThrow(/LINEAR_CLIENT_ID/);
76
+ });
77
+ it("fails when LINEAR_CLIENT_SECRET is missing", async () => {
78
+ delete process.env.LINEAR_CLIENT_SECRET;
79
+ await expect(selfAuthLinearCommand.parseAsync([], { from: "user" })).rejects.toThrow(/LINEAR_CLIENT_SECRET/);
80
+ });
81
+ it("rejects an out-of-range --port", async () => {
82
+ await expect(selfAuthLinearCommand.parseAsync(["--port", "999999"], { from: "user" })).rejects.toThrow(/--port must be an integer between 0 and 65535/);
83
+ await expect(selfAuthLinearCommand.parseAsync(["--port", "not-a-number"], {
84
+ from: "user",
85
+ })).rejects.toThrow(/--port must be an integer/);
86
+ });
87
+ it("fails when URATEAM_HOME does not exist (operator forgot 'ura init')", async () => {
88
+ rmSync(tmp, { recursive: true, force: true });
89
+ await expect(selfAuthLinearCommand.parseAsync([], { from: "user" })).rejects.toThrow(/ura init/i);
90
+ });
91
+ });
92
+ //# sourceMappingURL=self-auth-linear.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-auth-linear.test.js","sourceRoot":"","sources":["../../src/__tests__/self-auth-linear.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `ura self-auth-linear` — browser-based Linear OAuth flow.
3
+ *
4
+ * Preconditions:
5
+ * 1. `ura init` has been run (`$URATEAM_HOME` exists).
6
+ * 2. `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` are set (operator created
7
+ * an OAuth app in Linear's settings — see deploy/USER_LEVEL_INSTALL.md).
8
+ *
9
+ * Behavior:
10
+ * - Starts an ephemeral 127.0.0.1 HTTP server, opens the authorize URL in
11
+ * the operator's browser, verifies the HMAC-signed state on callback,
12
+ * exchanges the code for an access token, fetches workspace metadata.
13
+ * - Writes LINEAR_API_KEY=<access_token> to $URATEAM_HOME/.env, preserving
14
+ * unrelated keys.
15
+ * - Emits a `linear.oauth_completed` audit event opportunistically.
16
+ *
17
+ * The access token is never logged. The success-page HTML returned to the
18
+ * browser contains no token.
19
+ */
20
+ import { Command } from "commander";
21
+ export declare const selfAuthLinearCommand: Command;
22
+ //# sourceMappingURL=self-auth-linear.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-auth-linear.d.ts","sourceRoot":"","sources":["../../src/commands/self-auth-linear.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwDpC,eAAO,MAAM,qBAAqB,SAwE9B,CAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `ura self-auth-linear` — browser-based Linear OAuth flow.
3
+ *
4
+ * Preconditions:
5
+ * 1. `ura init` has been run (`$URATEAM_HOME` exists).
6
+ * 2. `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` are set (operator created
7
+ * an OAuth app in Linear's settings — see deploy/USER_LEVEL_INSTALL.md).
8
+ *
9
+ * Behavior:
10
+ * - Starts an ephemeral 127.0.0.1 HTTP server, opens the authorize URL in
11
+ * the operator's browser, verifies the HMAC-signed state on callback,
12
+ * exchanges the code for an access token, fetches workspace metadata.
13
+ * - Writes LINEAR_API_KEY=<access_token> to $URATEAM_HOME/.env, preserving
14
+ * unrelated keys.
15
+ * - Emits a `linear.oauth_completed` audit event opportunistically.
16
+ *
17
+ * The access token is never logged. The success-page HTML returned to the
18
+ * browser contains no token.
19
+ */
20
+ import { Command } from "commander";
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { userInfo } from "node:os";
24
+ import { createDb, logAuditEvent, linearOauthCompletedEvent, } from "@urateam/core";
25
+ import { resolveUserLevelHome, userLevelDataDir, } from "../lib/user-level-config.js";
26
+ import { runLinearOAuth } from "../lib/linear-oauth.js";
27
+ import { defaultBrowserOpen, defaultFetchTokenEndpoint, defaultFetchViewer, } from "../lib/linear-oauth-deps.js";
28
+ import { upsertEnvFile } from "../lib/env-file.js";
29
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
30
+ const DEFAULT_SCOPE = "read,write";
31
+ /**
32
+ * Fixed default port for the callback server. Linear's OAuth flow requires
33
+ * the redirect URI in the authorize request to EXACTLY match the URI
34
+ * registered in the OAuth app settings (host + port + path), so a random
35
+ * port would force the operator to re-register every time. 9898 is
36
+ * unprivileged and unlikely to collide with common dev-server ports. The
37
+ * `--port` flag overrides it when 9898 is taken — but the operator then
38
+ * has to register the new URI in Linear.
39
+ */
40
+ const DEFAULT_PORT = 9898;
41
+ async function tryEmitAuditEvent(args) {
42
+ try {
43
+ const dbPath = join(userLevelDataDir(), "urateam.db");
44
+ if (!existsSync(dbPath))
45
+ return;
46
+ const db = await createDb({ connectionString: dbPath });
47
+ const actor = `cli:${userInfo().username ?? "unknown"}`;
48
+ await logAuditEvent(db, linearOauthCompletedEvent({
49
+ workspaceId: args.workspaceId,
50
+ workspaceName: args.workspaceName,
51
+ actor,
52
+ }));
53
+ }
54
+ catch {
55
+ // Audit failure must not break the OAuth flow.
56
+ }
57
+ }
58
+ export const selfAuthLinearCommand = new Command("self-auth-linear")
59
+ .description("Browser-based Linear OAuth flow; stores the token as LINEAR_API_KEY in ~/.urateam/.env")
60
+ .option("--timeout-ms <ms>", "How long to wait for the operator to authorize (default 5 minutes)", String(DEFAULT_TIMEOUT_MS))
61
+ .option("--scope <scope>", "OAuth scopes to request (comma-separated)", DEFAULT_SCOPE)
62
+ .option("--port <port>", "Local port for the callback server (must match the redirect URI registered in Linear)", String(DEFAULT_PORT))
63
+ .action(async (opts) => {
64
+ const home = resolveUserLevelHome();
65
+ if (!existsSync(home)) {
66
+ throw new Error(`ura self-auth-linear: ${home} does not exist. Run 'ura init' first.`);
67
+ }
68
+ const clientId = process.env.LINEAR_CLIENT_ID;
69
+ const clientSecret = process.env.LINEAR_CLIENT_SECRET;
70
+ if (!clientId) {
71
+ throw new Error("ura self-auth-linear: LINEAR_CLIENT_ID is not set. Create a Linear OAuth app at https://linear.app/settings/api/applications/new and set LINEAR_CLIENT_ID + LINEAR_CLIENT_SECRET in ~/.urateam/.env before running this command.");
72
+ }
73
+ if (!clientSecret) {
74
+ throw new Error("ura self-auth-linear: LINEAR_CLIENT_SECRET is not set. See https://linear.app/settings/api/applications and copy the client secret into ~/.urateam/.env.");
75
+ }
76
+ const port = Number(opts.port);
77
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
78
+ throw new Error(`ura self-auth-linear: --port must be an integer between 0 and 65535 (got '${opts.port}')`);
79
+ }
80
+ console.log(`ura self-auth-linear: opening Linear in your browser (callback on http://127.0.0.1:${port}/callback)…`);
81
+ const result = await runLinearOAuth({
82
+ clientId,
83
+ clientSecret,
84
+ scope: opts.scope,
85
+ timeoutMs: Number(opts.timeoutMs),
86
+ port,
87
+ openBrowser: defaultBrowserOpen,
88
+ fetchTokenEndpoint: defaultFetchTokenEndpoint,
89
+ fetchViewer: defaultFetchViewer,
90
+ });
91
+ upsertEnvFile(join(home, ".env"), {
92
+ LINEAR_API_KEY: result.accessToken,
93
+ });
94
+ console.log(`ura self-auth-linear: authorized for workspace ${result.workspaceName ?? result.workspaceId}; token written to ${join(home, ".env")}.`);
95
+ await tryEmitAuditEvent({
96
+ workspaceId: result.workspaceId,
97
+ workspaceName: result.workspaceName,
98
+ });
99
+ });
100
+ //# sourceMappingURL=self-auth-linear.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-auth-linear.js","sourceRoot":"","sources":["../../src/commands/self-auth-linear.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ import { initCommand } from "./commands/init.js";
29
29
  import { repoCommand } from "./commands/repo.js";
30
30
  import { uninstallCommand } from "./commands/uninstall.js";
31
31
  import { serviceCommand } from "./commands/service.js";
32
+ import { selfAuthLinearCommand } from "./commands/self-auth-linear.js";
32
33
  const program = new Command();
33
34
  program
34
35
  .name("ura")
@@ -48,5 +49,6 @@ program.addCommand(initCommand);
48
49
  program.addCommand(repoCommand);
49
50
  program.addCommand(uninstallCommand);
50
51
  program.addCommand(serviceCommand);
52
+ program.addCommand(selfAuthLinearCommand);
51
53
  program.parse();
52
54
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,oCAAoC;AACpC,4EAA4E;AAC5E,2EAA2E;AAC3E,mEAAmE;AACnE,6EAA6E;AAC7E,oEAAoE;AACpE,IAAI,CAAC;IACH,OAAO,CAAC,WAAW,EAAE,CAAC;AACxB,CAAC;AAAC,OAAO,GAAY,EAAE,CAAC;IACtB,MAAM,IAAI,GAAI,GAA6B,EAAE,IAAI,CAAC;IAClD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CACV,qCAAqC,OAAO,CAAC,GAAG,EAAE,KAChD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,KAAK,CAAC;KACX,WAAW,CAAC,aAAa,CAAC;KAC1B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAEhC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACrC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AAEnC,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,oCAAoC;AACpC,4EAA4E;AAC5E,2EAA2E;AAC3E,mEAAmE;AACnE,6EAA6E;AAC7E,oEAAoE;AACpE,IAAI,CAAC;IACH,OAAO,CAAC,WAAW,EAAE,CAAC;AACxB,CAAC;AAAC,OAAO,GAAY,EAAE,CAAC;IACtB,MAAM,IAAI,GAAI,GAA6B,EAAE,IAAI,CAAC;IAClD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CACV,qCAAqC,OAAO,CAAC,GAAG,EAAE,KAChD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,KAAK,CAAC;KACX,WAAW,CAAC,aAAa,CAAC;KAC1B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAEhC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAClC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACjC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACrC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC;AAE1C,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function readEnvFile(path: string): Record<string, string>;
2
+ export declare function upsertEnvFile(path: string, updates: Record<string, string>): void;
3
+ //# sourceMappingURL=env-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.d.ts","sourceRoot":"","sources":["../../src/lib/env-file.ts"],"names":[],"mappings":"AAiBA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAahE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,IAAI,CA2CN"}
@@ -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":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgDpE,MAAM,YAAY,GAAG;;;;;eAKN,CAAC;AAEhB,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC;;;;KAI/B,GAAG;eACO,CAAC;AAEhB,MAAM,aAAa,GAAG,oCAAoC,CAAC;AAE3D,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAqB;IAErB,MAAM,WAAW,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAE5C,OAAO,MAAM,IAAI,OAAO,CAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC9D,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,EAAE,KAAK,EAAE,CAAC;YAChB,MAAM,CACJ,IAAI,KAAK,CACP,mEAAmE,IAAI,CAAC,SAAS,KAAK,CACvF,CACF,CAAC;QACJ,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAEnB,MAAM,QAAQ,GAAG,CACf,EAAc,EACd,GAAwB,EACxB,CAAS,EACH,EAAE;YACR,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,EAAE,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,GAAG;gBAAE,GAAG,CAAC,CAAC,CAAC,CAAC;;gBAChB,EAAE,EAAE,CAAC;QACZ,CAAC,CAAC;QAEF,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YACvC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;gBACxD,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;oBACjC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;oBACzB,OAAO;gBACT,CAAC;gBACD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;gBAC1D,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;gBACzD,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;oBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,wCAAwC,CAAC,CAAC,CAAC;oBAC9D,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAC7D,CAAC;oBACF,OAAO;gBACT,CAAC;gBACD,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,uCAAuC,CAAC,CAAC,CAAC;oBAC7D,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAC5D,CAAC;oBACF,OAAO;gBACT,CAAC;gBAED,MAAM,IAAI,GAAI,MAAO,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;gBAC1D,MAAM,WAAW,GAAG,oBAAoB,IAAI,WAAW,CAAC;gBAExD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC;oBAC1C,IAAI;oBACJ,SAAS,EAAE,IAAI,CAAC,QAAQ;oBACxB,aAAa,EAAE,IAAI,CAAC,YAAY;oBAChC,YAAY,EAAE,WAAW;oBACzB,UAAU,EAAE,oBAAoB;iBACjC,CAAC,CAAC;gBAEH,qEAAqE;gBACrE,6DAA6D;gBAC7D,oEAAoE;gBACpE,mEAAmE;gBACnE,8DAA8D;gBAC9D,yBAAyB;gBACzB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;gBACpD,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAEtB,IAAI,MAAuD,CAAC;gBAC5D,IAAI,CAAC;oBACH,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACtD,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;gBAChE,CAAC;gBAED,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,QAAQ,GAAG,IAAI,CAAC;oBAChB,YAAY,CAAC,OAAO,CAAC,CAAC;oBACtB,MAAM,EAAE,KAAK,EAAE,CAAC;oBAChB,OAAO,CAAC;wBACN,WAAW,EAAE,KAAK,CAAC,YAAY;wBAC/B,WAAW,EAAE,MAAM,CAAC,WAAW;wBAC/B,aAAa,EAAE,MAAM,CAAC,aAAa;wBACnC,KAAK,EAAE,KAAK,CAAC,KAAK;wBAClB,gBAAgB,EAAE,KAAK,CAAC,UAAU;qBACnC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,IAAI,CAAC;oBACH,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,4CAA4C,CAAC,CAAC,CAAC;gBACpE,CAAC;gBAAC,MAAM,CAAC;oBACP,+BAA+B;gBACjC,CAAC;gBACD,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,IAAI,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAC9C,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;YAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC9B,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,IAAI,KAAK,CACP,8BAA8B,IAAI,CAAC,IAAI,sBAAsB;oBAC3D,2FAA2F,CAC9F,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CACpD,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE;YAC/C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAI,MAAO,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;gBAC1D,MAAM,WAAW,GAAG,oBAAoB,IAAI,WAAW,CAAC;gBACxD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;gBACvC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACrD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;gBACtD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;gBAClD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC9C,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;gBACzC,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,QAAQ,CACN,GAAG,EAAE,GAAE,CAAC,EACR,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAChB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CACpD,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urateam/cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,8 +23,8 @@
23
23
  "commander": "^13.1.0",
24
24
  "postgres": "^3.4.0",
25
25
  "zod": "^4.3.6",
26
- "@urateam/core": "0.1.39",
27
- "@urateam/dashboard": "0.1.39"
26
+ "@urateam/core": "0.1.40",
27
+ "@urateam/dashboard": "0.1.40"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/better-sqlite3": "^7.6.0",