Files
stack/packages/bus/src/credentials.mjs
T
jason.woltjeandClaude Opus 5.5 38828a2cb3 feat(bus): the bus and the broker core (row 37, S2, rocko)
Rocko's round 2 candidate, approved by Darkwing (#1519 comment 26757).
build.patch 40d7e838, manifest 61519059, 24 files under packages/bus,
schema v3b (179ffe35, lead decision 60). Integration gate in a git
worktree of 942dca9e (S1 in the tree) plus the patch: bus 43/43 and
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Rulings from lead decisions 62 and 63: the human proof is cooperative in
slice 1, and a self-raised cross-role decision routes to the human.
Single-use gated approvals follow in row 43.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:15:53 -05:00

144 lines
4.8 KiB
JavaScript

import { openSync, closeSync, fstatSync, readFileSync, lstatSync, realpathSync, constants } from 'node:fs';
import { isAbsolute, relative, resolve } from 'node:path';
import { BusError } from './broker.mjs';
const deny = (code) => {
throw new BusError(code);
};
const inside = (root, path) => {
const r = relative(root, path);
return r === '' || (!r.startsWith('..') && !isAbsolute(r));
};
const day = (value) =>
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}$/.test(value) &&
!Number.isNaN(Date.parse(value)) &&
new Date(value).toISOString().slice(0, 10) === value;
export class Credentials {
#tokens = new Map();
constructor({ references = {}, repoRoots = [], dataRoot, env = process.env }) {
try {
for (const [instance, services] of Object.entries(references))
for (const [service, ref] of Object.entries(services)) {
if (!['gitea', 'vikunja'].includes(service) || !ref || typeof ref !== 'object')
deny('credential-reference');
const date = service === 'vikunja' ? ref.expires : ref.rotateBy;
if (!day(date)) deny('credential-date');
if (
(ref.service !== undefined && ref.service !== service) ||
Object.keys(ref).some(
(k) => !['service', 'file', 'env', service === 'vikunja' ? 'expires' : 'rotateBy'].includes(k),
) ||
Boolean(ref.file) === Boolean(ref.env)
)
deny('credential-reference');
let token;
if (ref.file) {
if (!isAbsolute(ref.file)) deny('credential-location');
let resolved, original;
try {
original = lstatSync(ref.file);
resolved = realpathSync(ref.file);
} catch {
deny('credential-file');
}
if (original.isSymbolicLink() || !original.isFile()) deny('credential-file');
if (
[...repoRoots, dataRoot].filter(Boolean).some((root) =>
inside(
(() => {
try {
return realpathSync(root);
} catch {
return resolve(root);
}
})(),
resolved,
),
)
)
deny('credential-location');
let fd;
try {
fd = openSync(ref.file, constants.O_RDONLY | constants.O_NOFOLLOW);
const s = fstatSync(fd);
if (
!s.isFile() ||
s.uid !== process.getuid() ||
(s.mode & 0o777) !== 0o600 ||
s.size > 16384 ||
s.ino !== original.ino ||
s.dev !== original.dev
)
deny('credential-file');
token = readFileSync(fd, 'utf8');
} catch (e) {
if (e instanceof BusError) throw e;
deny('credential-file');
} finally {
if (fd !== undefined) closeSync(fd);
}
} else {
if (!/^[A-Z_][A-Z0-9_]*$/.test(ref.env)) deny('credential-reference');
token = env[ref.env];
}
if (typeof token !== 'string' || token.length > 16384 || !/^[-A-Za-z0-9._~]+\n?$/.test(token))
deny('credential-format');
token = token.replace(/\n$/, '');
if (token.length < 16) deny('credential-format');
this.#tokens.set(instance + ':' + service, { token, date });
}
} catch (e) {
this.close();
throw e;
}
}
assertClean(value) {
let encoded;
try {
encoded = JSON.stringify(value);
} catch {
deny('invalid-request');
}
for (const { token } of this.#tokens.values()) if (encoded?.includes(token)) deny('credential-leak');
}
async use(instance, service, operation) {
const entry = this.#tokens.get(instance + ':' + service);
if (!entry) deny('credential-unavailable');
if (service === 'vikunja' && Date.now() >= Date.parse(entry.date + 'T00:00:00Z'))
deny('credential-expired');
let result;
try {
result = await operation(entry.token);
} catch {
deny('service-failed');
}
this.assertClean(result);
return result;
}
status() {
return [...this.#tokens.entries()].map(([key, { date }]) => {
const i = key.lastIndexOf(':'),
service = key.slice(i + 1),
left = Date.parse(date + 'T00:00:00Z') - Date.now();
return {
instance: key.slice(0, i),
service,
date,
state:
service === 'gitea'
? left <= 0
? 'rotation-due'
: 'valid'
: left <= 0
? 'expired'
: left < 7 * 86400000
? 'expiring'
: 'valid',
};
});
}
close() {
this.#tokens.clear();
}
}