const OPENFGA_MODEL_NAME = "hwlab-v02-openfga-authorization-v1"; const OPENFGA_CONTRACT_VERSION = "hwlab-openfga-authorization-v1"; const DEFAULT_TIMEOUT_MS = 1200; const SUPPORTED_MODES = new Set(["off", "enforce"]); export const HWLAB_TOOL_IDS = Object.freeze(["hwpod", "unidesk_ssh", "trans_cmd", "github_pr"]); export function createOpenFgaAuthorizer(options = {}) { return new OpenFgaAuthorizer(options); } export function openFgaUser(userId) { return `user:${sanitizeObjectId(userId)}`; } export function openFgaObject(type, id) { return `${sanitizeType(type)}:${sanitizeObjectId(id)}`; } export class OpenFgaAuthorizer { constructor({ env = process.env, fetchImpl = fetch, now = () => new Date().toISOString(), configStore = null } = {}) { this.env = env; this.fetchImpl = fetchImpl; this.now = now; this.configStore = configStore; this.mode = normalizeMode(env.HWLAB_OPENFGA_MODE); this.apiUrl = normalizeBaseUrl(env.HWLAB_OPENFGA_API_URL); this.token = textOr(env.HWLAB_OPENFGA_AUTHN_TOKEN ?? env.OPENFGA_API_TOKEN, ""); this.storeName = textOr(env.HWLAB_OPENFGA_STORE_NAME, "hwlab-v02"); this.timeoutMs = boundedTimeout(env.HWLAB_OPENFGA_TIMEOUT_MS); this.bootstrapEnabled = textOr(env.HWLAB_OPENFGA_BOOTSTRAP, "1") !== "0"; this.storeId = textOr(env.HWLAB_OPENFGA_STORE_ID ?? env.FGA_STORE_ID, ""); this.modelId = textOr(env.HWLAB_OPENFGA_MODEL_ID ?? env.FGA_MODEL_ID, ""); this.readyCache = null; } async describe({ refresh = false } = {}) { const readiness = await this.ensureReady({ refresh }); return { contractVersion: OPENFGA_CONTRACT_VERSION, mode: this.mode, configured: Boolean(this.apiUrl), ready: readiness.ready, status: readiness.ready ? "ready" : this.mode === "off" ? "disabled" : "degraded", apiUrlConfigured: Boolean(this.apiUrl), storeName: this.storeName, storeId: redactedId(readiness.storeId || this.storeId), modelId: redactedId(readiness.modelId || this.modelId), degradedReason: readiness.degradedReason, valuesRedacted: true }; } async check({ actor = null, userId = "", relation = "", object = "" } = {}) { const actorId = textOr(userId, actor?.id ?? ""); const rel = textOr(relation, ""); const obj = textOr(object, ""); if (!actorId || !rel || !obj) return decision({ mode: this.mode, allowed: false, decisionSource: "invalid-request", relation: rel, object: obj, userId: actorId }); if (this.mode === "off") return decision({ mode: this.mode, allowed: false, decisionSource: "openfga-disabled", relation: rel, object: obj, userId: actorId, degradedReason: "openfga_disabled" }); const readiness = await this.ensureReady(); if (!readiness.ready) { return decision({ mode: this.mode, allowed: false, fgaAllowed: null, decisionSource: "openfga-degraded", relation: rel, object: obj, userId: actorId, degradedReason: readiness.degradedReason }); } const fga = await this.checkOpenFga({ user: openFgaUser(actorId), relation: rel, object: obj, readiness }); if (fga.allowed) { return decision({ mode: this.mode, allowed: true, fgaAllowed: true, decisionSource: "openfga", relation: rel, object: obj, userId: actorId, storeId: readiness.storeId, modelId: readiness.modelId, degradedReason: null }); } const systemAdminObject = openFgaObject("system", "hwlab"); if (!(rel === "admin" && obj === systemAdminObject)) { const admin = await this.checkOpenFga({ user: openFgaUser(actorId), relation: "admin", object: systemAdminObject, readiness }); if (admin.allowed) { return decision({ mode: this.mode, allowed: true, fgaAllowed: true, decisionSource: "openfga-admin", relation: rel, object: obj, userId: actorId, storeId: readiness.storeId, modelId: readiness.modelId, degradedReason: null }); } } return decision({ mode: this.mode, allowed: false, fgaAllowed: false, decisionSource: "openfga", relation: rel, object: obj, userId: actorId, storeId: readiness.storeId, modelId: readiness.modelId, degradedReason: fga.error ?? null }); } async writeTuple({ userId, relation, object } = {}) { return this.writeTuples({ writes: [{ user: openFgaUser(userId), relation: textOr(relation, ""), object: textOr(object, "") }] }); } async deleteTuple({ userId, relation, object } = {}) { return this.writeTuples({ deletes: [{ user: openFgaUser(userId), relation: textOr(relation, ""), object: textOr(object, "") }] }); } async writeTuples({ writes = [], deletes = [] } = {}) { const sanitizedWrites = writes.map(sanitizeTuple).filter(Boolean); const sanitizedDeletes = deletes.map(sanitizeTuple).filter(Boolean); if (this.mode === "off") { return { ok: false, mode: this.mode, status: 503, error: { code: "openfga_disabled", message: "OpenFGA is disabled" }, written: 0, deleted: 0 }; } const readiness = await this.ensureReady(); if (!readiness.ready) { return { ok: false, mode: this.mode, status: 503, error: { code: "openfga_degraded", message: readiness.degradedReason }, written: 0, deleted: 0 }; } const body = { ...(sanitizedWrites.length > 0 ? { writes: { tuple_keys: sanitizedWrites, on_duplicate: "ignore" } } : {}), ...(sanitizedDeletes.length > 0 ? { deletes: { tuple_keys: sanitizedDeletes } } : {}), authorization_model_id: readiness.modelId }; const result = await this.request(`/stores/${encodeURIComponent(readiness.storeId)}/write`, { method: "POST", body }); return { ok: result.ok, mode: this.mode, status: result.status, written: sanitizedWrites.length, deleted: sanitizedDeletes.length, storeId: redactedId(readiness.storeId), modelId: redactedId(readiness.modelId), error: result.ok ? null : { code: "openfga_write_failed", message: result.message } }; } async ensureReady({ refresh = false } = {}) { if (this.mode === "off") return { ready: false, storeId: this.storeId, modelId: this.modelId, degradedReason: "openfga_disabled" }; if (!this.apiUrl) return { ready: false, storeId: "", modelId: "", degradedReason: "openfga_api_url_missing" }; if (this.readyCache && !refresh) return this.readyCache; const health = await this.request("/healthz", { method: "GET", allowNotJson: true }); if (!health.ok) return this.cacheReady({ ready: false, storeId: this.storeId, modelId: this.modelId, degradedReason: `openfga_health_failed:${health.status}` }); let storeId = this.storeId || await this.getConfigValue("openfga.storeId"); if (!storeId && this.bootstrapEnabled) { const created = await this.request("/stores", { method: "POST", body: { name: this.storeName } }); if (created.ok) { storeId = textOr(created.body?.id, ""); if (storeId) await this.setConfigValue("openfga.storeId", storeId); } } if (!storeId) return this.cacheReady({ ready: false, storeId: "", modelId: "", degradedReason: "openfga_store_id_missing" }); this.storeId = storeId; let modelId = this.modelId || await this.getConfigValue("openfga.modelId"); if (!modelId && this.bootstrapEnabled) { const written = await this.request(`/stores/${encodeURIComponent(storeId)}/authorization-models`, { method: "POST", body: HWLAB_AUTHORIZATION_MODEL }); if (written.ok) { modelId = textOr(written.body?.authorization_model_id ?? written.body?.id, ""); if (modelId) await this.setConfigValue("openfga.modelId", modelId); } else { return this.cacheReady({ ready: false, storeId, modelId: "", degradedReason: `openfga_model_write_failed:${written.status}` }); } } if (!modelId) return this.cacheReady({ ready: false, storeId, modelId: "", degradedReason: "openfga_model_id_missing" }); this.modelId = modelId; return this.cacheReady({ ready: true, storeId, modelId, degradedReason: null }); } cacheReady(value) { this.readyCache = value; return value; } async checkOpenFga({ user, relation, object, readiness }) { const result = await this.request(`/stores/${encodeURIComponent(readiness.storeId)}/check`, { method: "POST", body: { tuple_key: { user, relation, object }, authorization_model_id: readiness.modelId } }); if (!result.ok) return { allowed: false, error: `openfga_check_failed:${result.status}` }; return { allowed: result.body?.allowed === true, error: null }; } async request(path, { method = "GET", body = null, allowNotJson = false } = {}) { const headers = { accept: "application/json" }; if (body !== null) headers["content-type"] = "application/json"; if (this.token) headers.authorization = `Bearer ${this.token}`; try { const response = await fetchWithTimeout(this.fetchImpl, `${this.apiUrl}${path}`, { method, headers, ...(body !== null ? { body: JSON.stringify(body) } : {}) }, this.timeoutMs); const text = await response.text(); const json = allowNotJson ? parseJson(text, {}) : parseJson(text, {}); return { ok: response.status < 400, status: response.status, body: json, message: textOr(json?.message ?? json?.error_description ?? json?.error, text) }; } catch (error) { return { ok: false, status: 0, body: {}, message: error?.message ?? String(error) }; } } async getConfigValue(key) { return textOr(await this.configStore?.getAccessConfig?.(key), ""); } async setConfigValue(key, value) { await this.configStore?.setAccessConfig?.({ key, value, updatedAt: this.now() }); } } const HWLAB_AUTHORIZATION_MODEL = Object.freeze({ schema_version: "1.1", type_definitions: [ { type: "user" }, directTypeDefinition("system", ["admin", "access_manager", "can_manage_users", "can_manage_tools"]), directTypeDefinition("agent_session", ["owner", "viewer", "operator"]), directTypeDefinition("tool", ["can_use"]) ] }); function directTypeDefinition(type, relations) { return { type, relations: Object.fromEntries(relations.map((relation) => [relation, { this: {} }])), metadata: { relations: Object.fromEntries(relations.map((relation) => [relation, { directly_related_user_types: [{ type: "user" }] }])) } }; } function decision(input = {}) { return { contractVersion: OPENFGA_CONTRACT_VERSION, mode: input.mode, allowed: input.allowed === true, fgaAllowed: input.fgaAllowed ?? null, mismatch: input.mismatch === true, decisionSource: textOr(input.decisionSource, "unknown"), userId: textOr(input.userId, ""), relation: textOr(input.relation, ""), object: textOr(input.object, ""), storeId: redactedId(input.storeId), modelId: redactedId(input.modelId), degradedReason: input.degradedReason ?? null, valuesRedacted: true }; } function sanitizeTuple(tuple = {}) { const user = textOr(tuple.user, ""); const relation = textOr(tuple.relation, ""); const object = textOr(tuple.object, ""); if (!user || !relation || !object) return null; return { user, relation, object }; } function normalizeMode(value) { const mode = textOr(value, "off").toLowerCase(); if (mode === "shadow") return "enforce"; return SUPPORTED_MODES.has(mode) ? mode : "off"; } function sanitizeType(value) { return textOr(value, "object").replace(/[^A-Za-z0-9_]/gu, "_"); } function sanitizeObjectId(value) { return textOr(value, "unknown").replace(/[^A-Za-z0-9_.:-]/gu, "_"); } function normalizeBaseUrl(value) { const text = textOr(value, "").replace(/\/+$/u, ""); return /^https?:\/\//u.test(text) ? text : ""; } function boundedTimeout(value) { const parsed = Number.parseInt(String(value ?? ""), 10); return Math.min(Math.max(Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS, 100), 10000); } function redactedId(value) { const text = textOr(value, ""); if (!text) return null; if (text.length <= 12) return text; return `${text.slice(0, 8)}...${text.slice(-4)}`; } function textOr(value, fallback) { const text = String(value ?? "").trim(); return text || fallback; } function parseJson(value, fallback) { if (!value) return fallback; if (typeof value === "object") return value; try { return JSON.parse(String(value)); } catch { return fallback; } } async function fetchWithTimeout(fetchImpl, url, options, timeoutMs) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await fetchImpl(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timeout); } }