@urateam/cli 0.1.40 → 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.
- package/dist/__tests__/env-file.test.d.ts +2 -0
- package/dist/__tests__/env-file.test.d.ts.map +1 -0
- package/dist/__tests__/env-file.test.js +89 -0
- package/dist/__tests__/env-file.test.js.map +1 -0
- package/dist/__tests__/linear-oauth.test.d.ts +2 -0
- package/dist/__tests__/linear-oauth.test.d.ts.map +1 -0
- package/dist/__tests__/linear-oauth.test.js +162 -0
- package/dist/__tests__/linear-oauth.test.js.map +1 -0
- package/dist/__tests__/oauth-state.test.d.ts +2 -0
- package/dist/__tests__/oauth-state.test.d.ts.map +1 -0
- package/dist/__tests__/oauth-state.test.js +40 -0
- package/dist/__tests__/oauth-state.test.js.map +1 -0
- package/dist/__tests__/self-auth-linear.test.d.ts +2 -0
- package/dist/__tests__/self-auth-linear.test.d.ts.map +1 -0
- package/dist/__tests__/self-auth-linear.test.js +92 -0
- package/dist/__tests__/self-auth-linear.test.js.map +1 -0
- package/dist/__tests__/service-unit.test.d.ts +2 -0
- package/dist/__tests__/service-unit.test.d.ts.map +1 -0
- package/dist/__tests__/service-unit.test.js +65 -0
- package/dist/__tests__/service-unit.test.js.map +1 -0
- package/dist/__tests__/service.test.d.ts +2 -0
- package/dist/__tests__/service.test.d.ts.map +1 -0
- package/dist/__tests__/service.test.js +193 -0
- package/dist/__tests__/service.test.js.map +1 -0
- package/dist/commands/self-auth-linear.d.ts +22 -0
- package/dist/commands/self-auth-linear.d.ts.map +1 -0
- package/dist/commands/self-auth-linear.js +100 -0
- package/dist/commands/self-auth-linear.js.map +1 -0
- package/dist/commands/service.d.ts +13 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +164 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/env-file.d.ts +3 -0
- package/dist/lib/env-file.d.ts.map +1 -0
- package/dist/lib/env-file.js +69 -0
- package/dist/lib/env-file.js.map +1 -0
- package/dist/lib/linear-oauth-deps.d.ts +14 -0
- package/dist/lib/linear-oauth-deps.d.ts.map +1 -0
- package/dist/lib/linear-oauth-deps.js +54 -0
- package/dist/lib/linear-oauth-deps.js.map +1 -0
- package/dist/lib/linear-oauth.d.ts +46 -0
- package/dist/lib/linear-oauth.d.ts.map +1 -0
- package/dist/lib/linear-oauth.js +154 -0
- package/dist/lib/linear-oauth.js.map +1 -0
- package/dist/lib/oauth-state.d.ts +9 -0
- package/dist/lib/oauth-state.d.ts.map +1 -0
- package/dist/lib/oauth-state.js +43 -0
- package/dist/lib/oauth-state.js.map +1 -0
- package/dist/lib/service-unit.d.ts +25 -0
- package/dist/lib/service-unit.d.ts.map +1 -0
- package/dist/lib/service-unit.js +67 -0
- package/dist/lib/service-unit.js.map +1 -0
- package/package.json +3 -3
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"service-unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderLaunchdPlist, renderSystemdUserUnit, } from "../lib/service-unit.js";
|
|
3
|
+
const FIXTURE = {
|
|
4
|
+
binaryPath: "/usr/local/bin/ura",
|
|
5
|
+
urateamHome: "/Users/x/.urateam",
|
|
6
|
+
envFilePath: "/Users/x/.urateam/.env",
|
|
7
|
+
stdoutPath: "/Users/x/.urateam/data/daemon.log",
|
|
8
|
+
stderrPath: "/Users/x/.urateam/data/daemon.err.log",
|
|
9
|
+
};
|
|
10
|
+
describe("renderLaunchdPlist", () => {
|
|
11
|
+
it("includes the binary path under ProgramArguments", () => {
|
|
12
|
+
const out = renderLaunchdPlist(FIXTURE);
|
|
13
|
+
expect(out).toContain("<string>/usr/local/bin/ura</string>");
|
|
14
|
+
expect(out).toContain("<string>start</string>");
|
|
15
|
+
});
|
|
16
|
+
it("propagates URATEAM_HOME via EnvironmentVariables", () => {
|
|
17
|
+
const out = renderLaunchdPlist(FIXTURE);
|
|
18
|
+
expect(out).toContain("<key>URATEAM_HOME</key>");
|
|
19
|
+
expect(out).toContain("<string>/Users/x/.urateam</string>");
|
|
20
|
+
});
|
|
21
|
+
it("sets RunAtLoad and KeepAlive", () => {
|
|
22
|
+
const out = renderLaunchdPlist(FIXTURE);
|
|
23
|
+
expect(out).toMatch(/<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
24
|
+
expect(out).toMatch(/<key>KeepAlive<\/key>\s*<true\/>/);
|
|
25
|
+
});
|
|
26
|
+
it("emits stdout and stderr log paths", () => {
|
|
27
|
+
const out = renderLaunchdPlist(FIXTURE);
|
|
28
|
+
expect(out).toContain("/Users/x/.urateam/data/daemon.log");
|
|
29
|
+
expect(out).toContain("/Users/x/.urateam/data/daemon.err.log");
|
|
30
|
+
});
|
|
31
|
+
it("starts with the XML declaration", () => {
|
|
32
|
+
expect(renderLaunchdPlist(FIXTURE).startsWith('<?xml version="1.0"')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it("is deterministic — same input produces byte-for-byte same output", () => {
|
|
35
|
+
expect(renderLaunchdPlist(FIXTURE)).toBe(renderLaunchdPlist(FIXTURE));
|
|
36
|
+
});
|
|
37
|
+
it("matches the canonical snapshot", () => {
|
|
38
|
+
expect(renderLaunchdPlist(FIXTURE)).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("renderSystemdUserUnit", () => {
|
|
42
|
+
it("uses simple service type", () => {
|
|
43
|
+
expect(renderSystemdUserUnit(FIXTURE)).toContain("Type=simple");
|
|
44
|
+
});
|
|
45
|
+
it("sets ExecStart to the binary + 'start'", () => {
|
|
46
|
+
expect(renderSystemdUserUnit(FIXTURE)).toContain("ExecStart=/usr/local/bin/ura start");
|
|
47
|
+
});
|
|
48
|
+
it("propagates URATEAM_HOME via Environment", () => {
|
|
49
|
+
const out = renderSystemdUserUnit(FIXTURE);
|
|
50
|
+
expect(out).toContain("Environment=URATEAM_HOME=/Users/x/.urateam");
|
|
51
|
+
});
|
|
52
|
+
it("sets EnvironmentFile to the resolved .env path", () => {
|
|
53
|
+
expect(renderSystemdUserUnit(FIXTURE)).toContain("EnvironmentFile=/Users/x/.urateam/.env");
|
|
54
|
+
});
|
|
55
|
+
it("uses WantedBy=default.target so user-mode systemd picks it up", () => {
|
|
56
|
+
expect(renderSystemdUserUnit(FIXTURE)).toContain("WantedBy=default.target");
|
|
57
|
+
});
|
|
58
|
+
it("sets Restart=always", () => {
|
|
59
|
+
expect(renderSystemdUserUnit(FIXTURE)).toContain("Restart=always");
|
|
60
|
+
});
|
|
61
|
+
it("matches the canonical snapshot", () => {
|
|
62
|
+
expect(renderSystemdUserUnit(FIXTURE)).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=service-unit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-unit.test.js","sourceRoot":"","sources":["../../src/__tests__/service-unit.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service.test.ts"],"names":[],"mappings":""}
|