snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "@yaak/auth-ntlm",
"displayName": "NTLM Authentication",
"version": "0.1.0",
"private": true,
"description": "Authenticate requests using NTLM authentication",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-ntlm"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
},
"dependencies": {
"httpntlm": "^1.8.13"
}
}

View File

@@ -0,0 +1,94 @@
import type { PluginDefinition } from "@yaakapp/api";
import { ntlm } from "httpntlm";
function extractNtlmChallenge(headers: Array<{ name: string; value: string }>): string | null {
const authValues = headers
.filter((h) => h.name.toLowerCase() === "www-authenticate")
.flatMap((h) => h.value.split(","))
.map((v) => v.trim())
.filter(Boolean);
return authValues.find((v) => /^NTLM\s+\S+/i.test(v)) ?? null;
}
export const plugin: PluginDefinition = {
authentication: {
name: "windows",
label: "NTLM Auth",
shortLabel: "NTLM",
args: [
{
type: "banner",
color: "info",
inputs: [
{
type: "markdown",
content:
"NTLM is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).",
},
],
},
{
type: "text",
name: "username",
label: "Username",
optional: true,
},
{
type: "text",
name: "password",
label: "Password",
optional: true,
password: true,
},
{
type: "accordion",
label: "Advanced",
inputs: [
{ name: "domain", label: "Domain", type: "text", optional: true },
{ name: "workstation", label: "Workstation", type: "text", optional: true },
],
},
],
async onApply(ctx, { values, method, url }) {
const username = values.username ? String(values.username) : undefined;
const password = values.password ? String(values.password) : undefined;
const domain = values.domain ? String(values.domain) : undefined;
const workstation = values.workstation ? String(values.workstation) : undefined;
const options = {
url,
username,
password,
workstation,
domain,
};
const type1 = ntlm.createType1Message(options);
const { httpResponse: negotiateResponse } = await ctx.httpRequest.send({
httpRequest: {
method,
url,
headers: [
{ name: "Authorization", value: type1 },
{ name: "Connection", value: "keep-alive" },
],
},
});
const ntlmChallenge = extractNtlmChallenge(negotiateResponse.headers);
if (ntlmChallenge == null) {
throw new Error("Unable to find NTLM challenge in WWW-Authenticate response headers");
}
const type2 = ntlm.parseType2Message(ntlmChallenge, (err: Error | null) => {
if (err != null) throw err;
});
const type3 = ntlm.createType3Message(type2, options);
return { setHeaders: [{ name: "Authorization", value: type3 }] };
},
},
};

1
plugins/auth-ntlm/src/modules.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "httpntlm";

View File

@@ -0,0 +1,89 @@
import type { Context } from "@yaakapp/api";
import { beforeEach, describe, expect, test, vi } from "vite-plus/test";
const ntlmMock = vi.hoisted(() => ({
createType1Message: vi.fn(),
parseType2Message: vi.fn(),
createType3Message: vi.fn(),
}));
vi.mock("httpntlm", () => ({ ntlm: ntlmMock }));
import { plugin } from "../src";
describe("auth-ntlm", () => {
beforeEach(() => {
ntlmMock.createType1Message.mockReset();
ntlmMock.parseType2Message.mockReset();
ntlmMock.createType3Message.mockReset();
ntlmMock.createType1Message.mockReturnValue("NTLM TYPE1");
// oxlint-disable-next-line no-explicit-any
ntlmMock.parseType2Message.mockReturnValue({} as any);
ntlmMock.createType3Message.mockReturnValue("NTLM TYPE3");
});
test("uses NTLM challenge when Negotiate and NTLM headers are separate", async () => {
const send = vi.fn().mockResolvedValue({
httpResponse: {
headers: [
{ name: "WWW-Authenticate", value: "Negotiate" },
{ name: "WWW-Authenticate", value: "NTLM TlRMTVNTUAACAAAAAA==" },
],
},
});
const ctx = { httpRequest: { send } } as unknown as Context;
const result = await plugin.authentication?.onApply(ctx, {
values: {},
headers: [],
url: "https://example.local/resource",
method: "GET",
contextId: "ctx",
});
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
"NTLM TlRMTVNTUAACAAAAAA==",
expect.any(Function),
);
expect(result).toEqual({ setHeaders: [{ name: "Authorization", value: "NTLM TYPE3" }] });
});
test("uses NTLM challenge when auth schemes are comma-separated in one header", async () => {
const send = vi.fn().mockResolvedValue({
httpResponse: {
headers: [{ name: "www-authenticate", value: "Negotiate, NTLM TlRMTVNTUAACAAAAAA==" }],
},
});
const ctx = { httpRequest: { send } } as unknown as Context;
await plugin.authentication?.onApply(ctx, {
values: {},
headers: [],
url: "https://example.local/resource",
method: "GET",
contextId: "ctx",
});
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
"NTLM TlRMTVNTUAACAAAAAA==",
expect.any(Function),
);
});
test("throws a clear error when NTLM challenge is missing", async () => {
const send = vi.fn().mockResolvedValue({
httpResponse: { headers: [{ name: "WWW-Authenticate", value: "Negotiate" }] },
});
const ctx = { httpRequest: { send } } as unknown as Context;
await expect(
plugin.authentication?.onApply(ctx, {
values: {},
headers: [],
url: "https://example.local/resource",
method: "GET",
contextId: "ctx",
}),
).rejects.toThrow("Unable to find NTLM challenge in WWW-Authenticate response headers");
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}