@dev-mainsequence/codex-plugin 0.0.0-stage → 0.4.2
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/.codex-plugin/plugin.json +22 -0
- package/.mcp.json +16 -0
- package/LICENSE +21 -0
- package/NOTICE +13 -0
- package/README.md +155 -2
- package/assets/mainsequence-icon.png +0 -0
- package/assets/mainsequence-icon.svg +454 -0
- package/build-info.json +10 -0
- package/dist/hosts/codex.js +62 -0
- package/dist/hosts/codexPresentation.js +165 -0
- package/dist/hosts/discovery.js +138 -0
- package/dist/hosts/panel/panel.html +165 -0
- package/dist/package.json +1 -0
- package/dist/panel-server.js +135 -0
- package/dist/runner.js +71 -0
- package/dist/vendor/devtools/LICENSE +21 -0
- package/dist/vendor/devtools/NOTICE +12 -0
- package/dist/vendor/devtools/index.js +72 -0
- package/dist/vendor/devtools/package.json +5 -0
- package/dist/vendor/devtools/shared/accountScope.js +19 -0
- package/dist/vendor/devtools/shared/actions.js +164 -0
- package/dist/vendor/devtools/shared/agentReadiness.js +71 -0
- package/dist/vendor/devtools/shared/agents.js +240 -0
- package/dist/vendor/devtools/shared/arguments.js +142 -0
- package/dist/vendor/devtools/shared/cli.js +144 -0
- package/dist/vendor/devtools/shared/config.js +64 -0
- package/dist/vendor/devtools/shared/devcontainerJsonc.js +153 -0
- package/dist/vendor/devtools/shared/envfile.js +128 -0
- package/dist/vendor/devtools/shared/environments.js +38 -0
- package/dist/vendor/devtools/shared/hostSession.js +324 -0
- package/dist/vendor/devtools/shared/machineSession.js +338 -0
- package/dist/vendor/devtools/shared/mcp.js +421 -0
- package/dist/vendor/devtools/shared/mcpTransport.js +177 -0
- package/dist/vendor/devtools/shared/models.js +12 -0
- package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +300 -0
- package/dist/vendor/devtools/shared/organizationProjects.js +134 -0
- package/dist/vendor/devtools/shared/process.js +61 -0
- package/dist/vendor/devtools/shared/projectOperations.js +279 -0
- package/dist/vendor/devtools/shared/projects.js +390 -0
- package/dist/vendor/devtools/shared/pythonProjectStatus.js +86 -0
- package/dist/vendor/devtools/shared/pythonSdkStatus.js +26 -0
- package/dist/vendor/devtools/shared/repositories.js +215 -0
- package/dist/vendor/devtools/shared/repositoryClient.js +108 -0
- package/dist/vendor/devtools/shared/repositoryIdentity.js +116 -0
- package/dist/vendor/devtools/shared/repositoryModels.js +72 -0
- package/dist/vendor/devtools/shared/session.js +532 -0
- package/docs/adr/0002-conversational-project-mcp-client.md +59 -0
- package/docs/agents-tools-panel.md +63 -0
- package/docs/codex-verification.md +161 -0
- package/docs/conversational-mcp.md +54 -0
- package/hooks/hooks.json +17 -0
- package/package.json +50 -4
- package/skills/mainsequence-mcp/SKILL.md +40 -0
- package/skills/ms/SKILL.md +8 -0
- package/skills/ms/agents/openai.yaml +7 -0
- package/skills/ms/assets/mainsequence-icon.png +0 -0
- package/skills/ms/assets/mainsequence-icon.svg +454 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateDevcontainerJsonc = updateDevcontainerJsonc;
|
|
4
|
+
const models_1 = require("./models.js");
|
|
5
|
+
/** A small JSONC reader for targeted devcontainer edits. No evaluation or runtime dependency. */
|
|
6
|
+
function readObject(text) {
|
|
7
|
+
let at = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
8
|
+
const fail = () => { throw new models_1.ToolingError('INVALID_REQUEST', 'The existing devcontainer configuration is not a supported JSONC object. No file was changed.'); };
|
|
9
|
+
function trivia() {
|
|
10
|
+
for (;;) {
|
|
11
|
+
while (/[\t\r\n ]/.test(text[at] ?? '\0'))
|
|
12
|
+
at++;
|
|
13
|
+
if (text.startsWith('//', at)) {
|
|
14
|
+
while (at < text.length && text[at] !== '\n')
|
|
15
|
+
at++;
|
|
16
|
+
}
|
|
17
|
+
else if (text.startsWith('/*', at)) {
|
|
18
|
+
const end = text.indexOf('*/', at + 2);
|
|
19
|
+
if (end < 0)
|
|
20
|
+
fail();
|
|
21
|
+
at = end + 2;
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function string() {
|
|
28
|
+
if (text[at] !== '"')
|
|
29
|
+
return fail();
|
|
30
|
+
const start = at++;
|
|
31
|
+
while (at < text.length) {
|
|
32
|
+
const char = text[at++];
|
|
33
|
+
if (char === '\\') {
|
|
34
|
+
at++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (char === '"') {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text.slice(start, at));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return fail();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fail();
|
|
47
|
+
}
|
|
48
|
+
function node(depth) {
|
|
49
|
+
if (depth > 100)
|
|
50
|
+
return fail();
|
|
51
|
+
trivia();
|
|
52
|
+
const start = at;
|
|
53
|
+
if (text[at] === '{') {
|
|
54
|
+
at++;
|
|
55
|
+
trivia();
|
|
56
|
+
const value = Object.create(null);
|
|
57
|
+
const properties = [];
|
|
58
|
+
let trailingComma = false;
|
|
59
|
+
while (text[at] !== '}') {
|
|
60
|
+
const key = string();
|
|
61
|
+
trivia();
|
|
62
|
+
if (text[at++] !== ':' || Object.hasOwn(value, key))
|
|
63
|
+
return fail();
|
|
64
|
+
const child = node(depth + 1);
|
|
65
|
+
value[key] = child.value;
|
|
66
|
+
properties.push({ key, value: child });
|
|
67
|
+
trivia();
|
|
68
|
+
if (text[at] !== ',')
|
|
69
|
+
break;
|
|
70
|
+
at++;
|
|
71
|
+
trivia();
|
|
72
|
+
trailingComma = text[at] === '}';
|
|
73
|
+
}
|
|
74
|
+
if (text[at++] !== '}')
|
|
75
|
+
return fail();
|
|
76
|
+
return { start, end: at, value, properties, trailingComma };
|
|
77
|
+
}
|
|
78
|
+
if (text[at] === '[') {
|
|
79
|
+
at++;
|
|
80
|
+
trivia();
|
|
81
|
+
const value = [];
|
|
82
|
+
while (text[at] !== ']') {
|
|
83
|
+
value.push(node(depth + 1).value);
|
|
84
|
+
trivia();
|
|
85
|
+
if (text[at] !== ',')
|
|
86
|
+
break;
|
|
87
|
+
at++;
|
|
88
|
+
trivia();
|
|
89
|
+
}
|
|
90
|
+
if (text[at++] !== ']')
|
|
91
|
+
return fail();
|
|
92
|
+
return { start, end: at, value };
|
|
93
|
+
}
|
|
94
|
+
if (text[at] === '"')
|
|
95
|
+
return { start, value: string(), end: at };
|
|
96
|
+
for (const [literal, value] of [['true', true], ['false', false], ['null', null]]) {
|
|
97
|
+
if (text.startsWith(literal, at)) {
|
|
98
|
+
at += literal.length;
|
|
99
|
+
return { start, end: at, value };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const number = text.slice(at).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
|
|
103
|
+
if (!number || !Number.isFinite(Number(number[0])))
|
|
104
|
+
return fail();
|
|
105
|
+
at += number[0].length;
|
|
106
|
+
return { start, end: at, value: Number(number[0]) };
|
|
107
|
+
}
|
|
108
|
+
const result = node(0);
|
|
109
|
+
trivia();
|
|
110
|
+
if (at !== text.length || !result.properties)
|
|
111
|
+
return fail();
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/** Keeps every existing comment and unrelated value byte-for-byte; only edits owned fields. */
|
|
115
|
+
function updateDevcontainerJsonc(source, image) {
|
|
116
|
+
const text = source || '{}\n';
|
|
117
|
+
const root = readObject(text);
|
|
118
|
+
const object = root.value;
|
|
119
|
+
if (object.build !== undefined || object.dockerComposeFile !== undefined) {
|
|
120
|
+
throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'This devcontainer uses a build or Compose definition. An image-based rewrite would change its behavior, so the existing configuration was preserved.');
|
|
121
|
+
}
|
|
122
|
+
const desired = { image };
|
|
123
|
+
if (object.workspaceFolder == null || object.workspaceFolder === '')
|
|
124
|
+
desired.workspaceFolder = '/app';
|
|
125
|
+
if (object.workspaceMount == null || object.workspaceMount === '')
|
|
126
|
+
desired.workspaceMount = 'source=${localWorkspaceFolder},target=/app,type=bind,consistency=cached';
|
|
127
|
+
const edits = [];
|
|
128
|
+
const additions = [];
|
|
129
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
130
|
+
const current = root.properties.find(property => property.key === key);
|
|
131
|
+
if (current) {
|
|
132
|
+
if (current.value.value !== value)
|
|
133
|
+
edits.push({ start: current.value.start, end: current.value.end, value: JSON.stringify(value) });
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
additions.push(`${JSON.stringify(key)}: ${JSON.stringify(value)}`);
|
|
137
|
+
}
|
|
138
|
+
if (additions.length) {
|
|
139
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
140
|
+
const indentation = text.match(/(?:\r?\n)([\t ]+)"/)?.[1] ?? ' ';
|
|
141
|
+
const last = root.properties.at(-1);
|
|
142
|
+
const needsComma = Boolean(last && !root.trailingComma);
|
|
143
|
+
const adjacent = last?.value.end === root.end - 1;
|
|
144
|
+
if (needsComma && !adjacent)
|
|
145
|
+
edits.push({ start: last.value.end, end: last.value.end, value: ',' });
|
|
146
|
+
edits.push({ start: root.end - 1, end: root.end - 1, value: `${needsComma && adjacent ? ',' : ''}${newline}${indentation}${additions.join(`,${newline}${indentation}`)}${newline}` });
|
|
147
|
+
}
|
|
148
|
+
let result = text;
|
|
149
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
150
|
+
result = result.slice(0, edit.start) + edit.value + result.slice(edit.end);
|
|
151
|
+
readObject(result);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hasProjectCredentials = hasProjectCredentials;
|
|
37
|
+
exports.renderMachineEnv = renderMachineEnv;
|
|
38
|
+
exports.writeMachineEnv = writeMachineEnv;
|
|
39
|
+
// Machine-mode extraction of VS Code envfile.ts. No token projection or private fallback.
|
|
40
|
+
const fs = __importStar(require("fs/promises"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const models_1 = require("./models.js");
|
|
44
|
+
const repositories_1 = require("./repositories.js");
|
|
45
|
+
const session_1 = require("./session.js");
|
|
46
|
+
const RETIRED = new Set(['MAINSEQUENCE_ACCESS_TOKEN', 'MAINSEQUENCE_REFRESH_TOKEN', 'MAINSEQUENCE_RUNTIME_CREDENTIAL_ID', 'MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET', 'MAINSEQUENCE_TOKEN']);
|
|
47
|
+
// The block the VS Code extension owns in a project `.env`. In machine mode it holds the endpoint
|
|
48
|
+
// only; the extension (0.1.45 and later) writes the same block, so both tools leave the same file.
|
|
49
|
+
const MANAGED_START = '# BEGIN MAINSEQUENCE AUTH (managed by VS Code)';
|
|
50
|
+
const MANAGED_END = '# END MAINSEQUENCE AUTH';
|
|
51
|
+
// Every variable the extension manages in that file. They move into the block or, with no
|
|
52
|
+
// credential belonging in the file, are dropped.
|
|
53
|
+
const MANAGED = new Set([
|
|
54
|
+
...RETIRED,
|
|
55
|
+
'MAINSEQUENCE_ENDPOINT',
|
|
56
|
+
'MAIN_SEQUENCE_PROJECT_UID',
|
|
57
|
+
'MAIN_SEQUENCE_PROJECT_BRANCH_UID',
|
|
58
|
+
'MAINSEQUENCE_REPOSITORY_BRANCH',
|
|
59
|
+
'MAIN_SEQUENCE_ORGANIZATION_PROJECT_ENVIRONMENT_UID',
|
|
60
|
+
'MAIN_SEQUENCE_PROJECT_ID'
|
|
61
|
+
]);
|
|
62
|
+
function hasProjectCredentials(env) {
|
|
63
|
+
return [...RETIRED].some(key => key in env) || env.MAINSEQUENCE_AUTH_MODE === 'runtime_credential';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The project `.env` in machine mode: every unrelated line as it was, no credential, and the
|
|
67
|
+
* endpoint in the extension's block at the end, exactly as the extension writes it.
|
|
68
|
+
*/
|
|
69
|
+
function renderMachineEnv(text, backend) {
|
|
70
|
+
let inside = false;
|
|
71
|
+
const lines = text.replace(/\r/g, '').split('\n').filter(line => {
|
|
72
|
+
if (/^# BEGIN MAINSEQUENCE AUTH/.test(line)) {
|
|
73
|
+
inside = true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (/^# END MAINSEQUENCE AUTH/.test(line)) {
|
|
77
|
+
inside = false;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (inside)
|
|
81
|
+
return false;
|
|
82
|
+
const assignment = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
83
|
+
if (!assignment)
|
|
84
|
+
return true;
|
|
85
|
+
if (MANAGED.has(assignment[1]))
|
|
86
|
+
return false;
|
|
87
|
+
return !(assignment[1] === 'MAINSEQUENCE_AUTH_MODE' && /=\s*["']?runtime_credential["']?\s*(?:#.*)?$/.test(line));
|
|
88
|
+
});
|
|
89
|
+
while (lines.at(-1) === '')
|
|
90
|
+
lines.pop();
|
|
91
|
+
return [...lines, ...(lines.length ? [''] : []), MANAGED_START, `MAINSEQUENCE_ENDPOINT=${(0, session_1.requireBackend)(backend)}`, MANAGED_END, ''].join('\n');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Leave `root/.env` in its machine-mode form. A file that already has it is not written again:
|
|
95
|
+
* an editor holding the file with unsaved changes would then refuse to save over a newer copy.
|
|
96
|
+
*/
|
|
97
|
+
async function writeMachineEnv(host, root, backend) {
|
|
98
|
+
const envPath = path.join(root, '.env');
|
|
99
|
+
const stat = await fs.lstat(envPath).catch((error) => { if (error.code !== 'ENOENT')
|
|
100
|
+
throw error; return null; });
|
|
101
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink()))
|
|
102
|
+
throw new models_1.ToolingError('INVALID_REQUEST', 'Refusing to replace a non-regular project .env.');
|
|
103
|
+
const tracked = await host.runProcess({ executable: 'git', args: ['ls-files', '--error-unmatch', '--', '.env'], cwd: root, timeoutMs: 5000 });
|
|
104
|
+
if (tracked.exitCode === 0)
|
|
105
|
+
throw new models_1.ToolingError('INVALID_REQUEST', 'The project .env is tracked by Git. Untrack it before cleanup.');
|
|
106
|
+
if (tracked.exitCode !== 1)
|
|
107
|
+
throw new models_1.ToolingError('INVALID_GIT_CONTEXT', 'Could not check whether .env is tracked by Git.');
|
|
108
|
+
const original = stat ? await fs.readFile(envPath, 'utf8') : '';
|
|
109
|
+
const rendered = renderMachineEnv(original, backend);
|
|
110
|
+
if (stat && original === rendered)
|
|
111
|
+
return;
|
|
112
|
+
const ignore = await host.runProcess({ executable: 'git', args: ['check-ignore', '-q', '--', '.env'], cwd: root, timeoutMs: 5000 });
|
|
113
|
+
if (ignore.exitCode !== 0) {
|
|
114
|
+
const raw = await (0, repositories_1.git)(host, root, ['rev-parse', '--git-path', 'info/exclude']);
|
|
115
|
+
const exclude = path.resolve(root, raw);
|
|
116
|
+
await fs.mkdir(path.dirname(exclude), { recursive: true });
|
|
117
|
+
const current = await fs.readFile(exclude, 'utf8').catch(() => '');
|
|
118
|
+
await fs.appendFile(exclude, `${current && !current.endsWith('\n') ? '\n' : ''}/.env\n`);
|
|
119
|
+
}
|
|
120
|
+
const temporary = `${envPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
121
|
+
try {
|
|
122
|
+
await fs.writeFile(temporary, rendered, { flag: 'wx', mode: 0o600 });
|
|
123
|
+
await fs.rename(temporary, envPath);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await fs.rm(temporary, { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listEnvironments = listEnvironments;
|
|
4
|
+
const models_1 = require("./models.js");
|
|
5
|
+
const session_1 = require("./session.js");
|
|
6
|
+
/** One bounded directory reader for all hosts and domain clients. No settings writes. */
|
|
7
|
+
async function listEnvironments(session, inputBackend, signal) {
|
|
8
|
+
const backend = (0, session_1.requireBackend)(inputBackend), items = [], seen = new Set();
|
|
9
|
+
let expectedCount;
|
|
10
|
+
for (let offset = 0;;) {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const page = await session.request(backend, 'GET', `/api/v1/organization-environments/?limit=100&offset=${offset}`);
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
const rows = Array.isArray(page) ? page : page?.results;
|
|
15
|
+
const count = Array.isArray(page) ? page.length : page?.count;
|
|
16
|
+
if (!Array.isArray(rows) || !Number.isSafeInteger(count) || count < 0 || count > 10_000 || offset + rows.length > count
|
|
17
|
+
|| (expectedCount !== undefined && expectedCount !== count) || (offset > 0 && Array.isArray(page))) {
|
|
18
|
+
throw new models_1.ToolingError('API_ERROR', 'The environment pagination is invalid or changed. Refresh the directory.');
|
|
19
|
+
}
|
|
20
|
+
expectedCount = count;
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
if (typeof row?.uid !== 'string' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(row.uid)
|
|
23
|
+
|| typeof row.name !== 'string' || !row.name.trim() || /[\r\n\0]/.test(row.name)) {
|
|
24
|
+
throw new models_1.ToolingError('API_ERROR', 'The backend returned an invalid organization environment.');
|
|
25
|
+
}
|
|
26
|
+
const uid = row.uid.toLowerCase();
|
|
27
|
+
if (seen.has(uid))
|
|
28
|
+
throw new models_1.ToolingError('API_ERROR', 'Environment pagination returned duplicate rows.');
|
|
29
|
+
seen.add(uid);
|
|
30
|
+
items.push({ uid, name: row.name.trim(), isProduction: row.is_production === true });
|
|
31
|
+
}
|
|
32
|
+
offset += rows.length;
|
|
33
|
+
if (offset === count)
|
|
34
|
+
return items;
|
|
35
|
+
if (!rows.length)
|
|
36
|
+
throw new models_1.ToolingError('API_ERROR', 'Environment pagination stopped before completion.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HostAuthSession = void 0;
|
|
37
|
+
exports.authSecretKey = authSecretKey;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const config_1 = require("./config.js");
|
|
41
|
+
const machineSession_1 = require("./machineSession.js");
|
|
42
|
+
const SECRET_PREFIX = 'mainsequence.vscode.auth.v1';
|
|
43
|
+
const PROJECTIONS_KEY = 'mainsequence.auth.environmentProjections.v1';
|
|
44
|
+
const MACHINE_SESSION_AUTH_MODE = 'cli_browser';
|
|
45
|
+
// How long a session read from the machine's store is reused before the store is read again.
|
|
46
|
+
// Reading runs a program of the system, and the panel asks for the session many times in a row.
|
|
47
|
+
const MACHINE_SESSION_REUSE_MS = 5000;
|
|
48
|
+
/**
|
|
49
|
+
* The signed-in session of each backend.
|
|
50
|
+
*
|
|
51
|
+
* With a credential store of the operating system (`machineStore`), the session is the machine
|
|
52
|
+
* session the Main Sequence CLIs read and write: a login here serves them, and a login with a CLI
|
|
53
|
+
* serves the extension. Repository `.env` files then name the backend and hold no token. Without
|
|
54
|
+
* such a store the session stays in VS Code SecretStorage and its tokens are written to the
|
|
55
|
+
* registered repositories' `.env`, as before.
|
|
56
|
+
*/
|
|
57
|
+
class HostAuthSession {
|
|
58
|
+
secrets;
|
|
59
|
+
state;
|
|
60
|
+
now;
|
|
61
|
+
policy;
|
|
62
|
+
refreshes = new Map();
|
|
63
|
+
machineReads = new Map();
|
|
64
|
+
machineStore;
|
|
65
|
+
constructor(secrets, state, machineStore = null, now = Date.now, policy = {}) {
|
|
66
|
+
this.secrets = secrets;
|
|
67
|
+
this.state = state;
|
|
68
|
+
this.now = now;
|
|
69
|
+
this.policy = policy;
|
|
70
|
+
this.machineStore = machineStore;
|
|
71
|
+
}
|
|
72
|
+
/** Private host bridge. Never place these credentials in UI state, logs or action results. */
|
|
73
|
+
persistence() {
|
|
74
|
+
const owner = this;
|
|
75
|
+
return {
|
|
76
|
+
get name() { return owner.machineStore?.name ?? 'VS Code SecretStorage'; },
|
|
77
|
+
get mode() { return owner.usesMachineSession() ? 'machine' : 'host'; },
|
|
78
|
+
async read(backend) { const value = await owner.get(backend, true); return value.access || value.refresh ? value : null; },
|
|
79
|
+
async write(backend, value) { await owner.save(value, backend); },
|
|
80
|
+
async remove(backend) { await owner.clear(backend); }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Whether the session lives in the machine's credential store, shared with the CLIs. */
|
|
84
|
+
usesMachineSession() {
|
|
85
|
+
return this.machineStore !== null;
|
|
86
|
+
}
|
|
87
|
+
async initialize() {
|
|
88
|
+
const backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app');
|
|
89
|
+
this.checkMachineStore(backend);
|
|
90
|
+
this.policy.setTokenProjection?.(!this.machineStore);
|
|
91
|
+
if (this.machineStore) {
|
|
92
|
+
// Moves a session this extension saved before it used the machine's store.
|
|
93
|
+
await this.get(backend);
|
|
94
|
+
await this.removeProjectedTokens();
|
|
95
|
+
}
|
|
96
|
+
const legacy = normalizeTokens((this.policy.readLegacyTokens?.() ?? {}));
|
|
97
|
+
if (!legacy.access || !legacy.refresh)
|
|
98
|
+
return;
|
|
99
|
+
const existing = await this.get(backend);
|
|
100
|
+
if (!existing.access || !existing.refresh)
|
|
101
|
+
await this.save(legacy, backend);
|
|
102
|
+
if (!(this.policy.deleteLegacyTokens?.() ?? true))
|
|
103
|
+
throw new Error('Could not remove the legacy plaintext token store.');
|
|
104
|
+
}
|
|
105
|
+
async get(backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app'), fresh = false) {
|
|
106
|
+
if (!this.machineStore)
|
|
107
|
+
return this.storedInSecrets(backend);
|
|
108
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
109
|
+
const reused = this.machineReads.get(normalizedBackend);
|
|
110
|
+
if (!fresh && reused && reused.until > this.now())
|
|
111
|
+
return reused.tokens;
|
|
112
|
+
return this.readMachineSession(normalizedBackend);
|
|
113
|
+
}
|
|
114
|
+
async save(tokens, backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app')) {
|
|
115
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
116
|
+
const normalized = normalizeTokens(tokens);
|
|
117
|
+
if (!normalized.access || !normalized.refresh) {
|
|
118
|
+
throw new Error('Both access and refresh tokens are required.');
|
|
119
|
+
}
|
|
120
|
+
if (this.machineStore) {
|
|
121
|
+
(0, machineSession_1.saveSession)(this.machineStore, normalizedBackend, normalized);
|
|
122
|
+
await this.secrets.delete(authSecretKey(normalizedBackend));
|
|
123
|
+
return this.remember(normalizedBackend, { ...normalized, authMode: MACHINE_SESSION_AUTH_MODE });
|
|
124
|
+
}
|
|
125
|
+
await this.secrets.store(authSecretKey(normalizedBackend), JSON.stringify(normalized));
|
|
126
|
+
await this.synchronizeProjections(normalizedBackend, normalized);
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
async refresh(exchange, backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app')) {
|
|
130
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
131
|
+
const key = authSecretKey(normalizedBackend);
|
|
132
|
+
const active = this.refreshes.get(key);
|
|
133
|
+
if (active)
|
|
134
|
+
return active;
|
|
135
|
+
const operation = (async () => {
|
|
136
|
+
// Read from the store itself: a CLI may have renewed or replaced the session meanwhile.
|
|
137
|
+
const current = this.machineStore
|
|
138
|
+
? await this.readMachineSession(normalizedBackend)
|
|
139
|
+
: await this.get(normalizedBackend);
|
|
140
|
+
if (!current.refresh)
|
|
141
|
+
throw new Error('Not logged in.');
|
|
142
|
+
const refreshed = await exchange(current.refresh);
|
|
143
|
+
const access = String(refreshed.access || '').trim();
|
|
144
|
+
if (!access)
|
|
145
|
+
throw new Error('Refresh succeeded but no access token was returned.');
|
|
146
|
+
return this.save({
|
|
147
|
+
...current,
|
|
148
|
+
access,
|
|
149
|
+
refresh: String(refreshed.refresh || '').trim() || current.refresh
|
|
150
|
+
}, normalizedBackend);
|
|
151
|
+
})();
|
|
152
|
+
this.refreshes.set(key, operation);
|
|
153
|
+
try {
|
|
154
|
+
return await operation;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (this.refreshes.get(key) === operation)
|
|
158
|
+
this.refreshes.delete(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async environment(backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app')) {
|
|
162
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
163
|
+
const tokens = await this.get(normalizedBackend);
|
|
164
|
+
if (!tokens.access || !tokens.refresh)
|
|
165
|
+
throw new Error('Sign in before running this operation.');
|
|
166
|
+
return {
|
|
167
|
+
MAINSEQUENCE_ENDPOINT: normalizedBackend,
|
|
168
|
+
MAINSEQUENCE_ACCESS_TOKEN: tokens.access,
|
|
169
|
+
MAINSEQUENCE_REFRESH_TOKEN: tokens.refresh
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async registerProjection(repositoryPath, backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app')) {
|
|
173
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
174
|
+
const projections = this.projections().filter(item => item.repositoryPath !== repositoryPath);
|
|
175
|
+
projections.push({ repositoryPath, backend: normalizedBackend });
|
|
176
|
+
await this.state.update(PROJECTIONS_KEY, projections);
|
|
177
|
+
}
|
|
178
|
+
async clear(backend = (this.policy.backend?.() ?? process.env.MAINSEQUENCE_ENDPOINT ?? 'https://api.main-sequence.app')) {
|
|
179
|
+
const normalizedBackend = normalizeRequiredBackend(backend);
|
|
180
|
+
await this.secrets.delete(authSecretKey(normalizedBackend));
|
|
181
|
+
if (this.machineStore) {
|
|
182
|
+
this.machineReads.delete(normalizedBackend);
|
|
183
|
+
// Removes an entry it cannot read as well: deleting needs no consent.
|
|
184
|
+
(0, machineSession_1.clearSession)(this.machineStore, normalizedBackend);
|
|
185
|
+
}
|
|
186
|
+
const retained = [];
|
|
187
|
+
for (const projection of this.projections()) {
|
|
188
|
+
if (projection.backend !== normalizedBackend) {
|
|
189
|
+
retained.push(projection);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
// A repository keeps naming its backend when the session lives in the machine's store.
|
|
194
|
+
if (this.machineStore)
|
|
195
|
+
this.policy.writeProjection?.(projection.repositoryPath, '', '', normalizedBackend);
|
|
196
|
+
else
|
|
197
|
+
this.policy.removeProjection?.(projection.repositoryPath);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
retained.push(projection);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await this.state.update(PROJECTIONS_KEY, retained);
|
|
204
|
+
clearCanonicalProcessEnvironment(normalizedBackend);
|
|
205
|
+
if (!(this.policy.deleteLegacyTokens?.() ?? true))
|
|
206
|
+
throw new Error('Could not remove the legacy plaintext token store.');
|
|
207
|
+
}
|
|
208
|
+
/** A machine store that cannot be reached at all is not used: the extension works as before. */
|
|
209
|
+
checkMachineStore(backend) {
|
|
210
|
+
if (!this.machineStore)
|
|
211
|
+
return;
|
|
212
|
+
try {
|
|
213
|
+
(0, machineSession_1.readSession)(this.machineStore, backend);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// An entry another program wrote is replaced by the next login; the store itself works.
|
|
217
|
+
if (!(error instanceof machineSession_1.ForeignEntryError))
|
|
218
|
+
this.machineStore = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async storedInSecrets(backend) {
|
|
222
|
+
const raw = await this.secrets.get(authSecretKey(backend));
|
|
223
|
+
if (!raw)
|
|
224
|
+
return emptyTokens();
|
|
225
|
+
try {
|
|
226
|
+
return normalizeTokens(JSON.parse(raw));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return emptyTokens();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
remember(backend, tokens) {
|
|
233
|
+
this.machineReads.set(backend, { tokens, until: this.now() + MACHINE_SESSION_REUSE_MS });
|
|
234
|
+
return tokens;
|
|
235
|
+
}
|
|
236
|
+
async readMachineSession(backend) {
|
|
237
|
+
const store = this.machineStore;
|
|
238
|
+
try {
|
|
239
|
+
const session = (0, machineSession_1.readSession)(store, backend);
|
|
240
|
+
if (session)
|
|
241
|
+
return this.remember(backend, { ...session, authMode: MACHINE_SESSION_AUTH_MODE });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Unreadable is not signed in: a login replaces the entry.
|
|
245
|
+
}
|
|
246
|
+
// A session this extension saved before it used the machine's store moves there once.
|
|
247
|
+
const earlier = await this.storedInSecrets(backend);
|
|
248
|
+
if (earlier.access && earlier.refresh) {
|
|
249
|
+
try {
|
|
250
|
+
(0, machineSession_1.saveSession)(store, backend, earlier);
|
|
251
|
+
await this.secrets.delete(authSecretKey(backend));
|
|
252
|
+
return this.remember(backend, { ...earlier, authMode: MACHINE_SESSION_AUTH_MODE });
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return earlier;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return this.remember(backend, emptyTokens());
|
|
259
|
+
}
|
|
260
|
+
/** Once: repositories this extension wrote tokens into keep the backend and lose the tokens. */
|
|
261
|
+
async removeProjectedTokens() {
|
|
262
|
+
const retained = [];
|
|
263
|
+
for (const projection of this.projections()) {
|
|
264
|
+
// A repository that was deleted has nothing left to clean.
|
|
265
|
+
if (!fs.existsSync(projection.repositoryPath))
|
|
266
|
+
continue;
|
|
267
|
+
try {
|
|
268
|
+
this.policy.writeProjection?.(projection.repositoryPath, '', '', projection.backend);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
retained.push(projection);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
await this.state.update(PROJECTIONS_KEY, retained);
|
|
275
|
+
}
|
|
276
|
+
projections() {
|
|
277
|
+
const value = this.state.get(PROJECTIONS_KEY, []);
|
|
278
|
+
if (!Array.isArray(value))
|
|
279
|
+
return [];
|
|
280
|
+
return value.filter(item => Boolean(item?.repositoryPath && item?.backend));
|
|
281
|
+
}
|
|
282
|
+
async synchronizeProjections(backend, tokens) {
|
|
283
|
+
for (const projection of this.projections()) {
|
|
284
|
+
if (projection.backend !== backend)
|
|
285
|
+
continue;
|
|
286
|
+
try {
|
|
287
|
+
this.policy.writeProjection?.(projection.repositoryPath, tokens.access, tokens.refresh, backend);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// A missing or read-only checkout must not invalidate the authenticated session.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
exports.HostAuthSession = HostAuthSession;
|
|
296
|
+
function authSecretKey(backend) {
|
|
297
|
+
const normalized = normalizeRequiredBackend(backend);
|
|
298
|
+
const digest = crypto.createHash('sha256').update(normalized, 'utf8').digest('hex').slice(0, 16);
|
|
299
|
+
return `${SECRET_PREFIX}.${digest}`;
|
|
300
|
+
}
|
|
301
|
+
function normalizeRequiredBackend(backend) {
|
|
302
|
+
const normalized = (0, config_1.normalizeBackendUrl)(backend);
|
|
303
|
+
if (!normalized)
|
|
304
|
+
throw new Error('MainSequence backend URL is required.');
|
|
305
|
+
return normalized;
|
|
306
|
+
}
|
|
307
|
+
function normalizeTokens(value) {
|
|
308
|
+
return {
|
|
309
|
+
username: String(value?.username || ''),
|
|
310
|
+
access: String(value?.access || ''),
|
|
311
|
+
refresh: String(value?.refresh || ''),
|
|
312
|
+
authMode: String(value?.authMode || value?.auth_mode || 'cli_browser')
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function emptyTokens() {
|
|
316
|
+
return { username: '', access: '', refresh: '', authMode: 'cli_browser' };
|
|
317
|
+
}
|
|
318
|
+
function clearCanonicalProcessEnvironment(backend) {
|
|
319
|
+
if ((0, config_1.normalizeBackendUrl)(process.env.MAINSEQUENCE_ENDPOINT) === backend) {
|
|
320
|
+
delete process.env.MAINSEQUENCE_ENDPOINT;
|
|
321
|
+
delete process.env.MAINSEQUENCE_ACCESS_TOKEN;
|
|
322
|
+
delete process.env.MAINSEQUENCE_REFRESH_TOKEN;
|
|
323
|
+
}
|
|
324
|
+
}
|