Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ opencode.json
.omx/
tmp
tmp*
.tmp
.tmp*/
.tmp-*/
.tmp-*
Expand Down
7 changes: 6 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ The following are not treated as vulnerabilities in this repository:

## Dependency and Release Hygiene

Security override rationale (`package.json` -> `overrides`):

- `hono`: pinned to `^4.12.3` to keep builds out of the vulnerable `4.12.0-4.12.1` range reported in `GHSA-xh87-mx6m-69f3` (authentication bypass advisory).
- `rollup`: pinned to `^4.59.0` to keep the Vite and Vitest transitive graph above the vulnerable `<4.59.0` range surfaced by `npm audit`.

Before release and after dependency changes:

```bash
Expand All @@ -94,4 +99,4 @@ For non-vulnerability security questions, open a GitHub discussion.
---

This project is not affiliated with OpenAI.
For OpenAI platform security concerns, contact OpenAI directly.
For OpenAI platform security concerns, contact OpenAI directly.
2 changes: 1 addition & 1 deletion docs/releases/v0.1.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ All six PRs were audited, fixed, and unified into PR #14 before merge.

## Notes

- Legacy scoped package `@ndycode/codex-multi-auth` is migration-only.
- Legacy scoped prerelease package is migration-only.
- Canonical runtime paths are under `~/.codex/multi-auth`.
- Per-project accounts shared across linked worktrees via repo identity root.
- Settings writes use queued retry with EBUSY/EPERM/EAGAIN backoff (max 4 retries, exponential).
Expand Down
238 changes: 119 additions & 119 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,17 @@
"dependencies": {
"@openauthjs/openauth": "^0.4.3",
"@codex-ai/plugin": "file:vendor/codex-ai-plugin",
"hono": "^4.12.0",
"hono": "^4.12.3",
"zod": "^4.3.6"
},
"overrides": {
"hono": "^4.12.0",
"ajv": "^6.14.0",
"hono": "^4.12.3",
"minimatch": "^10.2.3",
"rollup": "^4.59.0",
Comment thread
ndycode marked this conversation as resolved.
"vite": "^7.3.1",
"@typescript-eslint/typescript-estree": {
"minimatch": "^9.0.5"
"minimatch": "^9.0.7"
}
}
}
44 changes: 31 additions & 13 deletions scripts/audit-dev-allowlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

import { spawnSync } from "node:child_process";

const ALLOWED_HIGH_OR_CRITICAL_PACKAGES = new Set([
"eslint",
"ajv",
"@eslint-community/eslint-utils",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
"@typescript-eslint/type-utils",
"@typescript-eslint/typescript-estree",
"@typescript-eslint/utils",
"minimatch",
const ALLOWED_HIGH_OR_CRITICAL_ADVISORY_SOURCES = new Set([
// Example: 1234567
]);

function summarizeSources(sourceIds) {
if (!Array.isArray(sourceIds) || sourceIds.length === 0) {
return "none";
}
return sourceIds.join(", ");
}

function summarizeVia(via) {
if (!Array.isArray(via)) return [];
return via
Expand All @@ -27,6 +26,19 @@ function summarizeVia(via) {
.slice(0, 5);
}

function getAdvisorySourceIds(via) {
if (!Array.isArray(via)) return [];
const sourceIds = new Set();
for (const item of via) {
if (!item || typeof item !== "object") continue;
const source = item.source;
if (typeof source === "number" && Number.isFinite(source)) {
sourceIds.add(source);
}
}
return Array.from(sourceIds);
}

const isWindows = process.platform === "win32";
const command = isWindows ? process.env.ComSpec || "cmd.exe" : "npm";
const commandArgs = isWindows
Expand Down Expand Up @@ -97,10 +109,16 @@ for (const [name, details] of Object.entries(vulnerabilities)) {
name,
severity,
via: summarizeVia(details.via),
advisorySources: getAdvisorySourceIds(details.via),
fixAvailable: details.fixAvailable ?? false,
};

if (ALLOWED_HIGH_OR_CRITICAL_PACKAGES.has(name)) {
if (
entry.advisorySources.length > 0 &&
entry.advisorySources.every((sourceId) =>
ALLOWED_HIGH_OR_CRITICAL_ADVISORY_SOURCES.has(sourceId),
)
) {
allowlisted.push(entry);
continue;
}
Expand All @@ -111,7 +129,7 @@ if (unexpected.length > 0) {
console.error("Unexpected high/critical vulnerabilities detected in dev dependency audit:");
for (const entry of unexpected) {
console.error(
`- ${entry.name} (${entry.severity}) via ${entry.via.join(", ") || "unknown"} fixAvailable=${String(entry.fixAvailable)}`,
`- ${entry.name} (${entry.severity}) advisorySources=${summarizeSources(entry.advisorySources)} via ${entry.via.join(", ") || "unknown"} fixAvailable=${String(entry.fixAvailable)}`,
);
}
process.exit(1);
Expand All @@ -121,7 +139,7 @@ if (allowlisted.length > 0) {
console.warn("Allowlisted high/critical dev vulnerabilities detected:");
for (const entry of allowlisted) {
console.warn(
`- ${entry.name} (${entry.severity}) via ${entry.via.join(", ") || "unknown"} fixAvailable=${String(entry.fixAvailable)}`,
`- ${entry.name} (${entry.severity}) advisorySources=${summarizeSources(entry.advisorySources)} via ${entry.via.join(", ") || "unknown"} fixAvailable=${String(entry.fixAvailable)}`,
);
}
console.warn("No unexpected high/critical vulnerabilities found.");
Expand Down
52 changes: 30 additions & 22 deletions test/documentation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, expect, it } from 'vitest';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
Expand Down Expand Up @@ -193,43 +192,52 @@ describe('Documentation Integrity', () => {
}
});

it('keeps CODEX_MULTI_AUTH_CONFIG_PATH fallback and env override precedence aligned with docs', () => {
it('keeps CODEX_MULTI_AUTH_CONFIG_PATH fallback and env override precedence aligned with docs', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'codex-doc-config-'));
const fallbackConfigPath = join(tempRoot, 'fallback-config.json');
const envKeys = [
'CODEX_MULTI_AUTH_DIR',
'CODEX_MULTI_AUTH_CONFIG_PATH',
'CODEX_MODE',
'HOME',
'USERPROFILE',
] as const;
const previousEnv = Object.fromEntries(
envKeys.map((key) => [key, process.env[key]]),
) as Record<(typeof envKeys)[number], string | undefined>;

try {
writeFileSync(
fallbackConfigPath,
`${JSON.stringify({ codexMode: false, toastDurationMs: 7777 }, null, 2)}\n`,
'utf-8',
);
const script = [
"import { loadPluginConfig, getCodexMode } from './dist/lib/config.js';",
'const loaded = loadPluginConfig();',
"process.stdout.write(JSON.stringify({ raw: loaded.codexMode, resolved: getCodexMode(loaded) }));",
].join('\n');
const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], {
cwd: projectRoot,
env: {
...process.env,
CODEX_MULTI_AUTH_DIR: tempRoot,
CODEX_MULTI_AUTH_CONFIG_PATH: fallbackConfigPath,
CODEX_MODE: '1',
HOME: tempRoot,
USERPROFILE: tempRoot,
},
encoding: 'utf-8',
});
const parsed = JSON.parse(output) as { raw: boolean; resolved: boolean };
expect(parsed.raw).toBe(false);
expect(parsed.resolved).toBe(true);

process.env.CODEX_MULTI_AUTH_DIR = tempRoot;
process.env.CODEX_MULTI_AUTH_CONFIG_PATH = fallbackConfigPath;
process.env.CODEX_MODE = '1';
process.env.HOME = tempRoot;
process.env.USERPROFILE = tempRoot;

const { loadPluginConfig, getCodexMode } = await import('../lib/config.js');
const loaded = loadPluginConfig();
expect(loaded.codexMode).toBe(false);
expect(getCodexMode(loaded)).toBe(true);

const configFlow = read('docs/development/CONFIG_FLOW.md');
const configGuide = read('docs/configuration.md');
expect(configFlow).toContain('Fallback file from `CODEX_MULTI_AUTH_CONFIG_PATH`');
expect(configFlow).toContain('After source selection, environment variables apply per-setting overrides.');
expect(configGuide).toContain('CODEX_MULTI_AUTH_CONFIG_PATH');
} finally {
for (const key of envKeys) {
const previousValue = previousEnv[key];
if (previousValue === undefined) {
delete process.env[key];
} else {
process.env[key] = previousValue;
}
}
rmSync(tempRoot, { recursive: true, force: true });
}
});
Expand Down