@gotcos/glasses-server 6.12.3 → 6.12.5
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/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/server/index.ts +2 -1
- package/server/lib/claude-run-ledger.ts +3 -2
- package/server/lib/codex-run-ledger.ts +3 -2
- package/server/lib/token-audit.ts +3 -2
- package/server/lib/token-auth.ts +26 -0
- package/server/routes/openai-compat.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.12.5
|
|
4
|
+
|
|
5
|
+
Extends the file-permission hardening to the remaining append-only logs that
|
|
6
|
+
the launch review named.
|
|
7
|
+
|
|
8
|
+
- **Run ledgers and the token-audit log are private at the file level.** The
|
|
9
|
+
Claude and Codex run ledgers and the token-audit JSONL now create with mode
|
|
10
|
+
0600 and repair existing files with chmod, matching the session-log and
|
|
11
|
+
atomic-fs writers. They already sat under the 0700 data directory; this
|
|
12
|
+
closes the file-level bit for defense in depth and covers the token-audit log
|
|
13
|
+
specifically, which lives beside the data directory rather than inside it.
|
|
14
|
+
|
|
15
|
+
## 6.12.4
|
|
16
|
+
|
|
17
|
+
Completes the public launch security review with a constant-time token check.
|
|
18
|
+
|
|
19
|
+
- **API tokens compare in constant time.** The `/api` middleware and the
|
|
20
|
+
OpenAI-compatible `/v1/chat/completions` Bearer check no longer use a plain
|
|
21
|
+
`!==` string compare, which short-circuits on the first differing byte and
|
|
22
|
+
leaks token bytes through response timing. Both now hash each side to a fixed
|
|
23
|
+
SHA-256 digest and compare with `crypto.timingSafeEqual`. Missing headers,
|
|
24
|
+
duplicated headers, and length-mismatched tokens fail closed without throwing.
|
|
25
|
+
A new `token-auth` test pins the behavior. 401 responses are otherwise
|
|
26
|
+
unchanged.
|
|
27
|
+
|
|
3
28
|
## 6.12.3
|
|
4
29
|
|
|
5
30
|
Security hardening from the public launch review, without changing app/server
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
isAllowedNetworkOrigin,
|
|
58
58
|
isTailscaleIpv4,
|
|
59
59
|
} from './lib/network-policy.js'
|
|
60
|
+
import { timingSafeTokenEqual } from './lib/token-auth.js'
|
|
60
61
|
|
|
61
62
|
const app = express()
|
|
62
63
|
const PORT = parseInt(process.env.PORT ?? '3141', 10)
|
|
@@ -128,7 +129,7 @@ app.use('/api', (req, res, next) => {
|
|
|
128
129
|
req.path === '/diag/client' ||
|
|
129
130
|
req.path === '/diag/health'
|
|
130
131
|
) return next()
|
|
131
|
-
if (req.headers['x-cos-token']
|
|
132
|
+
if (!timingSafeTokenEqual(req.headers['x-cos-token'], API_TOKEN)) {
|
|
132
133
|
return res.status(401).json({ error: 'unauthorized' })
|
|
133
134
|
}
|
|
134
135
|
next()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
5
5
|
import { dataPath } from './data-dir.js'
|
|
@@ -118,7 +118,8 @@ function appendEvent(event: ClaudeRunEvent): void {
|
|
|
118
118
|
try {
|
|
119
119
|
const path = getClaudeLedgerPath()
|
|
120
120
|
mkdirSync(dirname(path), { recursive: true })
|
|
121
|
-
appendFileSync(path, JSON.stringify(event) + '\n')
|
|
121
|
+
appendFileSync(path, JSON.stringify(event) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
122
|
+
chmodSync(path, 0o600)
|
|
122
123
|
} catch (err) {
|
|
123
124
|
console.warn('[claude-run-ledger] write skipped:', err)
|
|
124
125
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
5
5
|
import { cosBrainDir } from './launch-dir.js'
|
|
@@ -140,7 +140,8 @@ function appendEvent(event: CodexRunEvent): void {
|
|
|
140
140
|
try {
|
|
141
141
|
const path = getCodexLedgerPath()
|
|
142
142
|
mkdirSync(dirname(path), { recursive: true })
|
|
143
|
-
appendFileSync(path, JSON.stringify(event) + '\n')
|
|
143
|
+
appendFileSync(path, JSON.stringify(event) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
144
|
+
chmodSync(path, 0o600)
|
|
144
145
|
} catch (err) {
|
|
145
146
|
console.warn('[codex-run-ledger] write skipped:', err)
|
|
146
147
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Both Python (COS scripts) and TypeScript (G2 glasses) write to the same JSONL.
|
|
3
3
|
// No LLM calls. Pure file append.
|
|
4
4
|
|
|
5
|
-
import { appendFileSync } from 'node:fs'
|
|
5
|
+
import { appendFileSync, chmodSync } from 'node:fs'
|
|
6
6
|
import { resolve } from 'node:path'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
|
|
@@ -43,7 +43,8 @@ export function logTokenAudit(entry: TokenAuditEntry): void {
|
|
|
43
43
|
caller: entry.caller,
|
|
44
44
|
}
|
|
45
45
|
try {
|
|
46
|
-
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n')
|
|
46
|
+
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
47
|
+
chmodSync(AUDIT_FILE, 0o600)
|
|
47
48
|
} catch {
|
|
48
49
|
// Never let logging break the actual call
|
|
49
50
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Constant-time API-token comparison for the public server.
|
|
2
|
+
|
|
3
|
+
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compare a client-supplied API token against the expected token without
|
|
7
|
+
* leaking token bytes through response timing.
|
|
8
|
+
*
|
|
9
|
+
* A plain `provided !== expected` string compare short-circuits at the first
|
|
10
|
+
* differing byte, so an attacker measuring response latency can recover the
|
|
11
|
+
* token one byte at a time. Hashing both sides to a fixed 32-byte SHA-256
|
|
12
|
+
* digest before `timingSafeEqual` keeps the comparison constant time
|
|
13
|
+
* regardless of input length, and avoids `timingSafeEqual` throwing on
|
|
14
|
+
* length-mismatched buffers.
|
|
15
|
+
*
|
|
16
|
+
* Non-string input (missing header, duplicated header parsed as an array) and
|
|
17
|
+
* an empty expected token never match.
|
|
18
|
+
*/
|
|
19
|
+
export function timingSafeTokenEqual(provided: unknown, expected: string): boolean {
|
|
20
|
+
if (typeof provided !== 'string' || typeof expected !== 'string' || expected.length === 0) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
const a = createHash('sha256').update(provided).digest()
|
|
24
|
+
const b = createHash('sha256').update(expected).digest()
|
|
25
|
+
return timingSafeEqual(a, b)
|
|
26
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '../lib/codex-model-catalog.js'
|
|
14
14
|
import { tryInstantResponse } from '../lib/response-cache.js'
|
|
15
15
|
import crypto from 'node:crypto'
|
|
16
|
+
import { timingSafeTokenEqual } from '../lib/token-auth.js'
|
|
16
17
|
|
|
17
18
|
export const openaiCompatRouter = Router()
|
|
18
19
|
|
|
@@ -66,7 +67,7 @@ function validateAuth(req: any, res: any): boolean {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
const token = auth.slice(7)
|
|
69
|
-
if (token
|
|
70
|
+
if (!timingSafeTokenEqual(token, cosToken)) {
|
|
70
71
|
res.status(401).json({ error: { message: 'Invalid token', type: 'invalid_request_error' } })
|
|
71
72
|
return false
|
|
72
73
|
}
|