mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-21 23:27:54 +01:00
Compare commits
5 Commits
cli-improv
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5727b28c4 | ||
|
|
c62db7be06 | ||
|
|
4e56daa555 | ||
|
|
746bedf885 | ||
|
|
949c4a445a |
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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$/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
84
plugins/auth-ntlm/tests/index.test.ts
Normal file
84
plugins/auth-ntlm/tests/index.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user