import { migrate, db } from "../db.js"; import { kvSet, kvDelete } from "../kv-store.js"; import { upsertSecret, deleteSecret, listSecrets } from "../secrets-store.js"; import { upsertHttpPage, deleteHttpPage, listHttpPages, } from "../http-pages-store.js"; import { upsertHttpAuth, deleteHttpAuth, getHttpAuthInternal, } from "../http-auths-store.js"; import { authLabel, checkAnyHttpAuth, checkHttpAuth, coerceCredentialString, resolveAuthMechanism, resolveAuthMechanisms, resolveCredentialValue, resolveUnauthorizedSpec, sendHttpPageOrJson, } from "../http-trigger-auth.js"; import { validateWorkflowHttpTriggers } from "../workflow-http-validate.js"; import { log } from "../logger.js"; await migrate(); function assert(cond, msg) { if (!cond) throw new Error(msg); } function mockReq(headers = {}) { return { headers }; } const owner = "default"; const workflowKey = "default/auth-smoke.yaml"; const ctx = { owner, workflowKey }; // --- coerce --- assert(coerceCredentialString("abc") === "abc", "coerce string"); assert(coerceCredentialString(42) === "42", "coerce number"); assert(coerceCredentialString({ a: 1 }) === null, "coerce object fails"); // --- pages --- const page = await upsertHttpPage({ name: "deny-smoke", content: "

denied

