berget 2.2.7 → 2.2.8
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/.github/workflows/publish.yml +6 -6
- package/.github/workflows/test.yml +1 -1
- package/.prettierrc +5 -3
- package/dist/index.js +24 -25
- package/dist/package.json +5 -3
- package/dist/src/agents/app.js +8 -8
- package/dist/src/agents/backend.js +3 -3
- package/dist/src/agents/devops.js +8 -8
- package/dist/src/agents/frontend.js +3 -3
- package/dist/src/agents/fullstack.js +3 -3
- package/dist/src/agents/index.js +18 -18
- package/dist/src/agents/quality.js +8 -8
- package/dist/src/agents/security.js +8 -8
- package/dist/src/client.js +115 -127
- package/dist/src/commands/api-keys.js +195 -202
- package/dist/src/commands/auth.js +16 -25
- package/dist/src/commands/autocomplete.js +8 -8
- package/dist/src/commands/billing.js +10 -19
- package/dist/src/commands/chat.js +139 -170
- package/dist/src/commands/clusters.js +21 -30
- package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
- package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
- package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
- package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
- package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
- package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
- package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
- package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
- package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
- package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
- package/dist/src/commands/code/auth-sync.js +215 -228
- package/dist/src/commands/code/errors.js +15 -12
- package/dist/src/commands/code/setup.js +390 -425
- package/dist/src/commands/code.js +279 -294
- package/dist/src/commands/index.js +5 -5
- package/dist/src/commands/models.js +16 -25
- package/dist/src/commands/users.js +9 -18
- package/dist/src/constants/command-structure.js +138 -138
- package/dist/src/services/api-key-service.js +132 -152
- package/dist/src/services/auth-service.js +81 -95
- package/dist/src/services/browser-auth.js +121 -131
- package/dist/src/services/chat-service.js +369 -386
- package/dist/src/services/cluster-service.js +47 -62
- package/dist/src/services/collaborator-service.js +9 -21
- package/dist/src/services/flux-service.js +13 -25
- package/dist/src/services/helm-service.js +9 -21
- package/dist/src/services/kubectl-service.js +15 -29
- package/dist/src/utils/config-checker.js +7 -7
- package/dist/src/utils/config-loader.js +109 -109
- package/dist/src/utils/default-api-key.js +129 -139
- package/dist/src/utils/env-manager.js +55 -66
- package/dist/src/utils/error-handler.js +62 -62
- package/dist/src/utils/logger.js +74 -67
- package/dist/src/utils/markdown-renderer.js +28 -28
- package/dist/src/utils/opencode-validator.js +67 -69
- package/dist/src/utils/token-manager.js +67 -65
- package/dist/tests/commands/chat.test.js +30 -39
- package/dist/tests/commands/code.test.js +186 -195
- package/dist/tests/utils/config-loader.test.js +107 -107
- package/dist/tests/utils/env-manager.test.js +81 -90
- package/dist/tests/utils/opencode-validator.test.js +42 -41
- package/dist/vitest.config.js +1 -1
- package/eslint.config.mjs +50 -30
- package/index.ts +30 -31
- package/package.json +5 -3
- package/src/agents/app.ts +9 -9
- package/src/agents/backend.ts +4 -4
- package/src/agents/devops.ts +9 -9
- package/src/agents/frontend.ts +4 -4
- package/src/agents/fullstack.ts +4 -4
- package/src/agents/index.ts +27 -25
- package/src/agents/quality.ts +9 -9
- package/src/agents/security.ts +9 -9
- package/src/agents/types.ts +10 -10
- package/src/client.ts +85 -77
- package/src/commands/api-keys.ts +190 -185
- package/src/commands/auth.ts +15 -14
- package/src/commands/autocomplete.ts +10 -10
- package/src/commands/billing.ts +13 -12
- package/src/commands/chat.ts +145 -142
- package/src/commands/clusters.ts +20 -19
- package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
- package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
- package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
- package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
- package/src/commands/code/__tests__/fake-file-store.ts +15 -15
- package/src/commands/code/__tests__/fake-prompter.ts +86 -85
- package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
- package/src/commands/code/adapters/clack-prompter.ts +32 -30
- package/src/commands/code/adapters/fs-file-store.ts +18 -17
- package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
- package/src/commands/code/auth-sync.ts +210 -210
- package/src/commands/code/errors.ts +11 -11
- package/src/commands/code/ports/auth-services.ts +7 -7
- package/src/commands/code/ports/command-runner.ts +2 -2
- package/src/commands/code/ports/file-store.ts +3 -3
- package/src/commands/code/ports/prompter.ts +13 -13
- package/src/commands/code/setup.ts +408 -406
- package/src/commands/code.ts +288 -287
- package/src/commands/index.ts +11 -10
- package/src/commands/models.ts +19 -18
- package/src/commands/users.ts +11 -10
- package/src/constants/command-structure.ts +159 -159
- package/src/services/api-key-service.ts +85 -85
- package/src/services/auth-service.ts +55 -54
- package/src/services/browser-auth.ts +62 -62
- package/src/services/chat-service.ts +169 -170
- package/src/services/cluster-service.ts +28 -28
- package/src/services/collaborator-service.ts +6 -6
- package/src/services/flux-service.ts +17 -17
- package/src/services/helm-service.ts +11 -11
- package/src/services/kubectl-service.ts +12 -12
- package/src/types/api.d.ts +1933 -1933
- package/src/types/json.d.ts +1 -1
- package/src/utils/config-checker.ts +6 -6
- package/src/utils/config-loader.ts +130 -129
- package/src/utils/default-api-key.ts +81 -80
- package/src/utils/env-manager.ts +37 -37
- package/src/utils/error-handler.ts +64 -64
- package/src/utils/logger.ts +72 -66
- package/src/utils/markdown-renderer.ts +28 -28
- package/src/utils/opencode-validator.ts +72 -71
- package/src/utils/token-manager.ts +69 -68
- package/tests/commands/chat.test.ts +32 -31
- package/tests/commands/code.test.ts +182 -181
- package/tests/utils/config-loader.test.ts +111 -110
- package/tests/utils/env-manager.test.ts +83 -79
- package/tests/utils/opencode-validator.test.ts +43 -42
- package/tsconfig.json +2 -1
- package/vitest.config.ts +2 -2
|
@@ -1,348 +1,351 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
const vitest_1 = require("vitest");
|
|
13
4
|
const auth_sync_1 = require("../auth-sync");
|
|
5
|
+
const fake_api_key_service_1 = require("./fake-api-key-service");
|
|
6
|
+
const fake_auth_service_1 = require("./fake-auth-service");
|
|
14
7
|
const fake_file_store_1 = require("./fake-file-store");
|
|
15
8
|
const fake_prompter_1 = require("./fake-prompter");
|
|
16
|
-
const fake_auth_service_1 = require("./fake-auth-service");
|
|
17
|
-
const fake_api_key_service_1 = require("./fake-api-key-service");
|
|
18
9
|
function base64urlEncode(data) {
|
|
19
|
-
return Buffer.from(data).toString(
|
|
10
|
+
return Buffer.from(data).toString('base64url');
|
|
20
11
|
}
|
|
21
12
|
function makeJwt(payload) {
|
|
22
|
-
const header = base64urlEncode(JSON.stringify({ alg:
|
|
13
|
+
const header = base64urlEncode(JSON.stringify({ alg: 'none', typ: 'JWT' }));
|
|
23
14
|
const body = base64urlEncode(JSON.stringify(payload));
|
|
24
15
|
return `${header}.${body}.signature`;
|
|
25
16
|
}
|
|
26
|
-
const HOME =
|
|
27
|
-
const fakeCliAuth = (overrides = {}) => (
|
|
28
|
-
|
|
17
|
+
const HOME = '/home/user';
|
|
18
|
+
const fakeCliAuth = (overrides = {}) => ({
|
|
19
|
+
access_token: makeJwt({
|
|
29
20
|
exp: 9999999999999, // JWT exp in seconds (different from expires_at in ms)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
realm_access: { roles: ['default-roles-berget'] },
|
|
22
|
+
}),
|
|
23
|
+
expires_at: 9999999999999,
|
|
24
|
+
refresh_token: 'refreshtoken',
|
|
25
|
+
...overrides,
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.describe)('readCliAuth', () => {
|
|
28
|
+
(0, vitest_1.it)('returns null when auth file does not exist', async () => {
|
|
33
29
|
const files = new fake_file_store_1.FakeFileStore();
|
|
34
|
-
const result =
|
|
30
|
+
const result = await (0, auth_sync_1.readCliAuth)(files, HOME);
|
|
35
31
|
(0, vitest_1.expect)(result).toBeNull();
|
|
36
|
-
})
|
|
37
|
-
(0, vitest_1.it)(
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)('parses valid auth file', async () => {
|
|
38
34
|
const files = new fake_file_store_1.FakeFileStore();
|
|
39
35
|
const auth = fakeCliAuth();
|
|
40
|
-
files.seed(HOME +
|
|
41
|
-
const result =
|
|
36
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify(auth));
|
|
37
|
+
const result = await (0, auth_sync_1.readCliAuth)(files, HOME);
|
|
42
38
|
// The JWT's exp claim should be extracted and converted to milliseconds
|
|
43
|
-
const jwtPayload = JSON.parse(Buffer.from(auth.access_token.split(
|
|
39
|
+
const jwtPayload = JSON.parse(Buffer.from(auth.access_token.split('.')[1], 'base64url').toString());
|
|
44
40
|
const expectedAuth = {
|
|
45
41
|
access_token: auth.access_token,
|
|
46
|
-
refresh_token: auth.refresh_token,
|
|
47
42
|
expires_at: jwtPayload.exp * 1000,
|
|
43
|
+
refresh_token: auth.refresh_token,
|
|
48
44
|
};
|
|
49
45
|
(0, vitest_1.expect)(result).toEqual(expectedAuth);
|
|
50
|
-
})
|
|
51
|
-
(0, vitest_1.it)(
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)('returns null for malformed JSON', async () => {
|
|
52
48
|
const files = new fake_file_store_1.FakeFileStore();
|
|
53
|
-
files.seed(HOME +
|
|
54
|
-
const result =
|
|
49
|
+
files.seed(HOME + '/.berget/auth.json', 'not json');
|
|
50
|
+
const result = await (0, auth_sync_1.readCliAuth)(files, HOME);
|
|
55
51
|
(0, vitest_1.expect)(result).toBeNull();
|
|
56
|
-
})
|
|
57
|
-
(0, vitest_1.it)(
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)('returns null when fields are missing', async () => {
|
|
58
54
|
const files = new fake_file_store_1.FakeFileStore();
|
|
59
|
-
files.seed(HOME +
|
|
60
|
-
const result =
|
|
55
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify({ access_token: 'only' }));
|
|
56
|
+
const result = await (0, auth_sync_1.readCliAuth)(files, HOME);
|
|
61
57
|
(0, vitest_1.expect)(result).toBeNull();
|
|
62
|
-
})
|
|
58
|
+
});
|
|
63
59
|
});
|
|
64
|
-
(0, vitest_1.describe)(
|
|
65
|
-
(0, vitest_1.it)(
|
|
60
|
+
(0, vitest_1.describe)('isToolAuthenticated', () => {
|
|
61
|
+
(0, vitest_1.it)('returns false when auth file does not exist', async () => {
|
|
66
62
|
const files = new fake_file_store_1.FakeFileStore();
|
|
67
|
-
const result =
|
|
63
|
+
const result = await (0, auth_sync_1.isToolAuthenticated)(files, HOME, 'opencode');
|
|
68
64
|
(0, vitest_1.expect)(result).toBe(false);
|
|
69
|
-
})
|
|
70
|
-
(0, vitest_1.it)(
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('returns true when berget entry exists', async () => {
|
|
71
67
|
const files = new fake_file_store_1.FakeFileStore();
|
|
72
|
-
files.seed(HOME +
|
|
73
|
-
const result =
|
|
68
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ berget: { access: 'tok', type: 'oauth' } }));
|
|
69
|
+
const result = await (0, auth_sync_1.isToolAuthenticated)(files, HOME, 'opencode');
|
|
74
70
|
(0, vitest_1.expect)(result).toBe(true);
|
|
75
|
-
})
|
|
76
|
-
(0, vitest_1.it)(
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('returns false when berget entry is missing', async () => {
|
|
77
73
|
const files = new fake_file_store_1.FakeFileStore();
|
|
78
|
-
files.seed(HOME +
|
|
79
|
-
const result =
|
|
74
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ openai: { type: 'api' } }));
|
|
75
|
+
const result = await (0, auth_sync_1.isToolAuthenticated)(files, HOME, 'opencode');
|
|
80
76
|
(0, vitest_1.expect)(result).toBe(false);
|
|
81
|
-
})
|
|
82
|
-
(0, vitest_1.it)(
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)('checks correct path for pi', async () => {
|
|
83
79
|
const files = new fake_file_store_1.FakeFileStore();
|
|
84
|
-
files.seed(HOME +
|
|
85
|
-
const result =
|
|
80
|
+
files.seed(HOME + '/.pi/agent/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
|
|
81
|
+
const result = await (0, auth_sync_1.isToolAuthenticated)(files, HOME, 'pi');
|
|
86
82
|
(0, vitest_1.expect)(result).toBe(true);
|
|
87
|
-
})
|
|
83
|
+
});
|
|
88
84
|
});
|
|
89
|
-
(0, vitest_1.describe)(
|
|
90
|
-
(0, vitest_1.it)(
|
|
91
|
-
const payload = {
|
|
85
|
+
(0, vitest_1.describe)('decodeJwtPayload', () => {
|
|
86
|
+
(0, vitest_1.it)('decodes a valid JWT payload', () => {
|
|
87
|
+
const payload = { realm_access: { roles: ['admin'] }, sub: '123' };
|
|
92
88
|
const jwt = makeJwt(payload);
|
|
93
89
|
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)(jwt)).toEqual(payload);
|
|
94
90
|
});
|
|
95
|
-
(0, vitest_1.it)(
|
|
96
|
-
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)(
|
|
97
|
-
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)(
|
|
91
|
+
(0, vitest_1.it)('returns null for invalid format', () => {
|
|
92
|
+
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)('not.a')).toBeNull();
|
|
93
|
+
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)('onlyOnePart')).toBeNull();
|
|
98
94
|
});
|
|
99
|
-
(0, vitest_1.it)(
|
|
100
|
-
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)(
|
|
95
|
+
(0, vitest_1.it)('returns null for invalid base64', () => {
|
|
96
|
+
(0, vitest_1.expect)((0, auth_sync_1.decodeJwtPayload)('header.bad\.base64.signature')).toBeNull();
|
|
101
97
|
});
|
|
102
98
|
});
|
|
103
|
-
(0, vitest_1.describe)(
|
|
104
|
-
(0, vitest_1.it)(
|
|
99
|
+
(0, vitest_1.describe)('hasBergetCodeSeat', () => {
|
|
100
|
+
(0, vitest_1.it)('returns true when berget_code_seat is present', () => {
|
|
105
101
|
const token = makeJwt({
|
|
106
|
-
realm_access: { roles: [
|
|
102
|
+
realm_access: { roles: ['berget_code_seat', 'default-roles-berget'] },
|
|
107
103
|
});
|
|
108
104
|
(0, vitest_1.expect)((0, auth_sync_1.hasBergetCodeSeat)(token)).toBe(true);
|
|
109
105
|
});
|
|
110
|
-
(0, vitest_1.it)(
|
|
106
|
+
(0, vitest_1.it)('returns false when role is missing', () => {
|
|
111
107
|
const token = makeJwt({
|
|
112
|
-
realm_access: { roles: [
|
|
108
|
+
realm_access: { roles: ['default-roles-berget'] },
|
|
113
109
|
});
|
|
114
110
|
(0, vitest_1.expect)((0, auth_sync_1.hasBergetCodeSeat)(token)).toBe(false);
|
|
115
111
|
});
|
|
116
|
-
(0, vitest_1.it)(
|
|
117
|
-
const token = makeJwt({ sub:
|
|
112
|
+
(0, vitest_1.it)('returns false when realm_access is missing', () => {
|
|
113
|
+
const token = makeJwt({ sub: '123' });
|
|
118
114
|
(0, vitest_1.expect)((0, auth_sync_1.hasBergetCodeSeat)(token)).toBe(false);
|
|
119
115
|
});
|
|
120
|
-
(0, vitest_1.it)(
|
|
121
|
-
(0, vitest_1.expect)((0, auth_sync_1.hasBergetCodeSeat)(
|
|
116
|
+
(0, vitest_1.it)('returns false for invalid JWT', () => {
|
|
117
|
+
(0, vitest_1.expect)((0, auth_sync_1.hasBergetCodeSeat)('invalid')).toBe(false);
|
|
122
118
|
});
|
|
123
119
|
});
|
|
124
|
-
(0, vitest_1.describe)(
|
|
125
|
-
(0, vitest_1.it)(
|
|
120
|
+
(0, vitest_1.describe)('syncOAuthToTool', () => {
|
|
121
|
+
(0, vitest_1.it)('writes oauth tokens to opencode auth file', async () => {
|
|
126
122
|
const files = new fake_file_store_1.FakeFileStore();
|
|
127
123
|
const auth = fakeCliAuth();
|
|
128
|
-
|
|
124
|
+
await (0, auth_sync_1.syncOAuthToTool)(files, HOME, 'opencode', auth);
|
|
129
125
|
const written = files.getWrittenFiles();
|
|
130
|
-
const content = written.get(HOME +
|
|
126
|
+
const content = written.get(HOME + '/.local/share/opencode/auth.json');
|
|
131
127
|
const parsed = JSON.parse(content);
|
|
132
128
|
// The expires field should now use the JWT's exp claim (converted to milliseconds)
|
|
133
|
-
const jwtPayload = JSON.parse(Buffer.from(auth.access_token.split(
|
|
129
|
+
const jwtPayload = JSON.parse(Buffer.from(auth.access_token.split('.')[1], 'base64url').toString());
|
|
134
130
|
(0, vitest_1.expect)(parsed.berget).toEqual({
|
|
135
|
-
type: "oauth",
|
|
136
131
|
access: auth.access_token,
|
|
137
|
-
refresh: auth.refresh_token,
|
|
138
132
|
expires: jwtPayload.exp * 1000,
|
|
133
|
+
refresh: auth.refresh_token,
|
|
134
|
+
type: 'oauth',
|
|
139
135
|
});
|
|
140
|
-
})
|
|
141
|
-
(0, vitest_1.it)(
|
|
136
|
+
});
|
|
137
|
+
(0, vitest_1.it)('writes oauth tokens to pi auth file', async () => {
|
|
142
138
|
const files = new fake_file_store_1.FakeFileStore();
|
|
143
139
|
const auth = fakeCliAuth();
|
|
144
|
-
|
|
140
|
+
await (0, auth_sync_1.syncOAuthToTool)(files, HOME, 'pi', auth);
|
|
145
141
|
const written = files.getWrittenFiles();
|
|
146
|
-
const content = written.get(HOME +
|
|
142
|
+
const content = written.get(HOME + '/.pi/agent/auth.json');
|
|
147
143
|
const parsed = JSON.parse(content);
|
|
148
|
-
(0, vitest_1.expect)(parsed.berget.type).toBe(
|
|
149
|
-
})
|
|
150
|
-
(0, vitest_1.it)(
|
|
144
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('oauth');
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('merges with existing providers', async () => {
|
|
151
147
|
const files = new fake_file_store_1.FakeFileStore();
|
|
152
|
-
files.seed(HOME +
|
|
148
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ openai: { key: 'sk-openai', type: 'api' } }));
|
|
153
149
|
const auth = fakeCliAuth();
|
|
154
|
-
|
|
150
|
+
await (0, auth_sync_1.syncOAuthToTool)(files, HOME, 'opencode', auth);
|
|
155
151
|
const written = files.getWrittenFiles();
|
|
156
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
157
|
-
(0, vitest_1.expect)(parsed.openai).toEqual({
|
|
158
|
-
(0, vitest_1.expect)(parsed.berget.type).toBe(
|
|
159
|
-
})
|
|
160
|
-
(0, vitest_1.it)(
|
|
152
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
153
|
+
(0, vitest_1.expect)(parsed.openai).toEqual({ key: 'sk-openai', type: 'api' });
|
|
154
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('oauth');
|
|
155
|
+
});
|
|
156
|
+
(0, vitest_1.it)('sets 0o600 permissions on the auth file', async () => {
|
|
161
157
|
const files = new fake_file_store_1.FakeFileStore();
|
|
162
158
|
const auth = fakeCliAuth();
|
|
163
|
-
|
|
159
|
+
await (0, auth_sync_1.syncOAuthToTool)(files, HOME, 'opencode', auth);
|
|
164
160
|
const chmodCalls = files.getChmodCalls();
|
|
165
161
|
(0, vitest_1.expect)(chmodCalls).toHaveLength(1);
|
|
166
162
|
(0, vitest_1.expect)(chmodCalls[0]).toEqual({
|
|
167
|
-
path: HOME + "/.local/share/opencode/auth.json",
|
|
168
163
|
mode: 0o600,
|
|
164
|
+
path: HOME + '/.local/share/opencode/auth.json',
|
|
169
165
|
});
|
|
170
|
-
})
|
|
166
|
+
});
|
|
171
167
|
});
|
|
172
|
-
(0, vitest_1.describe)(
|
|
173
|
-
(0, vitest_1.it)('writes api key to opencode auth file with type "api"', () =>
|
|
168
|
+
(0, vitest_1.describe)('syncApiKeyToTool', () => {
|
|
169
|
+
(0, vitest_1.it)('writes api key to opencode auth file with type "api"', async () => {
|
|
174
170
|
const files = new fake_file_store_1.FakeFileStore();
|
|
175
|
-
|
|
171
|
+
await (0, auth_sync_1.syncApiKeyToTool)(files, HOME, 'opencode', 'sk_ber_test');
|
|
176
172
|
const written = files.getWrittenFiles();
|
|
177
|
-
const content = written.get(HOME +
|
|
173
|
+
const content = written.get(HOME + '/.local/share/opencode/auth.json');
|
|
178
174
|
const parsed = JSON.parse(content);
|
|
179
175
|
(0, vitest_1.expect)(parsed.berget).toEqual({
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
key: 'sk_ber_test',
|
|
177
|
+
type: 'api',
|
|
182
178
|
});
|
|
183
|
-
})
|
|
184
|
-
(0, vitest_1.it)('writes api key to pi auth file with type "api_key"', () =>
|
|
179
|
+
});
|
|
180
|
+
(0, vitest_1.it)('writes api key to pi auth file with type "api_key"', async () => {
|
|
185
181
|
const files = new fake_file_store_1.FakeFileStore();
|
|
186
|
-
|
|
182
|
+
await (0, auth_sync_1.syncApiKeyToTool)(files, HOME, 'pi', 'sk_ber_pi');
|
|
187
183
|
const written = files.getWrittenFiles();
|
|
188
|
-
const content = written.get(HOME +
|
|
184
|
+
const content = written.get(HOME + '/.pi/agent/auth.json');
|
|
189
185
|
const parsed = JSON.parse(content);
|
|
190
186
|
(0, vitest_1.expect)(parsed.berget).toEqual({
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
key: 'sk_ber_pi',
|
|
188
|
+
type: 'api_key',
|
|
193
189
|
});
|
|
194
|
-
})
|
|
195
|
-
(0, vitest_1.it)(
|
|
190
|
+
});
|
|
191
|
+
(0, vitest_1.it)('merges with existing providers', async () => {
|
|
196
192
|
const files = new fake_file_store_1.FakeFileStore();
|
|
197
|
-
files.seed(HOME +
|
|
198
|
-
|
|
193
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ anthropic: { key: 'sk-ant', type: 'api' } }));
|
|
194
|
+
await (0, auth_sync_1.syncApiKeyToTool)(files, HOME, 'opencode', 'sk_ber_test');
|
|
199
195
|
const written = files.getWrittenFiles();
|
|
200
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
201
|
-
(0, vitest_1.expect)(parsed.anthropic).toEqual({
|
|
202
|
-
})
|
|
203
|
-
(0, vitest_1.it)(
|
|
196
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
197
|
+
(0, vitest_1.expect)(parsed.anthropic).toEqual({ key: 'sk-ant', type: 'api' });
|
|
198
|
+
});
|
|
199
|
+
(0, vitest_1.it)('sets 0o600 permissions on the auth file', async () => {
|
|
204
200
|
const files = new fake_file_store_1.FakeFileStore();
|
|
205
|
-
|
|
201
|
+
await (0, auth_sync_1.syncApiKeyToTool)(files, HOME, 'opencode', 'sk_ber_test');
|
|
206
202
|
const chmodCalls = files.getChmodCalls();
|
|
207
203
|
(0, vitest_1.expect)(chmodCalls).toHaveLength(1);
|
|
208
204
|
(0, vitest_1.expect)(chmodCalls[0]).toEqual({
|
|
209
|
-
path: HOME + "/.local/share/opencode/auth.json",
|
|
210
205
|
mode: 0o600,
|
|
206
|
+
path: HOME + '/.local/share/opencode/auth.json',
|
|
211
207
|
});
|
|
212
|
-
})
|
|
208
|
+
});
|
|
213
209
|
});
|
|
214
|
-
(0, vitest_1.describe)(
|
|
215
|
-
const makeAuthDeps = (overrides = {}) => (
|
|
216
|
-
|
|
210
|
+
(0, vitest_1.describe)('configureAuth', () => {
|
|
211
|
+
const makeAuthDeps = (overrides = {}) => ({
|
|
212
|
+
apiKeyService: new fake_api_key_service_1.FakeApiKeyService('sk_ber_test'),
|
|
213
|
+
authService: new fake_auth_service_1.FakeAuthService(true),
|
|
214
|
+
files: new fake_file_store_1.FakeFileStore(),
|
|
215
|
+
homeDir: HOME,
|
|
216
|
+
prompter: new fake_prompter_1.FakePrompter([]),
|
|
217
|
+
...overrides,
|
|
218
|
+
});
|
|
219
|
+
(0, vitest_1.it)('Case A: already authenticated — chooses keep → skips flow', async () => {
|
|
217
220
|
const files = new fake_file_store_1.FakeFileStore();
|
|
218
|
-
files.seed(HOME +
|
|
219
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
221
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
|
|
222
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('keep')]);
|
|
220
223
|
const deps = makeAuthDeps({ files, prompter });
|
|
221
|
-
const result =
|
|
224
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
222
225
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
223
226
|
(0, vitest_1.expect)(deps.prompter.calls.length).toBe(1); // Only the select prompt
|
|
224
|
-
})
|
|
225
|
-
(0, vitest_1.it)(
|
|
227
|
+
});
|
|
228
|
+
(0, vitest_1.it)('Case A reconfigure: already authenticated — reconfigure with fresh browser login', async () => {
|
|
226
229
|
const files = new fake_file_store_1.FakeFileStore();
|
|
227
|
-
files.seed(HOME +
|
|
228
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
230
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
|
|
231
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('reconfigure'), (0, fake_prompter_1.select)('subscription')]);
|
|
229
232
|
const deps = makeAuthDeps({ files, prompter });
|
|
230
233
|
const authService = deps.authService;
|
|
231
|
-
const result =
|
|
234
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
232
235
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
233
236
|
// Should have called loginInteractive (no token reuse since no ~/.berget/auth.json seeded)
|
|
234
237
|
(0, vitest_1.expect)(authService.loginInteractiveCallCount).toBeGreaterThanOrEqual(1);
|
|
235
|
-
})
|
|
236
|
-
(0, vitest_1.it)(
|
|
238
|
+
});
|
|
239
|
+
(0, vitest_1.it)('Case A reconfigure: already authenticated — reconfigure with valid CLI token → skips browser', async () => {
|
|
237
240
|
const farFuture = Date.now() + 365 * 24 * 60 * 60 * 1000; // 1 year from now
|
|
238
241
|
const files = new fake_file_store_1.FakeFileStore();
|
|
239
|
-
files.seed(HOME +
|
|
240
|
-
files.seed(HOME +
|
|
241
|
-
access_token: makeJwt({ realm_access: { roles: [
|
|
242
|
-
refresh_token: "ref",
|
|
242
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
|
|
243
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify({
|
|
244
|
+
access_token: makeJwt({ exp: farFuture, realm_access: { roles: ['berget_code_seat'] } }),
|
|
243
245
|
expires_at: farFuture,
|
|
246
|
+
refresh_token: 'ref',
|
|
244
247
|
}));
|
|
245
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
248
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('reconfigure'), (0, fake_prompter_1.select)('subscription')]);
|
|
246
249
|
const authService = new fake_auth_service_1.FakeAuthService(true);
|
|
247
|
-
const deps = makeAuthDeps({ files, prompter
|
|
248
|
-
const result =
|
|
250
|
+
const deps = makeAuthDeps({ authService, files, prompter });
|
|
251
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
249
252
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
250
253
|
// Should NOT have called loginInteractive since token was reused
|
|
251
254
|
(0, vitest_1.expect)(authService.loginInteractiveCallCount).toBe(0);
|
|
252
|
-
})
|
|
253
|
-
(0, vitest_1.it)(
|
|
255
|
+
});
|
|
256
|
+
(0, vitest_1.it)('Case B: login success + berget_code_seat → chooses subscription', async () => {
|
|
254
257
|
const files = new fake_file_store_1.FakeFileStore();
|
|
255
258
|
const jwt = makeJwt({
|
|
256
|
-
realm_access: { roles: ["berget_code_seat"] },
|
|
257
259
|
exp: 9999999999999,
|
|
260
|
+
realm_access: { roles: ['berget_code_seat'] },
|
|
258
261
|
});
|
|
259
|
-
files.seed(HOME +
|
|
262
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify({
|
|
260
263
|
access_token: jwt,
|
|
261
|
-
refresh_token: "ref",
|
|
262
264
|
expires_at: 9999999999999,
|
|
265
|
+
refresh_token: 'ref',
|
|
263
266
|
}));
|
|
264
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
267
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('subscription')]);
|
|
265
268
|
const deps = makeAuthDeps({ files, prompter });
|
|
266
|
-
const result =
|
|
269
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
267
270
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
268
271
|
const written = files.getWrittenFiles();
|
|
269
|
-
(0, vitest_1.expect)(written.has(HOME +
|
|
270
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
271
|
-
(0, vitest_1.expect)(parsed.berget.type).toBe(
|
|
272
|
-
})
|
|
273
|
-
(0, vitest_1.it)(
|
|
272
|
+
(0, vitest_1.expect)(written.has(HOME + '/.local/share/opencode/auth.json')).toBe(true);
|
|
273
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
274
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('oauth');
|
|
275
|
+
});
|
|
276
|
+
(0, vitest_1.it)('Case B variant: login success + seat → chooses api_key', async () => {
|
|
274
277
|
const files = new fake_file_store_1.FakeFileStore();
|
|
275
278
|
const jwt = makeJwt({
|
|
276
|
-
realm_access: { roles: ["berget_code_seat"] },
|
|
277
279
|
exp: 9999999999999,
|
|
280
|
+
realm_access: { roles: ['berget_code_seat'] },
|
|
278
281
|
});
|
|
279
|
-
files.seed(HOME +
|
|
282
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify({
|
|
280
283
|
access_token: jwt,
|
|
281
|
-
refresh_token: "ref",
|
|
282
284
|
expires_at: 9999999999999,
|
|
285
|
+
refresh_token: 'ref',
|
|
283
286
|
}));
|
|
284
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
287
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('api_key')]);
|
|
285
288
|
const deps = makeAuthDeps({ files, prompter });
|
|
286
|
-
const result =
|
|
289
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
287
290
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
288
291
|
const written = files.getWrittenFiles();
|
|
289
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
290
|
-
(0, vitest_1.expect)(parsed.berget.type).toBe(
|
|
291
|
-
(0, vitest_1.expect)(parsed.berget.key).toBe(
|
|
292
|
-
})
|
|
293
|
-
(0, vitest_1.it)(
|
|
292
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
293
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('api');
|
|
294
|
+
(0, vitest_1.expect)(parsed.berget.key).toBe('sk_ber_test');
|
|
295
|
+
});
|
|
296
|
+
(0, vitest_1.it)('Case C: login success + no seat → creates api key', async () => {
|
|
294
297
|
const files = new fake_file_store_1.FakeFileStore();
|
|
295
298
|
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.confirm)(true)]);
|
|
296
|
-
const deps = makeAuthDeps({
|
|
297
|
-
const result =
|
|
299
|
+
const deps = makeAuthDeps({ authService: new fake_auth_service_1.FakeAuthService(true, false), files, prompter });
|
|
300
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
298
301
|
(0, vitest_1.expect)(result.authenticated).toBe(true);
|
|
299
302
|
const written = files.getWrittenFiles();
|
|
300
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
301
|
-
(0, vitest_1.expect)(parsed.berget.type).toBe(
|
|
302
|
-
(0, vitest_1.expect)(parsed.berget.key).toBe(
|
|
303
|
-
})
|
|
304
|
-
(0, vitest_1.it)(
|
|
303
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
304
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('api');
|
|
305
|
+
(0, vitest_1.expect)(parsed.berget.key).toBe('sk_ber_test');
|
|
306
|
+
});
|
|
307
|
+
(0, vitest_1.it)('Case D: login success + no seat → declines api key', async () => {
|
|
305
308
|
const files = new fake_file_store_1.FakeFileStore();
|
|
306
309
|
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.confirm)(false)]);
|
|
307
|
-
const deps = makeAuthDeps({
|
|
308
|
-
const result =
|
|
310
|
+
const deps = makeAuthDeps({ authService: new fake_auth_service_1.FakeAuthService(true, false), files, prompter });
|
|
311
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
309
312
|
(0, vitest_1.expect)(result.authenticated).toBe(false);
|
|
310
|
-
(0, vitest_1.expect)(files.getWrittenFiles().has(HOME +
|
|
311
|
-
})
|
|
312
|
-
(0, vitest_1.it)(
|
|
313
|
+
(0, vitest_1.expect)(files.getWrittenFiles().has(HOME + '/.local/share/opencode/auth.json')).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
(0, vitest_1.it)('Case E: login fails', async () => {
|
|
313
316
|
const files = new fake_file_store_1.FakeFileStore();
|
|
314
317
|
const authService = new fake_auth_service_1.FakeAuthService(false);
|
|
315
|
-
const deps = makeAuthDeps({
|
|
316
|
-
const result =
|
|
318
|
+
const deps = makeAuthDeps({ authService, files });
|
|
319
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
317
320
|
(0, vitest_1.expect)(result.authenticated).toBe(false);
|
|
318
|
-
})
|
|
319
|
-
(0, vitest_1.it)(
|
|
321
|
+
});
|
|
322
|
+
(0, vitest_1.it)('fails authentication when jwt decode fails', async () => {
|
|
320
323
|
const prompter = new fake_prompter_1.FakePrompter([]);
|
|
321
324
|
const deps = makeAuthDeps({
|
|
322
|
-
prompter,
|
|
323
325
|
authService: new fake_auth_service_1.FakeAuthService(true, true, false), // valid login, has seat, but invalid token
|
|
326
|
+
prompter,
|
|
324
327
|
});
|
|
325
|
-
const result =
|
|
328
|
+
const result = await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
326
329
|
(0, vitest_1.expect)(result.authenticated).toBe(false); // Should fail due to invalid JWT
|
|
327
330
|
const written = deps.files.getWrittenFiles();
|
|
328
331
|
(0, vitest_1.expect)(written.size).toBe(0); // No files should be written
|
|
329
|
-
})
|
|
330
|
-
(0, vitest_1.it)(
|
|
332
|
+
});
|
|
333
|
+
(0, vitest_1.it)('preserves existing providers during sync', async () => {
|
|
331
334
|
const files = new fake_file_store_1.FakeFileStore();
|
|
332
|
-
files.seed(HOME +
|
|
333
|
-
files.seed(HOME +
|
|
335
|
+
files.seed(HOME + '/.local/share/opencode/auth.json', JSON.stringify({ openai: { key: 'sk-openai', type: 'api' } }));
|
|
336
|
+
files.seed(HOME + '/.berget/auth.json', JSON.stringify({
|
|
334
337
|
access_token: makeJwt({
|
|
335
|
-
realm_access: { roles: [
|
|
338
|
+
realm_access: { exp: 9999999999999, roles: ['berget_code_seat'] },
|
|
336
339
|
}),
|
|
337
|
-
refresh_token: "ref",
|
|
338
340
|
expires_at: 9999999999999,
|
|
341
|
+
refresh_token: 'ref',
|
|
339
342
|
}));
|
|
340
|
-
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(
|
|
343
|
+
const prompter = new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('subscription')]);
|
|
341
344
|
const deps = makeAuthDeps({ files, prompter });
|
|
342
|
-
|
|
345
|
+
await (0, auth_sync_1.configureAuth)(deps, 'opencode');
|
|
343
346
|
const written = files.getWrittenFiles();
|
|
344
|
-
const parsed = JSON.parse(written.get(HOME +
|
|
345
|
-
(0, vitest_1.expect)(parsed.openai).toEqual({
|
|
347
|
+
const parsed = JSON.parse(written.get(HOME + '/.local/share/opencode/auth.json'));
|
|
348
|
+
(0, vitest_1.expect)(parsed.openai).toEqual({ key: 'sk-openai', type: 'api' });
|
|
346
349
|
(0, vitest_1.expect)(parsed.berget).toBeDefined();
|
|
347
|
-
})
|
|
350
|
+
});
|
|
348
351
|
});
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
exports.FakeApiKeyService = void 0;
|
|
13
4
|
class FakeApiKeyService {
|
|
5
|
+
_key;
|
|
14
6
|
constructor(key) {
|
|
15
7
|
this._key = key;
|
|
16
8
|
}
|
|
17
|
-
create(_options) {
|
|
18
|
-
return
|
|
19
|
-
return { key: this._key };
|
|
20
|
-
});
|
|
9
|
+
async create(_options) {
|
|
10
|
+
return { key: this._key };
|
|
21
11
|
}
|
|
22
12
|
}
|
|
23
13
|
exports.FakeApiKeyService = FakeApiKeyService;
|