Release 0.7.0

This commit is contained in:
hnzio committed 2026-10-25 18:40:01 +01:00
1 parent d586fbd8ce
commit 6d96900d81
27 files changed
+2060 -25

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
import { createHash, randomBytes, randomUUID } from 'node:crypto';
import type { AuthUser } from './auth.js';
import { getDatabase } from './database.js';
export type TechnicalPermission = 'read' | 'read_write';
export type TechnicalActor = { id: string; name: string; permissions: TechnicalPermission };
function tokenHash(token: string) {
return createHash('sha256').update(token).digest('hex');
}
export function listTechnicalAccesses() {
return getDatabase()
.prepare(
`SELECT id, name, permissions, created_at_utc AS createdAtUtc,
last_used_at_utc AS lastUsedAtUtc, revoked_at_utc AS revokedAtUtc FROM technical_accesses ORDER BY name`
)
.all() as {
id: string;
name: string;
permissions: TechnicalPermission;
createdAtUtc: string;
lastUsedAtUtc: string | null;
revokedAtUtc: string | null;
}[];
}
export function createTechnicalAccess(actor: AuthUser, nameValue: string, permissions: string) {
const name = nameValue.trim();
if (!name || name.length > 120 || !['read', 'read_write'].includes(permissions))
throw new Error('INVALID_ACCESS');
const token = randomBytes(32).toString('base64url');
const id = randomUUID();
const now = new Date().toISOString();
getDatabase()
.prepare(
`INSERT INTO technical_accesses
(id, name, token_hash, permissions, created_by_user_id, created_at_utc) VALUES (?, ?, ?, ?, ?, ?)`
)
.run(id, name, tokenHash(token), permissions, actor.id, now);
auditTechnical(
{ id: actor.id, name: actor.displayName, permissions: 'read_write' },
'technical_access.create',
'technical_access',
id,
'user'
);
return { id, token };
}
export function revokeTechnicalAccess(actor: AuthUser, id: string) {
const result = getDatabase()
.prepare(
'UPDATE technical_accesses SET revoked_at_utc = ? WHERE id = ? AND revoked_at_utc IS NULL'
)
.run(new Date().toISOString(), id);
if (!result.changes) throw new Error('NOT_FOUND');
auditTechnical(
{ id: actor.id, name: actor.displayName, permissions: 'read_write' },
'technical_access.revoke',
'technical_access',
id,
'user'
);
}
export function authenticateTechnical(request: Request, write = false): TechnicalActor {
const authorization = request.headers.get('authorization') ?? '';
const token = authorization.startsWith('Bearer ') ? authorization.slice(7).trim() : '';
if (!token) throw new Error('API_UNAUTHORIZED');
const row = getDatabase()
.prepare(
`SELECT id, name, permissions FROM technical_accesses
WHERE token_hash = ? AND revoked_at_utc IS NULL`
)
.get(tokenHash(token)) as TechnicalActor | undefined;
if (!row) throw new Error('API_UNAUTHORIZED');
if (write && row.permissions !== 'read_write') throw new Error('API_FORBIDDEN');
getDatabase()
.prepare('UPDATE technical_accesses SET last_used_at_utc = ? WHERE id = ?')
.run(new Date().toISOString(), row.id);
return row;
}
export function auditTechnical(
actor: TechnicalActor,
action: string,
targetType: string,
targetId: string,
actorType: 'technical' | 'user' = 'technical'
) {
getDatabase()
.prepare(
`INSERT INTO audit_events (id, actor_type, actor_id, action, target_type, target_id, created_at_utc)
VALUES (?, ?, ?, ?, ?, ?, ?)`
)
.run(
randomUUID(),
actorType,
actor.id,
action.slice(0, 120),
targetType.slice(0, 80),
targetId.slice(0, 200),
new Date().toISOString()
);
}
export function idempotentResponse(
actor: TechnicalActor,
request: Request,
body: unknown,
operation: () => { status: number; body: unknown }
) {
const key = request.headers.get('idempotency-key')?.trim() ?? '';
if (!key || key.length > 200) throw new Error('API_IDEMPOTENCY_REQUIRED');
const hash = createHash('sha256')
.update(`${request.method} ${new URL(request.url).pathname}\n${JSON.stringify(body)}`)
.digest('hex');
const database = getDatabase();
return database.transaction(() => {
const existing = database
.prepare(
'SELECT request_hash, status_code, response_json FROM api_idempotency WHERE access_id = ? AND request_key = ?'
)
.get(actor.id, key) as
{ request_hash: string; status_code: number; response_json: string } | undefined;
if (existing) {
if (existing.request_hash !== hash) throw new Error('API_IDEMPOTENCY_CONFLICT');
return {
status: existing.status_code,
body: JSON.parse(existing.response_json),
duplicate: true
};
}
const result = operation();
database
.prepare(
`INSERT INTO api_idempotency
(access_id, request_key, request_hash, status_code, response_json, created_at_utc) VALUES (?, ?, ?, ?, ?, ?)`
)
.run(
actor.id,
key,
hash,
result.status,
JSON.stringify(result.body),
new Date().toISOString()
);
return { ...result, duplicate: false };
})();
}