", mime: "html", status: 403, }); assert(page.name === "deny-smoke", "page upsert"); // --- literal bearer --- { const mech = await resolveAuthMechanism({ type: "bearer", token: "secret-token-ok", }); assert(mech?.type === "bearer", "inline bearer"); const ok = await checkHttpAuth( mockReq({ authorization: "Bearer secret-token-ok" }), mech, ctx, ); assert(ok, "bearer match"); const bad = await checkHttpAuth( mockReq({ authorization: "Bearer wrong" }), mech, ctx, ); assert(!bad, "bearer mismatch"); const missing = await checkHttpAuth(mockReq({}), mech, ctx); assert(!missing, "bearer missing"); } // --- basic --- { const mech = await resolveAuthMechanism({ type: "basic", user: "alice", password: "hunter2", }); const encoded = Buffer.from("alice:hunter2").toString("base64"); const ok = await checkHttpAuth( mockReq({ authorization: `Basic ${encoded}` }), mech, ctx, ); assert(ok, "basic match"); const badEnc = Buffer.from("alice:wrong").toString("base64"); const bad = await checkHttpAuth( mockReq({ authorization: `Basic ${badEnc}` }), mech, ctx, ); assert(!bad, "basic mismatch"); } // --- header --- { const mech = await resolveAuthMechanism({ type: "header", header: "X-Webhook-Secret", value: "hdr-secret", }); const ok = await checkHttpAuth( mockReq({ "x-webhook-secret": "hdr-secret" }), mech, ctx, ); assert(ok, "header match"); const bad = await checkHttpAuth( mockReq({ "x-webhook-secret": "nope" }), mech, ctx, ); assert(!bad, "header mismatch"); } // --- KV ref --- await kvSet("auth", "webhook-token", "from-kv"); { const resolved = await resolveCredentialValue( { kv: "webhook-token", namespace: "auth" }, ctx, ); assert(resolved === "from-kv", "kv resolve"); const mech = await resolveAuthMechanism({ type: "bearer", token: { kv: "webhook-token", namespace: "auth" }, }); const ok = await checkHttpAuth( mockReq({ authorization: "Bearer from-kv" }), mech, ctx, ); assert(ok, "bearer from kv"); const missingKv = await checkHttpAuth( mockReq({ authorization: "Bearer from-kv" }), { type: "bearer", config: { token: { kv: "missing-key", namespace: "auth" } }, }, ctx, ); assert(!missingKv, "missing kv fails closed"); } await kvDelete("auth", "webhook-token"); // --- secret ref --- const secret = await upsertSecret({ owner, name: "http_auth_smoke_token", value: "encrypted-token-ok", }); { const resolved = await resolveCredentialValue( { secret: "http_auth_smoke_token" }, ctx, ); assert(resolved === "encrypted-token-ok", "secret resolve"); const mech = await resolveAuthMechanism({ type: "bearer", token: { secret: "http_auth_smoke_token" }, }); const ok = await checkHttpAuth( mockReq({ authorization: "Bearer encrypted-token-ok" }), mech, ctx, ); assert(ok, "bearer from secret"); const missingSec = await checkHttpAuth( mockReq({ authorization: "Bearer x" }), { type: "bearer", config: { token: { secret: "does_not_exist_xyz" } }, }, ctx, ); assert(!missingSec, "missing secret fails closed"); } // --- named profile (by id) --- const profile = await upsertHttpAuth({ name: "webhook-smoke", type: "bearer", config: { token: "named-token" }, unauthorized_status: 403, unauthorized_response: "deny-smoke", }); assert(typeof profile.id === "string" && profile.id.length > 0, "profile has id"); { const mech = await resolveAuthMechanism(profile.id); assert(mech?.label === "webhook-smoke", "profile by id"); const ok = await checkHttpAuth( mockReq({ authorization: "Bearer named-token" }), mech, ctx, ); assert(ok, "named profile match"); const { status, pageName } = resolveUnauthorizedSpec({}, mech); assert(status === 403, "profile unauth status"); assert(pageName === "deny-smoke", "profile unauth page"); } // --- rename keeps id --- { const renamed = await upsertHttpAuth({ id: profile.id, name: "webhook-renamed", type: "bearer", config: { token: { keep: true } }, unauthorized_status: 403, unauthorized_response: "deny-smoke", }); assert(renamed.id === profile.id, "rename keeps id"); assert(renamed.name === "webhook-renamed", "rename updates name"); const mech = await resolveAuthMechanism(profile.id); assert(mech?.label === "webhook-renamed", "resolve uses new name label"); const ok = await checkHttpAuth( mockReq({ authorization: "Bearer named-token" }), mech, ctx, ); assert(ok, "credentials survive rename"); } // --- multi-auth OR --- const basicProfile = await upsertHttpAuth({ name: "basic-smoke", type: "basic", config: { user: "bob", password: "p@ss" }, }); { const mechs = await resolveAuthMechanisms([profile.id, basicProfile.id]); assert(mechs.length === 2, "resolve two mechanisms"); assert( authLabel([profile.id, basicProfile.id], { [profile.id]: "webhook-renamed", [basicProfile.id]: "basic-smoke", }) === "webhook-renamed|basic-smoke", "authLabel", ); const viaBearer = await checkAnyHttpAuth( mockReq({ authorization: "Bearer named-token" }), mechs, ctx, ); assert(viaBearer, "OR accepts bearer"); const encoded = Buffer.from("bob:p@ss").toString("base64"); const viaBasic = await checkAnyHttpAuth( mockReq({ authorization: `Basic ${encoded}` }), mechs, ctx, ); assert(viaBasic, "OR accepts basic"); const neither = await checkAnyHttpAuth( mockReq({ authorization: "Bearer wrong" }), mechs, ctx, ); assert(!neither, "OR rejects when none match"); } // trigger-level override { const mech = await getHttpAuthInternal(profile.id); const { status, pageName } = resolveUnauthorizedSpec( { unauthorized: { status: 401, response: "deny-smoke" } }, mech, ); assert(status === 401, "trigger override status"); assert(pageName === "deny-smoke", "trigger override page"); } // default 401 { const { status, pageName } = resolveUnauthorizedSpec({}, null); assert(status === 401 && pageName == null, "default 401"); } // validation await validateWorkflowHttpTriggers({ triggers: [ { type: "HTTP", method: "POST", path: "/x", auth: [profile.id, basicProfile.id], response: "deny-smoke", }, ], }); let threw = false; try { await validateWorkflowHttpTriggers({ triggers: [{ type: "HTTP", path: "/x", auth: profile.id }], }); } catch { threw = true; } assert(threw, "non-array auth fails validation"); threw = false; try { await validateWorkflowHttpTriggers({ triggers: [{ type: "HTTP", path: "/x", auth: ["webhook-renamed"] }], }); } catch { threw = true; } assert(threw, "name string fails validation"); threw = false; try { await validateWorkflowHttpTriggers({ triggers: [ { type: "HTTP", path: "/x", auth: ["00000000-0000-4000-8000-000000000000"], }, ], }); } catch { threw = true; } assert(threw, "unknown auth id fails validation"); threw = false; try { await validateWorkflowHttpTriggers({ triggers: [{ type: "HTTP", path: "/x", response: "no-such-page" }], }); } catch { threw = true; } assert(threw, "unknown page fails validation"); // send page helper (mock reply) { /** @type {any} */ const reply = { _code: 200, _type: null, _body: null, code(c) { this._code = c; return this; }, type(t) { this._type = t; return this; }, send(b) { this._body = b; return this; }, }; await sendHttpPageOrJson(reply, 401, "deny-smoke", { error: "unauthorized" }); assert(reply._code === 401, "page status from arg"); assert(String(reply._type).includes("text/html"), "page mime"); assert(reply._body === "

denied

", "page body"); } // cleanup await deleteHttpAuth(profile.id); await deleteHttpAuth(basicProfile.id); await deleteHttpPage(page.id); await deleteSecret(secret.id); const leftover = (await listSecrets({ owner })).find( (s) => s.name === "http_auth_smoke_token", ); assert(!leftover, "secret cleaned"); assert(!(await listHttpPages()).some((p) => p.name === "deny-smoke"), "page cleaned"); log.info("http-trigger-auth-smoke ok"); await db.destroy(); process.exit(0);