Compare commits

..

5 Commits

Author SHA1 Message Date
Gregory Schier
f5727b28c4 faker: render Date outputs as ISO strings 2026-02-21 07:24:07 -08:00
Gregory Schier
c62db7be06 Add contribution policy docs and PR checklist template 2026-02-20 14:09:59 -08:00
Gregory Schier
4e56daa555 CLI send enhancements and shared plugin event routing (#398) 2026-02-20 13:21:55 -08:00
dependabot[bot]
746bedf885 Bump hono from 4.11.7 to 4.11.10 (#403)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 09:01:23 -08:00
Gregory Schier
949c4a445a Fix NTLM challenge parsing when WWW-Authenticate has Negotiate first (#402) 2026-02-20 08:48:27 -08:00
7 changed files with 120 additions and 13 deletions

8
package-lock.json generated
View File

@@ -7985,9 +7985,9 @@
} }
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.11.7", "version": "4.11.10",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.10.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "integrity": "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
@@ -16020,7 +16020,7 @@
"@hono/mcp": "^0.2.3", "@hono/mcp": "^0.2.3",
"@hono/node-server": "^1.19.7", "@hono/node-server": "^1.19.7",
"@modelcontextprotocol/sdk": "^1.26.0", "@modelcontextprotocol/sdk": "^1.26.0",
"hono": "^4.11.7", "hono": "^4.11.10",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -29,6 +29,7 @@ const modules = [
function normalizeResult(result: unknown): string { function normalizeResult(result: unknown): string {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
if (result instanceof Date) return result.toISOString();
return JSON.stringify(result); return JSON.stringify(result);
} }

View File

@@ -9,4 +9,18 @@ describe('template-function-faker', () => {
// accidental additions, removals, or renames across faker upgrades. // accidental additions, removals, or renames across faker upgrades.
expect(names).toMatchSnapshot(); expect(names).toMatchSnapshot();
}); });
it('renders date results as unquoted ISO strings', async () => {
const { plugin } = await import('../src/index');
const fn = plugin.templateFunctions?.find((fn) => fn.name === 'faker.date.future');
expect(fn?.onRender).toBeTypeOf('function');
const result = await fn!.onRender!(
{} as Parameters<NonNullable<typeof fn.onRender>>[0],
{ values: {} } as Parameters<NonNullable<typeof fn.onRender>>[1],
);
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
}); });

View File

@@ -18,7 +18,7 @@
"@hono/mcp": "^0.2.3", "@hono/mcp": "^0.2.3",
"@hono/node-server": "^1.19.7", "@hono/node-server": "^1.19.7",
"@modelcontextprotocol/sdk": "^1.26.0", "@modelcontextprotocol/sdk": "^1.26.0",
"hono": "^4.11.7", "hono": "^4.11.10",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -11,7 +11,8 @@
"version": "0.1.0", "version": "0.1.0",
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev" "dev": "yaakcli dev",
"test": "vitest --run tests"
}, },
"dependencies": { "dependencies": {
"httpntlm": "^1.8.13" "httpntlm": "^1.8.13"

View File

@@ -2,6 +2,16 @@ import type { PluginDefinition } from '@yaakapp/api';
import { ntlm } from 'httpntlm'; 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 = { export const plugin: PluginDefinition = {
authentication: { authentication: {
name: 'windows', name: 'windows',
@@ -68,15 +78,12 @@ export const plugin: PluginDefinition = {
}, },
}); });
const wwwAuthenticateHeader = negotiateResponse.headers.find( const ntlmChallenge = extractNtlmChallenge(negotiateResponse.headers);
(h) => h.name.toLowerCase() === 'www-authenticate', if (ntlmChallenge == null) {
); throw new Error('Unable to find NTLM challenge in WWW-Authenticate response headers');
if (!wwwAuthenticateHeader?.value) {
throw new Error('Unable to find www-authenticate response header for NTLM');
} }
const type2 = ntlm.parseType2Message(wwwAuthenticateHeader.value, (err: Error | null) => { const type2 = ntlm.parseType2Message(ntlmChallenge, (err: Error | null) => {
if (err != null) throw err; if (err != null) throw err;
}); });
const type3 = ntlm.createType3Message(type2, options); const type3 = ntlm.createType3Message(type2, options);

View File

@@ -0,0 +1,84 @@
import type { Context } from '@yaakapp/api';
import { beforeEach, describe, expect, test, vi } from 'vitest';
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');
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({
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({
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({
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');
});
});